diff --git a/appveyor.yml b/appveyor.yml index dd6efecb5f9ebb74cd081aa15337ff6051a50220..652647b26781a56474efff9b67857858caeb501f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,31 +6,48 @@ environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd" matrix: - - PYTHON: "C:\\Python34_64" + - PYTHON: "C:\\Python35_64" PYTHON_VERSION: "3.5" PYTHON_ARCH: "64" - CONDA_PY: "34" + CONDA_PY: "35" CONDA_NPY: "18" + QTDIR: "C:\\Qt\\5.6\\5.6\\msvc2015_64" + QDOWNLOAD: "http://download.qt.io/development_releases/qt/5.6/5.6.0-beta/qt-opensource-windows-x86-msvc2015_64-5.6.0-beta.exe" + QINSTALLER: "qt-opensource-windows-x86-msvc2015_64-5.6.0-beta.exe" platform: x64 - - PYTHON: "C:\\Python34_32" + - PYTHON: "C:\\Python35_32" PYTHON_VERSION: "3.5" PYTHON_ARCH: "32" - CONDA_PY: "34" + CONDA_PY: "35" CONDA_NPY: "18" + QTDIR: "C:\\Qt\\5.6\\5.6\\msvc2015" + QDOWNLOAD: "http://download.qt.io/development_releases/qt/5.6/5.6.0-beta/qt-opensource-windows-x86-msvc2015-5.6.0-beta.exe" + QINSTALLER: "qt-opensource-windows-x86-msvc2015-5.6.0-beta.exe" platform: x86 install: # this installs the appropriate Miniconda (Py2/Py3, 32/64 bit), # as well as pip, conda-build, and the binstar CLI - powershell .\\ci\\appveyor\\install.ps1 + + - IF NOT EXIST C:\Qt\5.6\5.6 curl -kLO %QDOWNLOAD% + - IF NOT EXIST C:\Qt\5.6\5.6 %QINSTALLER% --script ci\appveyor\qt-installer-noninteractive.qs + # - dir /b /s /ad c:\Qt + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - "IF EXIST %QTDIR%\\include\\QtNfc MOVE %QTDIR%\\include\\QtNfc %QTDIR%\\include\\QtNfc-disable" + # Add qt to path + - "set PATH=%QTDIR%\\bin;%PATH%" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "SET QT_QPA_PLATFORM_PLUGIN_PATH=%PYTHON%\\envs\\test-environment\\Scripts\\plugins" - - choco install -y vcredist2013 + - "SET QT_PLUGIN_PATH=C:\\Qt\\5.6\\5.6\\msvc_2015\\plugins" + - choco install -y vcredist2015 - "%CMD_IN_ENV% conda config --set always_yes yes --set changeps1 no" - "%CMD_IN_ENV% conda config --add channels inso/channel/sakia" - - "%CMD_IN_ENV% conda config --add channels pyzo" - - "%CMD_IN_ENV% conda create -q -n test-environment python=%PYTHON_VERSION% cx_freeze pyqt5 libsodium=1.0.3" + - "%CMD_IN_ENV% conda create -q -n test-environment python=%PYTHON_VERSION% pyqt5 libsodium=1.0.3" + +cache: + - C:\Qt\5.6\5.6 build_script: - ".\\ci\\appveyor\\build.cmd" diff --git a/ci/appveyor/build.cmd b/ci/appveyor/build.cmd index bd4410c0b09eb36a4b4391aff179bddf92770125..a1b9b313e4a0f0b3dc536e1148fd2f863329fbf0 100644 --- a/ci/appveyor/build.cmd +++ b/ci/appveyor/build.cmd @@ -3,7 +3,7 @@ call activate test-environment echo "%PATH%" -echo "%QT_QPA_PLATFORM_PLUGIN_PATH%" +echo "%QT_PLUGIN_PATH%" python -V call pyuic5 --version @@ -19,5 +19,5 @@ if %errorlevel% neq 0 exit /b 1s python gen_translations.py if %errorlevel% neq 0 exit /b 1 -python setup.py build -if %errorlevel% neq 0 exit /b 1 +@REM python setup.py build +@REM if %errorlevel% neq 0 exit /b 1 diff --git a/ci/appveyor/qt-installer-noninteractive.qs b/ci/appveyor/qt-installer-noninteractive.qs new file mode 100644 index 0000000000000000000000000000000000000000..9df892cdb83b2576853b6e9be5697dc049f3c04e --- /dev/null +++ b/ci/appveyor/qt-installer-noninteractive.qs @@ -0,0 +1,56 @@ +// Emacs mode hint: -*- mode: JavaScript -*- + +function Controller() { + installer.autoRejectMessageBoxes(); + installer.installationFinished.connect(function() { + gui.clickButton(buttons.NextButton); + }) +} + +Controller.prototype.WelcomePageCallback = function() { + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.CredentialsPageCallback = function() { + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.IntroductionPageCallback = function() { + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.TargetDirectoryPageCallback = function() +{ + gui.currentPageWidget().TargetDirectoryLineEdit.setText("C:\\Qt\\5.6"); + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.ComponentSelectionPageCallback = function() { + //var widget = gui.currentPageWidget(); + + //widget.selectAll(); + + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.LicenseAgreementPageCallback = function() { + gui.currentPageWidget().AcceptLicenseRadioButton.setChecked(true); + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.StartMenuDirectoryPageCallback = function() { + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.ReadyForInstallationPageCallback = function() +{ + gui.clickButton(buttons.NextButton); +} + +Controller.prototype.FinishedPageCallback = function() { +var checkBoxForm = gui.currentPageWidget().LaunchQtCreatorCheckBoxForm +if (checkBoxForm && checkBoxForm.launchQtCreatorCheckBox) { + checkBoxForm.launchQtCreatorCheckBox.checked = false; +} + gui.clickButton(buttons.FinishButton); +} diff --git a/ci/appveyor/tests.cmd b/ci/appveyor/tests.cmd index a2029cc5d5442022e4959cc771f92e0845de1efc..094bc394e38247295483cfccd0019374eccfd14e 100644 --- a/ci/appveyor/tests.cmd +++ b/ci/appveyor/tests.cmd @@ -3,7 +3,7 @@ call activate test-environment echo "%PATH%" -echo "%QT_QPA_PLATFORM_PLUGIN_PATH%" +echo "%QT_PLUGIN_PATH%" python -V call pyuic5 --version @@ -11,6 +11,8 @@ pyrcc5 -version lrelease -version +echo "%CWD%" + python setup.py test if %errorlevel% neq 0 exit /b 1 \ No newline at end of file diff --git a/ci/travis/before_install.sh b/ci/travis/before_install.sh index 679c5c359219d24967e843e06b2d05d009335b17..ba2085b4ef33aa5fb5759befc1bacf0ee8667cf3 100755 --- a/ci/travis/before_install.sh +++ b/ci/travis/before_install.sh @@ -57,10 +57,20 @@ then pyenv activate sakia-env if [ $TRAVIS_OS_NAME == "osx" ] then - python configure.py --confirm-license + python configure.py --confirm-license \ + --enable QtCore \ + --enable QtWidgets \ + --enable QtGui \ + --enable QtSvg\ + --enable QtTest elif [ $TRAVIS_OS_NAME == "linux" ] then - python configure.py --qmake "/usr/lib/x86_64-linux-gnu/qt5/bin/qmake" --confirm-license + python configure.py --qmake "/usr/lib/x86_64-linux-gnu/qt5/bin/qmake" --confirm-license \ + --enable QtCore \ + --enable QtWidgets \ + --enable QtGui \ + --enable QtSvg\ + --enable QtTest fi make -j 2 && make install diff --git a/requirements.txt b/requirements.txt index 58d74c97e088fd0fd603005a7c8048bdb9303abd..c4379dc46caef502b33db85fe8b273c1058bd20a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ ucoinpy>=0.13 -quamash +git+https://github.com/harvimt/quamash.git@gh45 asynctest -networkx \ No newline at end of file +git+https://github.com/networkx/networkx.git@v1.11 \ No newline at end of file diff --git a/res/icons/AUTHORS b/res/icons/AUTHORS index 23af4ae9d95d73a77e3cc96aee08e56473c1843c..ea1311c735ce47045d80a49dabef8c7a7bdebf42 100644 --- a/res/icons/AUTHORS +++ b/res/icons/AUTHORS @@ -25,4 +25,20 @@ noun_155533_cc.svg : by anbileru adaleru noun_155520_cc.svg : by anbileru adaleru noun_155540_cc.svg : by anbileru adaleru noun_100552_cc.svg : by Rui -noun_178537_cc.svg : by Nathan David Smith \ No newline at end of file +noun_178537_cc.svg : by Nathan David Smith +noun_213188_cc.svg : by Aha-Soft +noun_213886_cc.svg : by Aha-Soft +noun_213196_cc.svg : by Aha-Soft +noun_60040_cc.svg : by Dmitry Baranovskiy +noun_87601_cc.svg : by Arthur Shlain +noun_274635_cc.svg : by Pham Thi Dieu Linh +noun_198591_cc.svg : by Андрей Уханёв +noun_269788_cc.svg : by TMD +noun_269789_cc.svg : by TMD +noun_269790_cc.svg : by TMD +noun_269791_cc.svg : by TMD +noun_269792_cc.svg : by TMD +noun_269793_cc.svg : by TMD +noun_188924_cc.svg : by anbileru adaleru +noun_188905_cc.svg : by anbileru adaleru +noun_188906_cc.svg : by anbileru adaleru \ No newline at end of file diff --git a/res/icons/connected.svg b/res/icons/connected.svg deleted file mode 100644 index c6a97c9c66be3ebf88117731bd8e14d55542ef8b..0000000000000000000000000000000000000000 --- a/res/icons/connected.svg +++ /dev/null @@ -1,98 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="100" - height="100" - viewBox="0 0 100 100" - id="svg3336" - version="1.1" - inkscape:version="0.91 r13725" - sodipodi:docname="connected.svg"> - <defs - id="defs3338"> - <linearGradient - inkscape:collect="always" - id="linearGradient4150"> - <stop - style="stop-color:#ffffff;stop-opacity:1;" - offset="0" - id="stop4152" /> - <stop - style="stop-color:#ffffff;stop-opacity:0.04620462" - offset="1" - id="stop4154" /> - </linearGradient> - <radialGradient - inkscape:collect="always" - xlink:href="#linearGradient4150" - id="radialGradient4162" - cx="52.325901" - cy="1005.1627" - fx="52.325901" - fy="1005.1627" - r="42.926411" - gradientTransform="matrix(1.539681,0,0,1.5451884,-68.190831,-562.49866)" - gradientUnits="userSpaceOnUse" /> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="2.8284271" - inkscape:cx="18.741815" - inkscape:cy="33.829638" - inkscape:document-units="px" - inkscape:current-layer="layer1" - showgrid="false" - units="px" - inkscape:snap-bbox="false" - inkscape:window-width="1366" - inkscape:window-height="709" - inkscape:window-x="-4" - inkscape:window-y="0" - inkscape:window-maximized="1" /> - <metadata - id="metadata3341"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="Calque 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-952.36216)"> - <ellipse - style="opacity:1;fill:#17d017;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - id="path3346" - cx="51.785713" - cy="1005.5765" - rx="41.785713" - ry="40.714287" /> - <ellipse - style="opacity:0.65;fill:url(#radialGradient4162);fill-opacity:1;stroke:none;stroke-width:1.25129819;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - id="path4148" - cx="12.374369" - cy="990.66699" - rx="64.928177" - ry="65.10495" /> - </g> -</svg> diff --git a/res/icons/disconnected.svg b/res/icons/disconnected.svg deleted file mode 100644 index 6647ff69d1f4db23fc7130d611543229eb42b3af..0000000000000000000000000000000000000000 --- a/res/icons/disconnected.svg +++ /dev/null @@ -1,98 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="100" - height="100" - viewBox="0 0 100 100" - id="svg3336" - version="1.1" - inkscape:version="0.91 r13725" - sodipodi:docname="disconnected.svg"> - <defs - id="defs3338"> - <linearGradient - inkscape:collect="always" - id="linearGradient4150"> - <stop - style="stop-color:#ffffff;stop-opacity:1;" - offset="0" - id="stop4152" /> - <stop - style="stop-color:#ffffff;stop-opacity:0.04620462" - offset="1" - id="stop4154" /> - </linearGradient> - <radialGradient - inkscape:collect="always" - xlink:href="#linearGradient4150" - id="radialGradient4162" - cx="52.325901" - cy="1005.1627" - fx="52.325901" - fy="1005.1627" - r="42.926411" - gradientTransform="matrix(1.539681,0,0,1.5451884,-50.51316,-559.67022)" - gradientUnits="userSpaceOnUse" /> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="2.8284271" - inkscape:cx="18.741815" - inkscape:cy="33.829638" - inkscape:document-units="px" - inkscape:current-layer="layer1" - showgrid="false" - units="px" - inkscape:snap-bbox="false" - inkscape:window-width="1366" - inkscape:window-height="709" - inkscape:window-x="-4" - inkscape:window-y="0" - inkscape:window-maximized="1" /> - <metadata - id="metadata3341"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="Calque 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-952.36216)"> - <ellipse - style="opacity:1;fill:#c60002;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - id="path3346" - cx="51.785713" - cy="1005.5765" - rx="41.785713" - ry="40.714287" /> - <ellipse - style="opacity:0.65;fill:url(#radialGradient4162);fill-opacity:1;stroke:none;stroke-width:1.25129819;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - id="path4148" - cx="30.05204" - cy="993.49536" - rx="64.928177" - ry="65.10495" /> - </g> -</svg> diff --git a/res/icons/icons.qrc b/res/icons/icons.qrc index 1e3016df94ab4c91c9f41ed191f800230a3383a6..b889b5abe8699731a8a6ac86e25ca3bacc0c685f 100644 --- a/res/icons/icons.qrc +++ b/res/icons/icons.qrc @@ -29,8 +29,18 @@ <file alias="settings_app_icon">noun_42425_cc.svg</file> <file alias="settings_network_icon">noun_62146_cc.svg</file> <file alias="explorer_icon">noun_101791_cc.svg</file> - <file alias="connected">connected.svg</file> - <file alias="weak_connect">weak_connect.svg</file> - <file alias="disconnected">disconnected.svg</file> + <file alias="connected">noun_269788_cc.svg</file> + <file alias="weak_connect">noun_269792_cc.svg</file> + <file alias="disconnected">noun_269793_cc.svg</file> + <file alias="member">noun_213188_cc.svg</file> + <file alias="not_member">noun_213192_cc.svg</file> + <file alias="member_warning">noun_213886_cc.svg</file> + <file alias="forked">noun_60040_cc.svg</file> + <file alias="offline">noun_87601_cc.svg</file> + <file alias="synchronized">noun_274635_cc.svg</file> + <file alias="corrupted">noun_198591_cc.svg</file> + <file alias="dividend">noun_188924_cc.svg</file> + <file alias="received">noun_188906_cc.svg</file> + <file alias="sent">noun_188905_cc.svg</file> </qresource> </RCC> diff --git a/res/icons/noun_188905_cc.svg b/res/icons/noun_188905_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..6d017d53c8bd32c7c145eba03069ebd771521845 --- /dev/null +++ b/res/icons/noun_188905_cc.svg @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 125 100" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_188905_cc.svg" + width="125" + height="100"> + <metadata + id="metadata38"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs36" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview34" + showgrid="false" + inkscape:zoom="2.6700352" + inkscape:cx="-21.344737" + inkscape:cy="57.606196" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="g4" /> + <g + transform="matrix(1.1387208,0,0,1.1387208,-7.4656982,-1106.1042)" + id="g4" + style="fill:#aa8800"> + <rect + style="opacity:1;fill:#decd87;fill-opacity:0.61290325;fill-rule:evenodd;stroke:none;stroke-width:9;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4145" + width="86.172173" + height="42.09938" + x="8.2007217" + y="996.02557" /> + <path + style="opacity:1;fill:#e9ddaf;fill-opacity:0.99539173;fill-rule:evenodd;stroke:none;stroke-width:9;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path4147" + sodipodi:type="arc" + sodipodi:cx="93.715096" + sodipodi:cy="999.8078" + sodipodi:rx="21.04969" + sodipodi:ry="20.227436" + sodipodi:start="0" + sodipodi:end="6.2784729" + d="m 114.76479,999.8078 a 21.04969,20.227436 0 0 1 -21.024896,20.2274 21.04969,20.227436 0 0 1 -21.07443,-20.17974 21.04969,20.227436 0 0 1 20.975236,-20.27497 21.04969,20.227436 0 0 1 21.12385,20.13199 l -21.049454,0.0953 z" /> + <path + d="m 8,995.49506 c 0,14.88924 0,29.77844 0,44.66764 l 86.973908,0 0,-20.2133 c -0.320011,0.015 -0.634655,0.047 -0.9585,0.047 -0.323462,0 -0.638489,-0.032 -0.9585,-0.047 l 0,18.2963 -83.139908,0 0,-40.83364 63.827666,0 c 0.06646,-0.64846 0.170102,-1.28708 0.297007,-1.917 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path6" + inkscape:connector-curvature="0" /> + <path + d="m 18.910414,995.77609 -10.6307876,10.68831 1.3577472,1.3504 10.6334714,-10.68582 -1.360431,-1.35289 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path8" + inkscape:connector-curvature="0" /> + <path + d="m 24.22715,995.77609 -15.9475236,16.03241 1.3577472,1.3504 15.9500794,-16.02992 -1.360303,-1.35289 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path10" + inkscape:connector-curvature="0" /> + <path + d="m 72.751149,1032.9005 0,1.917 15.947523,0 0,-1.917 -15.947523,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path12" + inkscape:connector-curvature="0" /> + <path + d="m 19.589415,1006.1834 0,1.917 55.944962,0 c -0.28755,-0.6229 -0.543789,-1.2616 -0.768845,-1.917 l -55.176117,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path14" + inkscape:connector-curvature="0" /> + <path + d="m 19.589415,1011.5239 0,1.917 42.728142,0 0,-1.917 -42.728142,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path16" + inkscape:connector-curvature="0" /> + <path + d="m 19.589415,1016.8681 0,1.917 26.683235,0 0,-1.917 -26.683235,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path18" + inkscape:connector-curvature="0" /> + <path + d="m 94.015408,978.06876 c -11.703541,0 -21.336594,9.68673 -21.336594,21.44395 0,11.88299 9.640593,21.44399 21.336594,21.44399 11.821752,0 21.336592,-9.5684 21.336592,-21.44399 0,-11.74981 -9.50742,-21.44395 -21.336592,-21.44395 z m 0,1.917 c 10.783512,0 19.419592,8.80925 19.419592,19.52695 0,10.85549 -8.62867,19.52699 -19.419592,19.52699 -10.653153,0 -19.419594,-8.6789 -19.419594,-19.52699 0,-10.71028 8.773981,-19.52695 19.419594,-19.52695 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.90360552;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path20" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 95.14113,988.45949 -1.350387,1.35787 11.010267,10.96284 1.35289,-1.36037 -11.01277,-10.96034 z" + id="path22" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 104.80101,998.24667 -11.010267,10.96033 1.350387,1.3603 11.01277,-10.96276 -1.35289,-1.35787 z" + id="path24" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 81.874392,998.55369 0,1.91701 23.358448,0 0,-1.91701 -23.358448,0 z" + id="path26" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4aa00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.90360552;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 19.586867,1022.2202 c 0,2.4204 0,4.8408 0,7.2612 l 33.814583,0 0,-7.2612 z m 1.917,1.917 8.71636,0 0,3.4272 -8.71636,0 z m 10.63336,0 19.347223,0 0,3.4272 -19.347223,0 z" + id="path28" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_188906_cc.svg b/res/icons/noun_188906_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..74ceb8e2721bf7f240a06806fc677346f2e3294d --- /dev/null +++ b/res/icons/noun_188906_cc.svg @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 125 100" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_188906_cc.svg" + width="125" + height="100"> + <metadata + id="metadata38"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs36" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview34" + showgrid="false" + inkscape:zoom="3.776" + inkscape:cx="47.720009" + inkscape:cy="73.89717" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="g4" /> + <g + transform="matrix(1.4786603,0,0,1.4786603,-9.8292821,-1439.5248)" + id="g4" + style="fill:#338000"> + <path + style="opacity:1;fill:#aade87;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:27.23500061;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path4147" + sodipodi:type="arc" + sodipodi:cx="75.691109" + sodipodi:cy="995.31189" + sodipodi:rx="16.387802" + sodipodi:ry="17.01466" + sodipodi:start="0" + sodipodi:end="6.2783233" + d="M 92.078911,995.31189 A 16.387802,17.01466 0 0 1 75.711028,1012.3265 16.387802,17.01466 0 0 1 59.303355,995.35325 16.387802,17.01466 0 0 1 75.631351,978.29734 16.387802,17.01466 0 0 1 92.078717,995.22916" + sodipodi:open="true" /> + <rect + style="opacity:1;fill:#aade87;fill-opacity:0.61290325;fill-rule:evenodd;stroke:none;stroke-width:27.23500061;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4145" + width="67.879524" + height="34.208435" + x="7.9011359" + y="992.26715" /> + <path + d="m 8,991.70436 c 0,11.65044 0,23.30084 0,34.95124 l 68.0547,0 0,-15.8164 c -0.2504,0.012 -0.4966,0.037 -0.75,0.037 -0.2531,0 -0.4996,-0.025 -0.75,-0.037 l 0,14.3164 -65.0547,0 0,-31.95124 49.9434,0 c 0.052,-0.5074 0.1331,-1.0071 0.2324,-1.5 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path6" + inkscape:connector-curvature="0" /> + <path + d="m 16.5371,991.92426 -8.3183,8.36334 1.0624,1.0566 8.3204,-8.36134 -1.0645,-1.0586 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path8" + inkscape:connector-curvature="0" /> + <path + d="m 20.6973,991.92426 -12.4785,12.54494 1.0624,1.0566 12.4805,-12.54294 -1.0644,-1.0586 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path10" + inkscape:connector-curvature="0" /> + <path + d="m 58.666,1020.9731 0,1.5 12.4785,0 0,-1.5 -12.4785,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path12" + inkscape:connector-curvature="0" /> + <path + d="m 17.0684,1000.0677 0,1.5 43.7754,0 c -0.225,-0.4874 -0.4255,-0.9872 -0.6016,-1.5 l -43.1738,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path14" + inkscape:connector-curvature="0" /> + <path + d="m 17.0684,1004.2465 0,1.5 33.4336,0 0,-1.5 -33.4336,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path16" + inkscape:connector-curvature="0" /> + <path + d="m 17.0684,1008.4282 0,1.5 20.8789,0 0,-1.5 -20.8789,0 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path18" + inkscape:connector-curvature="0" /> + <path + d="m 75.3047,978.06876 c -9.1577,0 -16.6953,7.5796 -16.6953,16.7793 0,9.29814 7.5435,16.77934 16.6953,16.77934 9.2502,0 16.6953,-7.487 16.6953,-16.77934 0,-9.1939 -7.4393,-16.7793 -16.6953,-16.7793 z m 0,1.5 c 8.4378,0 15.1953,6.893 15.1953,15.2793 0,8.49414 -6.7517,15.27934 -15.1953,15.27934 -8.3358,0 -15.1953,-6.791 -15.1953,-15.27934 0,-8.3805 6.8654,-15.2793 15.1953,-15.2793 z" + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="path20" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 74.421873,986.19922 1.056641,1.0625 -8.615235,8.57812 -1.058593,-1.06445 8.617187,-8.57617 z" + id="path22" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 66.863279,993.85742 8.615235,8.57618 -1.056641,1.0644 -8.617187,-8.57808 1.058593,-1.0625 z" + id="path24" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 84.802732,994.09766 0,1.5 -18.277343,0 0,-1.5 18.277343,0 z" + id="path26" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#338000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.49999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 17.066406,60.253906 c 0,1.89388 0,3.787761 0,5.681641 l 26.458985,0 0,-5.681641 z m 1.5,1.5 6.820313,0 0,2.681641 -6.820313,0 z m 8.320313,0 15.138672,0 0,2.681641 -15.138672,0 z" + transform="translate(0,952.36216)" + id="path28" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_188924_cc.svg b/res/icons/noun_188924_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..efd676ef385e3c5b992f298d0234f8d06288a1d9 --- /dev/null +++ b/res/icons/noun_188924_cc.svg @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 125 100" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_188924_cc.svg" + width="125" + height="100"> + <metadata + id="metadata44"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs42" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview40" + showgrid="false" + inkscape:zoom="2.6700352" + inkscape:cx="2.5475798" + inkscape:cy="65.94635" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <path + style="opacity:1;fill:#afe9dd;fill-opacity:0.99539173;fill-rule:evenodd;stroke:none;stroke-width:9;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path4150" + sodipodi:type="arc" + sodipodi:cx="99.99868" + sodipodi:cy="32.959686" + sodipodi:rx="22.84614" + sodipodi:ry="23.220667" + sodipodi:start="0" + sodipodi:end="6.2784729" + d="M 122.84482,32.959686 A 22.84614,23.220667 0 0 1 100.0256,56.180337 22.84614,23.220667 0 0 1 77.152604,33.014399 22.84614,23.220667 0 0 1 99.917935,9.7391644 22.84614,23.220667 0 0 1 122.84457,32.850261 l -22.84589,0.109425 z" /> + <rect + style="opacity:1;fill:#afe9dd;fill-opacity:0.99539173;fill-rule:evenodd;stroke:none;stroke-width:9;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4148" + width="97.751518" + height="50.561131" + x="2.6216884" + y="29.588943" /> + <g + transform="matrix(1.4672496,0,0,1.4672496,-10.504946,-1426.072)" + id="g4" + style="fill:#005544"> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 24.7188,1013.3598 0,4.1816 1.5,0 0,-4.1816 -1.5,0 z" + id="path6" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.49999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 17.068359,1012.6152 c 0,1.8939 0,3.7878 0,5.6817 l 26.458985,0 0,-5.6817 z m 1.5,1.5 23.458985,0 0,2.6817 -23.458985,0 z" + id="path8" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 8,991.7044 c 0,11.6504 0,23.3008 0,34.9512 l 68.0547,0 0,-15.8164 c -0.2504,0.012 -0.4966,0.037 -0.75,0.037 -0.2531,0 -0.4996,-0.025 -0.75,-0.037 l 0,14.3164 -65.0547,0 0,-31.9512 49.9434,0 c 0.052,-0.50739 0.1331,-1.00719 0.2324,-1.5 z" + id="path10" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 16.5371,991.92423 -8.3183,8.36327 1.0624,1.0567 8.3204,-8.36138 -1.0645,-1.05859 z" + id="path12" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 20.6973,991.92423 -12.4785,12.54497 1.0624,1.0566 12.4805,-12.54298 -1.0644,-1.05859 z" + id="path14" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 58.666,1020.9731 0,1.5 12.4785,0 0,-1.5 -12.4785,0 z" + id="path16" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 17.0684,1000.0677 0,1.5 43.7754,0 c -0.225,-0.4875 -0.4255,-0.9873 -0.6016,-1.5 l -43.1738,0 z" + id="path18" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 17.0684,1004.2465 0,1.5 33.4336,0 0,-1.5 -33.4336,0 z" + id="path20" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 17.0684,1008.4281 0,1.5 20.8789,0 0,-1.5 -20.8789,0 z" + id="path22" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 75.3047,978.06876 c -9.1577,0 -16.6953,7.57955 -16.6953,16.7793 0,9.29814 7.5435,16.77924 16.6953,16.77924 9.2502,0 16.6953,-7.4869 16.6953,-16.77924 0,-9.19389 -7.4393,-16.7793 -16.6953,-16.7793 z m 0,1.5 c 8.4378,0 15.1953,6.89296 15.1953,15.2793 0,8.49414 -6.7517,15.27924 -15.1953,15.27924 -8.3358,0 -15.1953,-6.7909 -15.1953,-15.27924 0,-8.38048 6.8654,-15.2793 15.1953,-15.2793 z" + id="path24" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 74.347656,985.49805 0,10.07812 1.5,0 0,-10.07812 -1.5,0 z" + id="path26" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 66.265625,991.66211 -0.460937,1.42773 9.535156,3.08204 0.46289,-1.42774 -9.537109,-3.08203 z" + id="path28" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 84.34375,991.66211 -9.537109,3.08203 0.46289,1.42774 9.535157,-3.08204 -0.460938,-1.42773 z" + id="path30" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 74.251953,995.07812 -5.863281,8.12308 1.216797,0.8769 5.863281,-8.12107 -1.216797,-0.87891 z" + id="path32" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#005544;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 76.179688,995.19727 -1.216797,0.8789 5.863281,8.12113 1.216797,-0.877 -5.863281,-8.12303 z" + id="path34" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_198591_cc.svg b/res/icons/noun_198591_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..240d78a522479c22aba69d7ea760e52921bda485 --- /dev/null +++ b/res/icons/noun_198591_cc.svg @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 99.999 125" + enable-background="new 0 0 99.999 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_198591_cc.svg"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview10" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="49.9995" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><path + d="m 86.731,35.862 8.268,-8.267 -10.59,-10.593 -8.284,8.283 C 60.558,14.231 39.493,14.253 23.946,25.353 L 15.595,17 5.001,27.59 13.369,35.959 C 2.337,51.494 2.336,72.504 13.366,88.041 l -8.367,8.367 10.594,10.59 8.351,-8.35 c 15.545,11.099 36.609,11.121 52.179,0.066 L 84.407,107 94.997,96.406 86.729,88.138 C 97.854,72.561 97.854,51.438 86.731,35.862 Z M 72.891,28.52 50.001,51.409 27.18,28.586 C 40.896,19.178 59.15,19.155 72.891,28.52 Z M 16.604,84.805 C 7.257,71.1 7.257,52.897 16.603,39.191 L 39.409,62 16.604,84.805 Z m 10.577,10.606 22.82,-22.819 22.889,22.89 c -13.742,9.367 -31.995,9.34 -45.709,-0.071 z M 83.496,84.905 60.593,62 83.498,39.096 c 9.431,13.75 9.431,32.06 -0.002,45.809 z" + id="path4" + inkscape:connector-curvature="0" + style="fill:#6c5353" /></svg> \ No newline at end of file diff --git a/res/icons/noun_213188_cc.svg b/res/icons/noun_213188_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..53c003da2ca2e6692cd6bd8603a1f920771763b9 --- /dev/null +++ b/res/icons/noun_213188_cc.svg @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_213188_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.2059793,0,0,1.2059793,-9.8622,43.847222)" + id="g4" + style="fill:#008000"> + <path + d="M 50,5 C 39.766783,5 31.46875,15.218727 31.46875,27.8125 c 0,12.6 8.298033,22.8125 18.53125,22.8125 10.233219,0 18.53125,-10.2125 18.53125,-22.8125 C 68.53125,15.218727 60.233219,5 50,5 Z M 31.375,53.78125 c -2.404152,-0.0062 -4.971172,0.888365 -7.46875,2.9375 -6.869896,5.624221 -11.602724,13.02221 -14.1875,21.21875 -2.416609,7.667128 1.916284,9.677963 8.40625,10.53125 5.319031,0.703805 8.871669,2.39866 13.3125,3.8125 C 39.758607,94.940767 44.824222,95 50,95 c 5.175779,0 10.241392,-0.05923 18.5625,-2.71875 4.440831,-1.41384 7.993469,-3.108695 13.3125,-3.8125 6.489965,-0.853287 10.822859,-2.864122 8.40625,-10.53125 -2.584775,-8.19654 -7.317603,-15.594529 -14.1875,-21.21875 -3.998616,-3.276125 -8.168037,-3.638063 -11.5625,-2 -5.088582,2.460207 -9.598378,3.75 -14.53125,3.75 -4.932871,0 -9.442669,-1.289793 -14.53125,-3.75 -1.270588,-0.610381 -2.648768,-0.931273 -4.09375,-0.9375 z" + transform="translate(0,-36)" + style="fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_213192_cc.svg b/res/icons/noun_213192_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..04550973a4da468a256d21108d0c754b1bb03b07 --- /dev/null +++ b/res/icons/noun_213192_cc.svg @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_213192_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="-50.900424" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.1000428,0,0,1.1000428,-4.4991194,48.745336)" + id="g4" + style="fill:#aa0000"> + <path + d="M 37.84375,11.46875 C 29.634879,11.46875 23,19.647734 23,29.75 c 0,10.108496 6.634879,18.3125 14.84375,18.3125 8.208869,0 14.875,-8.204004 14.875,-18.3125 0,-10.102266 -6.666131,-18.28125 -14.875,-18.28125 z M 62.1875,47.125 c -0.211762,0 -0.400565,0.08807 -0.5625,0.25 l -7.78125,7.75 c -0.32387,0.32387 -0.32387,0.838607 0,1.15625 L 65.375,67.84375 53.84375,79.375 c -0.32387,0.323871 -0.32387,0.83238 0,1.15625 l 7.78125,7.75 c 0.32387,0.323871 0.83238,0.323871 1.15625,0 L 74.3125,76.75 85.84375,88.28125 c 0.317641,0.323871 0.832379,0.323871 1.15625,0 l 7.75,-7.75 c 0.32387,-0.32387 0.32387,-0.832379 0,-1.15625 L 83.21875,67.84375 94.75,56.28125 c 0.32387,-0.317643 0.32387,-0.83238 0,-1.15625 L 87,47.375 c -0.323871,-0.323871 -0.838609,-0.323871 -1.15625,0 L 74.3125,58.90625 62.78125,47.375 c -0.161935,-0.161936 -0.381989,-0.25 -0.59375,-0.25 z m -39.28125,3.46875 c -1.930766,-0.0062 -4.000722,0.705714 -6,2.34375 -5.512025,4.509274 -9.3009835,10.460417 -11.375,17.03125 -1.9369944,6.15354 1.5493878,7.752389 6.75,8.4375 4.26637,0.560544 7.131153,1.922726 10.6875,3.0625 6.676713,2.130071 10.720738,2.1875 14.875,2.1875 3.936271,0 7.771178,-0.05634 13.84375,-1.875 -0.498263,-0.803446 -0.736565,-1.715998 -0.46875,-2.71875 2.958433,-4.241456 7.074882,-7.494239 10.625,-11.21875 C 59.564201,65.557972 57.27955,63.285776 55,61 53.59241,59.349505 51.156033,58.110192 51.09375,55.65625 51.486131,53.600919 53.11153,52.188947 54.625,50.8125 52.806345,50.388976 51.050842,50.596357 49.5,51.34375 c -4.079521,1.968136 -7.701294,3 -11.65625,3 -3.954957,0 -7.576728,-1.031864 -11.65625,-3 -1.015209,-0.498261 -2.122791,-0.75 -3.28125,-0.75 z" + transform="translate(0,-36)" + style="fill:#aa0000;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_213886_cc.svg b/res/icons/noun_213886_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..8abb8ccabd91d0670153095f50581d762f4a334e --- /dev/null +++ b/res/icons/noun_213886_cc.svg @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_213886_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.0756045,0,0,1.0756045,-4.3771943,50.028402)" + id="g4" + style="fill:#d45500"> + <path + d="M 37.84375,11.46875 C 29.634795,11.46875 23,19.672651 23,29.78125 c 0,10.10237 6.634795,18.28125 14.84375,18.28125 8.208953,0 14.875,-8.17888 14.875,-18.28125 0,-10.108599 -6.666047,-18.3125 -14.875,-18.3125 z M 71.28125,47.125 c -0.429757,0 -0.844508,0.257528 -1.0625,0.625 L 47.75,86.6875 c -0.217992,0.367472 -0.211765,0.845049 0,1.21875 0.211763,0.367472 0.632745,0.625 1.0625,0.625 l 44.96875,0 c 0.429756,0 0.850737,-0.257528 1.0625,-0.625 0.217992,-0.373701 0.217965,-0.851278 0,-1.21875 l -22.5,-38.9375 c -0.211765,-0.367472 -0.638974,-0.625 -1.0625,-0.625 z m -48.375,3.46875 c -1.930786,-0.0062 -4.000702,0.705696 -6,2.34375 -5.512083,4.515547 -9.3009621,10.460349 -11.375,17.03125 -1.9370144,6.147373 1.5493347,7.752384 6.75,8.4375 4.266414,0.560551 7.131117,1.922712 10.6875,3.0625 6.676783,2.130092 10.720696,2.1875 14.875,2.1875 2.846352,0 5.650126,-0.02118 9.28125,-0.71875 C 52.032933,73.981145 57.280324,65.206788 62.40625,56.375 61.297605,55.166703 60.076745,54.002546 58.78125,52.9375 55.573654,50.315369 52.221787,50.060819 49.5,51.375 c -4.079563,1.968156 -7.701253,2.96875 -11.65625,2.96875 -3.954997,0 -7.545436,-1.000594 -11.625,-2.96875 -1.015219,-0.492041 -2.154029,-0.775022 -3.3125,-0.78125 z m 48.375,6.1875 c 0.566779,0 1.107615,0.320354 1.5,0.78125 0.392385,0.460898 0.682137,1.090012 0.9375,1.8125 0.523179,1.438747 0.84375,3.313528 0.84375,5.40625 0,2.192376 -0.677317,5.026916 -1.34375,7.375 -0.666432,2.348085 -1.34375,4.1875 -1.34375,4.1875 -0.09342,0.26159 -0.344617,0.4375 -0.59375,0.4375 -0.242905,0 -0.469074,-0.17591 -0.5625,-0.4375 0,0 -0.683546,-1.839415 -1.34375,-4.1875 -0.666433,-2.348084 -1.34375,-5.182624 -1.34375,-7.375 0,-2.092722 0.295548,-3.967503 0.8125,-5.40625 0.26159,-0.722488 0.576365,-1.351602 0.96875,-1.8125 0.392385,-0.460896 0.901971,-0.78125 1.46875,-0.78125 z m 0,21.4375 c 1.575769,0 2.875,1.261751 2.875,2.84375 0,1.581999 -1.299231,2.875 -2.875,2.875 -1.575771,0 -2.84375,-1.293001 -2.84375,-2.875 0,-1.57577 1.267979,-2.84375 2.84375,-2.84375 z" + transform="translate(0,-36)" + style="fill:#d45500;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_269788_cc.svg b/res/icons/noun_269788_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..0e7ff36d7682e4c2bdadf8f6f70f6eac6371a664 --- /dev/null +++ b/res/icons/noun_269788_cc.svg @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 262.20472 184.25197" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_269788_cc.svg" + width="74mm" + height="52mm"><metadata + id="metadata22"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs20" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview18" + showgrid="false" + inkscape:zoom="0.6675088" + inkscape:cx="168.27594" + inkscape:cy="16.790099" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" + units="mm" /><rect + x="11.055084" + y="144.259" + width="34.501839" + height="34.501839" + id="rect4" + style="fill:#00aa00" /><rect + x="60.344528" + y="109.75682" + width="34.501839" + height="69.006248" + id="rect6" + style="fill:#00aa00" /><rect + x="109.63396" + y="75.254662" + width="34.501839" + height="103.5081" + id="rect8" + style="fill:#00aa00" /><rect + x="158.92342" + y="40.752491" + width="34.499264" + height="138.00993" + id="rect10" + style="fill:#00aa00" /><rect + x="208.21284" + y="6.2480106" + width="34.501839" + height="172.51178" + id="rect12" + style="fill:#00aa00" /></svg> \ No newline at end of file diff --git a/res/icons/noun_269792_cc.svg b/res/icons/noun_269792_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..47fce937268e6fbab30233a12f9651e271087642 --- /dev/null +++ b/res/icons/noun_269792_cc.svg @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 184.25197 131.10236" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_269792_cc.svg" + width="52mm" + height="37mm"><metadata + id="metadata22"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs20" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview18" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="-23.710046" + inkscape:cy="47.263014" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" + units="mm" /><rect + x="5.8220339" + y="102.44091" + width="24.950264" + height="24.950264" + id="rect4" + style="fill:#d45500" /><rect + x="41.466068" + y="77.490646" + width="24.950264" + height="49.90239" + id="rect6" + style="fill:#d45500" /><rect + x="77.110092" + y="52.540382" + width="24.950264" + height="74.852654" + id="rect8" + style="fill:#d45500" /><path + d="m 137.70439,127.39303 -24.95025,0 0,-99.802926 24.9484,0 0,99.802926 z m -21.22745,-3.72281 17.50278,0 0,-92.357307 -17.50278,0 0,92.357307 z" + id="path10" + inkscape:connector-curvature="0" + style="fill:#d45500" /><path + d="m 173.34842,127.39303 -24.95025,0 0,-124.7550516 24.95025,0 0,124.7550516 z m -21.22745,-3.72281 17.50465,0 0,-117.3094328 -17.50465,0 0,117.3094328 z" + id="path12" + inkscape:connector-curvature="0" + style="fill:#d45500" /></svg> \ No newline at end of file diff --git a/res/icons/noun_269793_cc.svg b/res/icons/noun_269793_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..7c9bf0c5329c13d5fd2759eb7808b68b0d9db078 --- /dev/null +++ b/res/icons/noun_269793_cc.svg @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 184.25197 131.10236" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_269793_cc.svg" + width="52mm" + height="37mm"><metadata + id="metadata22"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs20" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview18" + showgrid="false" + inkscape:zoom="0.118" + inkscape:cx="-2954.4935" + inkscape:cy="962.76781" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" + units="mm" /><path + d="m 28.934386,129.85912 -25.8157419,0 0,-25.81766 25.8157419,0 0,25.81766 z m -21.9637959,-3.85193 18.1118499,0 0,-18.11185 -18.1118499,0 0,18.11185 z" + id="path4" + inkscape:connector-curvature="0" + style="fill:#d40000" /><path + d="m 65.814843,129.85912 -25.815743,0 0,-51.633399 25.815743,0 0,51.633399 z m -21.963796,-3.85193 18.111849,0 0,-43.929529 -18.111849,0 0,43.929529 z" + id="path6" + inkscape:connector-curvature="0" + style="fill:#d40000" /><path + d="m 102.6953,129.85912 -25.815743,0 0,-77.449138 25.815743,0 0,77.449138 z m -21.963796,-3.85193 18.111849,0 0,-69.745263 -18.111849,0 0,69.745263 z" + id="path8" + inkscape:connector-curvature="0" + style="fill:#d40000" /><path + d="m 139.57576,129.85912 -25.81575,0 0,-103.26488 25.81382,0 0,103.26488 z m -21.9638,-3.85193 18.10992,0 0,-95.561003 -18.10992,0 0,95.561003 z" + id="path10" + inkscape:connector-curvature="0" + style="fill:#d40000" /><path + d="m 176.45621,129.85912 -25.81574,0 0,-129.08254796 25.81574,0 0,129.08254796 z m -21.96379,-3.85193 18.11185,0 0,-121.3786714 -18.11185,0 0,121.3786714 z" + id="path12" + inkscape:connector-curvature="0" + style="fill:#d40000" /></svg> \ No newline at end of file diff --git a/res/icons/noun_274635_cc.svg b/res/icons/noun_274635_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..9534a54837711a4617e296c830b799abf5258f15 --- /dev/null +++ b/res/icons/noun_274635_cc.svg @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 30 37.5" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_274635_cc.svg"> + <metadata + id="metadata22"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs20" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview18" + showgrid="false" + inkscape:zoom="6.2933333" + inkscape:cx="15" + inkscape:cy="18.75" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <title + id="title4">action_012-history-arrow-time-update</title> + <desc + id="desc6">Created with Sketch.</desc> + <g + sketch:type="MSPage" + id="g8" + transform="matrix(1.0647348,0,0,1.0647348,-1.8454517,4.1873838)" + style="fill:#008000;fill-rule:evenodd;stroke:none;stroke-width:1"> + <g + sketch:type="MSArtboardGroup" + transform="translate(-90,-90)" + id="g10" + style="fill:#008000"> + <path + d="m 114.3382,97.08445 -1.1269,0.94558 c -0.84382,0.708053 -0.64338,1.40872 0.45043,1.562694 l 3.69774,0.520526 c 0.54582,0.0768 0.99829,-0.305762 1.01729,-0.85361 l 0.12949,-3.73195 c 0.0384,-1.106118 -0.61569,-1.424029 -1.46073,-0.714951 l -1.20361,1.009943 c -4.79252,-5.711508 -13.30772,-6.456492 -19.019228,-1.663968 -5.711508,4.792525 -6.456492,13.307726 -1.663968,19.019236 4.792525,5.7115 13.307726,6.45649 19.019236,1.66396 1.98848,-1.66854 3.43633,-3.84334 4.20465,-6.29948 0.16183,-0.51734 -0.12637,-1.06791 -0.6437,-1.22974 -0.51734,-0.16183 -1.06791,0.12636 -1.22974,0.6437 -0.65647,2.09858 -1.89232,3.95478 -3.59298,5.38181 -4.88103,4.09567 -12.15808,3.45901 -16.253749,-1.42202 -4.095669,-4.88103 -3.459009,-12.15808 1.422019,-16.253749 4.88103,-4.095669 12.15808,-3.459009 16.25375,1.422019 z m -14.780852,3.78713 -0.700482,0.70049 c -0.197092,0.19709 -0.195891,0.51784 0.0035,0.71724 l 6.350314,6.35031 c 0.19903,0.19903 0.52358,0.19718 0.71424,0.007 l 4.94913,-4.94913 c 0.19543,-0.19543 0.20252,-0.50521 -0.003,-0.71073 l -0.70049,-0.70048 c -0.19709,-0.19709 -0.518,-0.19573 -0.70405,-0.01 l -3.89545,3.89545 -5.303,-5.30299 c -0.19543,-0.19543 -0.505203,-0.20252 -0.710723,0.003 z" + sketch:type="MSShapeGroup" + id="path12" + inkscape:connector-curvature="0" + style="fill:#008000" /> + </g> + </g> +</svg> diff --git a/res/icons/noun_60040_cc.svg b/res/icons/noun_60040_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..65b0400d994538da3e9a552c3a406cab6c1e5779 --- /dev/null +++ b/res/icons/noun_60040_cc.svg @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_60040_cc.svg"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview10" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="18.19375" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><path + d="M 99.87499,38.500263 C 99.87499,31.625118 94.249868,26 87.374724,26 c -4.625098,0 -8.625182,2.500053 -10.875229,6.250132 l -53.251121,0 C 21.123329,28.500053 16.998242,26 12.373145,26 5.4979998,26 -0.127119,31.625118 -0.127119,38.500263 c 0,6.875145 5.6251188,12.500263 12.500264,12.500263 4.625097,0 8.625181,-2.500052 10.875229,-6.250131 l 53.376123,0 c 2.125045,3.750079 6.250132,6.250131 10.875229,6.250131 6.750142,0 12.375264,-5.625118 12.375264,-12.500263 z m -87.501845,7.500158 c -4.125087,0 -7.5001584,-3.375071 -7.5001584,-7.500158 0,-4.125087 3.3750714,-7.500158 7.5001584,-7.500158 4.125086,0 7.500158,3.375071 7.500158,7.500158 0,4.125087 -3.375072,7.500158 -7.500159,7.500158 z m 75.001579,0 c -4.125087,0 -7.500158,-3.375071 -7.500158,-7.500158 0,-4.125087 3.375071,-7.500158 7.500158,-7.500158 4.125087,0 7.500158,3.375071 7.500158,7.500158 0,4.125087 -3.375071,7.500158 -7.500158,7.500158 z m 0,5.000105 -13.125277,22.750479 6.875145,0 0,2.250048 c 0,1.875039 -0.625013,6.250131 -6.250132,6.250131 l -51.626086,0 c -2.125045,-3.750079 -6.250132,-6.250131 -10.875229,-6.250131 -6.8751452,0 -12.500264,5.625118 -12.500264,12.500263 0,6.875145 5.6251188,12.500264 12.500264,12.500264 4.625097,0 8.625181,-2.500054 10.875229,-6.250132 l 51.626086,0 c 13.75029,0 18.750395,-11.250237 18.750395,-18.750395 l 0,-2.250048 6.875145,0 L 87.374724,51.000526 Z M 12.373145,96.001474 c -4.125087,0 -7.5001584,-3.375071 -7.5001584,-7.500158 0,-4.125087 3.3750714,-7.500158 7.5001584,-7.500158 4.125086,0 7.500158,3.375071 7.500158,7.500158 0,4.125087 -3.375072,7.500158 -7.500159,7.500158 z" + id="path4" + inkscape:connector-curvature="0" + style="fill:#d45500" /></svg> \ No newline at end of file diff --git a/res/icons/noun_87601_cc.svg b/res/icons/noun_87601_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..21ae9b4b0944f8557e359cca97cea1bb2983b89d --- /dev/null +++ b/res/icons/noun_87601_cc.svg @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_87601_cc.svg"><metadata + id="metadata16"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs14" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><path + d="m 0.83531061,26.711164 c 0,4.97582 1.74826129,9.682677 5.11030139,13.448161 l 0,15.599867 c 0,5.513746 4.572376,10.086122 10.086122,10.086122 l 6.993044,0 c 1.479297,13.179198 12.775753,23.534282 26.492877,23.534282 13.717126,0 25.01358,-10.355084 26.62736,-23.534282 l 6.993044,0 c 1.882743,0 3.36204,1.479297 3.36204,3.36204 l 0,15.465385 c -3.227559,3.765486 -5.110301,8.472342 -5.110301,13.448166 l 0,24.879095 16.810202,0 0,-24.74462 c 0,-4.975817 -1.748261,-9.682673 -5.110301,-13.448158 l 0,-15.599868 c 0,-5.513746 -4.572375,-10.086121 -10.086121,-10.086121 l -6.993045,0 C 74.531235,45.942035 63.234781,35.58695 49.517655,35.58695 c -13.717124,0 -25.01358,10.355085 -26.62736,23.534283 l -6.993044,0 c -1.882742,0 -3.36204,-1.479297 -3.36204,-3.362041 l 0,-15.465385 c 3.227559,-3.765485 5.110302,-8.472342 5.110302,-13.448162 l 0,-24.8790979 -16.81020239,0 0,24.7446169 z M 88.248361,98.25538 c 0,-2.420666 0.537926,-4.572372 1.748261,-6.589597 1.075853,2.017225 1.74826,4.303412 1.74826,6.589597 l 0,7.93442 -3.36204,0 0,-7.93442 z m 0,14.6585 3.36204,0 0,3.36204 -3.36204,0 0,-3.36204 z M 49.517655,42.311031 c 11.161975,0 20.172242,9.010268 20.172242,20.172243 0,11.161973 -9.010267,20.172242 -20.172242,20.172242 -11.161973,0 -20.172242,-9.010269 -20.172242,-20.172242 0,-11.161975 9.010269,-20.172243 20.172242,-20.172243 z M 7.5593909,8.6906274 l 3.3620411,0 0,3.3620406 -3.3620411,0 0,-3.3620406 z m 0,10.0861206 3.3620411,0 0,7.934416 c 0,2.420669 -0.537927,4.572375 -1.748261,6.589599 C 8.097318,31.283539 7.4249098,28.997351 7.4249098,26.711164 l 0,-7.934416 z" + id="path4" + inkscape:connector-curvature="0" + style="fill:#d40000" /><polygon + points="46.5,50 41.2,55.3 44.7,58.8 50,53.5 55.3,58.8 58.8,55.3 53.5,50 58.8,44.7 55.3,41.2 50,46.5 44.7,41.2 41.2,44.7 " + id="polygon6" + transform="matrix(1.3448162,0,0,1.3448162,-17.723152,-4.7575344)" + style="fill:#d40000" /></svg> \ No newline at end of file diff --git a/res/ui/certification.ui b/res/ui/certification.ui index fda8aba450a1ca89b154570dfdceb5113173aeb4..b65ffecfa69f9f79bc2af2230537110e13fcff5e 100644 --- a/res/ui/certification.ui +++ b/res/ui/certification.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>399</width> - <height>216</height> + <width>715</width> + <height>477</height> </rect> </property> <property name="windowTitle"> @@ -16,6 +16,12 @@ <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QGroupBox" name="groupBox_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="title"> <string>Community</string> </property> @@ -31,55 +37,161 @@ <property name="title"> <string>Certify user</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> + <layout class="QHBoxLayout" name="horizontalLayout_5"> <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="topMargin"> + <number>6</number> + </property> <item> - <widget class="QRadioButton" name="radio_contact"> - <property name="text"> - <string>Contact</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QRadioButton" name="radio_contact"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Con&tact</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QComboBox" name="combo_contact"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> </item> <item> - <widget class="QComboBox" name="combo_contact"> - <property name="enabled"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QRadioButton" name="radio_pubkey"> - <property name="text"> - <string>User public key</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QRadioButton" name="radio_pubkey"> + <property name="text"> + <string>&User public key</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLineEdit" name="edit_pubkey"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="inputMask"> + <string/> + </property> + <property name="text"> + <string/> + </property> + <property name="placeholderText"> + <string>Key</string> + </property> + </widget> + </item> + </layout> </item> <item> - <widget class="QLineEdit" name="edit_pubkey"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="inputMask"> - <string/> - </property> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Key</string> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="topMargin"> + <number>6</number> </property> - </widget> + <item> + <widget class="QRadioButton" name="radio_search"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>S&earch user</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="SearchUserWidget" name="search_user" native="true"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> </item> </layout> </item> @@ -98,73 +210,16 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>SearchUserWidget</class> + <extends>QWidget</extends> + <header>sakia.gui.widgets.search_user</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> - <connections> - <connection> - <sender>button_box</sender> - <signal>accepted()</signal> - <receiver>CertificationDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_box</sender> - <signal>rejected()</signal> - <receiver>CertificationDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>radio_pubkey</sender> - <signal>toggled(bool)</signal> - <receiver>CertificationDialog</receiver> - <slot>recipient_mode_changed(bool)</slot> - <hints> - <hint type="sourcelabel"> - <x>87</x> - <y>51</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>244</y> - </hint> - </hints> - </connection> - <connection> - <sender>combo_community</sender> - <signal>currentIndexChanged(int)</signal> - <receiver>CertificationDialog</receiver> - <slot>change_current_community(int)</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>50</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>165</y> - </hint> - </hints> - </connection> - </connections> + <connections/> <slots> <slot>open_manage_wallet_coins()</slot> <slot>change_displayed_wallet(int)</slot> diff --git a/res/ui/identities_tab.ui b/res/ui/identities_tab.ui index 5f3f2e072c5778a32cbea7c6f2bfc1b77a0750c5..0dab4a2d41ed7e07fe2da897b2dc8bad20aa7496 100644 --- a/res/ui/identities_tab.ui +++ b/res/ui/identities_tab.ui @@ -63,8 +63,19 @@ </attribute> </widget> </item> + <item> + <widget class="Busy" name="busy" native="true"/> + </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>Busy</class> + <extends>QWidget</extends> + <header>sakia.gui.widgets.busy</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> <connections/> </ui> diff --git a/res/ui/member.ui b/res/ui/member.ui index ca9b92019f0ad59dbb5e57fa2061ac2c2c5ef38b..23bb9d1769f46c84eca8ca91a172ae5612de9dce 100644 --- a/res/ui/member.ui +++ b/res/ui/member.ui @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>DialogMember</class> - <widget class="QDialog" name="DialogMember"> + <class>MemberView</class> + <widget class="QWidget" name="MemberView"> <property name="geometry"> <rect> <x>0</x> <y>0</y> - <width>501</width> - <height>288</height> + <width>392</width> + <height>251</height> </rect> </property> <property name="windowTitle"> - <string>Informations</string> + <string>Member informations</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> @@ -34,8 +34,14 @@ QGroupBox::title { <string>Member</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> + <item row="0" column="1"> <widget class="QLabel" name="label_icon"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="maximumSize"> <size> <width>81</width> @@ -53,7 +59,17 @@ QGroupBox::title { </property> </widget> </item> - <item row="0" column="1"> + <item row="2" column="1" colspan="2"> + <widget class="QLabel" name="label_properties"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="0" column="2"> <widget class="QLabel" name="label_uid"> <property name="maximumSize"> <size> @@ -62,17 +78,14 @@ QGroupBox::title { </size> </property> <property name="text"> - <string>uid</string> + <string/> </property> </widget> </item> - <item row="1" column="0" colspan="2"> - <widget class="QLabel" name="label_properties"> + <item row="3" column="1" colspan="2"> + <widget class="QLabel" name="label_path"> <property name="text"> - <string>properties</string> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + <string/> </property> </widget> </item> @@ -83,7 +96,6 @@ QGroupBox::title { </widget> <resources> <include location="../icons/icons.qrc"/> - <include location="../icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/res/ui/transactions_tab.ui b/res/ui/transactions_tab.ui index f32e7ebae129d4805b8fd1d023e548617972a9bb..737ecfa4aa93d0e094da40eed917ae4b71e16457 100644 --- a/res/ui/transactions_tab.ui +++ b/res/ui/transactions_tab.ui @@ -37,6 +37,9 @@ </property> </widget> </item> + <item> + <widget class="Busy" name="busy_balance" native="true"/> + </item> </layout> </widget> </item> @@ -105,61 +108,16 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>Busy</class> + <extends>QWidget</extends> + <header>sakia.gui.widgets</header> + <container>1</container> + </customwidget> + </customwidgets> <resources> <include location="../icons/icons.qrc"/> </resources> - <connections> - <connection> - <sender>table_history</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>history_context_menu()</slot> - <hints> - <hint type="sourcelabel"> - <x>273</x> - <y>183</y> - </hint> - <hint type="destinationlabel"> - <x>830</x> - <y>802</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_from</sender> - <signal>dateChanged(QDate)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>102</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_to</sender> - <signal>dateChanged(QDate)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>297</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>history_context_menu()</slot> - <slot>dates_changed()</slot> - </slots> + <connections/> </ui> diff --git a/res/ui/transfer.ui b/res/ui/transfer.ui index d258290a8cd86addfc12749be43ec20cf359f106..64837315bef1d008698f420be49e8bcd95fc2b09 100644 --- a/res/ui/transfer.ui +++ b/res/ui/transfer.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>496</width> - <height>440</height> + <height>485</height> </rect> </property> <property name="windowTitle"> @@ -36,6 +36,12 @@ <layout class="QHBoxLayout" name="horizontalLayout_2"> <item> <widget class="QRadioButton" name="radio_contact"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> <string>Con&tact</string> </property> @@ -44,11 +50,33 @@ </property> </widget> </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> <item> <widget class="QComboBox" name="combo_contact"> <property name="enabled"> <bool>true</bool> </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> </widget> </item> </layout> @@ -57,6 +85,12 @@ <layout class="QHBoxLayout" name="horizontalLayout"> <item> <widget class="QRadioButton" name="radio_pubkey"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> <string>&Recipient public key</string> </property> @@ -65,11 +99,33 @@ </property> </widget> </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> <item> <widget class="QLineEdit" name="edit_pubkey"> <property name="enabled"> <bool>false</bool> </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="inputMask"> <string/> </property> @@ -83,6 +139,52 @@ </item> </layout> </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <widget class="QRadioButton" name="radio_search"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>S&earch user</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="SearchUserWidget" name="search_user" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> </item> @@ -188,121 +290,16 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>SearchUserWidget</class> + <extends>QWidget</extends> + <header>sakia.gui.widgets.search_user</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> - <connections> - <connection> - <sender>button_box</sender> - <signal>accepted()</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>248</x> - <y>254</y> - </hint> - <hint type="destinationlabel"> - <x>157</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_box</sender> - <signal>rejected()</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>316</x> - <y>260</y> - </hint> - <hint type="destinationlabel"> - <x>286</x> - <y>274</y> - </hint> - </hints> - </connection> - <connection> - <sender>combo_wallets</sender> - <signal>currentIndexChanged(int)</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>change_displayed_wallet(int)</slot> - <hints> - <hint type="sourcelabel"> - <x>82</x> - <y>264</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>244</y> - </hint> - </hints> - </connection> - <connection> - <sender>radio_pubkey</sender> - <signal>toggled(bool)</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>recipient_mode_changed(bool)</slot> - <hints> - <hint type="sourcelabel"> - <x>87</x> - <y>51</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>244</y> - </hint> - </hints> - </connection> - <connection> - <sender>combo_community</sender> - <signal>currentIndexChanged(int)</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>change_current_community(int)</slot> - <hints> - <hint type="sourcelabel"> - <x>199</x> - <y>50</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>165</y> - </hint> - </hints> - </connection> - <connection> - <sender>spinbox_relative</sender> - <signal>valueChanged(double)</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>relative_amount_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>320</x> - <y>269</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>165</y> - </hint> - </hints> - </connection> - <connection> - <sender>spinbox_amount</sender> - <signal>valueChanged(double)</signal> - <receiver>TransferMoneyDialog</receiver> - <slot>amount_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>209</x> - <y>292</y> - </hint> - <hint type="destinationlabel"> - <x>247</x> - <y>219</y> - </hint> - </hints> - </connection> - </connections> + <connections/> <slots> <slot>open_manage_wallet_coins()</slot> <slot>change_displayed_wallet(int)</slot> diff --git a/setup.py b/setup.py index dca0a8308b46987df54a3e7daafef65b5270428c..4cdfe1a466d7d481e36be932054b2f88af6d086c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ import sys, os, subprocess, multiprocessing, site -from cx_Freeze import setup, Executable from PyQt5 import QtCore from os import listdir from os.path import isfile, join @@ -8,11 +7,19 @@ import unittest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))) if "test" in sys.argv: + print(sys.path) + if "XDG_CONFIG_HOME" in os.environ: + os.environ["XDG_CONFIG_HOME"] = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tmp')) + elif "HOME" in os.environ: + os.environ["HOME"] = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tmp')) + elif "APPDATA" in os.environ: + os.environ["APPDATA"] = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tmp')) runner = unittest.TextTestRunner().run(unittest.defaultTestLoader.discover(start_dir='sakia.tests', pattern='test_*')) sys.exit(not runner.wasSuccessful()) else: + from cx_Freeze import setup, Executable print(sys.path) print("Environnement:") print(os.environ) @@ -117,7 +124,7 @@ else: setup( name = "sakia", - version = "0.11.4", + version = "0.12.0", description = "UCoin client", author = "Inso", options = {"build_exe": options}, diff --git a/src/sakia/__init__.py b/src/sakia/__init__.py index e92e1772fccf93ff2627cba943fed2e5091cbfbf..d4c85e473175c7b0fccd54e9748b783ab229ab3d 100644 --- a/src/sakia/__init__.py +++ b/src/sakia/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = ('0', '11', '3') +__version_info__ = ('0', '12', '0') __version__ = '.'.join(__version_info__) diff --git a/src/sakia/core/account.py b/src/sakia/core/account.py index 15a20055ba1bb70056c84f9ee35471792a811ee7..e713d64a4966c596c3d67068395eece17d7bff00 100644 --- a/src/sakia/core/account.py +++ b/src/sakia/core/account.py @@ -11,6 +11,7 @@ from ucoinpy.key import SigningKey import logging import time import asyncio +from distutils.version import StrictVersion from PyQt5.QtCore import QObject, pyqtSignal @@ -18,7 +19,8 @@ from . import money from .wallet import Wallet from .community import Community from .registry import LocalState -from ..tools.exceptions import ContactAlreadyExists, NoPeerAvailable +from ..tools.exceptions import ContactAlreadyExists +from .. import __version__ from ucoinpy.api import bma from ucoinpy.api.bma import PROTOCOL_VERSION from aiohttp.errors import ClientError @@ -61,6 +63,19 @@ class Account(QObject): self._identities_registry = identities_registry self._current_ref = 0 + self.notifications = {'membership_expire_soon': + [ + self.tr("Warning : Your membership is expiring soon."), + 0 + ], + 'warning_certifications': + [ + self.tr("Warning : Your could miss certifications soon."), + 0 + ], + 'warning_certifying_first_time': True, + } + @classmethod def create(cls, name, identities_registry): """ @@ -89,6 +104,10 @@ class Account(QObject): """ salt = json_data['salt'] pubkey = json_data['pubkey'] + if 'file_version' in json_data: + file_version = StrictVersion(json_data['file_version']) + else: + file_version = StrictVersion('0.11.5') name = json_data['name'] contacts = [] @@ -102,7 +121,7 @@ class Account(QObject): communities = [] for data in json_data['communities']: - community = Community.load(data) + community = Community.load(data, file_version) communities.append(community) account = cls(salt, pubkey, name, communities, wallets, @@ -497,7 +516,7 @@ class Account(QObject): await r.release() return result else: - return (False, self.tr("Could not find user self certification.")) + return False, self.tr("Could not find user self certification.") async def revoke(self, password, community): """ @@ -564,5 +583,6 @@ class Account(QObject): 'pubkey': self.pubkey, 'communities': data_communities, 'wallets': data_wallets, - 'contacts': self.contacts} + 'contacts': self.contacts, + 'file_version': __version__} return data diff --git a/src/sakia/core/app.py b/src/sakia/core/app.py index 9aa906e29e5fbf21d78d9756f6e8abbd9ba018ef..a5ac832eaa100403a5d86b70ffba408cf184dced 100644 --- a/src/sakia/core/app.py +++ b/src/sakia/core/app.py @@ -10,17 +10,15 @@ import tarfile import shutil import json import datetime -import asyncio import aiohttp -import time +from distutils.version import StrictVersion -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, \ -QUrl, QTranslator, QCoreApplication, QLocale +from PyQt5.QtCore import QObject, pyqtSignal, QTranslator, QCoreApplication, QLocale from ucoinpy.api.bma import API from aiohttp.connector import ProxyConnector from . import config from .account import Account -from .registry.identities import IdentitiesRegistry +from .registry import IdentitiesRegistry, Identity from .. import __version__ from ..tools.exceptions import NameAlreadyExists, BadAccountFile from ..tools.decorators import asyncify @@ -36,6 +34,7 @@ class Application(QObject): """ version_requested = pyqtSignal() + view_identity_in_wot = pyqtSignal(Identity) def __init__(self, qapp, loop, identities_registry): """ @@ -71,18 +70,6 @@ class Application(QObject): 'auto_refresh': False } - self.notifications = {'membership_expire_soon': - [ - self.tr("Warning : Your membership is expiring soon."), - 0 - ], - 'warning_certifications': - [ - self.tr("Warning : Your could miss certifications soon."), - 0 - ] - } - @classmethod def startup(cls, argv, qapp, loop): config.parse_arguments(argv) @@ -204,6 +191,7 @@ class Application(QObject): and stop the coroutines """ self.save_cache(self.current_account) + self.save_notifications(self.current_account) self.current_account.stop_coroutines() def load(self): @@ -261,6 +249,17 @@ class Application(QObject): account.rollback_transaction(self, co)) community.network.root_nodes_changed.connect(lambda acc=account: self.save(acc)) + account_notifications_path = os.path.join(config.parameters['home'], + account_name, '__notifications__') + + try: + with open(account_notifications_path, 'r') as json_data: + data = json.load(json_data) + account.notifications = data + except FileNotFoundError: + logging.debug("Could not find notifications file") + pass + def load_cache(self, account): """ Load an account cache @@ -280,7 +279,7 @@ class Application(QObject): with open(network_path, 'r') as json_data: data = json.load(json_data) logging.debug("Merging network : {0}".format(data)) - community.network.merge_with_json(data['network']) + community.network.merge_with_json(data['network'], StrictVersion(data['version'])) if os.path.exists(bma_path): with open(bma_path, 'r') as json_data: @@ -350,6 +349,18 @@ class Application(QObject): account_path = os.path.join(config.parameters['home'], account.name) shutil.rmtree(account_path) + def save_notifications(self, account): + """ + Save an account notifications + + :param account: The account object to save + """ + account_path = os.path.join(config.parameters['home'], + account.name) + notifications_path = os.path.join(account_path, '__notifications__') + with open(notifications_path, 'w') as outfile: + json.dump(account.notifications, outfile, indent=4, sort_keys=True) + def save_registries(self): """ Save the registries @@ -524,3 +535,5 @@ class Application(QObject): self.version_requested.emit() except (aiohttp.errors.ClientError, aiohttp.errors.TimeoutError) as e: logging.debug("Could not connect to github : {0}".format(str(e))) + except Exception as e: + pass diff --git a/src/sakia/core/community.py b/src/sakia/core/community.py index bc97eddeb913a1d9a7976a7e4c9772b4ef674204..cbc86b64e31cc79f449a7aa8ecd38924a411ccc6 100644 --- a/src/sakia/core/community.py +++ b/src/sakia/core/community.py @@ -57,28 +57,19 @@ class Community(QObject): return community @classmethod - def load(cls, json_data): + def load(cls, json_data, file_version): """ Load a community from json :param dict json_data: The community as a dict in json format + :param distutils.version.StrictVersion file_version: the file sakia version """ currency = json_data['currency'] - network = Network.from_json(currency, json_data['peers']) + network = Network.from_json(currency, json_data['peers'], file_version) bma_access = BmaAccess.create(network) community = cls(currency, network, bma_access) return community - def load_cache(self, bma_access_cache, network_cache): - """ - Load the community cache. - - :param dict bma_access_cache: The BmaAccess cache in json - :param dict network_cache: The network cache in json - """ - self._bma_access.load_from_json(bma_access_cache) - self._network.merge_with_json(network_cache) - @property def name(self): """ @@ -116,13 +107,16 @@ class Community(QObject): u = ord('\u24B6') + ord(letter) - ord('A') return chr(u) - async def dividend(self): + async def dividend(self, block_number=None): """ - Get the last generated community universal dividend. + Get the last generated community universal dividend before block_number. + If block_number is None, returns the last block_number. + + :param int block_number: The block at which we get the latest dividend :return: The last UD or 1 if no UD was generated. """ - block = await self.get_ud_block() + block = await self.get_ud_block(block_number=block_number) if block: return block['dividend'] else: @@ -152,18 +146,24 @@ class Community(QObject): else: return 1 - async def get_ud_block(self, x=0): + async def get_ud_block(self, x=0, block_number=None): """ Get a block with universal dividend + If x and block_number are passed to the result, + it returns the 'x' older block with UD in it BEFORE block_number :param int x: Get the 'x' older block with UD in it + :param int block_number: Get the latest dividend before this block number :return: The last block with universal dividend. + :rtype: dict """ try: udblocks = await self.bma_access.future_request(bma.blockchain.UD) blocks = udblocks['result']['blocks'] + if block_number: + blocks = [b for b in blocks if b <= block_number] if len(blocks) > 0: - index = len(blocks)-(1+x) + index = len(blocks) - (1+x) if index < 0: index = 0 block_number = blocks[index] diff --git a/src/sakia/core/money/__init__.py b/src/sakia/core/money/__init__.py index 05a4b506a6ff4f03c503992f6cae11774a6a4ab1..21b04a8f501cfb26ba65419a6e3590b5d54bb331 100644 --- a/src/sakia/core/money/__init__.py +++ b/src/sakia/core/money/__init__.py @@ -2,5 +2,6 @@ from .quantitative import Quantitative from .relative import Relative from .quant_zerosum import QuantitativeZSum from .relative_zerosum import RelativeZSum +from .relative_to_past import RelativeToPast -Referentials = (Quantitative, Relative, QuantitativeZSum, RelativeZSum) +Referentials = (Quantitative, Relative, QuantitativeZSum, RelativeZSum, RelativeToPast) diff --git a/src/sakia/core/money/base_referential.py b/src/sakia/core/money/base_referential.py new file mode 100644 index 0000000000000000000000000000000000000000..4040074b7695f74c10cd3d464da2cb4fbe3bed37 --- /dev/null +++ b/src/sakia/core/money/base_referential.py @@ -0,0 +1,41 @@ +from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QObject, QLocale +import asyncio + + +class BaseReferential: + """ + Interface to all referentials + """ + def __init__(self, amount, community, app, block_number=None): + self.amount = amount + self.community = community + self.app = app + self._block_number = block_number + + @classmethod + def translated_name(self): + pass + + @property + def units(self): + pass + + @property + def diff_units(self): + pass + + async def value(self): + pass + + async def differential(self): + pass + + @staticmethod + def to_si(value, digits): + pass + + async def localized(self, units=False, international_system=False): + pass + + async def diff_localized(self, units=False, international_system=False): + pass diff --git a/src/sakia/core/money/quant_zerosum.py b/src/sakia/core/money/quant_zerosum.py index 2c63322514e1d69c2952869965da43c8d61ab421..98f1f1c26569db4de65c5c729e314d31d4b5ef9e 100644 --- a/src/sakia/core/money/quant_zerosum.py +++ b/src/sakia/core/money/quant_zerosum.py @@ -1,29 +1,27 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from . import Quantitative -import asyncio +from .base_referential import BaseReferential -class QuantitativeZSum: +class QuantitativeZSum(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', 'Quant Z-sum') - _REF_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "{0} Q0 {1}") + _REF_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "{0} {1}Q0 {2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', "Q0 {0}") - def __init__(self, amount, community, app): - self.amount = amount - self.community = community - self.app = app + def __init__(self, amount, community, app, block_number=None): + super().__init__(amount, community, app, block_number) @classmethod def translated_name(cls): return QCoreApplication.translate('QuantitativeZSum', QuantitativeZSum._NAME_STR_) - @classmethod - def units(cls, currency): - return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._UNITS_STR_).format(currency) + @property + def units(self): + return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._UNITS_STR_).format(self.community.short_currency) - @classmethod - def diff_units(cls, currency): - return Quantitative.units(currency) + @property + def diff_units(self): + return self.units async def value(self): """ @@ -43,7 +41,7 @@ class QuantitativeZSum: ud_block = await self.community.get_ud_block() if ud_block and ud_block['membersCount'] > 0: monetary_mass = await self.community.monetary_mass() - average = monetary_mass / ud_block['membersCount'] + average = int(monetary_mass / ud_block['membersCount']) else: average = 0 return self.amount - average @@ -60,10 +58,11 @@ class QuantitativeZSum: else: localized_value = QLocale().toString(float(value), 'f', 0) - if units: + if units or international_system: return QCoreApplication.translate("QuantitativeZSum", QuantitativeZSum._REF_STR_) \ .format(localized_value, + prefix, self.community.short_currency if units else "") else: return localized_value diff --git a/src/sakia/core/money/quantitative.py b/src/sakia/core/money/quantitative.py index 811ff8f0c9cd2473410576f7d513f5ce176373ca..d1d07c151059c58ed1336fcf0da2b981c5b6666e 100644 --- a/src/sakia/core/money/quantitative.py +++ b/src/sakia/core/money/quantitative.py @@ -1,28 +1,26 @@ -from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QObject, QLocale -import asyncio +from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale +from .base_referential import BaseReferential -class Quantitative(): +class Quantitative(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('Quantitative', 'Units') _REF_STR_ = QT_TRANSLATE_NOOP('Quantitative', "{0} {1}{2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('Quantitative', "{0}") - def __init__(self, amount, community, app): - self.amount = amount - self.community = community - self.app = app + def __init__(self, amount, community, app, block_number=None): + super().__init__(amount, community, app, block_number) @classmethod def translated_name(cls): return QCoreApplication.translate('Quantitative', Quantitative._NAME_STR_) - @classmethod - def units(cls, currency): - return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(currency) + @property + def units(self): + return QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_).format(self.community.short_currency) - @classmethod - def diff_units(cls, currency): - return Quantitative.units(currency) + @property + def diff_units(self): + return self.units async def value(self): """ diff --git a/src/sakia/core/money/relative.py b/src/sakia/core/money/relative.py index 29b7d3f0d001d87a8d75b1341df748b0adff35d7..a757b762a1166bd1081c8773e6bd7d2dfecc1285 100644 --- a/src/sakia/core/money/relative.py +++ b/src/sakia/core/money/relative.py @@ -1,28 +1,26 @@ from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale -import asyncio +from .base_referential import BaseReferential -class Relative: +class Relative(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('Relative', 'UD') _REF_STR_ = QT_TRANSLATE_NOOP('Relative', "{0} {1}UD {2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('Relative', "UD {0}") - def __init__(self, amount, community, app): - self.amount = amount - self.community = community - self.app = app + def __init__(self, amount, community, app, block_number=None): + super().__init__(amount, community, app, block_number) @classmethod def translated_name(cls): return QCoreApplication.translate('Relative', Relative._NAME_STR_) - @classmethod - def units(self, currency): - return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(currency) + @property + def units(self): + return QCoreApplication.translate("Relative", Relative._UNITS_STR_).format(self.community.short_currency) - @classmethod - def diff_units(self, currency): - return self.units(currency) + @property + def diff_units(self): + return self.units async def value(self): """ @@ -42,7 +40,7 @@ class Relative: @staticmethod def to_si(value, digits): - prefixes = ['', 'd', 'c', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'] + prefixes = ['', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'] if value < 0: value = -value multiplier = -1 @@ -53,10 +51,7 @@ class Relative: prefix = "" while int(scientific_value) == 0 and scientific_value > 0.0: - if prefix_index > 3: - scientific_value *= 1000 - else: - scientific_value *= 10 + scientific_value *= 1000 prefix_index += 1 if prefix_index < len(prefixes): diff --git a/src/sakia/core/money/relative_to_past.py b/src/sakia/core/money/relative_to_past.py new file mode 100644 index 0000000000000000000000000000000000000000..8fdae53c34b54ec1aa3b136ac2219936b945494c --- /dev/null +++ b/src/sakia/core/money/relative_to_past.py @@ -0,0 +1,91 @@ +from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale, QDateTime +from .base_referential import BaseReferential +from . import Relative + + +class RelativeToPast(BaseReferential): + _NAME_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', 'Past UD') + _REF_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', "{0} {1}UD({2}) {3}") + _UNITS_STR_ = QT_TRANSLATE_NOOP('RelativeToPast', "UD({0}) {1}") + + def __init__(self, amount, community, app, block_number=None): + super().__init__(amount, community, app, block_number) + + @classmethod + def translated_name(cls): + return QCoreApplication.translate('RelativeToPast', RelativeToPast._NAME_STR_) + + @property + def units(self): + return QCoreApplication.translate("RelativeToPast", RelativeToPast._UNITS_STR_).format('t', + self.community.short_currency) + + @property + def diff_units(self): + return self.units + + async def value(self): + """ + Return relative to past value of amount + :return: float + """ + dividend = await self.community.dividend() + if dividend > 0: + return self.amount / float(dividend) + else: + return self.amount + + async def differential(self): + """ + Return relative to past differential value of amount + :return: float + """ + dividend = await self.community.dividend(self._block_number) + if dividend > 0: + return self.amount / float(dividend) + else: + return self.amount + + async def localized(self, units=False, international_system=False): + value = await self.value() + block = await self.community.get_block() + prefix = "" + if international_system: + localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) + else: + localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) + + if units or international_system: + return QCoreApplication.translate("RelativeToPast", RelativeToPast._REF_STR_) \ + .format(localized_value, + prefix, + QLocale.toString( + QLocale(), + QDateTime.fromTime_t(block['medianTime']).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ), + self.community.short_currency if units else "") + else: + return localized_value + + async def diff_localized(self, units=False, international_system=False): + value = await self.differential() + block = await self.community.get_block(self._block_number) + prefix = "" + if international_system and value != 0: + localized_value, prefix = Relative.to_si(value, self.app.preferences['digits_after_comma']) + else: + localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) + + if units or international_system: + return QCoreApplication.translate("RelativeToPast", RelativeToPast._REF_STR_)\ + .format(localized_value, + prefix, + QLocale.toString( + QLocale(), + QDateTime.fromTime_t(block['medianTime']).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ), + self.community.short_currency if units else "") + else: + return localized_value diff --git a/src/sakia/core/money/relative_zerosum.py b/src/sakia/core/money/relative_zerosum.py index 5a6b823985ec04ade40327397fffe1337c2df790..797f7cb4d80fa78622c2b76b9a909b8702267b79 100644 --- a/src/sakia/core/money/relative_zerosum.py +++ b/src/sakia/core/money/relative_zerosum.py @@ -1,29 +1,27 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from .relative import Relative -import asyncio +from .base_referential import BaseReferential -class RelativeZSum: +class RelativeZSum(BaseReferential): _NAME_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', 'Relat Z-sum') - _REF_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "{0} R0 {1}") + _REF_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "{0} {1}R0 {2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', "R0 {0}") - def __init__(self, amount, community, app): - self.amount = amount - self.community = community - self.app = app + def __init__(self, amount, community, app, block_number=None): + super().__init__(amount, community, app, block_number) @classmethod def translated_name(cls): return QCoreApplication.translate('RelativeZSum', RelativeZSum._NAME_STR_) - @classmethod - def units(cls, currency): - return QCoreApplication.translate("RelativeZSum", RelativeZSum._UNITS_STR_).format(currency) + @property + def units(self): + return QCoreApplication.translate("RelativeZSum", RelativeZSum._UNITS_STR_).format(self.community.short_currency) - @classmethod - def diff_units(cls, currency): - return Relative.units(currency) + @property + def diff_units(self): + return self.units async def value(self): """ @@ -41,7 +39,7 @@ class RelativeZSum: :return: float """ ud_block = await self.community.get_ud_block() - ud_block_minus_1 = await self.community.get_ud_block(1) + ud_block_minus_1 = await self.community.get_ud_block(x=1) if ud_block_minus_1 and ud_block['membersCount'] > 0: median = ud_block_minus_1['monetaryMass'] / ud_block['membersCount'] relative_value = self.amount / float(ud_block['dividend']) @@ -63,9 +61,10 @@ class RelativeZSum: else: localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - if units: + if units or international_system: return QCoreApplication.translate("RelativeZSum", RelativeZSum._REF_STR_)\ .format(localized_value, + prefix, self.community.short_currency if units else "") else: return localized_value @@ -79,8 +78,8 @@ class RelativeZSum: else: localized_value = QLocale().toString(float(value), 'f', self.app.preferences['digits_after_comma']) - if units: + if units or international_system: return QCoreApplication.translate("RelativeZSum", RelativeZSum._REF_STR_)\ - .format(localized_value, self.community.short_currency if units else "") + .format(localized_value, prefix, self.community.short_currency if units else "") else: return localized_value diff --git a/src/sakia/core/net/api/bma/access.py b/src/sakia/core/net/api/bma/access.py index f6d645e0654bb49e83a9d06ca12512a7def01d16..409380126cbf5cada8504519fe0f96f41277bb20 100644 --- a/src/sakia/core/net/api/bma/access.py +++ b/src/sakia/core/net/api/bma/access.py @@ -1,5 +1,4 @@ from PyQt5.QtCore import QObject, pyqtSlot -from PyQt5.QtNetwork import QNetworkReply from ucoinpy.api import bma from .....tools.exceptions import NoPeerAvailable from ..... import __version__ @@ -259,7 +258,7 @@ class BmaAccess(QObject): :param class request: A bma request class calling for data :param dict req_args: Arguments to pass to the request constructor :param dict get_args: Arguments to pass to the request __get__ method - :return: The returned data if cached = True else return the QNetworkReply + :return: The returned data """ nodes = self.filter_nodes(request, self._network.synced_nodes) if len(nodes) > 0: @@ -293,12 +292,14 @@ class BmaAccess(QObject): :param req_args: Arguments to pass to the request constructor :param post_args: Arguments to pass to the request __post__ method :return: All nodes replies - :rtype: tuple of QNetworkReply + :rtype: tuple of aiohttp replies .. note:: If one node accept the requests (returns 200), the broadcast should be considered accepted by the network. """ - nodes = self._network.online_nodes + nodes = random.sample(self._network.synced_nodes, 6) \ + if len(self._network.synced_nodes) > 6 \ + else self._network.synced_nodes replies = [] if len(nodes) > 0: for node in nodes: diff --git a/src/sakia/core/net/network.py b/src/sakia/core/net/network.py index eeb9d3a50c4b70420f53898ad0091e188d4028c8..4ba45bff087dddbe73a8d167f7854003cf43e1d9 100644 --- a/src/sakia/core/net/network.py +++ b/src/sakia/core/net/network.py @@ -57,15 +57,16 @@ class Network(QObject): network = cls(node.currency, nodes) return network - def merge_with_json(self, json_data): + def merge_with_json(self, json_data, file_version): """ We merge with knew nodes when we last stopped sakia :param dict json_data: Nodes in json format + :param distutils.version.StrictVersion file_version: The node version """ for data in json_data: - node = Node.from_json(self.currency, data) + node = Node.from_json(self.currency, data, file_version) if node.pubkey not in [n.pubkey for n in self.nodes]: self.add_node(node) logging.debug("Loading : {:}".format(data['pubkey'])) @@ -74,6 +75,7 @@ class Network(QObject): other_node._uid = node.uid other_node._version = node.version other_node._software = node.software + other_node._peer = node.peer switch = False if other_node.block and node.block: if other_node.block['hash'] != node.block['hash']: @@ -86,16 +88,17 @@ class Network(QObject): other_node.state = node.state @classmethod - def from_json(cls, currency, json_data): + def from_json(cls, currency, json_data, file_version): """ Load a network from a configured community :param str currency: The currency name of a community :param dict json_data: A json_data view of a network + :param distutils.version.StrictVersion file_version: the version of the json file """ nodes = [] for data in json_data: - node = Node.from_json(currency, data) + node = Node.from_json(currency, data, file_version) nodes.append(node) network = cls(currency, nodes) return network @@ -340,6 +343,10 @@ class Network(QObject): self.nodes_changed.emit() except InvalidNodeCurrency as e: logging.debug(str(e)) + else: + node = [n for n in self.nodes if n.pubkey == pubkey][0] + if BlockId.from_str(node.peer.blockid).number < BlockId.from_str(peer.blockid).number: + node.peer = peer @pyqtSlot() def handle_identity_change(self): diff --git a/src/sakia/core/net/node.py b/src/sakia/core/net/node.py index 80983ec5f922fb9f65dc92f984c235a622947509..37fdce2275e2cc1378d1cb5626b5dd9c44f24099 100644 --- a/src/sakia/core/net/node.py +++ b/src/sakia/core/net/node.py @@ -5,6 +5,7 @@ Created on 21 févr. 2015 """ from ucoinpy.documents.peer import Peer, Endpoint, BMAEndpoint +from ucoinpy.documents import Block, BlockId from ...tools.exceptions import InvalidNodeCurrency from ...tools.decorators import asyncify from ucoinpy.api import bma as bma @@ -12,11 +13,14 @@ from ucoinpy.api.bma import ConnectionHandler from aiohttp.errors import ClientError, DisconnectedError, TimeoutError, \ WSClientDisconnectedError, WSServerHandshakeError, ClientResponseError +from aiohttp.errors import ClientError, DisconnectedError +from asyncio import TimeoutError import logging import time import jsonschema import asyncio import aiohttp +from distutils.version import StrictVersion from socket import gaierror from PyQt5.QtCore import QObject, pyqtSignal @@ -42,26 +46,27 @@ class Node(QObject): identity_changed = pyqtSignal() neighbour_found = pyqtSignal(Peer, str) - def __init__(self, currency, endpoints, uid, pubkey, block, + def __init__(self, peer, uid, pubkey, block, state, last_change, last_merkle, software, version, fork_window): """ Constructor """ super().__init__() - self._endpoints = endpoints + self._peer = peer self._uid = uid self._pubkey = pubkey self._block = block self.main_chain_previous_block = None self._state = state self._neighbours = [] - self._currency = currency self._last_change = last_change self._last_merkle = last_merkle self._software = software self._version = version self._fork_window = fork_window self._refresh_counter = 0 + self._ws_opened = {'block': False, + 'peer': False} @classmethod async def from_address(cls, currency, address, port): @@ -84,8 +89,7 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, - [Endpoint.from_inline(e.inline()) for e in peer.endpoints], + node = cls(peer, "", peer.pubkey, None, Node.ONLINE, time.time(), {'root': "", 'leaves': []}, "", "", 0) logging.debug("Node from address : {:}".format(str(node))) @@ -106,8 +110,7 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, peer.endpoints, - "", pubkey, None, + node = cls(peer, "", pubkey, None, Node.OFFLINE, time.time(), {'root': "", 'leaves': []}, "", "", 0) @@ -115,7 +118,16 @@ class Node(QObject): return node @classmethod - def from_json(cls, currency, data): + def from_json(cls, currency, data, file_version): + """ + Loads a node from json data + + :param str currency: the currency of the community + :param dict data: the json data of the node + :param StrictVersion file_version: the version of the file + :return: A new node + :rtype: Node + """ endpoints = [] uid = "" pubkey = "" @@ -126,12 +138,6 @@ class Node(QObject): last_change = time.time() state = Node.OFFLINE logging.debug(data) - for endpoint_data in data['endpoints']: - endpoints.append(Endpoint.from_inline(endpoint_data)) - - if currency in data: - currency = data['currency'] - if 'uid' in data: uid = data['uid'] @@ -156,11 +162,23 @@ class Node(QObject): if 'fork_window' in data: fork_window = data['fork_window'] - node = cls(currency, endpoints, - uid, pubkey, block, + if file_version < StrictVersion("0.12"): + for endpoint_data in data['endpoints']: + endpoints.append(Endpoint.from_inline(endpoint_data)) + + if currency in data: + currency = data['currency'] + + peer = Peer("1", currency, pubkey, str(BlockId(0, Block.Empty_Hash)), endpoints, "SOMEFAKESIGNATURE") + else: + if 'peer' in data: + peer = Peer.from_signed_raw(data['peer']) + + node = cls(peer, uid, pubkey, block, state, last_change, {'root': "", 'leaves': []}, software, version, fork_window) + logging.debug("Node from json : {:}".format(str(node))) return node @@ -168,18 +186,14 @@ class Node(QObject): logging.debug("Saving root node : {:}".format(str(self))) data = {'pubkey': self._pubkey, 'uid': self._uid, - 'currency': self._currency} - endpoints = [] - for e in self._endpoints: - endpoints.append(e.inline()) - data['endpoints'] = endpoints + 'peer': self._peer.signed_raw()} return data def jsonify(self): logging.debug("Saving node : {:}".format(str(self))) data = {'pubkey': self._pubkey, 'uid': self._uid, - 'currency': self._currency, + 'peer': self._peer.signed_raw(), 'state': self._state, 'last_change': self._last_change, 'block': self.block, @@ -187,10 +201,6 @@ class Node(QObject): 'version': self._version, 'fork_window': self._fork_window } - endpoints = [] - for e in self._endpoints: - endpoints.append(e.inline()) - data['endpoints'] = endpoints return data @property @@ -199,7 +209,7 @@ class Node(QObject): @property def endpoint(self) -> BMAEndpoint: - return next((e for e in self._endpoints if type(e) is BMAEndpoint)) + return next((e for e in self._peer.endpoints if type(e) is BMAEndpoint)) @property def block(self): @@ -214,7 +224,7 @@ class Node(QObject): @property def currency(self): - return self._currency + return self._peer.currency @property def neighbours(self): @@ -232,6 +242,16 @@ class Node(QObject): def software(self): return self._software + @property + def peer(self): + return self._peer + + @peer.setter + def peer(self, new_peer): + if self._peer != new_peer: + self._peer = new_peer + self.changed.emit() + @software.setter def software(self, new_soft): if self._software != new_soft: @@ -260,8 +280,6 @@ class Node(QObject): # self.state, new_state)) if self._state != new_state: - if self.pubkey[:5] in ("6YfbK", "J78bP"): - pass self.last_change = time.time() self._state = new_state self.changed.emit() @@ -283,7 +301,7 @@ class Node(QObject): Refresh all data of this node :param bool manual: True if the refresh was manually initiated """ - asyncio.ensure_future(self.connect_current_block()) + self.connect_current_block() self.refresh_peers() if self._refresh_counter % 20 == 0 or manual: @@ -294,62 +312,41 @@ class Node(QObject): else: self._refresh_counter += 1 - async def connect_current_block(self): - try: - conn_handler = self.endpoint.conn_handler() - async with bma.websocket.Block(conn_handler).connect() as ws: - async for msg in ws: - if msg.tp == aiohttp.MsgType.text: - pass - elif msg.tp == aiohttp.MsgType.closed: - break - elif msg.tp == aiohttp.MsgType.error: - break - else: - pass - except (WSServerHandshakeError, WSClientDisconnectedError) as e: - logging.debug("Websocket error : {0}".format(str(e))) - except ClientResponseError as e: - logging.debug("Client response error : {0}".format(str(e))) - - @asyncify - async def refresh_block(self): + async def connect_current_block(self): """ - Refresh the blocks of this node + Connects to the websocket entry point of the node + If the connection fails, it tries the fallback mode on HTTP GET + """ + if not self._ws_opened['block']: + try: + conn_handler = self.endpoint.conn_handler() + self._ws_opened['block'] = True + block_websocket = bma.websocket.Block(conn_handler) + async with block_websocket.connect() as ws: + async for msg in ws: + if msg.tp == aiohttp.MsgType.text: + block_data = block_websocket.parse(msg.data) + await self.refresh_block(block_data) + elif msg.tp == aiohttp.MsgType.closed: + break + elif msg.tp == aiohttp.MsgType.error: + break + except (WSServerHandshakeError, WSClientDisconnectedError, ClientResponseError) as e: + logging.debug("Websocket error : {0}".format(str(e))) + self.request_current_block() + finally: + self._ws_opened['block'] = False + + async def request_current_block(self): + """ + Request a node on the HTTP GET interface + If an error occurs, the node is considered offline """ - conn_handler = self.endpoint.conn_handler() - - logging.debug("Requesting {0}".format(conn_handler)) try: + conn_handler = self.endpoint.conn_handler() block_data = await bma.blockchain.Current(conn_handler).get() - block_hash = block_data['hash'] - self.state = Node.ONLINE - - if not self.block or block_hash != self.block['hash']: - try: - if self.block: - self.main_chain_previous_block = await bma.blockchain.Block(conn_handler, - self.block['number']).get() - except ValueError as e: - if '404' in str(e): - self.main_chain_previous_block = None - else: - self.state = Node.OFFLINE - logging.debug("Error in previous block reply : {0}".format(self.pubkey)) - logging.debug(str(e)) - self.changed.emit() - except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) - self.state = Node.OFFLINE - except jsonschema.ValidationError: - logging.debug("Validation error : {0}".format(self.pubkey)) - self.state = Node.CORRUPTED - finally: - self.set_block(block_data) - logging.debug("Changed block {0} -> {1}".format(self.block['number'], - block_data['number'])) - self.changed.emit() + await self.refresh_block(block_data) except ValueError as e: if '404' in str(e): self.main_chain_previous_block = None @@ -366,6 +363,42 @@ class Node(QObject): logging.debug("Validation error : {0}".format(self.pubkey)) self.state = Node.CORRUPTED + async def refresh_block(self, block_data): + """ + Refresh the blocks of this node + :param dict block_data: The block data in json format + """ + conn_handler = self.endpoint.conn_handler() + + logging.debug("Requesting {0}".format(conn_handler)) + block_hash = block_data['hash'] + self.state = Node.ONLINE + + if not self.block or block_hash != self.block['hash']: + try: + if self.block: + self.main_chain_previous_block = await bma.blockchain.Block(conn_handler, + self.block['number']).get() + except ValueError as e: + if '404' in str(e): + self.main_chain_previous_block = None + else: + self.state = Node.OFFLINE + logging.debug("Error in previous block reply : {0}".format(self.pubkey)) + logging.debug(str(e)) + self.changed.emit() + except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: + logging.debug("{0} : {1}".format(str(e), self.pubkey)) + self.state = Node.OFFLINE + except jsonschema.ValidationError: + logging.debug("Validation error : {0}".format(self.pubkey)) + self.state = Node.CORRUPTED + finally: + self.set_block(block_data) + logging.debug("Changed block {0} -> {1}".format(self.block['number'], + block_data['number'])) + self.changed.emit() + @asyncify async def refresh_informations(self): """ @@ -379,6 +412,11 @@ class Node(QObject): node_currency = peering_data["currency"] self.state = Node.ONLINE + if peering_data['raw'] != self.peer.raw(): + peer = Peer.from_signed_raw("{0}{1}\n".format(peering_data['raw'], peering_data['signature'])) + if BlockId.from_str(peer.blockid).number > BlockId.from_str(self.peer.blockid).number: + self.peer = Peer.from_signed_raw("{0}{1}\n".format(peering_data['raw'], peering_data['signature'])) + if node_pubkey != self.pubkey: self._pubkey = node_pubkey self.identity_changed.emit() @@ -393,7 +431,7 @@ class Node(QObject): self.state = Node.OFFLINE self.changed.emit() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) + logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) self.state = Node.OFFLINE except jsonschema.ValidationError: logging.debug("Validation error : {0}".format(self.pubkey)) @@ -420,7 +458,7 @@ class Node(QObject): self.state = Node.OFFLINE self.changed.emit() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) + logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) self.state = Node.OFFLINE except jsonschema.ValidationError: logging.debug("Validation error : {0}".format(self.pubkey)) @@ -455,7 +493,7 @@ class Node(QObject): self.state = Node.OFFLINE self.identity_changed.emit() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) + logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) self.state = Node.OFFLINE except jsonschema.ValidationError: logging.debug("Validation error : {0}".format(self.pubkey)) @@ -491,7 +529,7 @@ class Node(QObject): self.state = Node.OFFLINE self.changed.emit() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) + logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) self.state = Node.OFFLINE except jsonschema.ValidationError: logging.debug("Validation error : {0}".format(self.pubkey)) @@ -503,7 +541,7 @@ class Node(QObject): self.state = Node.OFFLINE self.changed.emit() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.pubkey)) + logging.debug("{0} : {1}".format(type(e).__name__, self.pubkey)) self.state = Node.OFFLINE except jsonschema.ValidationError: logging.debug("Validation error : {0}".format(self.pubkey)) diff --git a/src/sakia/core/registry/identities.py b/src/sakia/core/registry/identities.py index bb2608e906e0e2277c22e80da3320d5d031e1d13..8ef736f2ead3abbbc9caff83b9c21c6f1f494651 100644 --- a/src/sakia/core/registry/identities.py +++ b/src/sakia/core/registry/identities.py @@ -1,4 +1,3 @@ -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from ucoinpy.api import bma from .identity import Identity, LocalState, BlockchainState diff --git a/src/sakia/core/transfer.py b/src/sakia/core/transfer.py index 53d130111371bfb547180b3d6cf434365f569d5f..f86bb625ed444ed3671f5ef16eee2f9124dd3b9d 100644 --- a/src/sakia/core/transfer.py +++ b/src/sakia/core/transfer.py @@ -352,5 +352,16 @@ class Transfer(QObject): else: await r.text() self.run_state_transitions(([r.status for r in responses], block_doc)) - self.run_state_transitions(([r.status for r in responses])) + self.run_state_transitions(([r.status for r in responses], )) return result + + async def get_raw_document(self, community): + """ + Get the raw documents of this transfer + """ + block = await community.get_block(self.blockid.number) + if block: + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + for tx in block_doc.transactions: + if tx.sha_hash == self.sha_hash: + return tx diff --git a/src/sakia/gui/certification.py b/src/sakia/gui/certification.py index da2bbfba295e91475f53d1a45dcc6270f6b5abe8..4b847b300bf28b50e7a4356b38cfd87730d779a1 100644 --- a/src/sakia/gui/certification.py +++ b/src/sakia/gui/certification.py @@ -6,89 +6,174 @@ Created on 24 dec. 2014 import asyncio import logging -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QApplication +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QApplication, QMessageBox -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QObject from ..gen_resources.certification_uic import Ui_CertificationDialog -from sakia.gui.widgets import toast -from sakia.gui.widgets.dialogs import QAsyncMessageBox +from .widgets import toast +from .widgets.dialogs import QAsyncMessageBox +from .member import MemberDialog from ..tools.decorators import asyncify, once_at_a_time from ..tools.exceptions import NoPeerAvailable -class CertificationDialog(QDialog, Ui_CertificationDialog): +class CertificationDialog(QObject): """ - classdocs + A dialog to certify individuals """ - def __init__(self, app, certifier, password_asker): + def __init__(self, app, account, password_asker, widget, ui): """ - Constructor + Constructor if a certification dialog + + :param sakia.core.Application app: + :param sakia.core.Account account: + :param sakia.gui.password_asker.PasswordAsker password_asker: + :param PyQt5.QtWidgets widget: the widget of the dialog + :param sakia.gen_resources.certification_uic.Ui_CertificationDialog view: the view of the certification dialog + :return: """ super().__init__() - self.setupUi(self) + self.widget = widget + self.ui = ui + self.ui.setupUi(self.widget) self.app = app - self.account = certifier + self.account = account self.password_asker = password_asker self.community = self.account.communities[0] + self.ui.radio_contact.toggled.connect(lambda c, radio="contact": self.recipient_mode_changed(radio)) + self.ui.radio_pubkey.toggled.connect(lambda c, radio="pubkey": self.recipient_mode_changed(radio)) + self.ui.radio_search.toggled.connect(lambda c, radio="search": self.recipient_mode_changed(radio)) + self.ui.button_box.accepted.connect(self.accept) + self.ui.button_box.rejected.connect(self.widget.reject) + for community in self.account.communities: - self.combo_community.addItem(community.currency) + self.ui.combo_community.addItem(community.currency) + + for contact_name in sorted([c['name'] for c in account.contacts], key=str.lower): + self.ui.combo_contact.addItem(contact_name) + + if len(account.contacts) == 0: + self.ui.radio_pubkey.setChecked(True) + self.ui.radio_contact.setEnabled(False) + + self.ui.member_widget = MemberDialog.as_widget(self.ui.groupBox, self.app, self.account, self.community, None) + self.ui.horizontalLayout_5.addWidget(self.ui.member_widget.widget) + + self.ui.search_user.button_reset.hide() + self.ui.search_user.init(self.app) + self.ui.search_user.change_account(self.account) + self.ui.search_user.change_community(self.community) + self.ui.combo_contact.currentIndexChanged.connect(self.refresh_member) + self.ui.edit_pubkey.textChanged.connect(self.refresh_member) + self.ui.search_user.identity_selected.connect(self.refresh_member) + self.ui.radio_contact.toggled.connect(self.refresh_member) + self.ui.radio_search.toggled.connect(self.refresh_member) + self.ui.radio_pubkey.toggled.connect(self.refresh_member) + self.ui.combo_community.currentIndexChanged.connect(self.change_current_community) - for contact_name in sorted([c['name'] for c in certifier.contacts], key=str.lower): - self.combo_contact.addItem(contact_name) - if len(certifier.contacts) == 0: - self.radio_pubkey.setChecked(True) - self.radio_contact.setEnabled(False) + @classmethod + def open_dialog(cls, app, account, password_asker): + """ + Certify and identity + :param sakia.core.Application app: the application + :param sakia.core.Account account: the account certifying the identity + :param sakia.gui.password_asker.PasswordAsker password_asker: the password asker + :return: + """ + dialog = cls(app, account, password_asker, QDialog(), Ui_CertificationDialog()) + return dialog.exec() @classmethod async def certify_identity(cls, app, account, password_asker, community, identity): - dialog = cls(app, account, password_asker) - dialog.combo_community.setCurrentText(community.name) - dialog.edit_pubkey.setText(identity.pubkey) - dialog.radio_pubkey.setChecked(True) - return (await dialog.async_exec()) + """ + Certify and identity + :param sakia.core.Application app: the application + :param sakia.core.Account account: the account certifying the identity + :param sakia.gui.password_asker.PasswordAsker password_asker: the password asker + :param sakia.core.Community community: the community + :param sakia.core.registry.Identity identity: the identity certified + :return: + """ + dialog = cls(app, account, password_asker, QDialog(), Ui_CertificationDialog()) + dialog.ui.combo_community.setCurrentText(community.name) + dialog.ui.edit_pubkey.setText(identity.pubkey) + dialog.ui.radio_pubkey.setChecked(True) + return await dialog.async_exec() @asyncify async def accept(self): - if self.radio_contact.isChecked(): - for contact in self.account.contacts: - if contact['name'] == self.combo_contact.currentText(): - pubkey = contact['pubkey'] - break - else: - pubkey = self.edit_pubkey.text() - - password = await self.password_asker.async_exec() - if password == "": - return - QApplication.setOverrideCursor(Qt.WaitCursor) - result = await self.account.certify(password, self.community, pubkey) - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Certification"), - self.tr("Success sending certification")) - else: - await QAsyncMessageBox.information(self, self.tr("Certification"), - self.tr("Success sending certification")) - QApplication.restoreOverrideCursor() - super().accept() - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Certification"), self.tr("Could not broadcast certification : {0}" - .format(result[1]))) + """ + Validate the dialog + """ + pubkey = self.selected_pubkey() + if pubkey: + password = await self.password_asker.async_exec() + if password == "": + self.ui.button_box.setEnabled(True) + return + QApplication.setOverrideCursor(Qt.WaitCursor) + result = await self.account.certify(password, self.community, pubkey) + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("Certification"), + self.tr("Success sending certification")) + else: + await QAsyncMessageBox.information(self.widget, self.tr("Certification"), + self.tr("Success sending certification")) + QApplication.restoreOverrideCursor() + self.widget.accept() else: - await QAsyncMessageBox.critical(self, self.tr("Certification"), - self.tr("Could not broadcast certification : {0}" - .format(result[1]))) - QApplication.restoreOverrideCursor() + if self.app.preferences['notifications']: + toast.display(self.tr("Certification"), self.tr("Could not broadcast certification : {0}" + .format(result[1]))) + else: + await QAsyncMessageBox.critical(self.widget, self.tr("Certification"), + self.tr("Could not broadcast certification : {0}" + .format(result[1]))) + QApplication.restoreOverrideCursor() + self.ui.button_box.setEnabled(True) def change_current_community(self, index): self.community = self.account.communities[index] + self.ui.search_user.change_community(self.community) if self.isVisible(): self.refresh() + def selected_pubkey(self): + """ + Get selected pubkey in the widgets of the window + :return: the current pubkey + :rtype: str + """ + pubkey = None + if self.ui.radio_contact.isChecked(): + for contact in self.account.contacts: + if contact['name'] == self.ui.combo_contact.currentText(): + pubkey = contact['pubkey'] + break + elif self.ui.radio_search.isChecked(): + if self.ui.search_user.current_identity(): + pubkey = self.ui.search_user.current_identity().pubkey + else: + pubkey = self.ui.edit_pubkey.text() + return pubkey + + @asyncify + async def refresh_member(self, checked=False): + """ + Refresh the member widget + """ + current_pubkey = self.selected_pubkey() + if current_pubkey: + identity = await self.app.identities_registry.future_find(current_pubkey, self.community) + else: + identity = None + self.ui.member_widget.identity = identity + self.ui.member_widget.refresh() + @once_at_a_time @asyncify async def refresh(self): @@ -104,19 +189,39 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): block_0 = None if is_member or not block_0: - self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) - self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("&Ok")) + self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(True) + self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("&Ok")) else: - self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) - self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Not a member")) - - def recipient_mode_changed(self, pubkey_toggled): - self.edit_pubkey.setEnabled(pubkey_toggled) - self.combo_contact.setEnabled(not pubkey_toggled) + self.ui.button_box.button(QDialogButtonBox.Ok).setEnabled(False) + self.ui.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Not a member")) + + def showEvent(self, event): + super().showEvent(event) + self.first_certification_check() + + def first_certification_check(self): + if self.account.notifications['warning_certifying_first_time']: + self.account.notifications['warning_certifying_first_time'] = False + QMessageBox.warning(self, "Certifying individuals", """Please follow the following guidelines : +1.) Don't certify an account if you believe the issuers identity might be faked. +2.) Don't certify an account if you believe the issuer already has another certified account. +3.) Don't certify an account if you believe the issuer purposely or carelessly violates rule 1 or 2 (the issuer certifies faked or double accounts +""") + + def recipient_mode_changed(self, radio): + """ + :param str radio: + """ + self.ui.edit_pubkey.setEnabled(radio == "pubkey") + self.ui.combo_contact.setEnabled(radio == "contact") + self.ui.search_user.setEnabled(radio == "search") def async_exec(self): future = asyncio.Future() - self.finished.connect(lambda r: future.set_result(r)) - self.open() + self.widget.finished.connect(lambda r: future.set_result(r)) + self.widget.open() self.refresh() return future + + def exec(self): + self.widget.exec() diff --git a/src/sakia/gui/certifications_tab.py b/src/sakia/gui/certifications_tab.py deleted file mode 100644 index 5690f71ccbbbd62248e288a7545892e47e953275..0000000000000000000000000000000000000000 --- a/src/sakia/gui/certifications_tab.py +++ /dev/null @@ -1,272 +0,0 @@ -import logging -import asyncio - -from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView, QDialog, \ - QMenu, QAction, QApplication, QMessageBox -from PyQt5.QtCore import Qt, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent - -from PyQt5.QtGui import QCursor - -from ..gen_resources.certifications_tab_uic import Ui_certificationsTabWidget -from ..models.certifications import HistoryTableModel, CertsFilterProxyModel -from .contact import ConfigureContactDialog -from .member import MemberDialog -from .certification import CertificationDialog -from ..core.wallet import Wallet -from ..core.registry import Identity -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from .transfer import TransferMoneyDialog -from sakia.gui.widgets import toast -from sakia.gui.widgets.busy import Busy - - -class CertificationsTabWidget(QWidget, Ui_certificationsTabWidget): - """ - classdocs - """ - view_in_wot = pyqtSignal(Identity) - - def __init__(self, app): - """ - Init - - :param sakia.core.app.Application app: Application instance - :return: - """ - - super().__init__() - self.setupUi(self) - self.app = app - self.account = None - self.community = None - self.password_asker = None - self.busy_resume = Busy(self.groupbox_balance) - self.busy_resume.hide() - - ts_from = self.date_from.dateTime().toTime_t() - ts_to = self.date_to.dateTime().toTime_t() - - model = HistoryTableModel(self.app, self.account, self.community) - proxy = CertsFilterProxyModel(ts_from, ts_to) - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setSortRole(Qt.DisplayRole) - - self.table_history.setModel(proxy) - self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_history.setSortingEnabled(True) - self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.table_history.resizeColumnsToContents() - - model.modelAboutToBeReset.connect(lambda: self.table_history.setEnabled(False)) - model.modelReset.connect(lambda: self.table_history.setEnabled(True)) - - self.progressbar.hide() - self.refresh() - - def cancel_once_tasks(self): - cancel_once_task(self, self.refresh_minimum_maximum) - cancel_once_task(self, self.refresh_resume) - cancel_once_task(self, self.history_context_menu) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - self.account = account - self.password_asker = password_asker - self.table_history.model().sourceModel().change_account(account) - if account: - self.connect_progress() - - def change_community(self, community): - self.cancel_once_tasks() - self.community = community - self.progressbar.hide() - self.table_history.model().sourceModel().change_community(self.community) - self.refresh() - - @once_at_a_time - @asyncify - async def refresh_minimum_maximum(self): - try: - block = await self.community.get_block(1) - minimum_datetime = QDateTime() - minimum_datetime.setTime_t(block['medianTime']) - minimum_datetime.setTime(QTime(0, 0)) - - self.date_from.setMinimumDateTime(minimum_datetime) - self.date_from.setDateTime(minimum_datetime) - self.date_from.setMaximumDateTime(QDateTime().currentDateTime()) - - self.date_to.setMinimumDateTime(minimum_datetime) - tomorrow_datetime = QDateTime().currentDateTime().addDays(1) - self.date_to.setDateTime(tomorrow_datetime) - self.date_to.setMaximumDateTime(tomorrow_datetime) - except NoPeerAvailable as e: - logging.debug(str(e)) - except ValueError as e: - logging.debug(str(e)) - - def refresh(self): - if self.community: - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() - self.refresh_minimum_maximum() - self.refresh_resume() - - def connect_progress(self): - def progressing(community, value, maximum): - if community == self.community: - self.progressbar.show() - self.progressbar.setValue(value) - self.progressbar.setMaximum(maximum) - self.account.loading_progressed.connect(progressing) - self.account.loading_finished.connect(self.stop_progress) - - def stop_progress(self, community, received_list): - if community == self.community: - self.progressbar.hide() - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() - self.notification_reception(received_list) - - @asyncify - @asyncio.coroutine - def notification_reception(self, received_list, sent_list): - if len(received_list) > 0: - text = self.tr("Received {nb_received} ; Sent {nb_sent}").format(nb_received=len(received_list) , - nb_sent=len(sent_list)) - if self.app.preferences['notifications']: - toast.display(self.tr("New certifications"), text) - - @once_at_a_time - @asyncify - async def refresh_resume(self): - self.busy_resume.show() - self.busy_resume.hide() - - @once_at_a_time - @asyncify - async def history_context_menu(self, point): - index = self.table_history.indexAt(point) - model = self.table_history.model() - if index.row() < model.rowCount(QModelIndex()): - menu = QMenu(self.tr("Actions"), self) - source_index = model.mapToSource(index) - state_col = model.sourceModel().columns_types.index('state') - state_index = model.sourceModel().index(source_index.row(), - state_col) - state_data = model.sourceModel().data(state_index, Qt.DisplayRole) - - pubkey_col = model.sourceModel().columns_types.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - identity = await self.app.identities_registry.future_find(pubkey, self.community) - - if isinstance(identity, Identity): - informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.menu_informations) - informations.setData(identity) - menu.addAction(informations) - - add_as_contact = QAction(self.tr("Add as contact"), self) - add_as_contact.triggered.connect(self.menu_add_as_contact) - add_as_contact.setData(identity) - menu.addAction(add_as_contact) - - send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.menu_send_money) - send_money.setData(identity) - menu.addAction(send_money) - - if isinstance(identity, Identity): - view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.view_wot) - view_wot.setData(identity) - menu.addAction(view_wot) - - copy_pubkey = QAction(self.tr("Copy pubkey to clipboard"), self) - copy_pubkey.triggered.connect(self.copy_pubkey_to_clipboard) - copy_pubkey.setData(identity) - menu.addAction(copy_pubkey) - - # Show the context menu. - menu.popup(QCursor.pos()) - - def copy_pubkey_to_clipboard(self): - data = self.sender().data() - clipboard = QApplication.clipboard() - if data.__class__ is Wallet: - clipboard.setText(data.pubkey) - elif data.__class__ is Identity: - clipboard.setText(data.pubkey) - elif data.__class__ is str: - clipboard.setText(data) - - def menu_informations(self): - person = self.sender().data() - self.identity_informations(person) - - def menu_add_as_contact(self): - person = self.sender().data() - self.add_identity_as_contact({'name': person.uid, - 'pubkey': person.pubkey}) - - def menu_send_money(self): - identity = self.sender().data() - self.send_money_to_identity(identity) - - def identity_informations(self, person): - dialog = MemberDialog(self.app, self.account, self.community, person) - dialog.exec_() - - def add_identity_as_contact(self, person): - dialog = ConfigureContactDialog(self.account, self.window(), person) - result = dialog.exec_() - if result == QDialog.Accepted: - self.window().refresh_contacts() - - @asyncify - async def send_money_to_identity(self, identity): - await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, - self.community, identity) - self.table_history.model().sourceModel().refresh_transfers() - - @asyncify - async def certify_identity(self, identity): - await CertificationDialog.certify_identity(self.app, self.account, self.password_asker, - self.community, identity) - - def view_wot(self): - identity = self.sender().data() - self.view_in_wot.emit(identity) - - def dates_changed(self): - logging.debug("Changed dates") - if self.table_history.model(): - qdate_from = self.date_from - qdate_from.setTime(QTime(0, 0, 0)) - qdate_to = self.date_to - qdate_to.setTime(QTime(0, 0, 0)) - ts_from = qdate_from.dateTime().toTime_t() - ts_to = qdate_to.dateTime().toTime_t() - - self.table_history.model().set_period(ts_from, ts_to) - - self.refresh_resume() - - def resizeEvent(self, event): - self.busy_resume.resize(event.size()) - super().resizeEvent(event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - self.refresh() - return super(CertificationsTabWidget, self).changeEvent(event) diff --git a/src/sakia/gui/community_view.py b/src/sakia/gui/community_view.py index 5db08b0bb898ec57593cb1ac92cd9937ce34e607..9c26feeb7157d86a47b31b5ce3caac94a7fdf267 100644 --- a/src/sakia/gui/community_view.py +++ b/src/sakia/gui/community_view.py @@ -7,14 +7,13 @@ Created on 2 févr. 2014 import logging import time -from PyQt5.QtCore import pyqtSlot, QDateTime, QLocale, QEvent, QT_TRANSLATE_NOOP -from PyQt5.QtGui import QIcon +from PyQt5.QtCore import pyqtSlot, QDateTime, QLocale, QEvent, QT_TRANSLATE_NOOP, Qt +from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QWidget, QMessageBox, QDialog, QPushButton, QTabBar, QAction from .graphs.wot_tab import WotTabWidget from .widgets import toast from .widgets.dialogs import QAsyncMessageBox -from .certifications_tab import CertificationsTabWidget from .identities_tab import IdentitiesTabWidget from .informations_tab import InformationsTabWidget from .network_tab import NetworkTabWidget @@ -41,8 +40,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): _action_publish_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Publish UID") _action_revoke_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Revoke UID") - - def __init__(self, app, status_label): + def __init__(self, app, status_label, label_icon): """ Constructor """ @@ -52,6 +50,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.community = None self.password_asker = None self.status_label = status_label + self.label_icon = label_icon self.status_info = [] @@ -59,7 +58,6 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.tab_identities = IdentitiesTabWidget(self.app) self.tab_history = TransactionsTabWidget(self.app) self.tab_informations = InformationsTabWidget(self.app) - self.tab_certifications = CertificationsTabWidget(self.app) self.tab_network = NetworkTabWidget(self.app) self.tab_explorer = ExplorerTabWidget(self.app) @@ -71,21 +69,21 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): super().setupUi(self) self.tab_identities.view_in_wot.connect(self.tab_wot.draw_graph) - self.tab_identities.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot)) + self.tab_identities.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot.widget)) self.tab_history.view_in_wot.connect(self.tab_wot.draw_graph) - self.tab_history.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot)) - self.tab_identities.money_sent.connect(lambda: self.tab_history.table_history.model().sourceModel().refresh_transfers()) - self.tab_wot.money_sent.connect(lambda: self.tab_history.table_history.model().sourceModel().refresh_transfers()) + self.tab_history.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot.widget)) + self.tab_identities.money_sent.connect(lambda: self.tab_history.widget.table_history.model().sourceModel().refresh_transfers()) + self.tab_wot.money_sent.connect(lambda: self.tab_history.widget.table_history.model().sourceModel().refresh_transfers()) - self.tabs.addTab(self.tab_history, + self.tabs.addTab(self.tab_history.widget, QIcon(':/icons/tx_icon'), self.tr(CommunityWidget._tab_history_label)) - self.tabs.addTab(self.tab_wot, + self.tabs.addTab(self.tab_wot.widget, QIcon(':/icons/wot_icon'), self.tr(CommunityWidget._tab_wot_label)) - self.tabs.addTab(self.tab_identities, + self.tabs.addTab(self.tab_identities.widget, QIcon(':/icons/members_icon'), self.tr(CommunityWidget._tab_identities_label)) @@ -99,7 +97,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.toolbutton_menu.addAction(action_showinfo) action_showexplorer = QAction(self.tr("Show explorer"), self.toolbutton_menu) - action_showexplorer.triggered.connect(lambda : self.show_closable_tab(self.tab_explorer, + action_showexplorer.triggered.connect(lambda : self.show_closable_tab(self.tab_explorer.widget, QIcon(":/icons/explorer_icon"), self.tr("Explorer"))) self.toolbutton_menu.addAction(action_showexplorer) @@ -190,10 +188,10 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.status_info.append('membership_expire_soon') if self.app.preferences['notifications'] and\ - self.app.notifications['membership_expire_soon'][1]+24*3600 < time.time(): + self.account.notifications['membership_expire_soon'][1]+24*3600 < time.time(): toast.display(self.tr("Membership expiration"), self.tr("<b>Warning : Membership expiration in {0} days</b>").format(days)) - self.app.notifications['membership_expire_soon'][1] = time.time() + self.account.notifications['membership_expire_soon'][1] = time.time() certifiers_of = await person.unique_valid_certifiers_of(self.app.identities_registry, self.community) @@ -201,12 +199,12 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): if 'warning_certifications' not in self.status_info: self.status_info.append('warning_certifications') if self.app.preferences['notifications'] and\ - self.app.notifications['warning_certifications'][1]+24*3600 < time.time(): + self.account.notifications['warning_certifications'][1]+24*3600 < time.time(): toast.display(self.tr("Certifications number"), self.tr("<b>Warning : You are certified by only {0} persons, need {1}</b>") .format(len(certifiers_of), parameters['sigQty'])) - self.app.notifications['warning_certifications'][1] = time.time() + self.account.notifications['warning_certifications'][1] = time.time() except MembershipNotFoundError as e: pass @@ -257,14 +255,14 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.button_send_money.setEnabled(True) if self.community.network.quality > 0.66: - icon = '<img src=":/icons/connected" width="12" height="12"/>' + icon = ':/icons/connected' elif self.community.network.quality > 0.33: - icon = '<img src=":/icons/weak_connect" width="12" height="12"/>' + icon = ':/icons/weak_connect' else: - icon = '<img src=":/icons/disconnected" width="12" height="12"/>' + icon = ':/icons/disconnected' status_infotext = " - ".join([self.app.notifications[info][0] for info in self.status_info]) - label_text = "{0}{1}".format(icon, text) + label_text = text if status_infotext != "": label_text += " - {0}".format(status_infotext) @@ -279,6 +277,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): .format("#") self.status_label.setText(label_text) + self.label_icon.setPixmap(QPixmap(icon).scaled(24, 24, Qt.KeepAspectRatio, Qt.SmoothTransformation)) @once_at_a_time @asyncify @@ -410,11 +409,11 @@ The process to join back the community later will have to be done again.""") :param widget: :return: """ - self.tabs.setTabText(self.tabs.indexOf(self.tab_wot), self.tr(CommunityWidget._tab_wot_label)) + self.tabs.setTabText(self.tabs.indexOf(self.tab_wot.widget), self.tr(CommunityWidget._tab_wot_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_network), self.tr(CommunityWidget._tab_network_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_informations), self.tr(CommunityWidget._tab_informations_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_history), self.tr(CommunityWidget._tab_history_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_identities), self.tr(CommunityWidget._tab_identities_label)) + self.tabs.setTabText(self.tabs.indexOf(self.tab_history.widget), self.tr(CommunityWidget._tab_history_label)) + self.tabs.setTabText(self.tabs.indexOf(self.tab_identities.widget), self.tr(CommunityWidget._tab_identities_label)) self.action_publish_uid.setText(self.tr(CommunityWidget._action_publish_uid_text)) self.action_revoke_uid.setText(self.tr(CommunityWidget._action_revoke_uid_text)) self.action_showinfo.setText(self.tr(CommunityWidget._action_showinfo_text)) diff --git a/src/sakia/gui/contact.py b/src/sakia/gui/contact.py index bc6f550e84995a28f26d5a1c9fbd9a3817c2f721..457a8002a3390f1818f9e9ca32e5c070b3d85b87 100644 --- a/src/sakia/gui/contact.py +++ b/src/sakia/gui/contact.py @@ -37,6 +37,14 @@ class ConfigureContactDialog(QDialog, Ui_ConfigureContactDialog): self.edit_name.setText(self.contact['name']) self.edit_pubkey.setText(self.contact['pubkey']) + @classmethod + def from_identity(cls, parent, account, identity): + contact = { + 'name': identity.uid, + 'pubkey': identity.pubkey + } + return ConfigureContactDialog(account, parent, contact) + def accept(self): name = self.edit_name.text() pubkey = self.edit_pubkey.text() diff --git a/src/sakia/gui/graphs/explorer_tab.py b/src/sakia/gui/graphs/explorer_tab.py index 75321719cb4994d532b1b4e070209e5a437735a5..b39ee555759d06f08b01380046b79748f6e76628 100644 --- a/src/sakia/gui/graphs/explorer_tab.py +++ b/src/sakia/gui/graphs/explorer_tab.py @@ -1,11 +1,9 @@ import logging -from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP - -from ucoinpy.api import bma +from PyQt5.QtCore import QEvent, pyqtSignal +from PyQt5.QtWidgets import QWidget from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task -from ...tools.exceptions import NoPeerAvailable from ...core.graph import ExplorerGraph from .graph_tab import GraphTabWidget from ...gen_resources.explorer_tab_uic import Ui_ExplorerTabWidget @@ -15,20 +13,22 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): money_sent = pyqtSignal() - def __init__(self, app): + def __init__(self, app, account=None, community=None, password_asker=None, + widget=QWidget, view=Ui_ExplorerTabWidget): """ :param sakia.core.app.Application app: Application instance + :param sakia.core.app.Application app: Application instance + :param sakia.core.Account account: The account displayed in the widget + :param sakia.core.Community community: The community displayed in the widget + :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords """ # construct from qtDesigner - super().__init__(app) - self.setupUi(self) - self.search_user_widget.init(app) - - self.set_scene(self.graphicsView.scene()) + super().__init__(app, account, community, password_asker, widget) + self.ui = view() + self.ui.setupUi(self.widget) + self.ui.search_user_widget.init(app) - self.account = None - self.community = None - self.password_asker = None + self.set_scene(self.ui.graphicsView.scene()) self.graph = None self.app = app self.draw_task = None @@ -38,16 +38,16 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): # create node metadata from account self._current_identity = None - self.button_go.clicked.connect(self.go_clicked) - self.search_user_widget.identity_selected.connect(self.draw_graph) - self.search_user_widget.reset.connect(self.reset) + self.ui.button_go.clicked.connect(self.go_clicked) + self.ui.search_user_widget.identity_selected.connect(self.draw_graph) + self.ui.search_user_widget.reset.connect(self.reset) def cancel_once_tasks(self): cancel_once_task(self, self.refresh_informations_frame) cancel_once_task(self, self.reset) def change_account(self, account, password_asker): - self.search_user_widget.change_account(account) + self.ui.search_user_widget.change_account(account) self.account = account self.password_asker = password_asker @@ -57,8 +57,8 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): self.graph.stop_exploration() self.graph = ExplorerGraph(self.app, self.community) self.graph.graph_changed.connect(self.refresh) - self.search_user_widget.change_community(community) - self.graph.current_identity_changed.connect(self.graphicsView.scene().update_current_identity) + self.ui.search_user_widget.change_community(community) + self.graph.current_identity_changed.connect(self.ui.graphicsView.scene().update_current_identity) self.reset() def go_clicked(self): @@ -79,11 +79,11 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): if self._current_identity != identity: self._current_identity = identity - self.graph.start_exploration(identity, self.steps_slider.value()) + self.graph.start_exploration(identity, self.ui.steps_slider.value()) # draw graph in qt scene - self.graphicsView.scene().clear() - self.graphicsView.scene().update_wot(self.graph.nx_graph, identity, self.steps_slider.maximum()) + self.ui.graphicsView.scene().clear() + self.ui.graphicsView.scene().update_wot(self.graph.nx_graph, identity, self.ui.steps_slider.maximum()) def refresh(self): """ @@ -91,7 +91,7 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): """ if self._current_identity: # draw graph in qt scene - self.graphicsView.scene().update_wot(self.graph.nx_graph, self._current_identity, self.steps_slider.maximum()) + self.ui.graphicsView.scene().update_wot(self.graph.nx_graph, self._current_identity, self.ui.steps_slider.maximum()) else: self.reset() @@ -103,8 +103,8 @@ class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): """ if self.account and self.community: parameters = await self.community.parameters() - self.steps_slider.setMaximum(parameters['stepMax']) - self.steps_slider.setValue(int(0.33 * parameters['stepMax'])) + self.ui.steps_slider.setMaximum(parameters['stepMax']) + self.ui.steps_slider.setValue(int(0.33 * parameters['stepMax'])) identity = await self.account.identity(self.community) self.draw_graph(identity) diff --git a/src/sakia/gui/graphs/graph_tab.py b/src/sakia/gui/graphs/graph_tab.py index 18ec848cfaf37cbc6da5c248fde16fd58cf15ecf..d308245b821fb6a43169b3780b0204436cc7c47a 100644 --- a/src/sakia/gui/graphs/graph_tab.py +++ b/src/sakia/gui/graphs/graph_tab.py @@ -1,35 +1,44 @@ -from PyQt5.QtWidgets import QWidget, QDialog -from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime, pyqtSignal +from PyQt5.QtWidgets import QWidget +from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime, pyqtSignal, QObject +from PyQt5.QtGui import QCursor from ...tools.exceptions import MembershipNotFoundError from ...tools.decorators import asyncify, once_at_a_time from ...core.registry import BlockchainState -from ...gui.member import MemberDialog -from ...gui.certification import CertificationDialog -from ...gui.transfer import TransferMoneyDialog -from ...gui.contact import ConfigureContactDialog +from ..widgets.context_menu import ContextMenu -class GraphTabWidget(QWidget): +class GraphTabWidget(QObject): money_sent = pyqtSignal() - def __init__(self, app): + + def __init__(self, app, account=None, community=None, password_asker=None, widget=QWidget): """ :param sakia.core.app.Application app: Application instance + :param sakia.core.app.Application app: Application instance + :param sakia.core.Account account: The account displayed in the widget + :param sakia.core.Community community: The community displayed in the widget + :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords + :param class widget: The class of the graph tab """ super().__init__() - self.password_asker = None + self.widget = widget() + self.account = account + self.community = community + self.password_asker = password_asker + self.app = app def set_scene(self, scene): + """ + Set the scene and connects the signals + :param sakia.gui.views.scenes.base_scene.BaseScene scene: the scene + :return: + """ # add scene events + scene.node_context_menu_requested.connect(self.node_context_menu) scene.node_clicked.connect(self.handle_node_click) - scene.node_signed.connect(self.sign_node) - scene.node_transaction.connect(self.send_money_to_node) - scene.node_contact.connect(self.add_node_as_contact) - scene.node_member.connect(self.identity_informations) - scene.node_copy_pubkey.connect(self.copy_node_pubkey) @once_at_a_time @asyncify @@ -147,59 +156,19 @@ class GraphTabWidget(QWidget): """ pass - def identity_informations(self, pubkey, metadata): - identity = self.app.identities_registry.from_handled_data( - metadata['text'], - pubkey, - None, - BlockchainState.VALIDATED, - self.community - ) - dialog = MemberDialog(self.app, self.account, self.community, identity) - dialog.exec_() - @asyncify - async def sign_node(self, pubkey, metadata): - identity = self.app.identities_registry.from_handled_data( - metadata['text'], - pubkey, - None, - BlockchainState.VALIDATED, - self.community - ) - await CertificationDialog.certify_identity(self.app, self.account, self.password_asker, - self.community, identity) + async def node_context_menu(self, pubkey): + """ + Open the node context menu + :param str pubkey: the pubkey of the node to open + """ + identity = await self.app.identities_registry.future_find(pubkey, self.community) + menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, + (identity,)) + menu.view_identity_in_wot.connect(self.draw_graph) - @asyncify - async def send_money_to_node(self, pubkey, metadata): - identity = self.app.identities_registry.from_handled_data( - metadata['text'], - pubkey, - None, - BlockchainState.VALIDATED, - self.community - ) - result = await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, - self.community, identity) - if result == QDialog.Accepted: - self.money_sent.emit() - - def copy_node_pubkey(self, pubkey): - cb = self.app.qapp.clipboard() - cb.clear(mode=cb.Clipboard) - cb.setText(pubkey, mode=cb.Clipboard) - - def add_node_as_contact(self, pubkey, metadata): - # check if contact already exists... - if pubkey == self.account.pubkey \ - or pubkey in [contact['pubkey'] for contact in self.account.contacts]: - return False - dialog = ConfigureContactDialog(self.account, self.window(), {'name': metadata['text'], - 'pubkey': pubkey, - }) - result = dialog.exec_() - if result == QDialog.Accepted: - self.window().refresh_contacts() + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) def changeEvent(self, event): """ diff --git a/src/sakia/gui/graphs/wot_tab.py b/src/sakia/gui/graphs/wot_tab.py index ce12bd290e659bde210edc643d3f2d9b80427e2b..a3119357ab7964b474c826e578bbb92443d7cc14 100644 --- a/src/sakia/gui/graphs/wot_tab.py +++ b/src/sakia/gui/graphs/wot_tab.py @@ -1,7 +1,8 @@ import logging import asyncio -from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP +from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP, QObject +from PyQt5.QtWidgets import QWidget from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task from ...core.graph import WoTGraph from ...gen_resources.wot_tab_uic import Ui_WotTabWidget @@ -9,31 +10,40 @@ from ...gui.widgets.busy import Busy from .graph_tab import GraphTabWidget -class WotTabWidget(GraphTabWidget, Ui_WotTabWidget): +class WotTabWidget(GraphTabWidget): money_sent = pyqtSignal() - def __init__(self, app): + def __init__(self, app, account=None, community=None, password_asker=None, widget=QWidget, view=Ui_WotTabWidget): """ :param sakia.core.app.Application app: Application instance + :param sakia.core.app.Application app: Application instance + :param sakia.core.Account account: The account displayed in the widget + :param sakia.core.Community community: The community displayed in the widget + :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords + :param class widget: The class of the PyQt5 widget used for this tab + :param class view: The class of the UI View for this tab """ - super().__init__(app) + super().__init__(app, account, community, password_asker, widget) # construct from qtDesigner - self.setupUi(self) - self.search_user_widget.init(app) - self.busy = Busy(self.graphicsView) + self.ui = view() + self.ui.setupUi(self.widget) + + self.ui.search_user_widget.init(app) + self.widget.installEventFilter(self) + self.busy = Busy(self.ui.graphicsView) self.busy.hide() - self.set_scene(self.graphicsView.scene()) + self.set_scene(self.ui.graphicsView.scene()) - self.account = None - self.community = None - self.password_asker = None + self.account = account + self.community = community + self.password_asker = password_asker self.app = app self.draw_task = None - self.search_user_widget.identity_selected.connect(self.draw_graph) - self.search_user_widget.reset.connect(self.reset) + self.ui.search_user_widget.identity_selected.connect(self.draw_graph) + self.ui.search_user_widget.reset.connect(self.reset) # create node metadata from account self._current_identity = None @@ -45,13 +55,13 @@ class WotTabWidget(GraphTabWidget, Ui_WotTabWidget): def change_account(self, account, password_asker): self.cancel_once_tasks() - self.search_user_widget.change_account(account) + self.ui.search_user_widget.change_account(account) self.account = account self.password_asker = password_asker def change_community(self, community): self.cancel_once_tasks() - self.search_user_widget.change_community(community) + self.ui.search_user_widget.change_community(community) self._auto_refresh(community) self.community = community self.reset() @@ -87,14 +97,14 @@ class WotTabWidget(GraphTabWidget, Ui_WotTabWidget): graph = WoTGraph(self.app, self.community) await graph.initialize(identity, identity_account) # draw graph in qt scene - self.graphicsView.scene().update_wot(graph.nx_graph, identity) + self.ui.graphicsView.scene().update_wot(graph.nx_graph, identity) # if selected member is not the account member... if identity.pubkey != identity_account.pubkey: # add path from selected member to account member path = await graph.get_shortest_path_to_identity(identity_account, identity) if path: - self.graphicsView.scene().update_path(graph.nx_graph, path) + self.ui.graphicsView.scene().update_path(graph.nx_graph, path) self.busy.hide() @once_at_a_time @@ -116,9 +126,11 @@ class WotTabWidget(GraphTabWidget, Ui_WotTabWidget): else: self.reset() - def resizeEvent(self, event): - self.busy.resize(event.size()) - super().resizeEvent(event) + def eventFilter(self, source, event): + if event.type() == QEvent.Resize: + self.busy.resize(event.size()) + self.widget.resizeEvent(event) + return self.widget.eventFilter(source, event) def changeEvent(self, event): """ diff --git a/src/sakia/gui/identities_tab.py b/src/sakia/gui/identities_tab.py index a84857fed2875cddf865ea9055fcdc2755750245..02badc49cf9e7b5c52531f7ca038002c00097377 100644 --- a/src/sakia/gui/identities_tab.py +++ b/src/sakia/gui/identities_tab.py @@ -4,10 +4,9 @@ Created on 2 févr. 2014 @author: inso """ -import asyncio import logging -from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QT_TRANSLATE_NOOP +from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QT_TRANSLATE_NOOP, QObject from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QWidget, QAction, QMenu, QDialog, \ QAbstractItemView @@ -15,62 +14,62 @@ from ucoinpy.api import bma from ..models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel from ..gen_resources.identities_tab_uic import Ui_IdentitiesTab -from .contact import ConfigureContactDialog -from .member import MemberDialog -from .transfer import TransferMoneyDialog -from sakia.gui.widgets.busy import Busy -from .certification import CertificationDialog from ..core.registry import Identity, BlockchainState from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task +from .widgets.context_menu import ContextMenu -class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): +class IdentitiesTabWidget(QObject): """ classdocs """ - view_in_wot = pyqtSignal(Identity) + view_in_wot = pyqtSignal(object) money_sent = pyqtSignal() _direct_connections_text = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Search direct certifications") _search_placeholder = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Research a pubkey, an uid...") - def __init__(self, app): + def __init__(self, app, account=None, community=None, password_asker=None, + widget=QWidget, view=Ui_IdentitiesTab): """ Init - :param sakia.core.account.Account account: Account instance - :param sakia.core.community.Community community: Community instance - :param sakia.gui.password_asker.PasswordAskerDialog password_asker: Password asker dialog - :return: + + :param sakia.core.app.Application app: Application instance + :param sakia.core.Account account: The account displayed in the widget + :param sakia.core.Community community: The community displayed in the widget + :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords + :param class widget: The class of the PyQt5 widget used for this tab + :param class view: The class of the UI View for this tab """ super().__init__() + self.widget = widget() + self.ui = view() + self.ui.setupUi(self.widget) + self.app = app - self.community = None - self.account = None - self.password_asker = None + self.community = community + self.account = account + self.password_asker = password_asker self.direct_connections = QAction(self.tr(IdentitiesTabWidget._direct_connections_text), self) - self.setupUi(self) - self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) + self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) identities_model = IdentitiesTableModel() proxy = IdentitiesFilterProxyModel() proxy.setSourceModel(identities_model) - self.table_identities.setModel(proxy) - self.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_identities.customContextMenuRequested.connect(self.identity_context_menu) - self.table_identities.sortByColumn(0, Qt.AscendingOrder) - self.table_identities.resizeColumnsToContents() - identities_model.modelAboutToBeReset.connect(lambda: self.table_identities.setEnabled(False)) - identities_model.modelReset.connect(lambda: self.table_identities.setEnabled(True)) + self.ui.table_identities.setModel(proxy) + self.ui.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) + self.ui.table_identities.customContextMenuRequested.connect(self.identity_context_menu) + self.ui.table_identities.sortByColumn(0, Qt.AscendingOrder) + self.ui.table_identities.resizeColumnsToContents() + identities_model.modelAboutToBeReset.connect(lambda: self.ui.table_identities.setEnabled(False)) + identities_model.modelReset.connect(lambda: self.ui.table_identities.setEnabled(True)) self.direct_connections.triggered.connect(self._async_search_direct_connections) - self.button_search.addAction(self.direct_connections) - self.button_search.clicked.connect(self._async_execute_search_text) - - self.busy = Busy(self.table_identities) - self.busy.hide() + self.ui.button_search.addAction(self.direct_connections) + self.ui.button_search.clicked.connect(self._async_execute_search_text) def cancel_once_tasks(self): cancel_once_task(self, self.identity_context_menu) @@ -88,14 +87,14 @@ class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): def change_community(self, community): self.cancel_once_tasks() self.community = community - self.table_identities.model().change_community(community) + self.ui.table_identities.model().change_community(community) self._async_search_direct_connections() @once_at_a_time @asyncify async def identity_context_menu(self, point): - index = self.table_identities.indexAt(point) - model = self.table_identities.model() + index = self.ui.table_identities.indexAt(point) + model = self.ui.table_identities.model() if index.isValid() and index.row() < model.rowCount(): source_index = model.mapToSource(index) pubkey_col = model.sourceModel().columns_ids.index('pubkey') @@ -103,102 +102,20 @@ class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): pubkey_col) pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) identity = await self.app.identities_registry.future_find(pubkey, self.community) - menu = QMenu(self) - - informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.menu_informations) - informations.setData(identity) - add_contact = QAction(self.tr("Add as contact"), self) - add_contact.triggered.connect(self.menu_add_as_contact) - add_contact.setData(identity) - - send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.menu_send_money) - send_money.setData(identity) - - certify = QAction(self.tr("Certify identity"), self) - certify.triggered.connect(self.menu_certify_member) - certify.setData(identity) - - view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.view_wot) - view_wot.setData(identity) - - copy_pubkey = QAction(self.tr("Copy pubkey"), self) - copy_pubkey.triggered.connect(self.copy_identity_pubkey) - copy_pubkey.setData(identity) - - menu.addAction(informations) - menu.addAction(add_contact) - menu.addAction(send_money) - menu.addAction(certify) - menu.addAction(view_wot) - menu.addAction(copy_pubkey) + menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, + (identity,)) + menu.view_identity_in_wot.connect(self.view_in_wot) # Show the context menu. - menu.popup(QCursor.pos()) - - def menu_informations(self): - person = self.sender().data() - self.identity_informations(person) - - def menu_add_as_contact(self): - person = self.sender().data() - self.add_identity_as_contact({'name': person.uid, - 'pubkey': person.pubkey}) - - def menu_send_money(self): - person = self.sender().data() - self.send_money_to_identity(person) - - def menu_certify_member(self): - person = self.sender().data() - self.certify_identity(person) - - def identity_informations(self, person): - dialog = MemberDialog(self.app, self.account, self.community, person) - dialog.exec_() - - def add_identity_as_contact(self, person): - dialog = ConfigureContactDialog(self.account, self.window(), person) - result = dialog.exec_() - if result == QDialog.Accepted: - self.window().refresh_contacts() - - @asyncify - async def send_money_to_identity(self, identity): - result = await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, - self.community, identity) - if result == QDialog.Accepted: - self.money_sent.emit() - - @asyncify - async def certify_identity(self, identity): - await CertificationDialog.certify_identity(self.app, self.account, self.password_asker, - self.community, identity) - - def copy_identity_pubkey(self): - """ - Copy the identity pubkey to the clipboard - - :param sakia.core.registry.Identity identity: The identity - """ - identity = self.sender().data() - cb = self.app.qapp.clipboard() - cb.clear(mode=cb.Clipboard) - cb.setText(identity.pubkey, mode=cb.Clipboard) - - def view_wot(self): - identity = self.sender().data() - self.view_in_wot.emit(identity) + menu.qmenu.popup(QCursor.pos()) @once_at_a_time @asyncify async def _async_execute_search_text(self, checked): cancel_once_task(self, self._async_search_direct_connections) - self.busy.show() - text = self.edit_textsearch.text() + self.ui.busy.show() + text = self.ui.edit_textsearch.text() if len(text) < 2: return try: @@ -212,13 +129,13 @@ class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): BlockchainState.BUFFERED) identities.append(identity) - self.edit_textsearch.clear() - self.edit_textsearch.setPlaceholderText(text) + self.ui.edit_textsearch.clear() + self.ui.edit_textsearch.setPlaceholderText(text) await self.refresh_identities(identities) except ValueError as e: logging.debug(str(e)) finally: - self.busy.hide() + self.ui.busy.hide() @once_at_a_time @asyncify @@ -230,9 +147,9 @@ class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): if self.account and self.community: try: - self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) + self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) await self.refresh_identities([]) - self.busy.show() + self.ui.busy.show() self_identity = await self.account.identity(self.community) account_connections = [] certs_of = await self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) @@ -245,25 +162,25 @@ class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): certified_by = [p for p in account_connections if p.pubkey not in [i.pubkey for i in certifiers_of]] identities = certifiers_of + certified_by - self.busy.hide() + self.ui.busy.hide() await self.refresh_identities(identities) except NoPeerAvailable: - self.busy.hide() + self.ui.busy.hide() async def refresh_identities(self, identities): """ Refresh the table with specified identities. If no identities is passed, use the account connections. """ - await self.table_identities.model().sourceModel().refresh_identities(identities) - self.table_identities.resizeColumnsToContents() + await self.ui.table_identities.model().sourceModel().refresh_identities(identities) + self.ui.table_identities.resizeColumnsToContents() def retranslateUi(self, widget): self.direct_connections.setText(self.tr(IdentitiesTabWidget._direct_connections_text)) super().retranslateUi(self) def resizeEvent(self, event): - self.busy.resize(event.size()) + self.ui.busy.resize(event.size()) super().resizeEvent(event) def changeEvent(self, event): diff --git a/src/sakia/gui/informations_tab.py b/src/sakia/gui/informations_tab.py index bfad9b2f535dcc205a324538fe00a965aac780fd..790556d52ccf8e4de8b76b6354fa73bc49796139 100644 --- a/src/sakia/gui/informations_tab.py +++ b/src/sakia/gui/informations_tab.py @@ -70,7 +70,7 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): logging.debug('community get_ud_block error : ' + str(e)) return False try: - block_ud_minus_1 = await self.community.get_ud_block(1) + block_ud_minus_1 = await self.community.get_ud_block(x=1) except NoPeerAvailable as e: logging.debug('community get_ud_block error : ' + str(e)) return False @@ -125,15 +125,15 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): """).format( localized_ud, self.tr('Universal Dividend UD(t) in'), - self.account.current_ref.diff_units(self.community.currency), + self.account.current_ref(0, self.community, self.app, None).diff_units, localized_mass_minus_1, self.tr('Monetary Mass M(t-1) in'), - self.account.current_ref.diff_units(self.community.currency), + self.account.current_ref(0, self.community, self.app, None).units, block_ud['membersCount'], self.tr('Members N(t)'), localized_mass_minus_1_per_member, self.tr('Monetary Mass per member M(t-1)/N(t) in'), - self.account.current_ref.diff_units(self.community.currency), + self.account.current_ref(0, self.community, self.app, None).diff_units, float(0) if block_ud['membersCount'] == 0 or block_ud_minus_1['monetaryMass'] == 0 else block_ud['dividend'] / (block_ud_minus_1['monetaryMass'] / block_ud['membersCount']), @@ -179,10 +179,10 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): self.tr('{:} = MAX {{ {:} {:} ; {:2.0%} × {:} {:} / {:} }}').format( localized_ud_plus_1, localized_ud, - self.account.current_ref.diff_units(self.community.currency), + self.account.current_ref(0, self.community, self.app, None).diff_units, params['c'], localized_mass, - self.account.current_ref.diff_units(self.community.currency), + self.account.current_ref(0, self.community, self.app, None).diff_units, block_ud['membersCount'] ), self.tr('Universal Dividend (computed)') diff --git a/src/sakia/gui/mainwindow.py b/src/sakia/gui/mainwindow.py index a7f6e69ef671ec613ee7fd21e60a4f57e6b7ae49..808ce45a8cd7eaae3ae479da77da5133e1d943eb 100644 --- a/src/sakia/gui/mainwindow.py +++ b/src/sakia/gui/mainwindow.py @@ -54,9 +54,12 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.version_requested.connect(self.latest_version_requested) + self.label_icon = QLabel("", self) + self.statusbar.addPermanentWidget(self.label_icon, 1) + self.status_label = QLabel("", self) self.status_label.setTextFormat(Qt.RichText) - self.statusbar.addPermanentWidget(self.status_label, 1) + self.statusbar.addPermanentWidget(self.status_label, 2) self.label_time = QLabel("", self) self.statusbar.addPermanentWidget(self.label_time) @@ -76,7 +79,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.centralWidget().layout().addWidget(self.homescreen) self.homescreen.toolbutton_connect.setMenu(self.menu_change_account) - self.community_view = CommunityWidget(self.app, self.status_label) + self.community_view = CommunityWidget(self.app, self.status_label, self.label_icon) self.community_view.button_home.clicked.connect(lambda: self.change_community(None)) self.community_view.button_certification.clicked.connect(self.open_certification_dialog) self.community_view.button_send_money.clicked.connect(self.open_transfer_money_dialog) @@ -173,14 +176,13 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.password_asker, self.community_view.community, None) - if dialog.exec_() == QDialog.Accepted: + if dialog.exec() == QDialog.Accepted: self.community_view.tab_history.table_history.model().sourceModel().refresh_transfers() def open_certification_dialog(self): - dialog = CertificationDialog(self.app, + CertificationDialog.open_dialog(self.app, self.app.current_account, self.password_asker) - dialog.exec_() def open_add_contact_dialog(self): dialog = ConfigureContactDialog(self.app.current_account, self) diff --git a/src/sakia/gui/member.py b/src/sakia/gui/member.py index 0b7fe87e4ef59d1db500e8628fa09ee8554b8e66..406ab6abafa8d40ae376d72c01da008cfee83f0d 100644 --- a/src/sakia/gui/member.py +++ b/src/sakia/gui/member.py @@ -1,20 +1,21 @@ import datetime -import asyncio -from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QObject, QEvent, QLocale, QDateTime +from PyQt5.QtWidgets import QDialog, QWidget from ..core.graph import WoTGraph +from .widgets.busy import Busy from ..tools.decorators import asyncify -from ..gen_resources.member_uic import Ui_DialogMember +from ..gen_resources.member_uic import Ui_MemberView from ..tools.exceptions import MembershipNotFoundError -class MemberDialog(QDialog, Ui_DialogMember): +class MemberDialog(QObject): """ - classdocs + A widget showing informations about a member """ - def __init__(self, app, account, community, identity): + def __init__(self, app, account, community, identity, widget, ui): """ Init MemberDialog @@ -22,30 +23,80 @@ class MemberDialog(QDialog, Ui_DialogMember): :param sakia.core.account.Account account: Account instance :param sakia.core.community.Community community: Community instance :param sakia.core.registry.identity.Identity identity: Identity instance + :param PyQt5.QtWidget widget: The class of the widget + :param sakia.gen_resources.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget :return: """ super().__init__() - self.setupUi(self) + self.widget = widget + self.ui = ui + self.ui.setupUi(self.widget) + self.ui.busy = Busy(self.widget) + self.widget.installEventFilter(self) self.app = app self.community = community self.account = account self.identity = identity - self.label_uid.setText(identity.uid) - self.refresh() + + @classmethod + def open_dialog(cls, app, account, community, identity): + dialog = cls(app, account, community, identity, QDialog(), Ui_MemberView()) + dialog.refresh() + dialog.refresh_path() + dialog.exec() + + @classmethod + def as_widget(cls, parent_widget, app, account, community, identity): + return cls(app, account, community, identity, QWidget(parent_widget), Ui_MemberView()) @asyncify async def refresh(self): + if self.identity: + self.ui.busy.show() + self.ui.label_uid.setText(self.identity.uid) + self.ui.label_properties.setText("") + try: + join_date = await self.identity.get_join_date(self.community) + except MembershipNotFoundError: + join_date = None + + if join_date is None: + join_date = self.tr('not a member') + else: + join_date = datetime.datetime.fromtimestamp(join_date).strftime("%d/%m/%Y %I:%M") + - try: - join_date = await self.identity.get_join_date(self.community) - except MembershipNotFoundError: - join_date = None + identity_selfcert = await self.identity.selfcert(self.community) + uid_publish_date = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(identity_selfcert.timestamp), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) - if join_date is None: - join_date = self.tr('not a member') - else: - join_date = datetime.datetime.fromtimestamp(join_date).strftime("%d/%m/%Y %I:%M") + text = self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> + """).format( + self.tr('Public key'), + self.identity.pubkey, + self.tr('UID Published on'), + uid_publish_date, + self.tr('Join date'), + join_date + ) + # close html text + text += "</table>" + # set text in label + self.ui.label_properties.setText(text) + self.ui.busy.hide() + + @asyncify + async def refresh_path(self): + text = "" + self.ui.label_path.setText("") # calculate path to account member graph = WoTGraph(self.app, self.community) path = None @@ -56,17 +107,6 @@ class MemberDialog(QDialog, Ui_DialogMember): path = await graph.get_shortest_path_to_identity(self.identity, account_identity) - text = self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> - """).format( - self.tr('Public key'), - self.identity.pubkey, - self.tr('Join date'), - join_date - ) - if path: distance = len(path) - 1 text += self.tr( @@ -76,18 +116,26 @@ class MemberDialog(QDialog, Ui_DialogMember): index = 0 for node in path: if index == 0: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""").format( + text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ + .format( self.tr('Path'), node['text']) else: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""").format('', - node[ - 'text']) + text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ + .format('', + node[ + 'text']) if index == distance and node['id'] != self.account.pubkey: - text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""").format('', - self.account.name) + text += self.tr("""<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""")\ + .format('', + self.account.name) index += 1 - # close html text - text += "</table>" + self.ui.label_path.setText(text) + + def eventFilter(self, source, event): + if event.type() == QEvent.Resize: + self.ui.busy.resize(event.size()) + self.widget.resizeEvent(event) + return self.widget.eventFilter(source, event) - # set text in label - self.label_properties.setText(text) + def exec(self): + self.widget.exec() diff --git a/src/sakia/gui/transactions_tab.py b/src/sakia/gui/transactions_tab.py index b35cf48c9538690e6699c68cfce87addf051c401..cead5a8578d629063199e5d4382f8432e65ec0c6 100644 --- a/src/sakia/gui/transactions_tab.py +++ b/src/sakia/gui/transactions_tab.py @@ -1,68 +1,69 @@ import logging import asyncio -from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView, QDialog, \ - QMenu, QAction, QApplication, QMessageBox -from PyQt5.QtCore import Qt, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent - +from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView +from PyQt5.QtCore import Qt, QObject, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent from PyQt5.QtGui import QCursor from ..gen_resources.transactions_tab_uic import Ui_transactionsTabWidget from ..models.txhistory import HistoryTableModel, TxFilterProxyModel -from ..core.transfer import Transfer, TransferState -from .contact import ConfigureContactDialog -from .member import MemberDialog -from .certification import CertificationDialog -from ..core.wallet import Wallet -from ..core.registry import Identity from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from .transfer import TransferMoneyDialog +from .widgets.context_menu import ContextMenu from sakia.gui.widgets import toast -from sakia.gui.widgets.busy import Busy -class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): +class TransactionsTabWidget(QObject): """ classdocs """ - view_in_wot = pyqtSignal(Identity) + view_in_wot = pyqtSignal(object) - def __init__(self, app): + def __init__(self, app, account=None, community=None, password_asker=None, + widget=QWidget, view=Ui_transactionsTabWidget): """ Init :param sakia.core.app.Application app: Application instance - :return: + :param sakia.core.Account account: The account displayed in the widget + :param sakia.core.Community community: The community displayed in the widget + :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords + :param class widget: The class of the PyQt5 widget used for this tab + :param class view: The class of the UI View for this tab """ super().__init__() - self.setupUi(self) + self.widget = widget() + self.ui = view() + self.ui.setupUi(self.widget) self.app = app - self.account = None - self.community = None - self.password_asker = None - self.busy_balance = Busy(self.groupbox_balance) - self.busy_balance.hide() - - ts_from = self.date_from.dateTime().toTime_t() - ts_to = self.date_to.dateTime().toTime_t() + self.account = account + self.community = community + self.password_asker = password_asker + self.ui.busy_balance.hide() + + ts_from = self.ui.date_from.dateTime().toTime_t() + ts_to = self.ui.date_to.dateTime().toTime_t() model = HistoryTableModel(self.app, self.account, self.community) proxy = TxFilterProxyModel(ts_from, ts_to) proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) proxy.setSortRole(Qt.DisplayRole) - self.table_history.setModel(proxy) - self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_history.setSortingEnabled(True) - self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.table_history.resizeColumnsToContents() + self.ui.table_history.setModel(proxy) + self.ui.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) + self.ui.table_history.setSortingEnabled(True) + self.ui.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + self.ui.table_history.resizeColumnsToContents() - model.modelAboutToBeReset.connect(lambda: self.table_history.setEnabled(False)) - model.modelReset.connect(lambda: self.table_history.setEnabled(True)) + self.ui.table_history.customContextMenuRequested['QPoint'].connect(self.history_context_menu) + self.ui.date_from.dateChanged['QDate'].connect(self.dates_changed) + self.ui.date_to.dateChanged['QDate'].connect(self.dates_changed) - self.progressbar.hide() + model.modelAboutToBeReset.connect(lambda: self.ui.table_history.setEnabled(False)) + model.modelReset.connect(lambda: self.ui.table_history.setEnabled(True)) + + self.ui.progressbar.hide() self.refresh() def cancel_once_tasks(self): @@ -74,15 +75,15 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): self.cancel_once_tasks() self.account = account self.password_asker = password_asker - self.table_history.model().sourceModel().change_account(account) + self.ui.table_history.model().sourceModel().change_account(account) if account: self.connect_progress() def change_community(self, community): self.cancel_once_tasks() self.community = community - self.progressbar.hide() - self.table_history.model().sourceModel().change_community(self.community) + self.ui.progressbar.hide() + self.ui.table_history.model().sourceModel().change_community(self.community) self.refresh() @once_at_a_time @@ -94,14 +95,14 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): minimum_datetime.setTime_t(block['medianTime']) minimum_datetime.setTime(QTime(0, 0)) - self.date_from.setMinimumDateTime(minimum_datetime) - self.date_from.setDateTime(minimum_datetime) - self.date_from.setMaximumDateTime(QDateTime().currentDateTime()) + self.ui.date_from.setMinimumDateTime(minimum_datetime) + self.ui.date_from.setDateTime(minimum_datetime) + self.ui.date_from.setMaximumDateTime(QDateTime().currentDateTime()) - self.date_to.setMinimumDateTime(minimum_datetime) + self.ui.date_to.setMinimumDateTime(minimum_datetime) tomorrow_datetime = QDateTime().currentDateTime().addDays(1) - self.date_to.setDateTime(tomorrow_datetime) - self.date_to.setMaximumDateTime(tomorrow_datetime) + self.ui.date_to.setDateTime(tomorrow_datetime) + self.ui.date_to.setMaximumDateTime(tomorrow_datetime) except NoPeerAvailable as e: logging.debug(str(e)) except ValueError as e: @@ -109,25 +110,25 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): def refresh(self): if self.community: - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() + self.ui.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.resizeColumnsToContents() self.refresh_minimum_maximum() self.refresh_balance() def connect_progress(self): def progressing(community, value, maximum): if community == self.community: - self.progressbar.show() - self.progressbar.setValue(value) - self.progressbar.setMaximum(maximum) + self.ui.progressbar.show() + self.ui.progressbar.setValue(value) + self.ui.progressbar.setMaximum(maximum) self.account.loading_progressed.connect(progressing) self.account.loading_finished.connect(self.stop_progress) def stop_progress(self, community, received_list): if community == self.community: - self.progressbar.hide() - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() + self.ui.progressbar.hide() + self.ui.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.resizeColumnsToContents() self.notification_reception(received_list) @asyncify @@ -148,162 +149,60 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): @once_at_a_time @asyncify async def refresh_balance(self): - self.busy_balance.show() + self.ui.busy_balance.show() amount = await self.app.current_account.amount(self.community) localized_amount = await self.app.current_account.current_ref(amount, self.community, self.app).localized(units=True, international_system=self.app.preferences['international_system_of_units']) # set infos in label - self.label_balance.setText( + self.ui.label_balance.setText( self.tr("{:}") .format( localized_amount ) ) - self.busy_balance.hide() + self.ui.busy_balance.hide() @once_at_a_time @asyncify async def history_context_menu(self, point): - index = self.table_history.indexAt(point) - model = self.table_history.model() + index = self.ui.table_history.indexAt(point) + model = self.ui.table_history.model() if index.isValid() and index.row() < model.rowCount(QModelIndex()): - menu = QMenu(self.tr("Actions"), self) source_index = model.mapToSource(index) - state_col = model.sourceModel().columns_types.index('state') - state_index = model.sourceModel().index(source_index.row(), - state_col) - state_data = model.sourceModel().data(state_index, Qt.DisplayRole) pubkey_col = model.sourceModel().columns_types.index('pubkey') pubkey_index = model.sourceModel().index(source_index.row(), pubkey_col) pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) + identity = await self.app.identities_registry.future_find(pubkey, self.community) transfer = model.sourceModel().transfers()[source_index.row()] - if state_data == TransferState.REFUSED or state_data == TransferState.TO_SEND: - send_back = QAction(self.tr("Send again"), self) - send_back.triggered.connect(lambda checked, tr=transfer: self.send_again(checked, tr)) - send_back.setData(transfer) - menu.addAction(send_back) - - cancel = QAction(self.tr("Cancel"), self) - cancel.triggered.connect(self.cancel_transfer) - cancel.setData(transfer) - menu.addAction(cancel) - else: - if isinstance(identity, Identity): - informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.menu_informations) - informations.setData(identity) - menu.addAction(informations) - - add_as_contact = QAction(self.tr("Add as contact"), self) - add_as_contact.triggered.connect(self.menu_add_as_contact) - add_as_contact.setData(identity) - menu.addAction(add_as_contact) - - send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.menu_send_money) - send_money.setData(identity) - menu.addAction(send_money) - - if isinstance(identity, Identity): - view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.view_wot) - view_wot.setData(identity) - menu.addAction(view_wot) - - copy_pubkey = QAction(self.tr("Copy pubkey to clipboard"), self) - copy_pubkey.triggered.connect(self.copy_pubkey_to_clipboard) - copy_pubkey.setData(identity) - menu.addAction(copy_pubkey) + menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, + (identity, transfer)) + menu.view_identity_in_wot.connect(self.view_in_wot) # Show the context menu. - menu.popup(QCursor.pos()) - - def copy_pubkey_to_clipboard(self): - data = self.sender().data() - clipboard = QApplication.clipboard() - if data.__class__ is Wallet: - clipboard.setText(data.pubkey) - elif data.__class__ is Identity: - clipboard.setText(data.pubkey) - elif data.__class__ is str: - clipboard.setText(data) - - def menu_informations(self): - person = self.sender().data() - self.identity_informations(person) - - def menu_add_as_contact(self): - person = self.sender().data() - self.add_identity_as_contact({'name': person.uid, - 'pubkey': person.pubkey}) - - def menu_send_money(self): - identity = self.sender().data() - self.send_money_to_identity(identity) - - def identity_informations(self, person): - dialog = MemberDialog(self.app, self.account, self.community, person) - dialog.exec_() - - def add_identity_as_contact(self, person): - dialog = ConfigureContactDialog(self.account, self.window(), person) - result = dialog.exec_() - if result == QDialog.Accepted: - self.window().refresh_contacts() - - @asyncify - async def send_money_to_identity(self, identity): - await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, - self.community, identity) - self.table_history.model().sourceModel().refresh_transfers() - - @asyncify - async def certify_identity(self, identity): - await CertificationDialog.certify_identity(self.app, self.account, self.password_asker, - self.community, identity) - - def view_wot(self): - identity = self.sender().data() - self.view_in_wot.emit(identity) - - @asyncify - async def send_again(self, checked=False, transfer=None): - result = await TransferMoneyDialog.send_transfer_again(self.app, self.app.current_account, - self.password_asker, self.community, transfer) - self.table_history.model().sourceModel().refresh_transfers() - - def cancel_transfer(self): - reply = QMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -This money transfer will be removed and not sent."""), -QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - transfer = self.sender().data() - transfer.cancel() - self.table_history.model().sourceModel().refresh_transfers() + menu.qmenu.popup(QCursor.pos()) def dates_changed(self): logging.debug("Changed dates") - if self.table_history.model(): - qdate_from = self.date_from + if self.ui.table_history.model(): + qdate_from = self.ui.date_from qdate_from.setTime(QTime(0, 0, 0)) - qdate_to = self.date_to + qdate_to = self.ui.date_to qdate_to.setTime(QTime(0, 0, 0)) ts_from = qdate_from.dateTime().toTime_t() ts_to = qdate_to.dateTime().toTime_t() - self.table_history.model().set_period(ts_from, ts_to) + self.ui.table_history.model().set_period(ts_from, ts_to) self.refresh_balance() def resizeEvent(self, event): - self.busy_balance.resize(event.size()) + self.ui.busy_balance.resize(event.size()) super().resizeEvent(event) def changeEvent(self, event): diff --git a/src/sakia/gui/transfer.py b/src/sakia/gui/transfer.py index e97939ee51759746d6628e34d4fc24195c274b2f..69847078168e6ddf58397af743d08c758fcd2156 100644 --- a/src/sakia/gui/transfer.py +++ b/src/sakia/gui/transfer.py @@ -6,7 +6,7 @@ Created on 2 févr. 2014 import asyncio from PyQt5.QtWidgets import QDialog, QApplication -from PyQt5.QtCore import QRegExp, Qt +from PyQt5.QtCore import QRegExp, Qt, QObject from PyQt5.QtGui import QRegExpValidator @@ -16,24 +16,31 @@ from sakia.gui.widgets.dialogs import QAsyncMessageBox, QMessageBox from ..tools.decorators import asyncify -class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): +class TransferMoneyDialog(QObject): """ classdocs """ - def __init__(self, app, sender, password_asker, community, transfer): + def __init__(self, app, account, password_asker, community, transfer, widget=QDialog, view=Ui_TransferMoneyDialog): """ Constructor :param sakia.core.Application app: The application - :param sakia.core.Account sender: The sender + :param sakia.core.Account account: The account :param sakia.gui.password_asker.Password_Asker password_asker: The password asker + :param sakia.core.Community community: + :param sakia.core.Transfer transfer: + :param class widget: + :param class view: :return: """ super().__init__() - self.setupUi(self) + self.widget = widget() + self.ui = view() + self.ui.setupUi(self.widget) + self.app = app - self.account = sender + self.account = account self.password_asker = password_asker self.recipient_trusts = [] self.transfer = transfer @@ -41,41 +48,54 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): self.community = community if community else self.account.communities[0] self.wallet = self.account.wallets[0] + self.ui.radio_contact.toggled.connect(lambda c, radio="contact": self.recipient_mode_changed(radio)) + self.ui.radio_pubkey.toggled.connect(lambda c, radio="pubkey": self.recipient_mode_changed(radio)) + self.ui.radio_search.toggled.connect(lambda c, radio="search": self.recipient_mode_changed(radio)) + self.ui.button_box.accepted.connect(self.accept) + self.ui.button_box.rejected.connect(self.widget.reject) + self.ui.combo_wallets.currentIndexChanged.connect(self.change_displayed_wallet) + self.ui.combo_community.currentIndexChanged.connect(self.change_current_community) + self.ui.spinbox_relative.valueChanged.connect(self.relative_amount_changed) + self.ui.spinbox_amount.valueChanged.connect(self.amount_changed) + self.ui.search_user.button_reset.hide() + self.ui.search_user.init(self.app) + self.ui.search_user.change_account(self.account) + self.ui.search_user.change_community(self.community) + regexp = QRegExp('^([ a-zA-Z0-9-_:/;*?\[\]\(\)\\\?!^+=@&~#{}|<>%.]{0,255})$') validator = QRegExpValidator(regexp) - self.edit_message.setValidator(validator) + self.ui.edit_message.setValidator(validator) for community in self.account.communities: - self.combo_community.addItem(community.currency) + self.ui.combo_community.addItem(community.currency) for wallet in self.account.wallets: - self.combo_wallets.addItem(wallet.name) + self.ui.combo_wallets.addItem(wallet.name) - for contact_name in sorted([c['name'] for c in sender.contacts], key=str.lower): - self.combo_contact.addItem(contact_name) + for contact_name in sorted([c['name'] for c in account.contacts], key=str.lower): + self.ui.combo_contact.addItem(contact_name) if len(self.account.contacts) == 0: - self.combo_contact.setEnabled(False) - self.radio_contact.setEnabled(False) - self.radio_pubkey.setChecked(True) + self.ui.combo_contact.setEnabled(False) + self.ui.radio_contact.setEnabled(False) + self.ui.radio_pubkey.setChecked(True) - self.combo_community.setCurrentText(self.community.name) + self.ui.combo_community.setCurrentText(self.community.name) if self.transfer: - sender = self.transfer.metadata['issuer'] - wallet_index = [w.pubkey for w in app.current_account.wallets].index(sender) - self.combo_wallets.setCurrentIndex(wallet_index) - self.edit_pubkey.setText(transfer.metadata['receiver']) - self.radio_pubkey.setChecked(True) - self.edit_message.setText(transfer.metadata['comment']) - + account = self.transfer.metadata['issuer'] + wallet_index = [w.pubkey for w in app.current_account.wallets].index(account) + self.ui.combo_wallets.setCurrentIndex(wallet_index) + self.ui.edit_pubkey.setText(transfer.metadata['receiver']) + self.ui.radio_pubkey.setChecked(True) + self.ui.edit_message.setText(transfer.metadata['comment']) @classmethod async def send_money_to_identity(cls, app, account, password_asker, community, identity): dialog = cls(app, account, password_asker, community, None) dialog.edit_pubkey.setText(identity.pubkey) dialog.radio_pubkey.setChecked(True) - return (await dialog.async_exec()) + return await dialog.async_exec() @classmethod async def send_transfer_again(cls, app, account, password_asker, community, transfer): @@ -86,25 +106,32 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): dialog.spinbox_relative.setMaximum(relative) dialog.spinbox_amount.setValue(transfer.metadata['amount']) - return (await dialog.async_exec()) + return await dialog.async_exec() @asyncify async def accept(self): - comment = self.edit_message.text() + self.ui.button_box.setEnabled(False) + comment = self.ui.edit_message.text() - if self.radio_contact.isChecked(): + if self.ui.radio_contact.isChecked(): for contact in self.account.contacts: - if contact['name'] == self.combo_contact.currentText(): + if contact['name'] == self.ui.combo_contact.currentText(): recipient = contact['pubkey'] break + elif self.ui.radio_search.isChecked(): + if self.ui.search_user.current_identity(): + recipient = self.ui.search_user.current_identity().pubkey + else: + return else: - recipient = self.edit_pubkey.text() - amount = self.spinbox_amount.value() + recipient = self.ui.edit_pubkey.text() + amount = self.ui.spinbox_amount.value() if not amount: await QAsyncMessageBox.critical(self, self.tr("Money transfer"), self.tr("No amount. Please give the transfert amount"), QMessageBox.Ok) + self.ui.button_box.setEnabled(True) return password = await self.password_asker.async_exec() @@ -120,7 +147,7 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): toast.display(self.tr("Transfer"), self.tr("Success sending money to {0}").format(recipient)) else: - await QAsyncMessageBox.information(self, self.tr("Transfer"), + await QAsyncMessageBox.information(self.widget, self.tr("Transfer"), self.tr("Success sending money to {0}").format(recipient)) QApplication.restoreOverrideCursor() @@ -128,30 +155,31 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): if self.transfer: self.transfer.cancel() - super().accept() + self.widget.accept() else: if self.app.preferences['notifications']: toast.display(self.tr("Transfer"), "Error : {0}".format(result[1])) else: - await QAsyncMessageBox.critical(self, self.tr("Transfer"), result[1]) + await QAsyncMessageBox.critical(self.widget, self.tr("Transfer"), result[1]) QApplication.restoreOverrideCursor() + self.ui.button_box.setEnabled(True) @asyncify async def amount_changed(self, value): dividend = await self.community.dividend() relative = value / dividend - self.spinbox_relative.blockSignals(True) - self.spinbox_relative.setValue(relative) - self.spinbox_relative.blockSignals(False) + self.ui.spinbox_relative.blockSignals(True) + self.ui.spinbox_relative.setValue(relative) + self.ui.spinbox_relative.blockSignals(False) @asyncify async def relative_amount_changed(self, value): dividend = await self.community.dividend() amount = value * dividend - self.spinbox_amount.blockSignals(True) - self.spinbox_amount.setValue(amount) - self.spinbox_amount.blockSignals(False) + self.ui.spinbox_amount.blockSignals(True) + self.ui.spinbox_amount.setValue(amount) + self.ui.spinbox_amount.blockSignals(False) @asyncify async def change_current_community(self, index): @@ -161,13 +189,13 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): ref_text = await self.account.current_ref(amount, self.community, self.app)\ .diff_localized(units=True, international_system=self.app.preferences['international_system_of_units']) - self.label_total.setText("{0}".format(ref_text)) - self.spinbox_amount.setSuffix(" " + self.community.currency) + self.ui.label_total.setText("{0}".format(ref_text)) + self.ui.spinbox_amount.setSuffix(" " + self.community.currency) amount = await self.wallet.value(self.community) dividend = await self.community.dividend() relative = amount / dividend - self.spinbox_amount.setMaximum(amount) - self.spinbox_relative.setMaximum(relative) + self.ui.spinbox_amount.setMaximum(amount) + self.ui.spinbox_relative.setMaximum(relative) @asyncify async def change_displayed_wallet(self, index): @@ -176,19 +204,23 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): ref_text = await self.account.current_ref(amount, self.community, self.app)\ .diff_localized(units=True, international_system=self.app.preferences['international_system_of_units']) - self.label_total.setText("{0}".format(ref_text)) + self.ui.label_total.setText("{0}".format(ref_text)) amount = await self.wallet.value(self.community) dividend = await self.community.dividend() relative = amount / dividend - self.spinbox_amount.setMaximum(amount) - self.spinbox_relative.setMaximum(relative) + self.ui.spinbox_amount.setMaximum(amount) + self.ui.spinbox_relative.setMaximum(relative) - def recipient_mode_changed(self, pubkey_toggled): - self.edit_pubkey.setEnabled(pubkey_toggled) - self.combo_contact.setEnabled(not pubkey_toggled) + def recipient_mode_changed(self, radio): + self.ui.edit_pubkey.setEnabled(radio == "pubkey") + self.ui.combo_contact.setEnabled(radio == "contact") + self.ui.search_user.setEnabled(radio == "search") def async_exec(self): future = asyncio.Future() - self.finished.connect(lambda r: future.set_result(r)) - self.open() + self.widget.finished.connect(lambda r: future.set_result(r)) + self.widget.open() return future + + def exec(self): + self.widget.exec() \ No newline at end of file diff --git a/src/sakia/gui/views/nodes/base_node.py b/src/sakia/gui/views/nodes/base_node.py index f486d1c556f02bb0d5876a8b953e0ebda91bd42e..04aadd2f43f37099306b6751f48bda7d4972658b 100644 --- a/src/sakia/gui/views/nodes/base_node.py +++ b/src/sakia/gui/views/nodes/base_node.py @@ -1,9 +1,8 @@ -from PyQt5.QtWidgets import QGraphicsEllipseItem, \ - QMenu, QAction, QGraphicsSceneHoverEvent, \ +from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsSceneHoverEvent, \ QGraphicsSceneContextMenuEvent -from PyQt5.QtCore import Qt, QCoreApplication, QT_TRANSLATE_NOOP, pyqtSignal +from PyQt5.QtCore import Qt from PyQt5.QtGui import QMouseEvent -from sakia.core.graph.constants import NodeStatus +from ....core.graph.constants import NodeStatus class BaseNode(QGraphicsEllipseItem): @@ -65,68 +64,4 @@ class BaseNode(QGraphicsEllipseItem): # no menu on the wallet node if self.status_wallet: return None - # create node context menus - self.menu = QMenu() - # action show member - QT_TRANSLATE_NOOP('WoT.Node', 'Informations') - self.action_show_member = QAction(QCoreApplication.translate('WoT.Node', 'Informations'), self.scene()) - self.menu.addAction(self.action_show_member) - self.action_show_member.triggered.connect(self.member_action) - # action add identity as contact - QT_TRANSLATE_NOOP('WoT.Node', 'Add as contact') - self.action_contact = QAction(QCoreApplication.translate('WoT.Node', 'Add as contact'), self.scene()) - self.menu.addAction(self.action_contact) - self.action_contact.triggered.connect(self.contact_action) - # action transaction toward identity - QT_TRANSLATE_NOOP('WoT.Node', 'Send money') - self.action_transaction = QAction(QCoreApplication.translate('WoT.Node', 'Send money'), self.scene()) - self.menu.addAction(self.action_transaction) - self.action_transaction.triggered.connect(self.transaction_action) - # action sign identity - QT_TRANSLATE_NOOP('WoT.Node', 'Certify identity') - self.action_sign = QAction(QCoreApplication.translate('WoT.Node', 'Certify identity'), self.scene()) - self.menu.addAction(self.action_sign) - self.action_sign.triggered.connect(self.sign_action) - # action copy identity pubkey - QT_TRANSLATE_NOOP('WoT.Node', 'Copy pubkey') - self.action_copy = QAction(QCoreApplication.translate('WoT.Node', 'Copy pubkey'), self.scene()) - self.menu.addAction(self.action_copy) - self.action_copy.triggered.connect(self.copy_action) - - # run menu - self.menu.exec(event.screenPos()) - - def member_action(self): - """ - Transaction action to identity node - """ - # trigger scene signal - self.scene().node_member.emit(self.id, self.metadata) - - def contact_action(self): - """ - Transaction action to identity node - """ - # trigger scene signal - self.scene().node_contact.emit(self.id, self.metadata) - - def sign_action(self): - """ - Sign identity node - """ - # trigger scene signal - self.scene().node_signed.emit(self.id, self.metadata) - - def copy_action(self): - """ - Copy identity node pubkey - """ - # trigger scene signal - self.scene().node_copy_pubkey.emit(self.id) - - def transaction_action(self): - """ - Transaction action to identity node - """ - # trigger scene signal - self.scene().node_transaction.emit(self.id, self.metadata) + self.scene().node_context_menu_requested.emit(self.id) diff --git a/src/sakia/gui/views/nodes/explorer_node.py b/src/sakia/gui/views/nodes/explorer_node.py index bedc9b9df18957407021d3a5e1058122b7cfa3ac..7f985e91950a9d25c3a4195568cd908892c7d827 100644 --- a/src/sakia/gui/views/nodes/explorer_node.py +++ b/src/sakia/gui/views/nodes/explorer_node.py @@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt, QPointF, QTimeLine, QTimer from PyQt5.QtGui import QTransform, QColor, QPen, QBrush, QRadialGradient from ....core.graph.constants import NodeStatus from .base_node import BaseNode -import logging import math diff --git a/src/sakia/gui/views/scenes/base_scene.py b/src/sakia/gui/views/scenes/base_scene.py index 7a4762b0c4926d4d9fc473c06b840c789828c3c6..36cd72880a4def8abc332cd47f0b5c06e68aaede 100644 --- a/src/sakia/gui/views/scenes/base_scene.py +++ b/src/sakia/gui/views/scenes/base_scene.py @@ -1,16 +1,12 @@ from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QGraphicsScene +from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneContextMenuEvent class BaseScene(QGraphicsScene): # This defines signals taking string arguments - node_clicked = pyqtSignal(str, dict) - node_signed = pyqtSignal(str, dict) - node_transaction = pyqtSignal(str, dict) - node_contact = pyqtSignal(str, dict) - node_member = pyqtSignal(str, dict) - node_copy_pubkey = pyqtSignal(str) + node_context_menu_requested = pyqtSignal(str) node_hovered = pyqtSignal(str) + node_clicked = pyqtSignal(str, dict) def __init__(self, parent=None): - super().__init__(parent) \ No newline at end of file + super().__init__(parent) diff --git a/src/sakia/gui/views/wot.py b/src/sakia/gui/views/wot.py index c4e874e971f68cad026eacfe17d819c1c3d657a5..056962d84469f4000daec002861fac40f9066ad9 100644 --- a/src/sakia/gui/views/wot.py +++ b/src/sakia/gui/views/wot.py @@ -1,10 +1,6 @@ -import networkx -from PyQt5.QtCore import Qt, QPoint, pyqtSignal +from PyQt5.QtCore import Qt from PyQt5.QtGui import QPainter, QWheelEvent -from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene - -from .edges import WotEdge -from .nodes import WotNode +from PyQt5.QtWidgets import QGraphicsView from .scenes import WotScene @@ -15,7 +11,7 @@ class WotView(QGraphicsView): :param parent: [Optional, default=None] Parent widget """ - super(WotView, self).__init__(parent) + super().__init__(parent) self.setScene(WotScene(self)) @@ -44,4 +40,4 @@ class WotView(QGraphicsView): # act normally on scrollbar else: # transmit event to parent class wheelevent - super().wheelEvent(event) + super().wheelEvent(event) \ No newline at end of file diff --git a/src/sakia/gui/widgets/context_menu.py b/src/sakia/gui/widgets/context_menu.py new file mode 100644 index 0000000000000000000000000000000000000000..e328bf39b15036e646bfb87fe28024e705bb148e --- /dev/null +++ b/src/sakia/gui/widgets/context_menu.py @@ -0,0 +1,213 @@ +from PyQt5.QtWidgets import QMenu, QAction, QApplication, QMessageBox +from PyQt5.QtCore import QObject, pyqtSignal +from ucoinpy.documents import Block, Membership +from ..member import MemberDialog +from ..contact import ConfigureContactDialog +from ..transfer import TransferMoneyDialog +from ..certification import CertificationDialog +from ...tools.decorators import asyncify +from ...core.transfer import Transfer, TransferState +from ...core.registry import Identity + + +class ContextMenu(QObject): + view_identity_in_wot = pyqtSignal(object) + + def __init__(self, qmenu, app, account, community, password_asker): + """ + :param PyQt5.QtWidgets.QMenu: the qmenu widget + :param sakia.core.Application app: Application instance + :param sakia.core.Account account: The current account instance + :param sakia.core.Community community: The community instance + :param sakia.gui.PasswordAsker password_asker: The password dialog + """ + super().__init__() + self.qmenu = qmenu + self._app = app + self._community = community + self._account = account + self._password_asker = password_asker + + @staticmethod + def _add_identity_actions(menu, identity): + """ + :param ContextMenu menu: the qmenu to add actions to + :param Identity identity: the identity + """ + menu.qmenu.addSeparator().setText(identity.uid) + + informations = QAction(menu.qmenu.tr("Informations"), menu.qmenu.parent()) + informations.triggered.connect(lambda checked, i=identity: menu.informations(i)) + menu.qmenu.addAction(informations) + + add_as_contact = QAction(menu.qmenu.tr("Add as contact"), menu.qmenu.parent()) + add_as_contact.triggered.connect(lambda checked,i=identity: menu.add_as_contact(i)) + menu.qmenu.addAction(add_as_contact) + + send_money = QAction(menu.qmenu.tr("Send money"), menu.qmenu.parent()) + send_money.triggered.connect(lambda checked, i=identity: menu.send_money(i)) + menu.qmenu.addAction(send_money) + + certify = QAction(menu.tr("Certify identity"), menu.qmenu.parent()) + certify.triggered.connect(lambda checked, i=identity: menu.certify_identity(i)) + menu.qmenu.addAction(certify) + + view_wot = QAction(menu.qmenu.tr("View in Web of Trust"), menu.qmenu.parent()) + view_wot.triggered.connect(lambda checked, i=identity: menu.view_wot(i)) + menu.qmenu.addAction(view_wot) + + copy_pubkey = QAction(menu.qmenu.tr("Copy pubkey to clipboard"), menu.qmenu.parent()) + copy_pubkey.triggered.connect(lambda checked, i=identity: ContextMenu.copy_pubkey_to_clipboard(i)) + menu.qmenu.addAction(copy_pubkey) + + if menu._app.preferences['expert_mode']: + copy_membership = QAction(menu.qmenu.tr("Copy membership document to clipboard"), menu.qmenu.parent()) + copy_membership.triggered.connect(lambda checked, i=identity: menu.copy_membership_to_clipboard(i)) + # TODO: Copy membership when written field is available + #menu.qmenu.addAction(copy_membership) + + copy_selfcert = QAction(menu.qmenu.tr("Copy self-certification document to clipboard"), menu.qmenu.parent()) + copy_selfcert.triggered.connect(lambda checked, i=identity: menu.copy_selfcert_to_clipboard(i)) + menu.qmenu.addAction(copy_selfcert) + + @staticmethod + def _add_transfers_actions(menu, transfer): + """ + :param ContextMenu menu: the qmenu to add actions to + :param Transfer transfer: the transfer + """ + menu.qmenu.addSeparator().setText(menu.qmenu.tr("Transfer")) + if transfer.state in (TransferState.REFUSED, TransferState.TO_SEND): + send_back = QAction(menu.qmenu.tr("Send again"), menu.qmenu.parent()) + send_back.triggered.connect(lambda checked, tr=transfer: menu.send_again(tr)) + menu.qmenu.addAction(send_back) + + cancel = QAction(menu.qmenu.tr("Cancel"), menu.qmenu.parent()) + cancel.triggered.connect(lambda checked, tr=transfer: menu.cancel_transfer(tr)) + menu.qmenu.addAction(cancel) + + if menu._app.preferences['expert_mode']: + copy_doc = QAction(menu.qmenu.tr("Copy raw transaction to clipboard"), menu.qmenu.parent()) + copy_doc.triggered.connect(lambda checked, tx=transfer: menu.copy_transaction_to_clipboard(tx)) + menu.qmenu.addAction(copy_doc) + + copy_doc = QAction(menu.qmenu.tr("Copy transaction block to clipboard"), menu.qmenu.parent()) + copy_doc.triggered.connect(lambda checked, number=transfer.blockid.number: + menu.copy_block_to_clipboard(number)) + menu.qmenu.addAction(copy_doc) + + + @classmethod + def from_data(cls, parent, app, account, community, password_asker, data): + """ + Builds a QMenu from data passed as parameters + Data can be Identity or Transfer + + :param PyQt5.QtWidgets.QWidget parent: the parent widget + :param sakia.core.Application app: the application + :param sakia.core.Application app: Application instance + :param sakia.core.Account account: The current account instance + :param sakia.core.Community community: The community instance + :param sakia.gui.PasswordAsker password_asker: The password dialog + :param tuple data: a tuple of data to add to the menu + :rtype: ContextMenu + """ + menu = cls(QMenu(parent), app, account, community, password_asker) + build_actions = { + Identity: ContextMenu._add_identity_actions, + Transfer: ContextMenu._add_transfers_actions, + dict: lambda m, d: None + } + for d in data: + build_actions[type(d)](menu, d) + + return menu + + @staticmethod + def copy_pubkey_to_clipboard(identity): + clipboard = QApplication.clipboard() + clipboard.setText(identity.pubkey) + + def informations(self, identity): + MemberDialog.open_dialog(self._app, self._account, self._community, identity) + + def add_as_contact(self, identity): + dialog = ConfigureContactDialog.from_identity( self.parent(), self._account, identity) + dialog.exec_() + #TODO: Send signal from account to refresh contacts + # if result == QDialog.Accepted: + # self.parent().window().refresh_contacts() + + @asyncify + async def send_money(self, identity): + await TransferMoneyDialog.send_money_to_identity(self._app, self._account, self._password_asker, + self._community, identity) + #TODO: Send signal from account to refresh transfers + #self.ui.table_history.model().sourceModel().refresh_transfers() + + def view_wot(self, identity): + self.view_identity_in_wot.emit(identity) + + @asyncify + async def certify_identity(self, identity): + await CertificationDialog.certify_identity(self._app, self._account, self._password_asker, + self._community, identity) + + @asyncify + async def send_again(self, transfer): + await TransferMoneyDialog.send_transfer_again(self._app, self._app.current_account, + self._password_asker, self._community, transfer) + #TODO: Send signal from account to refresh transfers + #self.ui.table_history.model().sourceModel().refresh_transfers() + + def cancel_transfer(self, transfer): + reply = QMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +This money transfer will be removed and not sent."""), +QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + transfer.cancel() + #TODO: Send signal from transfer to refresh transfers + #self.ui.table_history.model().sourceModel().refresh_transfers() + + @asyncify + async def copy_transaction_to_clipboard(self, tx): + clipboard = QApplication.clipboard() + raw_doc = await tx.get_raw_document(self._community) + clipboard.setText(raw_doc.signed_raw()) + + @asyncify + async def copy_block_to_clipboard(self, number): + clipboard = QApplication.clipboard() + block = await self._community.get_block(number) + if block: + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + clipboard.setText(block_doc.signed_raw()) + + @asyncify + async def copy_membership_to_clipboard(self, identity): + """ + + :param sakia.core.registry.Identity identity: + :return: + """ + clipboard = QApplication.clipboard() + membership = await identity.membership(self._community) + if membership: + block_number = membership['blockNumber'] + block = await self._community.get_block(block_number) + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + for ms_doc in block_doc.joiners: + if ms_doc.issuer == identity.pubkey: + clipboard.setText(ms_doc.signed_raw()) + + @asyncify + async def copy_selfcert_to_clipboard(self, identity): + """ + + :param sakia.core.registry.Identity identity: + :return: + """ + clipboard = QApplication.clipboard() + selfcert = await identity.selfcert(self._community) + clipboard.setText(selfcert.signed_raw()) diff --git a/src/sakia/gui/widgets/search_user.py b/src/sakia/gui/widgets/search_user.py index 94114755631209cefe8ea8551beecb72cc4d0a59..e93b906e39abfcfd01d07b1a66651055ef9beaba 100644 --- a/src/sakia/gui/widgets/search_user.py +++ b/src/sakia/gui/widgets/search_user.py @@ -1,6 +1,6 @@ import logging -from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP +from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP, Qt from PyQt5.QtWidgets import QComboBox, QWidget from ucoinpy.api import bma @@ -37,6 +37,10 @@ class SearchUserWidget(QWidget, Ui_SearchUserWidget): self.community = None self.account = None self.app = None + self._current_identity = None + + def current_identity(self): + return self._current_identity def init(self, app): """ @@ -77,6 +81,15 @@ class SearchUserWidget(QWidget, Ui_SearchUserWidget): self.combobox_search.addItem(uid) self.blockSignals(False) self.combobox_search.showPopup() + except ValueError as e: + if '404' in str(e): + self.nodes = list() + self.blockSignals(True) + self.combobox_search.clear() + self.blockSignals(False) + self.combobox_search.showPopup() + else: + pass except NoPeerAvailable: pass @@ -85,17 +98,19 @@ class SearchUserWidget(QWidget, Ui_SearchUserWidget): Select node in graph when item is selected in combobox """ if index < 0 or index >= len(self.nodes): + self._current_identity = None return False node = self.nodes[index] metadata = {'id': node['pubkey'], 'text': node['uid']} - self.identity_selected.emit( - self.app.identities_registry.from_handled_data( + self._current_identity = self.app.identities_registry.from_handled_data( metadata['text'], metadata['id'], None, BlockchainState.VALIDATED, self.community ) + self.identity_selected.emit( + self._current_identity ) def retranslateUi(self, widget): @@ -104,3 +119,9 @@ class SearchUserWidget(QWidget, Ui_SearchUserWidget): """ self.combobox_search.lineEdit().setPlaceholderText(self.tr(SearchUserWidget._search_placeholder)) super().retranslateUi(self) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Return: + return + + super().keyPressEvent(event) diff --git a/src/sakia/main.py b/src/sakia/main.py index 65844f363335e26a36e549b38bc3f29c00c1f34a..8058c3324f3d79366edcf76c175031a18bd8ba19 100755 --- a/src/sakia/main.py +++ b/src/sakia/main.py @@ -16,7 +16,7 @@ import jsonschema # To force cx_freeze import import PyQt5.QtSvg -from quamash import QEventLoop +from quamash import QSelectorEventLoop from PyQt5.QtWidgets import QApplication from sakia.gui.mainwindow import MainWindow from sakia.core.app import Application @@ -61,7 +61,7 @@ if __name__ == '__main__': # activate ctrl-c interrupt signal.signal(signal.SIGINT, signal.SIG_DFL) sakia = QApplication(sys.argv) - loop = QEventLoop(sakia) + loop = QSelectorEventLoop(sakia) loop.set_exception_handler(async_exception_handler) asyncio.set_event_loop(loop) diff --git a/src/sakia/models/identities.py b/src/sakia/models/identities.py index 9cdbab2b69e9a56ed9d278935f12cdfb0d186ab8..c49e44f8f0a545c90dfec0f890e9d6948ea761ba 100644 --- a/src/sakia/models/identities.py +++ b/src/sakia/models/identities.py @@ -7,7 +7,7 @@ Created on 5 févr. 2014 from ..tools.exceptions import NoPeerAvailable, MembershipNotFoundError from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, \ QDateTime, QModelIndex, QLocale, QEvent -from PyQt5.QtGui import QColor +from PyQt5.QtGui import QColor, QIcon import logging import asyncio @@ -67,9 +67,19 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): if role == Qt.ForegroundRole: if expiration_data: if will_expire_soon: - return QColor(Qt.red) + return QColor("darkorange").darker(120) else: return QColor(Qt.blue) + + if role == Qt.DecorationRole and source_index.column() == self.sourceModel().columns_ids.index('uid'): + if expiration_data: + if will_expire_soon: + return QIcon(":/icons/member_warning") + else: + return QIcon(":/icons/member") + else: + return QIcon(":/icons/not_member") + return source_data diff --git a/src/sakia/models/network.py b/src/sakia/models/network.py index 3a5538eaf01a04726132b38307c7a231de58ca35..e8b1ee4f614dc3a6aabfacb89c993e9b04559757 100644 --- a/src/sakia/models/network.py +++ b/src/sakia/models/network.py @@ -8,7 +8,7 @@ import logging import asyncio from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel -from PyQt5.QtGui import QColor, QFont +from PyQt5.QtGui import QColor, QFont, QIcon from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task @@ -131,6 +131,12 @@ class NetworkTableModel(QAbstractTableModel): Node.DESYNCED: QColor('#ffbd81'), Node.CORRUPTED: QColor(Qt.lightGray) } + self.node_icons = { + Node.ONLINE: ":/icons/synchronized", + Node.OFFLINE: ":/icons/offline", + Node.DESYNCED: ":/icons/forked", + Node.CORRUPTED: ":/icons/corrupted" + } self.node_states = { Node.ONLINE: lambda: self.tr('Online'), Node.OFFLINE: lambda: self.tr('Offline'), @@ -214,6 +220,9 @@ class NetworkTableModel(QAbstractTableModel): if role == Qt.ToolTipRole: return self.node_states[node[self.columns_types.index('state')]]() + if role == Qt.DecorationRole and index.column() == 0: + return QIcon(self.node_icons[node[self.columns_types.index('state')]]) + return QVariant() def flags(self, index): diff --git a/src/sakia/models/txhistory.py b/src/sakia/models/txhistory.py index d138dada1f9d006d46f344bdc1ef6c6672f70a5e..5bea986af16723e039923a3fcc852c568e6a993a 100644 --- a/src/sakia/models/txhistory.py +++ b/src/sakia/models/txhistory.py @@ -13,7 +13,7 @@ from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ QDateTime, QLocale, QModelIndex -from PyQt5.QtGui import QFont, QColor +from PyQt5.QtGui import QFont, QColor, QIcon class TxFilterProxyModel(QSortFilterProxyModel): @@ -169,7 +169,6 @@ class TxFilterProxyModel(QSortFilterProxyModel): return self.tr("Confirming... {0} %").format(QLocale().toString(float(confirmation), 'f', 0)) return None - return source_data @@ -231,7 +230,8 @@ class HistoryTableModel(QAbstractTableModel): async def data_received(self, transfer): amount = transfer.metadata['amount'] - deposit = await self.account.current_ref(transfer.metadata['amount'], self.community, self.app)\ + deposit = await self.account.current_ref(transfer.metadata['amount'], self.community, + self.app, transfer.blockid.number)\ .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" if transfer.metadata['comment'] != "": @@ -254,7 +254,8 @@ class HistoryTableModel(QAbstractTableModel): async def data_sent(self, transfer): amount = transfer.metadata['amount'] - paiment = await self.account.current_ref(transfer.metadata['amount'], self.community, self.app)\ + paiment = await self.account.current_ref(transfer.metadata['amount'], self.community, + self.app, transfer.blockid.number)\ .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" if transfer.metadata['comment'] != "": @@ -277,7 +278,7 @@ class HistoryTableModel(QAbstractTableModel): async def data_dividend(self, dividend): amount = dividend['amount'] - deposit = await self.account.current_ref(dividend['amount'], self.community, self.app)\ + deposit = await self.account.current_ref(dividend['amount'], self.community, self.app, dividend['block_number'])\ .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" receiver = self.account.name @@ -334,7 +335,7 @@ class HistoryTableModel(QAbstractTableModel): if self.columns_types[section] == 'payment' or self.columns_types[section] == 'deposit': return '{:}\n({:})'.format( self.column_headers[section](), - self.account.current_ref.diff_units(self.community.short_currency) + self.account.current_ref(0, self.community, self.app, None).diff_units ) return self.column_headers[section]() @@ -352,6 +353,15 @@ class HistoryTableModel(QAbstractTableModel): if role == Qt.ToolTipRole: return self.transfers_data[row][col] + if role == Qt.DecorationRole and index.column() == 0: + transfer = self.transfers_data[row] + if transfer[self.columns_types.index('payment')] != "": + return QIcon(":/icons/sent") + elif transfer[self.columns_types.index('uid')] == self.account.name: + return QIcon(":/icons/dividend") + else: + return QIcon(":/icons/received") + def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled diff --git a/src/sakia/models/wallets.py b/src/sakia/models/wallets.py index a656f182d05107b72ba6dc777cbf32967a83c61d..4428dcbb9cc070488c58c4ac009857f0ff74541b 100644 --- a/src/sakia/models/wallets.py +++ b/src/sakia/models/wallets.py @@ -112,7 +112,7 @@ class WalletsTableModel(QAbstractTableModel): if self.columns_types[section] == 'amount': return '{:}\n({:})'.format( self.columns_headers[section], - self.account.current_ref.units(self.community.short_currency) + self.account.current_ref(0, self.community, self.app, None).units ) return self.columns_headers[section] diff --git a/src/sakia/tests/functional/certification/test_certification.py b/src/sakia/tests/functional/certification/test_certification.py index 322cb71c2a78d131391c70e765f9f7df9fc0d510..571766802261f27d67488786f7c2fc6fef7e2c73 100644 --- a/src/sakia/tests/functional/certification/test_certification.py +++ b/src/sakia/tests/functional/certification/test_certification.py @@ -1,24 +1,21 @@ import sys import unittest import asyncio -import quamash import time import logging from ucoinpy.documents.peer import BMAEndpoint from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication from PyQt5.QtCore import QLocale, Qt from PyQt5.QtTest import QTest -from ucoinpy.api.bma import API from sakia.tests.mocks.bma import init_new_community from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.certification import CertificationDialog +from sakia.gui.certification import CertificationDialog, Ui_CertificationDialog from sakia.gui.password_asker import PasswordAskerDialog from sakia.core.app import Application from sakia.core import Account, Community, Wallet from sakia.core.net import Network, Node from sakia.core.net.api.bma.access import BmaAccess from sakia.tests import QuamashTest -from ucoinpy.api import bma class TestCertificationDialog(unittest.TestCase, QuamashTest): @@ -30,8 +27,10 @@ class TestCertificationDialog(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False + self.mock_new_community = init_new_community.get_mock(self.lp) + self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50010) - self.node = Node("test_currency", [self.endpoint], + self.node = Node(self.mock_new_community.peer(), "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.14.0", 0) @@ -46,39 +45,35 @@ class TestCertificationDialog(unittest.TestCase, QuamashTest): # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", "john", [self.community], [self.wallet], [], self.identities_registry) - + self.account.notifications['warning_certifying_first_time'] = False self.password_asker = PasswordAskerDialog(self.account) self.password_asker.password = "testsakia" self.password_asker.remember = True - def tearDown(self): - self.tearDownQuamash() - def test_certification_init_community(self): - mock = init_new_community.get_mock(self.lp) time.sleep(2) certification_dialog = CertificationDialog(self.application, self.account, - self.password_asker) + self.password_asker, + QDialog(), + Ui_CertificationDialog()) async def open_dialog(certification_dialog): - srv, port, url = await mock.create_server() + srv, port, url = await self.mock_new_community.create_server() self.addCleanup(srv.close) - self.endpoint.port = port - result = await certification_dialog.async_exec() self.assertEqual(result, QDialog.Accepted) def close_dialog(): - if certification_dialog.isVisible(): - certification_dialog.close() + if certification_dialog.widget.isVisible(): + certification_dialog.widget.close() async def exec_test(): await asyncio.sleep(1) - QTest.mouseClick(certification_dialog.radio_pubkey, Qt.LeftButton) - QTest.keyClicks(certification_dialog.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") - QTest.mouseClick(certification_dialog.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) - await asyncio.sleep(1) + QTest.mouseClick(certification_dialog.ui.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(certification_dialog.ui.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + QTest.mouseClick(certification_dialog.ui.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + await asyncio.sleep(2) topWidgets = QApplication.topLevelWidgets() for w in topWidgets: if type(w) is QMessageBox: diff --git a/src/sakia/tests/functional/identities_tab/test_identities_table.py b/src/sakia/tests/functional/identities_tab/test_identities_table.py index 72be5f0dfb13e0b9c8166b063d4ba2c57fdd293c..bb7aeb65882868e74b4ed9d1e124cd440ef4e1d4 100644 --- a/src/sakia/tests/functional/identities_tab/test_identities_table.py +++ b/src/sakia/tests/functional/identities_tab/test_identities_table.py @@ -31,8 +31,8 @@ class TestIdentitiesTable(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50002) - self.node = Node("test_currency", [self.endpoint], + self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) + self.node = Node(self.mock_nice_blockchain.peer(), "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.14.0", 0) @@ -56,29 +56,27 @@ class TestIdentitiesTable(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_search_identity_found(self): - mock = nice_blockchain.get_mock(self.lp) time.sleep(2) identities_tab = IdentitiesTabWidget(self.application) future = asyncio.Future() def open_widget(): - identities_tab.show() + identities_tab.widget.show() return future def close_dialog(): - if identities_tab.isVisible(): - identities_tab.close() + if identities_tab.widget.isVisible(): + identities_tab.widget.close() future.set_result(True) async def exec_test(): - srv, port, url = await mock.create_server() + srv, port, url = await self.mock_nice_blockchain.create_server() self.addCleanup(srv.close) - self.endpoint.port = port identities_tab.change_account(self.account, self.password_asker) identities_tab.change_community(self.community) await asyncio.sleep(1) - urls = [mock.get_request(i).url for i in range(0, 7)] + urls = [self.mock_nice_blockchain.get_request(i).url for i in range(0, 7)] self.assertTrue('/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ' in urls, msg="Not found in {0}".format(urls)) self.assertTrue('/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ' in urls, @@ -90,17 +88,17 @@ class TestIdentitiesTable(unittest.TestCase, QuamashTest): # requests 1 to 3 are for getting certifiers-of and certified-by # on john, + a lookup - QTest.keyClicks(identities_tab.edit_textsearch, "doe") - QTest.mouseClick(identities_tab.button_search, Qt.LeftButton) + QTest.keyClicks(identities_tab.ui.edit_textsearch, "doe") + QTest.mouseClick(identities_tab.ui.button_search, Qt.LeftButton) await asyncio.sleep(2) req = 8 - self.assertEqual(mock.get_request(req).method, 'GET') - self.assertEqual(mock.get_request(req).url, + self.assertEqual(self.mock_nice_blockchain.get_request(req).method, 'GET') + self.assertEqual(self.mock_nice_blockchain.get_request(req).url, '/blockchain/memberships/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn') req += 1 - self.assertEqual(identities_tab.table_identities.model().rowCount(), 1) + self.assertEqual(identities_tab.ui.table_identities.model().rowCount(), 1) await asyncio.sleep(2) self.lp.call_soon(close_dialog) diff --git a/src/sakia/tests/functional/main_window/test_main_window_dialogs.py b/src/sakia/tests/functional/main_window/test_main_window_dialogs.py index 97d0a5f88887c751ea286230f6992c21a21bc2ae..2c054c338787e757bb59fd8cfa7f48b42a255144 100644 --- a/src/sakia/tests/functional/main_window/test_main_window_dialogs.py +++ b/src/sakia/tests/functional/main_window/test_main_window_dialogs.py @@ -1,9 +1,6 @@ import unittest -import asyncio -import quamash from PyQt5.QtWidgets import QDialog, QFileDialog from PyQt5.QtCore import QLocale, QTimer -from PyQt5.QtNetwork import QNetworkAccessManager from sakia.gui.mainwindow import MainWindow from sakia.core.app import Application from sakia.tests import QuamashTest diff --git a/src/sakia/tests/functional/process_cfg_community/test_add_community.py b/src/sakia/tests/functional/process_cfg_community/test_add_community.py index 7f97a3dee9b3712bbb078818bf49b5db67ad7ebd..20fe49931097a2e6c0617c4c3a47e3f92efbd84d 100644 --- a/src/sakia/tests/functional/process_cfg_community/test_add_community.py +++ b/src/sakia/tests/functional/process_cfg_community/test_add_community.py @@ -35,7 +35,6 @@ class ProcessAddCommunity(unittest.TestCase, QuamashTest): def tearDown(self): self.tearDownQuamash() - @unittest.skipIf(sys.platform== "darwin", "Test not working on OSX, but feature is OK") def test_register_community_empty_blockchain(self): mock = new_blockchain.get_mock(self.lp) time.sleep(2) @@ -64,7 +63,7 @@ class ProcessAddCommunity(unittest.TestCase, QuamashTest): await asyncio.sleep(1) self.assertEqual(mock.get_request(0).method, 'GET') self.assertEqual(mock.get_request(0).url, '/network/peering') - self.assertEqual(process_community._step_init.node._endpoints[0].port, port) + self.assertEqual(process_community._step_init.node.endpoint.port, port) self.assertEqual(mock.get_request(1).method, 'GET') self.assertEqual(mock.get_request(1).url, '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') @@ -96,7 +95,6 @@ class ProcessAddCommunity(unittest.TestCase, QuamashTest): self.lp.run_until_complete(process_community.async_exec()) self.assertEqual(process_community.result(), QDialog.Accepted) - @unittest.skipIf(sys.platform== "darwin", "Test not working on OSX, but feature is OK") def test_connect_community_empty_blockchain(self): mock = new_blockchain.get_mock(self.lp) time.sleep(2) @@ -143,7 +141,6 @@ class ProcessAddCommunity(unittest.TestCase, QuamashTest): asyncio.ensure_future(exec_test()) self.lp.run_until_complete(process_community.async_exec()) - @unittest.skipIf(sys.platform== "darwin", "Test not working on OSX, but feature is OK") def test_connect_community_wrong_pubkey(self): mock = nice_blockchain.get_mock(self.lp) time.sleep(2) @@ -185,7 +182,6 @@ Yours : wrong_pubkey, the network : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ self.lp.run_until_complete(process_community.async_exec()) self.assertEqual(process_community.result(), QDialog.Rejected) - @unittest.skipIf(sys.platform== "darwin", "Test not working on OSX, but feature is OK") def test_connect_community_wrong_uid(self): mock = nice_blockchain.get_mock(self.lp) time.sleep(2) @@ -227,7 +223,6 @@ Yours : wrong_uid, the network : john""") self.lp.run_until_complete(process_community.async_exec()) self.assertEqual(process_community.result(), QDialog.Rejected) - @unittest.skipIf(sys.platform== "darwin", "Test not working on OSX, but feature is OK") def test_connect_community_success(self): mock = nice_blockchain.get_mock(self.lp) time.sleep(2) diff --git a/src/sakia/tests/functional/transfer/test_transfer.py b/src/sakia/tests/functional/transfer/test_transfer.py index ee348926d27d7f35be38cbb16a4b5d264372c9a8..6ba1ca1ef1f8f7e78ef3c89efb800c57353d1714 100644 --- a/src/sakia/tests/functional/transfer/test_transfer.py +++ b/src/sakia/tests/functional/transfer/test_transfer.py @@ -31,8 +31,8 @@ class TestTransferDialog(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50002) - self.node = Node("test_currency", [self.endpoint], + self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) + self.node = Node(self.mock_nice_blockchain.peer(), "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.14.0", 0) @@ -56,8 +56,6 @@ class TestTransferDialog(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_transfer_nice_community(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) transfer_dialog = TransferMoneyDialog(self.application, self.account, self.password_asker, @@ -66,9 +64,8 @@ class TestTransferDialog(unittest.TestCase, QuamashTest): self.account.wallets[0].init_cache(self.application, self.community) async def open_dialog(transfer_dialog): - srv, port, url = await mock.create_server() + srv, port, url = await self.mock_nice_blockchain.create_server() self.addCleanup(srv.close) - self.endpoint.port = port result = await transfer_dialog.async_exec() self.assertEqual(result, QDialog.Accepted) @@ -80,10 +77,10 @@ class TestTransferDialog(unittest.TestCase, QuamashTest): async def exec_test(): await asyncio.sleep(1) self.account.wallets[0].caches[self.community.currency].available_sources = await self.wallet.sources(self.community) - QTest.mouseClick(transfer_dialog.radio_pubkey, Qt.LeftButton) - QTest.keyClicks(transfer_dialog.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") - transfer_dialog.spinbox_amount.setValue(10) - QTest.mouseClick(transfer_dialog.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + QTest.mouseClick(transfer_dialog.ui.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(transfer_dialog.ui.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + transfer_dialog.ui.spinbox_amount.setValue(10) + QTest.mouseClick(transfer_dialog.ui.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) await asyncio.sleep(1) topWidgets = QApplication.topLevelWidgets() for w in topWidgets: diff --git a/src/sakia/tests/functional/wot_tab/test_wot_tab.py b/src/sakia/tests/functional/wot_tab/test_wot_tab.py index 0bac059cb2e89515ed55bf521c5d3247026eeb85..fc3def5b612e047e706303fb2b5515bc3f74f56d 100644 --- a/src/sakia/tests/functional/wot_tab/test_wot_tab.py +++ b/src/sakia/tests/functional/wot_tab/test_wot_tab.py @@ -27,8 +27,8 @@ class TestWotTab(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50003) - self.node = Node("test_currency", [self.endpoint], + self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) + self.node = Node(self.mock_nice_blockchain.peer(), "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.14.0", 0) @@ -52,29 +52,26 @@ class TestWotTab(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_empty_wot_tab(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) wot_tab = WotTabWidget(self.application) future = asyncio.Future() def open_widget(): - wot_tab.show() + wot_tab.widget.show() return future async def async_open_widget(): - srv, port, url = await mock.create_server() + srv, port, url = await self.mock_nice_blockchain.create_server() self.addCleanup(srv.close) - self.endpoint.port = port await open_widget() def close_dialog(): - if wot_tab.isVisible(): - wot_tab.close() + if wot_tab.widget.isVisible(): + wot_tab.widget.close() future.set_result(True) async def exec_test(): await asyncio.sleep(1) - self.assertTrue(wot_tab.isVisible()) + self.assertTrue(wot_tab.widget.isVisible()) self.lp.call_soon(close_dialog) asyncio.ensure_future(exec_test()) diff --git a/src/sakia/tests/mocks/server.py b/src/sakia/tests/mocks/server.py index 9e25711c363f9b50497bae949bfe3de438c84efa..d614a72ec6a04fa06cbb87a18ac3adac0180389e 100644 --- a/src/sakia/tests/mocks/server.py +++ b/src/sakia/tests/mocks/server.py @@ -1,6 +1,7 @@ from aiohttp import web, log import json import socket +from ucoinpy.documents import Peer def bma_peering_generator(port): @@ -8,16 +9,28 @@ def bma_peering_generator(port): "version": 1, "currency": "test_currency", "endpoints": [ - "BASIC_MERKLED_API localhost 127.0.0.1 {port}".format(port=port) + "BASIC_MERKLED_API 127.0.0.1 {port}".format(port=port) ], "status": "UP", "block": "30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3", "signature": "cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA==", - "raw": "Version: 1\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\nEndpoints:\nBASIC_MERKLED_API localhost 127.0.0.1 {port}\n".format(port=port), + "raw": "Version: 1\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\nEndpoints:\nBASIC_MERKLED_API 127.0.0.1 {port}\n".format(port=port), "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" } +def peer_document_generator(port): + return Peer.from_signed_raw("""Version: 1 +Type: Peer +Currency: meta_brouzouf +PublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk +Block: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3 +Endpoints: +BASIC_MERKLED_API 127.0.0.1 {port} +cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA== +""".format(port=port)) + + class Request(): def __init__(self, method, url, content): self.url = url @@ -35,6 +48,8 @@ class MockServer(): keep_alive_on=False, access_log=log.access_logger) + self.port = self.find_unused_port() + def get_request(self, i): return self.requests[i] @@ -56,12 +71,14 @@ class MockServer(): s.close() return port + def peer(self): + return peer_document_generator(self.port) + async def create_server(self, ssl_ctx=None): - port = self.find_unused_port() - srv = await self.lp.create_server(self.handler, '127.0.0.1', port) + srv = await self.lp.create_server(self.handler, '127.0.0.1', self.port) protocol = "https" if ssl_ctx else "http" - url = "{}://127.0.0.1:{}".format(protocol, port) + url = "{}://127.0.0.1:{}".format(protocol, self.port) - self.add_route('GET', '/network/peering', bma_peering_generator(port)) + self.add_route('GET', '/network/peering', bma_peering_generator(self.port)) - return srv, port, url \ No newline at end of file + return srv, self.port, url \ No newline at end of file diff --git a/src/sakia/tests/quamash_utils.py b/src/sakia/tests/quamash_utils.py index ae7610528fbec45516a6238d4dca54d9fdd28a99..7c11293ca4cbacbda7815318d3923c4b91eca84f 100644 --- a/src/sakia/tests/quamash_utils.py +++ b/src/sakia/tests/quamash_utils.py @@ -9,7 +9,7 @@ _application_ = [] class QuamashTest: def setUpQuamash(self): self.qapplication = get_application() - self.lp = quamash.QEventLoop(self.qapplication) + self.lp = quamash.QSelectorEventLoop(self.qapplication) asyncio.set_event_loop(self.lp) self.lp.set_exception_handler(lambda l, c: unitttest_exception_handler(self, l, c)) self.exceptions = [] diff --git a/src/sakia/tests/unit/core/money/__init__.py b/src/sakia/tests/unit/core/money/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/sakia/tests/unit/core/money/test_quantitative.py b/src/sakia/tests/unit/core/money/test_quantitative.py new file mode 100644 index 0000000000000000000000000000000000000000..dd32a301ff91a7280f982bb66e68ade163d32be4 --- /dev/null +++ b/src/sakia/tests/unit/core/money/test_quantitative.py @@ -0,0 +1,144 @@ +import unittest +from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.core.money import Quantitative + + +class TestQuantitative(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Quantitative(0, community, app, None) + self.assertEqual(referential.units, "TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Quantitative(0, community, app, None) + self.assertEqual(referential.units, "TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_value(self, app, community): + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertEqual(value, 101010110) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_differential(self, app, community): + referential = Quantitative(110, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertEqual(value, 110) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.localized(units=True) + self.assertEqual(value, "101,010,110 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.localized(units=True, international_system=True) + self.assertEqual(value, "101.010110 MTC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=False) + self.assertEqual(value, "101,010,110") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=True) + self.assertEqual(value, "101.010110 M") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True) + self.assertEqual(value, "101,010,110 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True, international_system=True) + self.assertEqual(value, "101.010110 MTC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=False) + self.assertEqual(value, "101,010,110") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Quantitative(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=True) + self.assertEqual(value, "101.010110 M") + self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_quantitative_zsum.py b/src/sakia/tests/unit/core/money/test_quantitative_zsum.py new file mode 100644 index 0000000000000000000000000000000000000000..68ecaad7c751ba4b8f6fa014c93875e0bc85ebe7 --- /dev/null +++ b/src/sakia/tests/unit/core/money/test_quantitative_zsum.py @@ -0,0 +1,164 @@ +import unittest +from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.core.money import QuantitativeZSum + + +class TestQuantitativeZSum(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = QuantitativeZSum(0, community, app, None) + self.assertEqual(referential.units, "Q0 TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = QuantitativeZSum(0, community, app, None) + self.assertEqual(referential.units, "Q0 TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_value(self, app, community): + referential = QuantitativeZSum(110, community, app, None) + community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) + community.monetary_mass = CoroutineMock(return_value=500) + async def exec_test(): + value = await referential.value() + self.assertEqual(value, 10) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_differential(self, app, community): + community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) + community.monetary_mass = CoroutineMock(return_value=500) + referential = QuantitativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertEqual(value, 10) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) + community.monetary_mass = CoroutineMock(return_value=500) + referential = QuantitativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=True) + self.assertEqual(value, "10 Q0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(110 * 1000, community, app, None) + async def exec_test(): + value = await referential.localized(units=True, international_system=True) + self.assertEqual(value, "109.500000 kQ0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 5}) + community.monetary_mass = CoroutineMock(return_value=500) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=False) + self.assertEqual(value, "10") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(110 * 1000, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=True) + self.assertEqual(value, "109.500000 kQ0 ") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 1000}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) + referential = QuantitativeZSum(110 * 1000, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True) + self.assertEqual(value, "110,000 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True, international_system=True) + self.assertEqual(value, "101.010110 MTC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=False) + self.assertEqual(value, "101,010,110") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.get_ud_block = CoroutineMock(return_value={'membersCount': 10}) + community.monetary_mass = CoroutineMock(return_value=500 * 1000 * 1000) + app.preferences = { + 'digits_after_comma': 6 + } + referential = QuantitativeZSum(101010110, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=True) + self.assertEqual(value, "101.010110 M") + self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_relative.py b/src/sakia/tests/unit/core/money/test_relative.py new file mode 100644 index 0000000000000000000000000000000000000000..ae542b05ae8f5f4110dfebd8370ed67297342b46 --- /dev/null +++ b/src/sakia/tests/unit/core/money/test_relative.py @@ -0,0 +1,160 @@ +import unittest +from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.core.money import Relative + + +class TestRelative(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Relative(0, community, app, None) + self.assertEqual(referential.units, "UD TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = Relative(0, community, app, None) + self.assertEqual(referential.units, "UD TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_value(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + referential = Relative(10101011, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 1010.10110) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_differential(self, app, community): + community.dividend = CoroutineMock(return_value=1000) + referential = Relative(110, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 0.11) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(101, community, app, None) + async def exec_test(): + value = await referential.localized(units=True) + self.assertEqual(value, "0.101000 UD TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.localized(units=True, international_system=True) + self.assertEqual(value, "1.011000 mUD TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=False) + self.assertEqual(value, "0.101100") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=True) + self.assertEqual(value, "1.011000 mUD ") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True) + self.assertEqual(value, "0.101100 UD TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True, international_system=True) + self.assertEqual(value, "1.011000 mUD TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=False) + self.assertEqual(value, "0.001011") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = Relative(1011, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=True) + self.assertEqual(value, "1.011000 mUD ") + self.lp.run_until_complete(exec_test()) \ No newline at end of file diff --git a/src/sakia/tests/unit/core/money/test_relative_to_past.py b/src/sakia/tests/unit/core/money/test_relative_to_past.py new file mode 100644 index 0000000000000000000000000000000000000000..0a0f5e2d83b6512d01d745b1e66bdfb66d9342f5 --- /dev/null +++ b/src/sakia/tests/unit/core/money/test_relative_to_past.py @@ -0,0 +1,192 @@ +import unittest +from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock +from PyQt5.QtCore import QLocale, QDateTime +from sakia.tests import QuamashTest +from sakia.core.money import RelativeToPast + + +class TestRelativeToPast(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = RelativeToPast(0, community, app, 100) + self.assertEqual(referential.units, "UD(t) TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = RelativeToPast(0, community, app, 100) + self.assertEqual(referential.units, "UD(t) TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_value(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + referential = RelativeToPast(10101011, community, app, 100) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 1010.10110) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_differential(self, app, community): + community.dividend = CoroutineMock(return_value=1000) + referential = RelativeToPast(110, community, app, 100) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 0.11) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(101, community, app, 100) + async def exec_test(): + value = await referential.localized(units=True) + self.assertEqual(value, "0.101000 UD({0}) TC".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.localized(units=True, international_system=True) + self.assertEqual(value, "1.011000 mUD({0}) TC".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.localized(units=False, international_system=False) + self.assertEqual(value, "0.101100") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.localized(units=False, international_system=True) + self.assertEqual(value, "1.011000 mUD({0}) ".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.diff_localized(units=True) + self.assertEqual(value, "0.101100 UD({0}) TC".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.diff_localized(units=True, international_system=True) + self.assertEqual(value, "1.011000 mUD({0}) TC".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_no_si(self, app, community): + community.dividend = CoroutineMock(return_value=10000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=False) + self.assertEqual(value, "0.101100") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_with_si(self, app, community): + community.dividend = CoroutineMock(return_value=1000000) + community.get_block = CoroutineMock(return_value={'medianTime': 1452663088792}) + type(community).short_currency = PropertyMock(return_value="TC") + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeToPast(1011, community, app, 100) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=True) + self.assertEqual(value, "1.011000 mUD({0}) ".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(1452663088792).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ))) + self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/money/test_relative_zsum.py b/src/sakia/tests/unit/core/money/test_relative_zsum.py new file mode 100644 index 0000000000000000000000000000000000000000..2a8208997fc6f2a7a34c7fece2fdd5cfe46f0eda --- /dev/null +++ b/src/sakia/tests/unit/core/money/test_relative_zsum.py @@ -0,0 +1,184 @@ +import unittest +from asynctest.mock import Mock, CoroutineMock, patch, PropertyMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.core.money import RelativeZSum + + +class TestRelativeZSum(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = RelativeZSum(0, community, app, None) + self.assertEqual(referential.units, "R0 TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_units(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + referential = RelativeZSum(0, community, app, None) + self.assertEqual(referential.units, "R0 TC") + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_value(self, app, community): + referential = RelativeZSum(110, community, app, None) + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 0.10) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_differential(self, app, community): + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + referential = RelativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.value() + self.assertAlmostEqual(value, 0.10) + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + referential = RelativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=True) + self.assertEqual(value, "0.1 R0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=True, international_system=True) + self.assertEqual(value, "100.000000 mR0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=False) + self.assertEqual(value, "0.100000") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(110, community, app, None) + async def exec_test(): + value = await referential.localized(units=False, international_system=True) + self.assertEqual(value, "100.000000 mR0 ") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + referential = RelativeZSum(90, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True) + self.assertEqual(value, "0.9 R0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(90, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=True, international_system=True) + self.assertEqual(value, "900.000000 mR0 TC") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_no_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(90, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=False) + self.assertEqual(value, "0.900000") + self.lp.run_until_complete(exec_test()) + + @patch('sakia.core.Community') + @patch('sakia.core.Application') + def test_diff_localized_no_units_with_si(self, app, community): + type(community).short_currency = PropertyMock(return_value="TC") + community.dividend = CoroutineMock(return_value=100) + community.get_ud_block = CoroutineMock(side_effect=lambda *args, **kwargs: \ + {'membersCount': 5, "monetaryMass": 500, "dividend": 100} if 'x' in kwargs \ + else {'membersCount': 5, "monetaryMass": 1050, "dividend": 100} ) + app.preferences = { + 'digits_after_comma': 6 + } + referential = RelativeZSum(90, community, app, None) + async def exec_test(): + value = await referential.diff_localized(units=False, international_system=True) + self.assertEqual(value, "900.000000 mR0 ") + self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/test_bma_access.py b/src/sakia/tests/unit/core/test_bma_access.py index 293eb3ddf51a74dbc177c8b33a9825314c7e5380..d382a5bac015fa36ddb10f474d8defd2fecebc65 100644 --- a/src/sakia/tests/unit/core/test_bma_access.py +++ b/src/sakia/tests/unit/core/test_bma_access.py @@ -1,8 +1,4 @@ -import sys import unittest -import asyncio -import quamash -import logging import time from PyQt5.QtCore import QLocale from sakia.core.registry.identities import Identity, IdentitiesRegistry, LocalState, BlockchainState @@ -11,10 +7,8 @@ from sakia.tests.mocks.bma import nice_blockchain, corrupted from sakia.tests import QuamashTest from sakia.core import Application, Community from sakia.core.net import Network, Node -from ucoinpy.documents.peer import BMAEndpoint +from ucoinpy.documents.peer import Peer from sakia.core.net.api.bma.access import BmaAccess -from sakia.tools.exceptions import MembershipNotFoundError -from ucoinpy.api.bma import API class TestBmaAccess(unittest.TestCase, QuamashTest): @@ -26,8 +20,16 @@ class TestBmaAccess(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50004) - self.node = Node("test_currency", [self.endpoint], + self.peer = Peer.from_signed_raw("""Version: 1 +Type: Peer +Currency: meta_brouzouf +PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU +Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 +Endpoints: +BASIC_MERKLED_API ucoin.inso.ovh 80 +82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== +""") + self.node = Node(self.peer, "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.12.0", 0) diff --git a/src/sakia/tests/unit/core/test_community.py b/src/sakia/tests/unit/core/test_community.py index acf5c176be84783ec2fabde14674f3cd9e50e9bd..80e0f2f34de13303b2bf00ab29802b5ccd34bde5 100644 --- a/src/sakia/tests/unit/core/test_community.py +++ b/src/sakia/tests/unit/core/test_community.py @@ -1,6 +1,6 @@ import sys import unittest -import logging +from distutils.version import StrictVersion from PyQt5.QtCore import QLocale from sakia.core.net.api.bma.access import BmaAccess from sakia.core.net.network import Network @@ -22,6 +22,6 @@ class TestCommunity(unittest.TestCase, QuamashTest): community = Community("test_currency", network, bma_access) json_data = community.jsonify() - community_from_json = Community.load(json_data) + community_from_json = Community.load(json_data, StrictVersion('0.12.0')) self.assertEqual(community.name, community_from_json.name) self.assertEqual(len(community.network._nodes), len(community_from_json.network._nodes)) diff --git a/src/sakia/tests/unit/core/test_identity.py b/src/sakia/tests/unit/core/test_identity.py index ec7044f771f6e5e28bfffb9bc5eb4c48a5960d2e..26186f7a190b651fc46b6afb32fc6e3333a82019 100644 --- a/src/sakia/tests/unit/core/test_identity.py +++ b/src/sakia/tests/unit/core/test_identity.py @@ -27,8 +27,8 @@ class TestIdentity(unittest.TestCase, QuamashTest): self.application = Application(self.qapplication, self.lp, self.identities_registry) self.application.preferences['notifications'] = False - self.endpoint = BMAEndpoint("", "127.0.0.1", "", 50009) - self.node = Node("test_currency", [self.endpoint], + self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) + self.node = Node(self.mock_nice_blockchain.peer(), "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", None, Node.ONLINE, time.time(), {}, "ucoin", "0.12.0", 0) @@ -40,14 +40,13 @@ class TestIdentity(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_identity_certifiers_of(self): - mock = nice_blockchain.get_mock(self.lp) - + time.sleep(1) identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", 1441130831, LocalState.COMPLETED, BlockchainState.VALIDATED) async def exec_test(): - srv, port, url = await mock.create_server() - self.endpoint.port = port + srv, port, url = await self.mock_nice_blockchain.create_server() + self.addCleanup(srv.close) certifiers = await identity.certifiers_of(self.identities_registry, self.community) self.assertEqual(len(certifiers), 1) @@ -58,14 +57,13 @@ class TestIdentity(unittest.TestCase, QuamashTest): self.lp.run_until_complete(exec_test()) def test_identity_membership(self): - mock = nice_blockchain.get_mock(self.lp) - time.sleep(2) + time.sleep(1) identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", 1441130831, LocalState.COMPLETED, BlockchainState.VALIDATED) async def exec_test(): - srv, port, url = await mock.create_server() - self.endpoint.port = port + srv, port, url = await self.mock_nice_blockchain.create_server() + self.addCleanup(srv.close) ms = await identity.membership(self.community) self.assertEqual(ms["blockNumber"], 0) self.assertEqual(ms["blockHash"], "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709") @@ -76,15 +74,23 @@ class TestIdentity(unittest.TestCase, QuamashTest): def test_identity_corrupted_membership(self): mock = corrupted.get_mock(self.lp) - time.sleep(2) + time.sleep(1) + node = Node(mock.peer(), + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + None, Node.ONLINE, + time.time(), {}, "ucoin", "0.12.0", 0) + network = Network.create(node) + bma_access = BmaAccess.create(network) + community = Community("test_currency", network, bma_access) + identity = Identity("john", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", 1441130831, LocalState.COMPLETED, BlockchainState.VALIDATED) async def exec_test(): srv, port, url = await mock.create_server() - self.endpoint.port = port + self.addCleanup(srv.close) with self.assertRaises(MembershipNotFoundError): - await identity.membership(self.community) + await identity.membership(community) self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/core/test_node.py b/src/sakia/tests/unit/core/test_node.py new file mode 100644 index 0000000000000000000000000000000000000000..cf184e411e198291997e7ebb87460c96acbd85fc --- /dev/null +++ b/src/sakia/tests/unit/core/test_node.py @@ -0,0 +1,122 @@ +import unittest +from asynctest import CoroutineMock, patch +from ucoinpy.documents import Peer, BlockId +from PyQt5.QtCore import QLocale +from sakia.core.net import Node +from sakia.tests import QuamashTest +from sakia.tests.mocks.bma import nice_blockchain +from distutils.version import StrictVersion + + +class TestNode(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + def test_from_peer(self): + peer = Peer.from_signed_raw("""Version: 1 +Type: Peer +Currency: meta_brouzouf +PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU +Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 +Endpoints: +BASIC_MERKLED_API ucoin.inso.ovh 80 +82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== +""") + node = Node.from_peer('meta_brouzouf', peer, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API ucoin.inso.ovh 80") + self.assertEqual(node.currency, "meta_brouzouf") + + @patch('ucoinpy.api.bma.network.Peering') + def test_from_address(self, peering): + peering.return_value.get = CoroutineMock(return_value={ + "version": 1, + "currency": "meta_brouzouf", + "endpoints": [ + "BASIC_MERKLED_API ucoin.inso.ovh 80" + ], + "block": "48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8", + "signature": "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==", + "raw": "Version: 1\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU\nBlock: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8\nEndpoints:\nBASIC_MERKLED_API ucoin.inso.ovh 80\n", + "pubkey": "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" + }) + + async def exec_test(): + node = await Node.from_address("meta_brouzouf", "127.0.0.1", 9000) + self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API ucoin.inso.ovh 80") + self.assertEqual(node.currency, "meta_brouzouf") + + self.lp.run_until_complete(exec_test()) + + def test_from_json_011(self): + json_data = {"version": "0.12.0", "state": 1, "fork_window": 0, "uid": "cgeek", + "block": nice_blockchain.bma_blockchain_current, + "endpoints": ["BASIC_MERKLED_API metab.ucoin.io 88.174.120.187 9201"], + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "last_change": 1448199706.6561477, "currency": "meta_brouzouf", "sofware": "ucoin"} + node = Node.from_json("meta_brouzouf", json_data, StrictVersion('0.11.5')) + self.assertEqual(node.version, "0.12.0") + self.assertEqual(node.state, 1) + self.assertEqual(node.fork_window, 0) + self.assertEqual(node.uid, "cgeek") + self.assertEqual(node.block, nice_blockchain.bma_blockchain_current) + self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API metab.ucoin.io 88.174.120.187 9201") + self.assertEqual(node.pubkey, "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") + self.assertEqual(node.last_change, 1448199706.6561477) + self.assertEqual(node.currency, "meta_brouzouf") + self.assertEqual(node.peer.pubkey, "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") + self.assertEqual(BlockId.from_str(node.peer.blockid).number, 0) + + def test_from_json_to_json(self): + json_data = {"version": "0.12.0", "state": 1, "fork_window": 0, "uid": "inso", + "block": nice_blockchain.bma_blockchain_current, + "peer": """Version: 1 +Type: Peer +Currency: meta_brouzouf +PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU +Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 +Endpoints: +BASIC_MERKLED_API ucoin.inso.ovh 80 +82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== +""", + "pubkey": "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU", + "last_change": 1448199706.6561477, "software": "ucoin"} + node = Node.from_json("meta_brouzouf", json_data, StrictVersion('0.12.0')) + self.assertEqual(node.version, "0.12.0") + self.assertEqual(node.state, 1) + self.assertEqual(node.fork_window, 0) + self.assertEqual(node.uid, "inso") + self.assertEqual(node.block, nice_blockchain.bma_blockchain_current) + self.assertEqual(node.endpoint.inline(), "BASIC_MERKLED_API ucoin.inso.ovh 80") + self.assertEqual(node.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(node.last_change, 1448199706.6561477) + self.assertEqual(node.currency, "meta_brouzouf") + self.assertEqual(node.peer.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(BlockId.from_str(node.peer.blockid).number, 48698) + self.assertEqual(BlockId.from_str(node.peer.blockid).sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") + + result = node.jsonify() + for key in result: + self.assertEqual(result[key], json_data[key], "Error with key {0}".format(key)) + + def test_jsonify_root_node(self): + peer = Peer.from_signed_raw("""Version: 1 +Type: Peer +Currency: meta_brouzouf +PublicKey: 8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU +Block: 48698-000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8 +Endpoints: +BASIC_MERKLED_API ucoin.inso.ovh 80 +82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw== +""") + node = Node(peer, "inso", "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU", nice_blockchain.bma_blockchain_current, + Node.ONLINE, 1111111111, {}, "ucoin", "0.12", 0) + result = node.jsonify_root_node() + self.assertEqual(result['pubkey'], "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU") + self.assertEqual(result['uid'], "inso") + self.assertEqual(result['peer'], peer.signed_raw()) \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/test_context_menu.py b/src/sakia/tests/unit/gui/test_context_menu.py new file mode 100644 index 0000000000000000000000000000000000000000..299918431949ab12364406a421775b5fde4de434 --- /dev/null +++ b/src/sakia/tests/unit/gui/test_context_menu.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import patch, MagicMock, Mock +from asynctest.mock import CoroutineMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.tests.mocks.bma import nice_blockchain +from sakia.gui.widgets.context_menu import ContextMenu + + +class TestContextMenu(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + self.identity = Mock(specs='sakia.core.registry.Identity') + self.identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + self.identity.uid = "A" + + self.app = MagicMock(specs='sakia.core.Application') + self.account = MagicMock(specs='sakia.core.Account') + self.community = MagicMock(specs='sakia.core.Community') + self.password_asker = MagicMock(specs='sakia.gui.password_asker.PasswordAsker') + + def tearDown(self): + self.tearDownQuamash() + + @patch('PyQt5.QtWidgets.QMenu', create=True) + def test_view_in_wot(self, qmenu): + wot_refreshed = False + + def refresh_wot(identity): + nonlocal wot_refreshed + self.assertEqual(identity, self.identity) + wot_refreshed = True + + async def exec_test(): + context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) + context_menu.view_identity_in_wot.connect(refresh_wot) + context_menu.view_wot(self.identity) + + self.lp.run_until_complete(exec_test()) + self.assertTrue(wot_refreshed) + + @patch('PyQt5.QtWidgets.QMenu', create=True) + def test_copy_pubkey_to_clipboard(self, qmenu): + app = Mock('sakia.core.Application') + async def exec_test(): + context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) + context_menu.copy_pubkey_to_clipboard(self.identity) + self.lp.run_until_complete(exec_test()) + self.assertEqual(self.qapplication.clipboard().text(), "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") + + @patch('PyQt5.QtWidgets.QMenu', create=True) + def test_copy_block_to_clipboard(self, qmenu): + self.community.get_block = CoroutineMock(side_effect=lambda n: nice_blockchain.bma_blockchain_current if n == 15 \ + else nice_blockchain.bma_blockchain_0) + self.qapplication.clipboard().clear() + async def exec_test(): + context_menu = ContextMenu(qmenu, self.app, self.account, self.community, self.password_asker) + context_menu.community = self.community + context_menu.copy_block_to_clipboard(15) + + self.lp.run_until_complete(exec_test()) + raw_block = "{0}{1}\n".format(nice_blockchain.bma_blockchain_current["raw"], + nice_blockchain.bma_blockchain_current["signature"]) + self.assertEqual(self.qapplication.clipboard().text(), raw_block) diff --git a/src/sakia/tests/unit/gui/views/test_explorer_node.py b/src/sakia/tests/unit/gui/views/test_explorer_node.py index cd54b19f534f54e2931d1f092d872f1d1f3bc993..e684d5ab56d23107426561f4ed721e5126d30f84 100644 --- a/src/sakia/tests/unit/gui/views/test_explorer_node.py +++ b/src/sakia/tests/unit/gui/views/test_explorer_node.py @@ -72,9 +72,9 @@ class TestExplorerNode(unittest.TestCase, QuamashTest): async def exec_test(): node = ExplorerNode(("A", metadata), QPointF(0, 0), nx_pos, 0, 1) bounding_rect = node.boundingRect() - self.assertAlmostEqual(bounding_rect.x(), -0.5, delta=5) - self.assertAlmostEqual(bounding_rect.y(), -0.5, delta=5) - self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=5) - self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=5) + self.assertAlmostEqual(bounding_rect.x(), -0.5, delta=15) + self.assertAlmostEqual(bounding_rect.y(), -0.5, delta=15) + self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=15) + self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=15) self.lp.run_until_complete(exec_test()) diff --git a/src/sakia/tests/unit/gui/views/test_wot_node.py b/src/sakia/tests/unit/gui/views/test_wot_node.py index a92ba2daa72a6309c08b885178f3a16274b989db..6bf361fa23537deb98c11b0a71fbc03dcd91c5c1 100644 --- a/src/sakia/tests/unit/gui/views/test_wot_node.py +++ b/src/sakia/tests/unit/gui/views/test_wot_node.py @@ -74,7 +74,7 @@ class TestWotNode(unittest.TestCase, QuamashTest): bounding_rect = node.boundingRect() self.assertAlmostEqual(bounding_rect.x(), -0.5, delta=1) self.assertAlmostEqual(bounding_rect.y(), -0.5, delta=1) - self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=5) - self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=5) + self.assertAlmostEqual(bounding_rect.width(), 19.59375, delta=15) + self.assertAlmostEqual(bounding_rect.height(), 37.0, delta=15) self.lp.run_until_complete(exec_test())