From 03df1b329b5fb47c34bfde2571ecede471ac85fc Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 11 Mar 2014 14:05:42 -0400 Subject: [PATCH 001/107] added templates for very basic extensions One of the development goals of the Commotion_client (commotion-computer to the savvy) is to make extension of the toolkit as easy as possible for other developers. As such, this branch will focus on building a set of templates (shown in this commit) and extending them to create a full commotion core module. --- docs/extension_template/config.json | 9 +++++ docs/extension_template/main.py | 30 +++++++++++++++ docs/extension_template/settings.py | 28 ++++++++++++++ docs/extension_template/task_bar.py | 30 +++++++++++++++ docs/extension_template/test_suite.py | 53 +++++++++++++++++++++++++++ docs/writing_extensions.md | 21 +++++++++++ 6 files changed, 171 insertions(+) create mode 100644 docs/extension_template/config.json create mode 100644 docs/extension_template/main.py create mode 100644 docs/extension_template/settings.py create mode 100644 docs/extension_template/task_bar.py create mode 100644 docs/extension_template/test_suite.py create mode 100644 docs/writing_extensions.md diff --git a/docs/extension_template/config.json b/docs/extension_template/config.json new file mode 100644 index 0000000..d936546 --- /dev/null +++ b/docs/extension_template/config.json @@ -0,0 +1,9 @@ +{ +"name":"extension_template", +"menuItem":"Extension Template", +"parent":"Templates", +"settings":"settings", +"taskbar":"task_bar", +"main":"main", +"tests":"test_suite" +} diff --git a/docs/extension_template/main.py b/docs/extension_template/main.py new file mode 100644 index 0000000..537431a --- /dev/null +++ b/docs/extension_template/main.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +main + +An initial viewport template to make development easier. + +@brief Populates the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. + +@note This template ONLY includes the objects for the "main" component of the extension template. The other components can be found in their respective locations. + +""" + +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +from from extensions.extension_template.ui import Ui_main + +class ViewPort(Ui_main.ViewPort): + """ + + """ + + diff --git a/docs/extension_template/settings.py b/docs/extension_template/settings.py new file mode 100644 index 0000000..e1e5da3 --- /dev/null +++ b/docs/extension_template/settings.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +settings + +The settings page for the extension template. + +@brief The settings page for the extension. This page controls how the extension level settings should look and behave in the settings menu. If this is not included in the config file and a "settings" class is not found in the file listed under the "main" option the extension will not list a settings button in the extension settings page. + +@note This template ONLY includes the objects for the "settings" component of the extension template. The other components can be found in their respective locations. + +""" + +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +from extensions.extension_template.ui import Ui_settings + +class Settings(Ui_settings.ViewPort): + """ + """ + diff --git a/docs/extension_template/task_bar.py b/docs/extension_template/task_bar.py new file mode 100644 index 0000000..8539771 --- /dev/null +++ b/docs/extension_template/task_bar.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +task_bar + +The task bar for the extension template. + +@brief The object that contains the custom task-bar. If not set and a "taskbar" class is not found in the file listed under the "main" option the default taskbar will be implemented. + +@note This template ONLY includes the objects for the "task bar" component of the extension template. The other components can be found in their respective locations. + +""" + +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +from GUI import task_bar + + +class TaskBar(task_bar.TaskBar): + """ + + """ + diff --git a/docs/extension_template/test_suite.py b/docs/extension_template/test_suite.py new file mode 100644 index 0000000..1ce31e4 --- /dev/null +++ b/docs/extension_template/test_suite.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_suite + +The test suite for the extension template. + +""" + +#Standard Library Imports +import sys +import unittest + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui +from PyQt4.QtTest import QtTest + +#import python modules created by qtDesigner and converted using pyuic4 +from extensions.extension_template import main +from extensions.extension_template import settings +from extensions.extension_template import task_bar + +class MainTest(unittest.TestCase): + """ + Test the main viewport object. + """ + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + self.task_bar = main.ViewPort() + +class SettingsTest(unittest.TestCase): + """ + Test the settings object. + """ + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + self.task_bar = settings.ViewPort() + +class TaskBarTest(unittest.TestCase): + """ + Test the task bar object + """ + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + self.task_bar = task_bar.TaskBar() + + if __name__ == "__main__": + unittest.main() diff --git a/docs/writing_extensions.md b/docs/writing_extensions.md new file mode 100644 index 0000000..e021e7f --- /dev/null +++ b/docs/writing_extensions.md @@ -0,0 +1,21 @@ + + +# Writing Extensions + +## Design + +## The QT designer + +## Unit Tests + +## The Config + +## The Backend + +### Main + +### Settings + +### Taskbar + + From f7d4391afec2288257443f564e8ab3f649e3ae88 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 11 Mar 2014 14:15:35 -0400 Subject: [PATCH 002/107] Adding emacs auto-saves to .gitignore --- .gitignore | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8533a15..20bf2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,24 @@ +# Debian .deb cruft debian/files debian/commotion-linux-py.debhelper.log debian/commotion-linux-py.substvars debian/commotion-linux-py.postinst.debhelper debian/commotion-linux-py.prerm.debhelper debian/commotion-linux-py/ + # python build products *.pyc __pycache__ + # example code examples/ + # PyQt auto-created asset tracking file. *_rc.py commotion_client/temp/ + #All compiled versions of designer created UI files. -Ui_*.py \ No newline at end of file +Ui_*.py + +# Emacs auto-save cruft because s2e does not want to spend the time debugging his .emacs config right now. +\#.*# \ No newline at end of file From 54f1c89e78941c91f6fb8e4107a933b3603d4a4b Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 11 Mar 2014 16:58:29 -0400 Subject: [PATCH 003/107] Work on extension writing focusing on intro and Qt Designer This work includes adding some base assets to QT designer to support our core Commotion styles. --- commotion_client/assets/commotion_assets.qrc | 4 + .../extension_template/config.json | 0 .../extension_template/main.py | 0 .../extension_template/settings.py | 0 .../extension_template/task_bar.py | 0 .../extension_template/test_suite.py | 0 .../extension_template/ui/settings.ui | 108 ++++++++++ .../config_manager/ui/config_manager.ui | 202 ++++++++++++++++++ .../tutorial/images/design/ssid_sketch.png | Bin 0 -> 28806 bytes docs/extensions/writing_extensions.md | 134 ++++++++++++ docs/writing_extensions.md | 21 -- 11 files changed, 448 insertions(+), 21 deletions(-) rename docs/{ => extensions}/extension_template/config.json (100%) rename docs/{ => extensions}/extension_template/main.py (100%) rename docs/{ => extensions}/extension_template/settings.py (100%) rename docs/{ => extensions}/extension_template/task_bar.py (100%) rename docs/{ => extensions}/extension_template/test_suite.py (100%) create mode 100644 docs/extensions/extension_template/ui/settings.ui create mode 100644 docs/extensions/tutorial/config_manager/ui/config_manager.ui create mode 100644 docs/extensions/tutorial/images/design/ssid_sketch.png create mode 100644 docs/extensions/writing_extensions.md delete mode 100644 docs/writing_extensions.md diff --git a/commotion_client/assets/commotion_assets.qrc b/commotion_client/assets/commotion_assets.qrc index 860265a..5264746 100644 --- a/commotion_client/assets/commotion_assets.qrc +++ b/commotion_client/assets/commotion_assets.qrc @@ -14,5 +14,9 @@ images/alert62.png images/loading62.gif + + images/question_mark_filled41.png + images/question_mark_filled20.png + diff --git a/docs/extension_template/config.json b/docs/extensions/extension_template/config.json similarity index 100% rename from docs/extension_template/config.json rename to docs/extensions/extension_template/config.json diff --git a/docs/extension_template/main.py b/docs/extensions/extension_template/main.py similarity index 100% rename from docs/extension_template/main.py rename to docs/extensions/extension_template/main.py diff --git a/docs/extension_template/settings.py b/docs/extensions/extension_template/settings.py similarity index 100% rename from docs/extension_template/settings.py rename to docs/extensions/extension_template/settings.py diff --git a/docs/extension_template/task_bar.py b/docs/extensions/extension_template/task_bar.py similarity index 100% rename from docs/extension_template/task_bar.py rename to docs/extensions/extension_template/task_bar.py diff --git a/docs/extension_template/test_suite.py b/docs/extensions/extension_template/test_suite.py similarity index 100% rename from docs/extension_template/test_suite.py rename to docs/extensions/extension_template/test_suite.py diff --git a/docs/extensions/extension_template/ui/settings.ui b/docs/extensions/extension_template/ui/settings.ui new file mode 100644 index 0000000..6c318ca --- /dev/null +++ b/docs/extensions/extension_template/ui/settings.ui @@ -0,0 +1,108 @@ + + + Dialog + + + + 0 + 0 + 488 + 383 + + + + Dialog + + + + + 130 + 330 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 10 + 10 + 451 + 281 + + + + + + + + + TextLabel + + + + + + + + + + + + + + CheckBox + + + + + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/docs/extensions/tutorial/config_manager/ui/config_manager.ui b/docs/extensions/tutorial/config_manager/ui/config_manager.ui new file mode 100644 index 0000000..014ac96 --- /dev/null +++ b/docs/extensions/tutorial/config_manager/ui/config_manager.ui @@ -0,0 +1,202 @@ + + + Dialog + + + + 0 + 0 + 743 + 622 + + + + + 50 + false + false + + + + Dialog + + + + + 60 + 50 + 254 + 66 + + + + + + + + 11 + 50 + false + + + + + + + Security Settings + + + + + + + Security related settings. + + + + + + + + + 60 + 280 + 240 + 66 + + + + + + + + 11 + 50 + false + + + + + + + Networking Settings + + + + + + + Network related settings. + + + + + + + + + 60 + 420 + 240 + 66 + + + + + + + + 11 + 50 + false + + + + + + + Local Settings + + + + + + + Settings local to your computer + + + + + + + + + 400 + 150 + 111 + 21 + + + + Value Header + + + + + + 400 + 180 + 181 + 41 + + + + + + + 400 + 240 + 181 + 21 + + + + Help text that is always on + + + + + + 560 + 150 + 21 + 21 + + + + + + + :/filled?20.png + + + + + + 600 + 150 + 67 + 21 + + + + Help Text + + + + + + + + + diff --git a/docs/extensions/tutorial/images/design/ssid_sketch.png b/docs/extensions/tutorial/images/design/ssid_sketch.png new file mode 100644 index 0000000000000000000000000000000000000000..55949ebc329099fce03c65d1ab796310bac13b10 GIT binary patch literal 28806 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 B`kMd% literal 0 HcmV?d00001 diff --git a/docs/extensions/writing_extensions.md b/docs/extensions/writing_extensions.md new file mode 100644 index 0000000..8fa8384 --- /dev/null +++ b/docs/extensions/writing_extensions.md @@ -0,0 +1,134 @@ +# Writing Extensions + +## Intro + +This documentation will walk you through how to create a Commotion Client extension using the provided templates and Qt Designer. This documentation follows the development of the core extension responsable for managing Commotion configuration files. You can find the code for the version of the extension built in this tutorial in the "docs/extensions/tutorial" folder. The current Commotion config extension can be found in "commotion_client/extension/core/config_manager." This tutorial will not keep up to date with this extension as it evolves unless there are core changes in the commotion API that require us to update sections to maintain current with the API. + +## Design + +Commotion comes with a [JSON](http://json.org/) based network configuration file. This file contains the mesh settings for a Commotion network. These profiles currently contain the following values. +``` +{ + "announce": "true", + "bssid": "02:CA:FF:EE:BA:BE", + "bssidgen": "true", + "channel": "5", + "dns": "208.67.222.222", + "domain": "mesh.local", + "encryption": "psk2", + "ip": "100.64.0.0", + "ipgen": "true", + "ipgenmask": "255.192.0.0", + "key": "c0MM0t10n!r0cks", + "mdp_keyring": "/etc/commotion/keys.d/mdp.keyring/serval.keyring", + "mdp_sid": "0000000000000000000000000000000000000000000000000000000000000000", + "mode": "adhoc", + "netmask": "255.192.0.0", + "serval": "false", + "ssid": "commotionwireless.net", + "type": "mesh" +} +``` + +Any good user interface starts with a need and a user needs assessment. We *need* an interface that will allow a user to understand, and edit a configuration file. Our initial *user needs assessment* revealed two groups of users. + +Basic Users: + * These users want to be able to download, or be given, a config file and use it to connect to a network with the least ammount of manipulation. + * These users want interfaces based upon tasks, not configuration files, when they do modify their settings. + * When these users download a configuration file they want it to be named somthing that allows it to be easy to identify later/ + +Intermediate/Advanced Users + * These users desire all the attributes that are listed under the *basic user.* + * These users also want an interface where they can quickly manipulate all the network settings quickly without having to worry about abstractions that have been layered on for new users. + * These abstractions make advanced users feel lost and frustrated because they know what they want to do, but can not find the "user friendly" term that pairs with the actual device behavior. + + +Our needs assessment identified two different extensions. One extension is a device configuration interface that abstracts individual networking tasks into their component parts for easy configuration by a new user. The second extension is a configuration file loader, downloader, editor, and applier. This second extension is what we will build here. + +The Commotion [Human Interface Guidelines](http://commotionwireless.net/developer/hig/key-concepts) have some key concepts for user interface that we should use to guide our page design. + +**Common Language:** The Commotion config file uses a series of abbreviations for each section. Because this menu is focused on more advanced users we should provide not only the abbreviation, but the technical term for the object that any value interacts with. + +**Common UI Terms:** Beyond simply the config file key, and the true technical term, as new users start to interact more competantly with Commotion it will be confusing if the common terms we use in the basic interfaces is not also included. As such we will want to use the *common term* where one exists. + +Taking just one value we can sketch out how the interface will represent it. + +```"ssid": "commotionwireless.net"``` + +![A sketch of a possible SSID including all terms.](tutorial/images/design/ssid_sketch.png) + +Beyond the consitancy provided by common terms, common groupings are also important. In order to ensure that a user can easily modify related configurations. We have grouped the configuration values in the following three groups. + +security { + "announce" + "encryption" + "key + "mdp_keyring" + "mdp_sid" + "serval" +} + +network { + "channel" + "mode" + "type" + "ssid" + "bssid" + "bssidgen" + "domain" + "netmask" +} + +local { + "ip" + "ipgen" + "ipgenmask" + "dns" +} + + +TODO: + * Show process of designing section headers + * Show final design + +## The Qt Designer + +Now that we have the basic layout we can go to Qt Designer and create our page. Qt Designer is far more full featured than what we will cover here. A quick online search will show you far better demonstrations than I can give. Also, showing off Qt is not the focus of this tutorial. + +First we create a new dialogue. Since our design was a series of sections that are filled with a variety of values I am going to start by creating a single section title. + + +Using our design document I know that I want the section header to be about 15px and bold. I can do this in a few ways. I can set the font directly in the "property editor", use the "property editor" to create a styleSheet to apply to the section header, or use an existing style sheet using the "Add Resource" button in the styleSheet importer or in the code. I recoomend the last option because you can use existing Commotion style sheets to make your extension fit the overall style of the application. The "Main" section will have instructions on how to apply a existing Commotion style sheet to your GUI. + + + +Feel free to use whatever works for you. To make it easy for me to create consistant styling later I am just going to do everything unstyled in the Qt Designer and then call in a style-sheet in the "Main" section. + +Not that I have my section header I am going copy it three times and set the "objectName" and the user-facing text of each. Once this extension is functional I will go back through and replace all the text with user-tested text. For now, lets just get it working. + + + +Now that are section headers are created we are goign to have to go in and create our values. Qt Designer has a variety of widgets that you can choose from. I am only going to go over two types of widgets to show you how to put them together. + +First we are going to make a simple text-entry field following the design we created before. The following was created using four "label's" and one "plain text edit" box. + + + +I used a label for the question mark icon because non-interactive icons are easily made into graphics by using the "property editor." In the QLabel section click the "pixmap" property and choose "Choose Resource" in the dropdown menu to the right. This will allow you to choose a resource to use for your image. You can choose the "commotion_client/assets/commotion_assets.qrc" file to use any of the standard Commotion icons. We have tried to make sure that we have any sizes you might need of our standard icons. + + + +## Stylesheets + +## Unit Tests + +## The Config + +## The Backend + +### Main + +### Settings + +### Taskbar + diff --git a/docs/writing_extensions.md b/docs/writing_extensions.md deleted file mode 100644 index e021e7f..0000000 --- a/docs/writing_extensions.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# Writing Extensions - -## Design - -## The QT designer - -## Unit Tests - -## The Config - -## The Backend - -### Main - -### Settings - -### Taskbar - - From ad53e37731ae8c0cac660a53656a50f971da65cc Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 12 Mar 2014 17:38:14 -0400 Subject: [PATCH 004/107] added some extra functionality to main template. main.py will now create a simple working window after very little modification. This should increase usability. --- docs/extensions/extension_template/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/extensions/extension_template/main.py b/docs/extensions/extension_template/main.py index 537431a..b7dad62 100644 --- a/docs/extensions/extension_template/main.py +++ b/docs/extensions/extension_template/main.py @@ -20,11 +20,15 @@ from PyQt4 import QtGui #import python modules created by qtDesigner and converted using pyuic4 -from from extensions.extension_template.ui import Ui_main +from extensions.contrib.extension_template.ui import Ui_main class ViewPort(Ui_main.ViewPort): """ """ + def __init__(self, parent=None): + super().__init__() + self.setupUi(self) + From b7be74ad9ab2dc10f31513bda24e2d84ffcbf8da Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 12 Mar 2014 17:38:57 -0400 Subject: [PATCH 005/107] I don't knoiw why I had that half template anyway --- .../extension_template/ui/settings.ui | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 docs/extensions/extension_template/ui/settings.ui diff --git a/docs/extensions/extension_template/ui/settings.ui b/docs/extensions/extension_template/ui/settings.ui deleted file mode 100644 index 6c318ca..0000000 --- a/docs/extensions/extension_template/ui/settings.ui +++ /dev/null @@ -1,108 +0,0 @@ - - - Dialog - - - - 0 - 0 - 488 - 383 - - - - Dialog - - - - - 130 - 330 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 10 - 10 - 451 - 281 - - - - - - - - - TextLabel - - - - - - - - - - - - - - CheckBox - - - - - - - - - - - - - - - buttonBox - accepted() - Dialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - From 3cd03d758970e8033973ed5f21c88e60dfbaeb8c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 12 Mar 2014 17:39:42 -0400 Subject: [PATCH 006/107] added extended filled question mark sizes --- .../assets/images/question_mark_filled20.png | Bin 0 -> 627 bytes .../assets/images/question_mark_filled41.png | Bin 0 -> 1185 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 commotion_client/assets/images/question_mark_filled20.png create mode 100644 commotion_client/assets/images/question_mark_filled41.png diff --git a/commotion_client/assets/images/question_mark_filled20.png b/commotion_client/assets/images/question_mark_filled20.png new file mode 100644 index 0000000000000000000000000000000000000000..19333755574d36709e6f258a708f153799288d4a GIT binary patch literal 627 zcmV-(0*w8MP)75Jp05d01=(wYA;xuxstQ^M?Yz zFburk_kG?E=6!!7Y}=+vvA6;vK(Ooqcn2g(@8?)%?(kMo1&qbd0t-M_^+^><0oSn1 z_s+mrTmx;%#91A7B7_se${I9meAAz={(ro7>@2<~y6a z+k~51snZR&PJ6J-q_BtP^%AV(u9i#z`FqD4B@h5SO1@$}o1soO7(CPGW)DiB+Y1QrCcVbc>!%0=GyRuoFlybbUDB;m zu7`=2{4Nx@+Y{kt=YXeIQjC$Ol#~p%ontH<^|P)jirKvA<+9FS<(Lsb3gHJ#NQ!sR z{m36*IWafP>Bsl{U;rx;cmaHHb112yYZ|r6H<2menQ!y}e*W4apmb_9|SPBA`5VWmT3#m#h z7SRR}2@%E9M2RMN8AGIhOf=py(Zm=MBNG)(6}=gB!UbI$oj3WY*glByQ~4Zt110^nNU8lV^$2L1rf z1HS>?Ks%ZkwbGPi3R2Z&VEeyEJ&wykHNjqAiCV@sY@nv){7Sec0 zdP)BfPp46!22J#Z1M!eYfi+f~eSr!3hA;5*u$POzF%}h;Q0|_`(z)}Ikj&ck0=J-v z5JV5DsyA8z{NU6vUg>S8|I!75Nxl>-OUrnnb|Xy{tIgO}16zPY5bjB1COHXInsNH} z_p_`$`1*`;U#;83p6c~x?9Tzq(1btyc5gKc2;fp?J}b6-*G7+bz>Ix4uoI>}LEFsS z9afd+IyP0@1VEM~v$C`dQCY`*i&s%JYj#ZTWGF~m|8cWTYzB}*&+eZM`~|qIfQAV1 zMgMWulr5pul^==scn7F$eK{ugu7%6#-299gdp}U30PCy*0!T=a-i(%KP63K669@() zu{qgh7EORLnXH#F)`X7hvCa=;00F8zi>z2R$QTP&M|I%zF*<%d6%)(L&gH?S)mH2l z$biQQu&{c(1MKShC?Wp#o!cpQms+v(I0`xR_b40M-eTN85fgvu_9h-)ajR8ZkAj2q z^zQCYxab>;oXe8LJL`7PTv=zwwn)azR2+wQju`3;)59A2t@Y{w}OoQwek z@CRpHMVU^Y!PI*4An=nDt`6!P@E(LAH4y}!x9V1y>tf52n<;kX;ZkzAVXlXb<*V#M z?nM)RaB#Qiuwr(L@fD0_;8MnsVs#nV83u%SNKNzu4*-G8azzvbwxNmB(ZR%8kS4l- z-5G+4*K=s1JwBMcV5NzDz!PMyM}xo~H1S5Vi&v{p)&rlC-rCS8#t Date: Wed, 12 Mar 2014 17:41:27 -0400 Subject: [PATCH 007/107] continued writing tutorial through ui creation Created a config file for the tutorial object, a main.py fil that does nothing, and a full GUI (with the desperate need for the final application text to be completed. --- .../tutorial/config_manager/config.json | 9 + .../tutorial/config_manager/main.py | 34 + .../config_manager/ui/config_manager.ui | 2058 +++++++++++++++-- docs/extensions/writing_extensions.md | 105 +- 4 files changed, 2023 insertions(+), 183 deletions(-) create mode 100644 docs/extensions/tutorial/config_manager/config.json create mode 100644 docs/extensions/tutorial/config_manager/main.py diff --git a/docs/extensions/tutorial/config_manager/config.json b/docs/extensions/tutorial/config_manager/config.json new file mode 100644 index 0000000..d936546 --- /dev/null +++ b/docs/extensions/tutorial/config_manager/config.json @@ -0,0 +1,9 @@ +{ +"name":"extension_template", +"menuItem":"Extension Template", +"parent":"Templates", +"settings":"settings", +"taskbar":"task_bar", +"main":"main", +"tests":"test_suite" +} diff --git a/docs/extensions/tutorial/config_manager/main.py b/docs/extensions/tutorial/config_manager/main.py new file mode 100644 index 0000000..d377113 --- /dev/null +++ b/docs/extensions/tutorial/config_manager/main.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +main + +An initial viewport template to make development easier. + +@brief Populates the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. + +@note This template ONLY includes the objects for the "main" component of the extension template. The other components can be found in their respective locations. + +""" + +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +#from extensions.core.config_manager.ui import Ui_config_manager.py +from docs.extensions.tutorial.config_manager.ui import Ui_config_manager.py + +class ViewPort(Ui_main.ViewPort): + """ + + """ + + def __init__(self, parent=None): + super().__init__() + self.setupUi(self) + diff --git a/docs/extensions/tutorial/config_manager/ui/config_manager.ui b/docs/extensions/tutorial/config_manager/ui/config_manager.ui index 014ac96..cd142e2 100644 --- a/docs/extensions/tutorial/config_manager/ui/config_manager.ui +++ b/docs/extensions/tutorial/config_manager/ui/config_manager.ui @@ -1,13 +1,13 @@ - Dialog - + ViewPort + 0 0 - 743 - 622 + 822 + 2413 @@ -17,186 +17,1912 @@ false + + Qt::NoContextMenu + - Dialog + Commotion Configuration Manager + + + + :/logo16.png:/logo16.png + + + THIS NEEDS NEW POP UP TEXT!!! - 60 + 30 50 - 254 - 66 - - - - - - - - 11 - 50 - false - - - - - - - Security Settings - - - - - - - Security related settings. - - - - - - - - - 60 - 280 - 240 - 66 - - - - - - - - 11 - 50 - false - - - - - - - Networking Settings - - - - - - - Network related settings. - - - - - - - - - 60 - 420 - 240 - 66 + 746 + 2336 - - - - 11 - 50 - false - - - - - - - Local Settings - - + + + + + + + + 11 + 50 + false + + + + + + + Security Settings + + + + + + + Security related settings. + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + announce + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Setting this to true will cause your device to advertise any gateway it has to the internet to the mesh. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + True/False + + + + + + + + 0 + 0 + + + + + + + Advertise your gateway to the mesh. + + + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + encryption + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Encrypt data over the mesh using WPA-PSK2 and a shared network key. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + + + + On/Off + + + + + + + + 0 + 0 + + + + + + + Choose whether or not to encrypt data sent between mesh devices for added security. + + + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + key (Mesh Encryption Password) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + To encrypt data between devices, each device must share a common mesh encryption password. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + Key + + + + + + + + + + + + + + Confirm + + + + + + + + + + + + + 0 + 0 + + + + To encrypt data between devices, each device must share a common mesh encryption password. This password must be between 8 and 63 printable ASCII characters. + + + true + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + serval + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Route signing is the signing of known/trusted routes by nodes on the network. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + On/Off + + + + + + + + 0 + 0 + + + + + + + Use serval route signing to have devices on this mesh sign and authenticate routes that they receive. + + + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + mdp_keyring (Mesh Keychain) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + To ensure that only authorized devices can route traffic on your Commotion mesh network, one Shared Mesh Keychain file can be generated and shared by all devices. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + Browse... + + + + + + + New + + + + + + + + + + 0 + 0 + + + + If a Shared Mesh Keychain file was provided to you by a network administrator or another community member, you can browse your computer for it here to join this device to an existing mesh network. Otherwise, you can create a new keychain to share with those you wish to mesh with. + + + true + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + mdp_sid (Keychain Fingerprint) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + This is the fingerprint of the above mesh keychain. It will change depending upon the keyring uploaded. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + 0000000000000000000000000000000000000000000000000000000000000000 + + + + + + + + - - - Settings local to your computer - - + + + + + + + + 11 + 50 + false + + + + + + + Networking Settings + + + + + + + Network related settings. + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + routing (Mesh Routing Protocol) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + The method of communication that devices use to communicate with each other on the mesh. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + O.L.S.R (Optimized Link State Routing Protocol) + + + + + Babel + + + + + B.A.T.M.A.N (Better Approach To Mobile Adhoc Networking) + + + + + + + + + 0 + 0 + + + + The mesh routing protocol used by devices to communicate over the mesh. + + + true + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + mode + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + This is the fingerprint of the above mesh keychain. It will change depending upon the keyring uploaded. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + adhoc + + + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + type + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + This is the fingerprint of the above mesh keychain. It will change depending upon the keyring uploaded. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + 0 + 0 + + + + mesh + + + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + channel + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + All of the mesh devices on a mesh network need to be on the same frequency and channel to communicate. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + 2.4 GHz + + + + + + + 5 GHz + + + + + + + + + + + + Select the radio frequency that devices on this will use to connect to the mesh. + + + + + + + + + + + + + 0 + 0 + + + + What channel should devices use to communicate on this mesh. + + + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + ssid (Mesh Network Name) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Commotion networks share a network-wide name. This must be the same across all devices on the same mesh. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + Commotion networks share a network-wide name. This must be the same across all devices on the same mesh. + + + + true + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + bssidgen (Auto-Generate BSSID) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + + + + + On/Off + + + + + + + + 0 + 0 + + + + Auto-generate a bssid based upon the SSID and channel set in the profile. + + + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + bssid (Basic Identifier) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + The adhoc BSSID must be shared by all devices in a particular mesh network. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + This is the basic identifier of a wireless mesh network (this takes priority over SSID) + + + + true + + + + + + + + + + + 6 + + + + + + 0 + 0 + + + + family (Internet Protocol Family) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + This will determine the acceptable values for all addresses in the profile. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + IPV4 + + + + + + + IPV6 + + + + + + + + + + + + The communication protocol that determines the basic addressing of the network. + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + netmask + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + The adhoc BSSID must be shared by all devices in a particular mesh network. + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + FIX THIS TEXT FILL WITH REAL TEXT GIVE ME CONTENT!!!!!!!!!!!! + + + true + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + ipgen (Auto-Generate the IP Address) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + I NEED HELP TXT!!!! + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + FIX THIS TEXT FILL WITH REAL TEXT GIVE ME CONTENT!!!!!!!!!!!! + + + true + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + ipgenmask (IP Mask forAuto-Generated IP Address) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + I NEED HELP TXT!!!! + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + FIX THIS TEXT FILL WITH REAL TEXT GIVE ME CONTENT!!!!!!!!!!!! + + + true + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + ip (IP Address) + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + I NEED HELP TXT!!!! + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + FIX THIS TEXT FILL WITH REAL TEXT GIVE ME CONTENT!!!!!!!!!!!! + + + true + + + + + + + + + 6 + + + + + 6 + + + + + + 0 + 0 + + + + dns + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + I NEED HELP TXT!!!! + + + + + + + :/filled?20.png:/filled?20.png + + + + 20 + 20 + + + + false + + + true + + + true + + + + + + + + + + + + + 0 + 0 + + + + FIX THIS TEXT FILL WITH REAL TEXT GIVE ME CONTENT!!!!!!!!!!!! + + + true + + + + + + - - - - 400 - 150 - 111 - 21 - - - - Value Header - - - - - - 400 - 180 - 181 - 41 - - - - - - - 400 - 240 - 181 - 21 - - - - Help text that is always on - - - - - - 560 - 150 - 21 - 21 - - - - - - - :/filled?20.png - - - - - - 600 - 150 - 67 - 21 - - - - Help Text - - - + diff --git a/docs/extensions/writing_extensions.md b/docs/extensions/writing_extensions.md index 8fa8384..4d418e7 100644 --- a/docs/extensions/writing_extensions.md +++ b/docs/extensions/writing_extensions.md @@ -27,6 +27,8 @@ Commotion comes with a [JSON](http://json.org/) based network configuration file "serval": "false", "ssid": "commotionwireless.net", "type": "mesh" + "routing": "olsr" + "family":"1pv4" } ``` @@ -57,29 +59,29 @@ Taking just one value we can sketch out how the interface will represent it. ![A sketch of a possible SSID including all terms.](tutorial/images/design/ssid_sketch.png) -Beyond the consitancy provided by common terms, common groupings are also important. In order to ensure that a user can easily modify related configurations. We have grouped the configuration values in the following three groups. +Beyond the consitancy provided by common terms, common groupings are also important. In order to ensure that a user can easily modify related configurations. We have grouped the configuration values in the following two groups. + security { "announce" "encryption" "key + "serval" "mdp_keyring" "mdp_sid" - "serval" } -network { - "channel" - "mode" +networking { + "routing" + "mode" "type" + "channel" "ssid" - "bssid" "bssidgen" + "bssid" "domain" + "family" "netmask" -} - -local { "ip" "ipgen" "ipgenmask" @@ -97,7 +99,6 @@ Now that we have the basic layout we can go to Qt Designer and create our page. First we create a new dialogue. Since our design was a series of sections that are filled with a variety of values I am going to start by creating a single section title. - Using our design document I know that I want the section header to be about 15px and bold. I can do this in a few ways. I can set the font directly in the "property editor", use the "property editor" to create a styleSheet to apply to the section header, or use an existing style sheet using the "Add Resource" button in the styleSheet importer or in the code. I recoomend the last option because you can use existing Commotion style sheets to make your extension fit the overall style of the application. The "Main" section will have instructions on how to apply a existing Commotion style sheet to your GUI. @@ -108,27 +109,97 @@ Not that I have my section header I am going copy it three times and set the "ob -Now that are section headers are created we are goign to have to go in and create our values. Qt Designer has a variety of widgets that you can choose from. I am only going to go over two types of widgets to show you how to put them together. +Now that are section headers are created we are goign to have to go in and create our values. Qt Designer has a variety of widgets that you can choose from. I am only going to go over the creation of one widget to show you how to put them together. -First we are going to make a simple text-entry field following the design we created before. The following was created using four "label's" and one "plain text edit" box. +First we are going to make a simple text-entry field following the design we created before. The following was created using four "label's" and one "line edit" box. -I used a label for the question mark icon because non-interactive icons are easily made into graphics by using the "property editor." In the QLabel section click the "pixmap" property and choose "Choose Resource" in the dropdown menu to the right. This will allow you to choose a resource to use for your image. You can choose the "commotion_client/assets/commotion_assets.qrc" file to use any of the standard Commotion icons. We have tried to make sure that we have any sizes you might need of our standard icons. +In the end, I won't be using labels for the help-text pop-up or the question mark. I realized that the easiest way to show the help-text pop up was to simply use the question-marks existing tool-tip object. There are two ways I could have implemented the question-mark icon. + +I can use a label for the question mark icon because non-interactive icons are easily made into graphics by using the "property editor." In the QLabel section click the "pixmap" property and choose "Choose Resource" in the dropdown menu to the right. This will allow you to choose a resource to use for your image. You can choose the "commotion_client/assets/commotion_assets.qrc" file to use any of the standard Commotion icons. We have tried to make sure that we have any sizes you might need of our standard icons. + +I would like the question mark to be clickable to make the help-text tooltip pop up without forcing the user to wait. To do this I added a push button, chose the same question mark item for its "icon," set its text to be blank, and checked the "flat" attribute. +Now that we have the objects needed for the value we will use a layout to place them next to each other. I will use a horizontal layout to place the value header and the question mark next to each other. I have placed a "horizontal spacer" between them to push them to the edges of the horizontal layout. After copying my value header I am going to place it, the text entry box, and the static help text in a vertical layout. +Not that I have the basic components created I am going to create the first value that I need with a text-box entry form, BSSID. After copying the object, the first thing I did was change the objectName of each element to reflect its value. I already have text for this field so I added it where appropriate. For the tooltip, I clicked on the question mark button and and edited the toolTip text. -## Stylesheets +After completing this widget I copied it for every text box, and used its parts to construct all the other widgets I needed. Once each set of values in a section were complete I select the section header and all section values and use the "Lay Out Vertcally" button to place them in a singular layout. I then went through and named all the layouts to accurately reflect the contents inside of them. -## Unit Tests -## The Config +Once I have completed all the values I have to add the finishing touches before I can connect this to its backend. By clicking on some empty space outside of all my objects I selected the main window. I gave it the "objectName" ViewPort, a "windowIcon" form the commotion assets, and a "windowTitle." Now that the window is also set I can save this object and load it up in my back-end. + +This object saves as a ui file. If you are developing from within the commotion_clients repository you can use the existing makefile with the commant ```make test``` to have it compile your ui file into a python file you can sub-class from. It will also create a temporary compiled commotion_assets_rc.py file that will be needed to use any of the core Commotion assets without fully building the pyqt project. Once you have run ```make test``` A python file named "Ui_.py" will be created in the same directory as your .ui file. + ## The Backend +#### The Config + +Before the main window will load your application it needs a configuration file to load it from. This config file should be placed in your extensions main directory. For testing, you can place a copy of it in the folder "commotion_client/data/extensions/." The Commotion client will then automatically load your extension from its place in the "commotion_client/extensions/contrib" directory. We will cover how to package your extension for installation in the last section. + +Create a file in your main extension directory called ```config.json```. In that file place a json structure including the following items. + +{ +"name":"config_manager", +"menuItem":"Configuration Editor", +"parent":"Advanced", +"settings":"settings", +"taskbar":"task_bar", +"main":"main", +"tests":"test_suite" +} + +The "taskbar," "tests," and "settings," values are optional. But we will be making them in this tutorial. Here are explanations of each value. + +name: The name of the extension. This will be the name that the commotion client will use to import the extension after installation, and MUST be unique across the extensions that the user has installed. [from extensions import name] + +menuItem: The name displayed in the sub-menu that will load this extension. + +menuLevel: The level at which this sub-menu item will be displayed in relation to other (Non-Core) sub-menu items. The lower the number the higher up in the sub-menu. Core extension sub-menu items are ranked first, with other extensions being placed below them in order of ranking. + +parent: The top-level menu-item that this extension falls under. If this top-level menu does not exist it will be created. The top-level menu-item is simply a container that when clicked reveals the items below it. + +settings: (optional) The file that contains the settings page for the extension. If this is not included in the config file and a “settings” class is not found in the file listed under the “main” option the extension will not list a settings button in the extension settings page. [self.settings = name.settings.Settings(settingsViewport)] + +taskbar: (optional) The file that contains the function that will return the custom task-bar when run. The implementation of this is still in development. If not set and a “taskbar” class is not found in the file listed under the “main” option the default taskbar will be implemented. [self.taskbar = name.taskbar.TaskBar(mainTaskbar)] + +main: (optional) The file name to use to populate the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. [self.viewport = name.main.ViewPort(mainViewport)] + +tests: (optional, but bad form if missing) The file that contains the unitTests for this extension. This will be run when the main test_suite is called. If missing you will make the Commotion development team cry. [self.viewport = name.main.ViewPort(mainViewport)] + +Once you have a config file in place we can actually create the logic behind our application. + ### Main +The main component of your extension is the "main" python file as identified by the config. This file should be placed in the root of your extension's directory structure. I reccomend starting from the "main.py" template in the "docs/extension_template" directory in the commotion structure. That is what I will be starting from here. + +#### Loading your extensions GUI + +First you wan't to import your extension's ui. If you are creating an add on you will use an import from the extensions directory using a style similar to ```from extensions.contrib..ui import Ui_```. Since I will be building a core extension I will be using the following statement. +``` +from extensions.core.config_manager.ui import Ui_config_manager.py +``` + +Then you will extend the GUI that you created using the ViewPort class. + +``` +class ViewPort(Ui_config_manager.ViewPort): +``` + +If you are using the template the configuration is ready to apply stylesheets, and setup unit tests. + +#### Stylesheets + +Before we create our back-end we should finish the front end. The last step is going to be applying a style sheet to our object. I will quickly go over pyqt style sheets and then show you how to apply one of the default Commotion style sheets to your object. + +#### Unit Tests + +#### Taskbar + ### Settings -### Taskbar +### Packaging an extension + From 7af5e287583b2b2d8b6e27dd56a37077fcd4acb9 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 11:51:20 -0400 Subject: [PATCH 008/107] added creash and error signals to template and tutorial --- docs/extensions/extension_template/main.py | 11 +++++++++++ docs/extensions/tutorial/config_manager/main.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/extensions/extension_template/main.py b/docs/extensions/extension_template/main.py index b7dad62..7d100b0 100644 --- a/docs/extensions/extension_template/main.py +++ b/docs/extensions/extension_template/main.py @@ -26,9 +26,20 @@ class ViewPort(Ui_main.ViewPort): """ """ + #Signals for data collection, reporting, and alerting on errors + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + def __init__(self, parent=None): super().__init__() self.setupUi(self) + def send_signal(self): + self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) + + def send_error(self): + self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") + diff --git a/docs/extensions/tutorial/config_manager/main.py b/docs/extensions/tutorial/config_manager/main.py index d377113..38b106f 100644 --- a/docs/extensions/tutorial/config_manager/main.py +++ b/docs/extensions/tutorial/config_manager/main.py @@ -27,8 +27,19 @@ class ViewPort(Ui_main.ViewPort): """ """ - + + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + def __init__(self, parent=None): super().__init__() self.setupUi(self) + + def send_signal(self): + self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) + + def send_error(self): + self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") + From f0a079a8dcc98d16bcc05d72fc70975f2e00b0c3 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 11:51:47 -0400 Subject: [PATCH 009/107] added a basic commotion form stylesheet --- commotion_client/assets/stylesheets/forms.ss | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 commotion_client/assets/stylesheets/forms.ss diff --git a/commotion_client/assets/stylesheets/forms.ss b/commotion_client/assets/stylesheets/forms.ss new file mode 100644 index 0000000..e29d01b --- /dev/null +++ b/commotion_client/assets/stylesheets/forms.ss @@ -0,0 +1,56 @@ +/* + +Commotion style sheet for entry forms. + +Color Pallet: + +PRIMARY COLORS + * White FFFFFF + * Black 000000 + * Pink FF739C + +SECONDARY COLORS + * Electric Yellow E8FF00 + * Electric Purple 877AED + * Electric Green 00FFcF + * Blue 63CCF5 + * Gold C7BA38 + * Grey E6E6E6 + +Color Usage Ratio: + * 70% White + * 15% Black + * 10% Pink + * 5% Electric Purple + +Font Sizeing: + * 40 px Headings + * 13 Px [ALL CAPS]: Subheadings + * 13 px: Body Text +per: https://github.com/opentechinstitute/commotion-docs/blob/staging/commotionwireless.net/files/HIG_57_0.png *They meant pixel, not point. + + */ + +/* Defaults */ +* { font-size: 13px; } + +/* Section Header */ +.QLabel[style_sheet_type = "section_header"] { + font-size: 40px; + font-style: bold; +} + +/* Value Header */ +.QLabel[style_sheet_type = "value_header"] { + color: #877AED; + font-style: bold; +} + +/* Help Pop Up */ +.QToolTip[style_sheet_type = "value_help_text"] { background-color: #E6E6E6 } + +/* Help Text */ +.QLabel[style_sheet_type = "help_text"] { font-style: italic; } + +/* Static / Automatic / Unchangable Value */ +.QLabel[style_sheet_type = "help_text"] { background-color: #E6E6E6 } From 2e4cb5e0837d093e4ba9559b6926a7c0313760f7 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 11:54:54 -0400 Subject: [PATCH 010/107] Prepared main_window for loading extensions Moved setup functionality into modules and added a basic welcome page as the first extension loaded --- commotion_client/GUI/main_window.py | 102 +++++++++++++++--------- commotion_client/GUI/ui/welcome_page.ui | 53 ++++++++++++ commotion_client/GUI/welcome_page.py | 34 ++++++++ 3 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 commotion_client/GUI/ui/welcome_page.ui create mode 100644 commotion_client/GUI/welcome_page.py diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index 25b4242..e6f12e8 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -22,7 +22,7 @@ from assets import commotion_assets_rc from GUI.menu_bar import MenuBar from GUI.crash_report import CrashReport - +from GUI.welcome_page import ViewPort class MainWindow(QtGui.QMainWindow): """ @@ -35,18 +35,30 @@ class MainWindow(QtGui.QMainWindow): def __init__(self, parent=None): super().__init__() - self.dirty = False #The variable to keep track of state for tracking if the gui needs any clean up. - #set function logger - self.log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. + #Keep track of if the gui needs any clean up / saving. + self.dirty = False + self.log = logging.getLogger("commotion_client."+__name__) try: - self.crash_report = CrashReport() + self.setup_crash_reporter() except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) + self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup Crash reporter.")) + self.log.debug(_excp, exc_info=1) + raise + + try: + self.setup_menu_bar() + except Exception as _excp: + self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup Menu Bar.")) + self.log.debug(_excp, exc_info=1) + raise + + try: + self.setup_view_port() + except Exception as _excp: + self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup view port.")) self.log.debug(_excp, exc_info=1) raise - else: - self.crash_report.crash.connect(self.crash) #Default Paramiters #TODO to be replaced with paramiters saved between instances later try: @@ -55,36 +67,24 @@ def __init__(self, parent=None): self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load window settings.")) self.log.debug(_excp, exc_info=1) raise - + #set main menu to not close application on exit events self.exitOnClose = False self.remove_on_close = False - - - #Set up Main Viewport - #self.viewport = Viewport(self) - - - #REMOVE THIS TEST CENTRAL WIDGET SECTION #================================== - from tests.extensions.test_ext001 import myMain - self.centralwidget = QtGui.QWidget(self) - self.centralwidget.setMinimumSize(600, 600) - self.central_app = myMain.viewport(self) - self.setCentralWidget(self.central_app) - - #connect central app to crash reporter - self.central_app.data_report.connect(self.crash_report.crash_info) - self.crash_report.crash_override.connect(self.central_app.start_report_collection) - #connect error reporter to crash reporter - self.central_app.error_report.connect(self.crash_report.alert_user) - - #================================== - #Set up menu bar. + def toggle_menu_bar(self): + #if menu shown... then + #DockToHide = self.findChild(name="MenuBarDock") + #QMainWindow.removeDockWidget (self, QDockWidget dockwidget) + #else + #bool QMainWindow.restoreDockWidget (self, QDockWidget dockwidget) + pass + + def setup_menu_bar(self): + """ Set up menu bar. """ self.menu_bar = MenuBar(self) - #Create dock for menu-bar TEST self.menu_dock = QtGui.QDockWidget(self) #turn off title bar @@ -100,16 +100,40 @@ def __init__(self, parent=None): #Create slot to monitor when menu-bar wants the main window to change the main-viewport self.connect(self.menu_bar, QtCore.SIGNAL("viewportRequested()"), self.changeViewport) - - def toggle_menu_bar(self): - #if menu shown... then - #DockToHide = self.findChild(name="MenuBarDock") - #QMainWindow.removeDockWidget (self, QDockWidget dockwidget) - #else - #bool QMainWindow.restoreDockWidget (self, QDockWidget dockwidget) - pass + def setup_crash_reporter(self): + try: + self.crash_report = CrashReport() + except Exception as _excp: + self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) + self.log.debug(_excp, exc_info=1) + raise + else: + self.crash_report.crash.connect(self.crash) + + def setup_view_port(self): + #Set up Main Viewport + self.viewport = ViewPort(self) + self.setCentralWidget(self.viewport) + + #connect viewport extension to crash reporter + self.viewport.data_report.connect(self.crash_report.crash_info) + self.crash_report.crash_override.connect(self.viewport.start_report_collection) + #connect error reporter to crash reporter + self.viewport.error_report.connect(self.crash_report.alert_user) + + + #REMOVE THIS TEST CENTRAL WIDGET SECTION + #================================== + #from tests.extensions.test_ext001 import myMain + #self.centralwidget = QtGui.QWidget(self) + #self.centralwidget.setMinimumSize(600, 600) + #self.central_app = myMain.viewport(self) + #self.setCentralWidget(self.central_app) + + + def changeViewport(self, viewport): self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport received.")) self.viewport.setViewport(viewport) diff --git a/commotion_client/GUI/ui/welcome_page.ui b/commotion_client/GUI/ui/welcome_page.ui new file mode 100644 index 0000000..e6a93fa --- /dev/null +++ b/commotion_client/GUI/ui/welcome_page.ui @@ -0,0 +1,53 @@ + + + ViewPort + + + + 0 + 0 + 640 + 480 + + + + Form + + + + + 240 + 200 + 144 + 77 + + + + + + + + + + :/logo62.png + + + Qt::AlignCenter + + + + + + + Commotion Computer + + + + + + + + + + + diff --git a/commotion_client/GUI/welcome_page.py b/commotion_client/GUI/welcome_page.py new file mode 100644 index 0000000..a7f00b7 --- /dev/null +++ b/commotion_client/GUI/welcome_page.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +welcome_page + +The welcome page for the main window. + +Key components handled within. + * being pretty and welcoming to new users + +""" + +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +from GUI.ui import Ui_welcome_page + +class ViewPort(Ui_welcome_page.ViewPort): + """ + """ + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__() + self.log = logging.getLogger("commotion_client."+__name__) + self.setupUi(self) #run setup function from Ui_main_window From e6326b37daa9dd651e3dc6e90d296c2ed2da7aa4 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 11:56:03 -0400 Subject: [PATCH 011/107] Changed custom exit function name to not overwrite default exit function --- commotion_client/commotion_client.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index d8f5516..cbfedb9 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -92,10 +92,7 @@ def main(): else: log.info(app.translate("logs", "application is already running. Application will be brought to foreground")) app.send_message("showMain") - app.exit("Only one instance of a commotion application may be running at any time.") - - #initialize client (GUI, controller, etc) - app.init_client() + app.end("Only one instance of a commotion application may be running at any time.") sys.exit(app.exec_()) log.debug(app.translate("logs", "Shutting down")) @@ -119,7 +116,7 @@ def run(self): if self.restart_complete: self.log.debug(QtCore.QCoreApplication.translate("logs", "Restart event identified. Thread quitting")) break - self.exit() + self.end() class CommotionClientApplication(single_application.SingleApplicationWithMessaging): """ @@ -141,6 +138,10 @@ def __init__(self, key, status, argv): self.main = False self.sys_tray = False + #initialize client (GUI, controller, etc) upon event loop start so that exit/quit works on errors. + QtCore.QTimer.singleShot(0, self.init_client) + + #================================================= # CLIENT LOGIC #================================================= @@ -158,7 +159,7 @@ def init_client(self): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not fully initialize applicaiton. Application must be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) def start_full(self): """ @@ -171,7 +172,7 @@ def start_full(self): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create Main Window. Application must be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.init_main() try: @@ -180,7 +181,7 @@ def start_full(self): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create system tray. Application must be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.init_sys_tray() @@ -223,7 +224,7 @@ def stop_client(self, force_close=None): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not cleanly close client. Application must be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be closed.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) @@ -246,7 +247,7 @@ def restart_client(self, force_close=None): _catch_all = QtCore.QCoreApplication.translate("logs", "Client could not be restarted. Applicaiton will now be halted") self.log.error(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be restarted.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) @@ -363,7 +364,7 @@ def close_main_window(self, force_close=None): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close main window using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close main window.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) @@ -410,7 +411,7 @@ def close_controller(self, force_close=None): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close controller using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not cleanly close controller.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) @@ -469,7 +470,7 @@ def close_sys_tray(self, force_close=None): _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close system tray using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close system tray.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) @@ -494,14 +495,15 @@ def process_message(self, message): else: self.log.info(self.translate("logs", "message \"{0}\" not a supported type.".format(message))) - def exit(self, message=None): + def end(self, message=None): """ Handles properly exiting the application. @param message string optional exit message to print to standard error on application close. This will FORCE the application to close in an unclean way. """ if message: - self.exit(message) + self.log.error(self.translate("logs", message)) + self.exit(1) else: self.quit() From 95697931c06469bc84635aff5195a137c3e73387 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 17:52:17 -0400 Subject: [PATCH 012/107] added clean up functions to tutorial files --- docs/extensions/extension_template/main.py | 12 ++++++++++-- docs/extensions/tutorial/config_manager/main.py | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/extensions/extension_template/main.py b/docs/extensions/extension_template/main.py index 7d100b0..b8e19da 100644 --- a/docs/extensions/extension_template/main.py +++ b/docs/extensions/extension_template/main.py @@ -30,11 +30,13 @@ class ViewPort(Ui_main.ViewPort): start_report_collection = QtCore.pyqtSignal() data_report = QtCore.pyqtSignal(str, dict) error_report = QtCore.pyqtSignal(str) + on_stop = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__() + self._dirty = False self.setupUi(self) - + self.start_report_collection.connect(self.send_signal) def send_signal(self): self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) @@ -42,4 +44,10 @@ def send_signal(self): def send_error(self): self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") - + @property + def is_dirty(self): + """The current state of the viewport object """ + return self.dirty + + def clean_up(self): + self.on_stop.emit() diff --git a/docs/extensions/tutorial/config_manager/main.py b/docs/extensions/tutorial/config_manager/main.py index 38b106f..63ed6cf 100644 --- a/docs/extensions/tutorial/config_manager/main.py +++ b/docs/extensions/tutorial/config_manager/main.py @@ -35,6 +35,7 @@ class ViewPort(Ui_main.ViewPort): def __init__(self, parent=None): super().__init__() self.setupUi(self) + self.start_report_collection.connect(self.send_signal) def send_signal(self): From 957adfb4308678674cacf7342ec8728c652d9568 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 17:53:15 -0400 Subject: [PATCH 013/107] added viewport clean-up and swap functions --- commotion_client/GUI/main_window.py | 79 +++++++++++++---------------- 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index e6f12e8..d68ff33 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -22,43 +22,28 @@ from assets import commotion_assets_rc from GUI.menu_bar import MenuBar from GUI.crash_report import CrashReport -from GUI.welcome_page import ViewPort +from GUI import welcome_page class MainWindow(QtGui.QMainWindow): """ The central widget for the commotion client. This widget initalizes all other sub-widgets and extensions as well as defines the paramiters of the main GUI container. """ - #Closing Signal used by children to do any clean-up or saving needed - closing = QtCore.pyqtSignal() + #Clean up signal atched by children to do any clean-up or saving needed + clean_up = QtCore.pyqtSignal() app_message = QtCore.pyqtSignal(str) def __init__(self, parent=None): super().__init__() #Keep track of if the gui needs any clean up / saving. - self.dirty = False + self._dirty = False self.log = logging.getLogger("commotion_client."+__name__) - try: - self.setup_crash_reporter() - except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup Crash reporter.")) - self.log.debug(_excp, exc_info=1) - raise - - try: - self.setup_menu_bar() - except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup Menu Bar.")) - self.log.debug(_excp, exc_info=1) - raise - - try: - self.setup_view_port() - except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to setup view port.")) - self.log.debug(_excp, exc_info=1) - raise + self.setup_crash_reporter() + self.setup_menu_bar() + + self.next_viewport = welcome_page.ViewPort(self) + self.set_viewport() #Default Paramiters #TODO to be replaced with paramiters saved between instances later try: @@ -99,7 +84,7 @@ def setup_menu_bar(self): self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.menu_dock) #Create slot to monitor when menu-bar wants the main window to change the main-viewport - self.connect(self.menu_bar, QtCore.SIGNAL("viewportRequested()"), self.changeViewport) + self.connect(self.menu_bar, QtCore.SIGNAL("viewportRequested()"), self.change_viewport) def setup_crash_reporter(self): try: @@ -111,9 +96,13 @@ def setup_crash_reporter(self): else: self.crash_report.crash.connect(self.crash) - def setup_view_port(self): - #Set up Main Viewport - self.viewport = ViewPort(self) + def set_viewport(self): + """Set viewport to next viewport and load viewport """ + self.viewport = self.next_viewport + self.load_viewport() + + def load_viewport(self): + """Apply current viewport to the central widget and set up proper signal's for communication. """ self.setCentralWidget(self.viewport) #connect viewport extension to crash reporter @@ -123,20 +112,18 @@ def setup_view_port(self): #connect error reporter to crash reporter self.viewport.error_report.connect(self.crash_report.alert_user) + #Attach clean up signal + self.clean_up.connect(self.viewport.clean_up) - #REMOVE THIS TEST CENTRAL WIDGET SECTION - #================================== - #from tests.extensions.test_ext001 import myMain - #self.centralwidget = QtGui.QWidget(self) - #self.centralwidget.setMinimumSize(600, 600) - #self.central_app = myMain.viewport(self) - #self.setCentralWidget(self.central_app) - - - - def changeViewport(self, viewport): + def change_viewport(self, viewport): + """Prepare next viewport for loading and start loading process when ready.""" self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport received.")) - self.viewport.setViewport(viewport) + self.next_viewport = viewport + if self.viewport.is_dirty: + self.viewport.on_stop.connect(self.set_viewport) + self.clean_up.emit() + else: + self.set_viewport() def purge(self): """ @@ -174,8 +161,8 @@ def exitEvent(self): self.close() def cleanup(self): - self.closing.emit() #send signal for others to clean up if they need to - if self.dirty: + self.clean_up.emit() #send signal for others to clean up if they need to + if self.is_dirty: self.save_settings() @@ -233,10 +220,16 @@ def crash(self, crash_type): """ Emits a closing signal to allow other windows who need to clean up to clean up and then exits the application. """ - self.closing.emit() #send signal for others to clean up if they need to + self.clean_up.emit() #send signal for others to clean up if they need to if crash_type == "restart": self.app_message.emit("restart") else: self.exitOnClose = True self.close() + + @property + def is_dirty(self): + """Get the current state of the main window""" + return self._dirty + From 6f34f711d830e6bf43d5a8184d87bc59bf47b3a6 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 13 Mar 2014 17:53:38 -0400 Subject: [PATCH 014/107] added clean up functions to welcome page --- commotion_client/GUI/welcome_page.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/commotion_client/GUI/welcome_page.py b/commotion_client/GUI/welcome_page.py index a7f00b7..46e8bed 100644 --- a/commotion_client/GUI/welcome_page.py +++ b/commotion_client/GUI/welcome_page.py @@ -27,8 +27,20 @@ class ViewPort(Ui_welcome_page.ViewPort): start_report_collection = QtCore.pyqtSignal() data_report = QtCore.pyqtSignal(str, dict) error_report = QtCore.pyqtSignal(str) + on_stop = QtCore.pyqtSignal() + def __init__(self, parent=None): super().__init__() self.log = logging.getLogger("commotion_client."+__name__) self.setupUi(self) #run setup function from Ui_main_window + self._dirty = False + + @property + def is_dirty(self): + """The current state of the viewport object """ + return self.dirty + + def clean_up(self): + self.on_stop.emit() + From 79afaf720077690c548fd1643e02c832c6b16841 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 14 Mar 2014 09:12:47 -0400 Subject: [PATCH 015/107] replaced exception log.debug logging with log.exception calls --- commotion_client/GUI/main_window.py | 15 +++++---- commotion_client/commotion_client.py | 48 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index d68ff33..494fc10 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -39,7 +39,7 @@ def __init__(self, parent=None): self._dirty = False self.log = logging.getLogger("commotion_client."+__name__) - self.setup_crash_reporter() + self.init_crash_reporter() self.setup_menu_bar() self.next_viewport = welcome_page.ViewPort(self) @@ -50,7 +50,7 @@ def __init__(self, parent=None): self.load_settings() except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load window settings.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise #set main menu to not close application on exit events @@ -86,12 +86,13 @@ def setup_menu_bar(self): #Create slot to monitor when menu-bar wants the main window to change the main-viewport self.connect(self.menu_bar, QtCore.SIGNAL("viewportRequested()"), self.change_viewport) - def setup_crash_reporter(self): + def init_crash_reporter(self): + """ """ try: self.crash_report = CrashReport() except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise else: self.crash_report.crash.connect(self.crash) @@ -190,14 +191,14 @@ def load_settings(self): geometry = _settings.value("geometry") or defaults['geometry'] except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not load window geometry from settings file or defaults.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise _settings.endGroup() try: self.setGeometry(geometry) except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Cannot create GUI window.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise def save_settings(self): @@ -212,7 +213,7 @@ def save_settings(self): _settings.setValue("geometry", self.geometry()) except Exception as _excp: self.log.warn(QtCore.QCoreApplication.translate("logs", "Could not save window geometry. Will continue without saving window geometry.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) _settings.endGroup() diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index cbfedb9..97a6041 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -158,7 +158,7 @@ def init_client(self): except Exception as _excp: #log failure here and exit _catch_all = QtCore.QCoreApplication.translate("logs", "Could not fully initialize applicaiton. Application must be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) def start_full(self): @@ -171,7 +171,7 @@ def start_full(self): except Exception as _excp: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create Main Window. Application must be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.init_main() @@ -180,7 +180,7 @@ def start_full(self): except Exception as _excp: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create system tray. Application must be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.init_sys_tray() @@ -195,7 +195,7 @@ def start_daemon(self): self.hide_main_window(force=True, errors="strict") except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not close down existing GUI componenets to switch to daemon mode.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise try: #create controller and sys tray @@ -204,7 +204,7 @@ def start_daemon(self): # self.controller = create_controller() except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not start daemon. Application must be halted.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise else: self.init_sys_tray() @@ -223,12 +223,12 @@ def stop_client(self, force_close=None): if force_close: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not cleanly close client. Application must be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be closed.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) def restart_client(self, force_close=None): """ @@ -246,12 +246,12 @@ def restart_client(self, force_close=None): if force_close: _catch_all = QtCore.QCoreApplication.translate("logs", "Client could not be restarted. Applicaiton will now be halted") self.log.error(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be restarted.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise _restart.end() @@ -271,7 +271,7 @@ def create_main_window(self): _main = main_window.MainWindow() except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not create Main Window. Application must be halted.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise else: return _main @@ -287,13 +287,13 @@ def init_main(self): self.sys_tray.show_main.connect(self.main.bring_front) except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not initialize connections between the main window and other application components.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise try: self.main.show() except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not show the main window.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise def hide_main_window(self, force=None, errors=None): @@ -308,7 +308,7 @@ def hide_main_window(self, force=None, errors=None): self.main.exit() except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not hide main window. Attempting to close all and only open taskbar.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) if force: try: self.main.purge() @@ -316,7 +316,7 @@ def hide_main_window(self, force=None, errors=None): self.main = self.create_main_window() except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not force main window restart.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise elif errors == "strict": raise @@ -334,7 +334,7 @@ def hide_main_window(self, force=None, errors=None): self.main.app_message.connect(self.process_message) except: self.log.error(QtCore.QCoreApplication.translate("logs", "Could close and re-open the main window.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) if errors == "strict": raise else: @@ -363,12 +363,12 @@ def close_main_window(self, force_close=None): except Exception as _excp: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close main window using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close main window.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise #================================================= @@ -385,7 +385,7 @@ def create_controller(self): #self.controller.init() #?????? except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not create controller. Application must be halted.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise def init_controller(self): @@ -410,12 +410,12 @@ def close_controller(self, force_close=None): except Exception as _excp: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close controller using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not cleanly close controller.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise @@ -433,7 +433,7 @@ def init_sys_tray(self): self.sys_tray.show_main.connect(self.main.bring_front) except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not initialize connections between the system tray and other application components.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise def create_sys_tray(self): @@ -444,7 +444,7 @@ def create_sys_tray(self): tray = system_tray.TrayIcon() except Exception as _excp: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not start system tray.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise else: return tray @@ -469,12 +469,12 @@ def close_sys_tray(self, force_close=None): except: _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close system tray using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) self.end(_catch_all) else: self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close system tray.")) self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) - self.log.debug(_excp, exc_info=1) + self.log.exception(_excp) raise #================================================= From 0f9f973872bf581f57ee09179e73fcb2c185d1c6 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 14 Mar 2014 10:32:32 -0400 Subject: [PATCH 016/107] cleaned up exception handling Added proper logging and removed unneeded global exception handling --- commotion_client/utils/config.py | 72 +++++++++++++++----------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index 49b4403..a7daee5 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -19,18 +19,18 @@ #set function logger log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. -def findConfigs(configType, name=None): +def find_configs(config_type, name=None): """ Function used to obtain the path to a config file. - @param configType The type of configuration file sought. (global, user, extension) + @param config_type The type of configuration file sought. (global, user, extension) @param name optional The name of the configuration file if known @return list of tuples containing the path and name of found config files or False if a config matching the description cannot be found. """ - configFiles = getConfigPaths(configType) - if configFiles: - configs = getConfig(configFiles) + config_files = get_config_paths(config_type) + if config_files: + configs = get_config(config_files) return configs elif name != None: for conf in configs: @@ -42,35 +42,35 @@ def findConfigs(configType, name=None): log.error(QtCore.QCoreApplication.translate("logs", "No Configs of the chosed type found")) return False -def getConfigPaths(configType): +def get_config_paths(config_type): configLocations = {"global":"data/global/", "user":"data/user/", "extension":"data/extensions/"} - configFiles = [] + config_files = [] try: - path = configLocations[configType] - except KeyError as e: - log.error(QtCore.QCoreApplication.translate("logs", "Cannot search for config type {0} as it is an unsupported type.".format(configType))) - self.log.debug(e, exc_info=1) + path = configLocations[config_type] + except KeyError as _excp: + log.error(QtCore.QCoreApplication.translate("logs", "Cannot search for config type {0} as it is an unsupported type.".format(config_type))) + self.log.exception(_excp) return False try: for root, dirs, files in fsUtils.walklevel(path): for file_name in files: if file_name.endswith(".conf"): - configFiles.append(os.path.join(root, file_name)) - except AssertionError as e: + config_files.append(os.path.join(root, file_name)) + except AssertionError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "Config file folder at path {0} does not exist. No Config files loaded.".format(path))) - self.log.debug(e, exc_info=1) - except TypeError as e: + self.log.exception(_excp) + except TypeError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) - self.log.debug(e, exc_info=1) - if configFiles: - return configFiles + self.log.exception(_excp) + if config_files: + return config_files else: return False -def getConfig(paths): +def get_config(paths): """ Generator to retreive config files for the paths passed to it @@ -79,19 +79,15 @@ def getConfig(paths): """ #load config file for path in paths: - try: - if fsUtils.is_file(path): - config = loadConfig(path) - if config: - yield config - else: - log.error(QtCore.QCoreApplication.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) - except Exception as e: - log.error(QtCore.QCoreApplication.translate("logs", "Config file {0} cannot be loaded.".format(path))) - self.log.debug(e, exc_info=1) + if fsUtils.is_file(path): + config = load_config(path) + if config: + yield config + else: + log.error(QtCore.QCoreApplication.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) -def loadConfig(config): +def load_config(config): """ This function loads a json formatted config file and returns it. @@ -102,13 +98,13 @@ def loadConfig(config): #Open the file try: f = open(config, mode='r', encoding="utf-8", errors="strict") - except ValueError as e: + except ValueError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(config))) - self.log.debug(e, exc_info=1) + self.log.exception(_excp) return False - except Exception as e: + except Exception as _excp: log.error(QtCore.QCoreApplication.translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file exists and is not corrupted.".format(config))) - self.log.debug(e, exc_info=1) + self.log.exception(_excp) return False else: tmpMsg = f.read() @@ -117,10 +113,10 @@ def loadConfig(config): data = json.loads(tmpMsg) log.debug(QtCore.QCoreApplication.translate("logs", "Successfully loaded {0}".format(config))) return data - except ValueError as e: + except ValueError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(config))) - self.log.debug(e, exc_info=1) + self.log.exception(_excp) return False - except Exception as e: + except Exception as _excp: log.error(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to an unknown error.".format(config))) - self.log.debug(e, exc_info=1) + self.log.exception(_excp) From 55990681bde7d05b4a3ebf4695d6cf3d1c09ce68 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 17 Mar 2014 12:59:37 -0400 Subject: [PATCH 017/107] added extension manager core Extension manager created with core functionality to load an installed extension, get a section from a extension, and check the installed extensions. --- commotion_client/extensions/__init__.py | 0 .../extensions/extension_manager.py | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 commotion_client/extensions/__init__.py create mode 100644 commotion_client/extensions/extension_manager.py diff --git a/commotion_client/extensions/__init__.py b/commotion_client/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/extensions/extension_manager.py b/commotion_client/extensions/extension_manager.py new file mode 100644 index 0000000..e07c915 --- /dev/null +++ b/commotion_client/extensions/extension_manager.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +extension_manager + +The extension management object. + +Key componenets handled within: + * finding, loading, and unloading extensions + * installing extensions + +""" +#Standard Library Imports +import logging +import importlib + +#PyQt imports +from PyQt4 import QtCore + +#Commotion Client Imports +from utils import config +import extensions + +class ExtensionManager(): + """ """ + def __init__(self, parent=None): + self.log = logging.getLogger("commotion_client."+__name__) + self.extensions = self.check_installed() + + + def check_installed(self): + """Creates dictionary keyed with the name of installed extensions with each extensions type. + + @returns dict Dictionary keyed with the name of installed extensions with each extensions type. + """ + extensions = {} + _settings = QtCore.QSettings() + _settings.beginGroup("extensions") + _settings.beginGroup("core") + core = settings.allKeys(); + _settings.endGroup() + _settings.beginGroup("contrib") + contrib = settings.allKeys(); + _settings.endGroup() + _settings.endGroup() + for ext in core: + extensions[ext] = "core" + for ext in contrib: + extensions[ext] = "contrib" + return extensions + + + def get(self, extension_name, subsection=None): + """Return the full extension object or one of its primary sub-objects (settings or main) + + @subsection str Name of a objects sub-section. (settings or main) + """ + config = config.find_configs("user", extension_name) + if subsection: + subsection = config[subsection] + extension = self.import_extension(extension_name, subsection) + if subsection: + return extension.ViewPort() + else: + return extension + + def import_extension(self, extension_name, subsection=None): + """load extensions by string name.""" + if subsection: + extension = importlib.import_module("."+subsection, "extensions."+extension_name) + else: + extension = importlib.import_module("."+extension_name, "extensions") + return extension + From c435185d9516a0feb40d35914b97d8bb7e141290 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 17 Mar 2014 13:11:32 -0400 Subject: [PATCH 018/107] added proper usage of default values to settings loading. --- commotion_client/GUI/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index 494fc10..84aadc6 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -188,7 +188,7 @@ def load_settings(self): #Load settings from saved, or use defaults try: - geometry = _settings.value("geometry") or defaults['geometry'] + geometry = _settings.value("geometry", defaults['geometry']) except Exception as _excp: self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not load window geometry from settings file or defaults.")) self.log.exception(_excp) From f97012f75d5f32fd30142c6f5742a27e41b2bfe0 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 17 Mar 2014 13:57:04 -0400 Subject: [PATCH 019/107] Fixed filename to == PEP8 --- commotion_client/utils/{fsUtils.py => fs_utils.py} | 1 - 1 file changed, 1 deletion(-) rename commotion_client/utils/{fsUtils.py => fs_utils.py} (99%) diff --git a/commotion_client/utils/fsUtils.py b/commotion_client/utils/fs_utils.py similarity index 99% rename from commotion_client/utils/fsUtils.py rename to commotion_client/utils/fs_utils.py index fa1ad24..ca62c93 100644 --- a/commotion_client/utils/fsUtils.py +++ b/commotion_client/utils/fs_utils.py @@ -13,7 +13,6 @@ #set function logger log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. - def is_file(unknown): """ Determines if a file is accessable. It does NOT check to see if the file contains any data. From 1eb22ed8a19f844ec719b106b4bdae9df13ac0b6 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 16:59:19 -0400 Subject: [PATCH 020/107] added a config file validation object --- commotion_client/utils/validate.py | 259 +++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 commotion_client/utils/validate.py diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py new file mode 100644 index 0000000..8a63262 --- /dev/null +++ b/commotion_client/utils/validate.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +validate + +A collection of validation functions + +Key componenets handled within: + +""" +#Standard Library Imports +import logging +import sys +import re + +#PyQt imports +from PyQt4 import QtCore + + +class ClientConfig(): + + def __init__(self, config=None, directory=None): + if config: + self._config = config.load_config(config) + self._directory = directory + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + + def set_extension(directory, config=None): + """Set the default values + + @param config string The absolute path to a config file for this extension + @param directory string The absolute path to this extensions directory + """ + self._directory = directory + if config: + self._config = config.load_config(config) + else: + default_config_path = os.path.join( self._directory, "config.json" ) + if fs_utils.is_file( default_config_path ): + self._config = config.load_config(default_config_path) + else: + raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) + + def validate_all(self): + """Run all validation functions on an uncompressed extension. + + @brief Will set self.errors if any errors are found. + @return bool True if valid, False if invalid. + """ + self.errors = None + if not directory: + if not self.directory: + raise NameError(self.translate("logs", "ClientConfig validator requires at least an extension directory to be specified")) + else: + directory = self.directory + errors = [] + if not self.name(): + errors.append("name") + if not self.tests(): + errors.append("tests") + if not self.menu_level(): + errors.append("menu_level") + if not self.menu_item(): + errors.append("menu_item") + if not self.parent(): + errors.append("parent") + else: + for gui_name in ['main', 'settings', 'toolbar']: + if not self.gui( gui_name ): + errors.append( gui_name ) + if errors: + self.errors = errors + return False + else: + return True + + + def gui( self, gui_name ): + """Validate of one of the gui objects config values. (main, settings, or toolbar) + + @param gui_name string "main", "settings", or "toolbar" + """ + try: + val = str( self._config[ gui_name ] ) + except KeyError: + if gui_name != "main": + try: + val = str( self._config[ "main" ] ) + except KeyError: + val = str( 'main' ) + else: + val = str( 'main' ) + file_name = val + ".py" + if not self.check_path( file_name ): + self.log.warn(self.translate( "logs", "The extensions {0} file name is invalid for this system.".format( gui_name ) )) + return False + if not self.check_exists( file_name ): + self.log.warn(self.translate( "logs", "The extensions {0} file does not exist.".format( gui_name ) )) + return False + return True + + def name( self ): + try: + name_val = str( self._config['name'] ) + except KeyError: + self.log.warn(self.translate( "logs", "There is no name value in the config file. This value is required." )) + return False + if not self.check_path_length( name_val ): + self.log.warn(self.translate( "logs", "This value is too long for your system." )) + return False + if not self.check_file_chars( name_val ): + self.log.warn(self.translate( "logs", "This value uses invalid characters for your system." )) + return False + return True + + def menu_item( self ): + """Validate a menu item value.""" + try: + val = str( self._config[ "menu_item" ] ) + except KeyError: + if self.name(): + val = str( self._config[ "name" ] ) + else: + self.log.warn(self.translate( "logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid." )) + return False + if not check_menu_text( val ): + self.log.warn(self.translate("logs", "The menu_item value is invalid")) + return False + return True + + def parent( self ): + """Validate a parent value.""" + try: + val = str( self._config[ "parent" ] ) + except KeyError: + self.log.info(self.translate( "logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used." )) + return True + if not self.check_menu_text( val ): + self.log.warn(self.translate("logs", "The parent value is invalid")) + return False + return True + + def menu_level( self ): + """Validate a Menu Level Config item.""" + try: + val = int( self._config[ "menu_level" ] ) + except KeyError: + self.log.info(self.translate( "logs", "There is no 'menu_level' value set in the config. As such the default value of 10 will be used." )) + return True + except ValueError: + self.log.info(self.translate( "logs", "The 'menu_level' value set in the config is not a number and is therefore invalid." )) + return False + if not ( 0 < val > 100 ): + self.log.warn(self.translate("logs", "The menu_level is invalid. Choose a number between 1 and 100")) + return False + return True + + def tests( self ): + """Validate a tests config menu item.""" + try: + val = str( self._config[ "tests" ] ) + except KeyError: + val = str( 'tests' ) + file_name = val + ".py" + if not self.check_path( file_name ): + self.log.warn(self.translate( "logs", "The extensions 'tests' file name is invalid for this system.")) + return False + if not self.check_exists( file_name ): + self.log.warn(self.translate( "logs", "The extensions 'tests' file does not exist." )) + return False + return True + + def check_menu_text( self, menu_text ): + """ + Checks that menu text fits within the accepted string length bounds. + + @param menu_text string The text that will appear in the menu. + """ + if not ( 3 < len( str( menu_text )) > 40 ): + self.log.warn(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) + return False + + def check_exists(self, file_name): + """Checks if a specified file exists within a directory + + @param file_name string The file name from a config file + """ + files = QtCore.QDir(self.directory).entryList() + if not ( str( file_name )) in files: + self.log.warn(self.translate("logs", "The specified file does not exist.")) + return False + else: + return True + + def check_path(self, file_name): + """Runs all path checking functions on a string. + + @param file_name string The string to check for validity. + """ + if not check_path_length( file_name ): + self.log.warn(self.translate( "logs", "This value is too long for your system." )) + return False + if not check_file_chars( file_name ): + self.log.warn(self.translate( "logs", "This value uses invalid characters for your system." )) + return False + return True + + def check_path_chars(self, file_name): + """Checks if a string is a valid file name on this system. + + @param file_name string The string to check for validity + """ + # file length limit + platform = sys.platform + reserved = { "cygwin" : "[\|\\\?\*<\":>\+\[\]/]", + "win32" : "[\|\\\?\*<\":>\+\[\]/]", + "darwin" : "[:]", + "linux" : "[/\x00]" } + if platform and reserved[platform]: + if re.search( file_name, reserved[platform] ): + self.log.warn(self.translate("logs", "The extension's config file contains an invalid main value.")) + return False + else: + return True + else: + self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file uses chars that your system does not allow.").format(platform)) + return True + + + def check_path_length(self, file_name=None): + """Checks if a string will be of a valid length for a file name and full path on this system. + + @param file_name string The string to check for validity. + """ + # file length limit + platform = sys.platform + # OSX(name<=255), linux(name<=255) + name_limit = ['linux', 'darwin'] + # Win(name+path<=260), + path_limit = ['win32', 'cygwin'] + if platform in path_limit: + if self.name(): #check valid name before using it + extension_path = os.path.join( QtCore.QDir.currentPath(), "extensions" ) + full_path = os.path.join( extension_path, file_name ) + else: + self.log.warn(self.translate("logs", "The extension's config file 'main' value requires a valid 'name' value. Which this extension does not have.")) + return False + if len( str( full_path )) > 255: + self.log.warn(self.translate("logs", "The full extension path cannot be greater than 260 chars")) + return False + else if platform in name_limit: + if len( str( file_name ) ) > 260: + self.log.warn(self.translate("logs", "File names can not be greater than 260 chars on your system")) + return False + else: + self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) + return True From 3d0c03c12be71ccf95fe226e3efddebe18dfaa45 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 17:01:12 -0400 Subject: [PATCH 021/107] applied a shorter variable for qt logging functions --- commotion_client/GUI/main_window.py | 33 ++++++------ commotion_client/GUI/menu_bar.py | 13 ++--- commotion_client/commotion_client.py | 75 ++++++++++++++-------------- 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index 84aadc6..b65a0d0 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -23,6 +23,8 @@ from GUI.menu_bar import MenuBar from GUI.crash_report import CrashReport from GUI import welcome_page +from utils import config +from extensions.extension_manager import ExtensionManager class MainWindow(QtGui.QMainWindow): """ @@ -37,7 +39,8 @@ def __init__(self, parent=None): super().__init__() #Keep track of if the gui needs any clean up / saving. self._dirty = False - self.log = logging.getLogger("commotion_client."+__name__) + self.log = logging.getLogger("commotion_client."+__name__ + self.translate = QtCore.QCoreApplication.translate self.init_crash_reporter() self.setup_menu_bar() @@ -49,7 +52,7 @@ def __init__(self, parent=None): try: self.load_settings() except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load window settings.")) + self.log.critical(self.translate("logs", "Failed to load window settings.")) self.log.exception(_excp) raise @@ -84,24 +87,24 @@ def setup_menu_bar(self): self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.menu_dock) #Create slot to monitor when menu-bar wants the main window to change the main-viewport - self.connect(self.menu_bar, QtCore.SIGNAL("viewportRequested()"), self.change_viewport) + self.menu_bar.viewport_requested.connect(self.change_viewport) def init_crash_reporter(self): """ """ try: self.crash_report = CrashReport() except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) + self.log.critical(self.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) self.log.exception(_excp) raise else: self.crash_report.crash.connect(self.crash) def set_viewport(self): - """Set viewport to next viewport and load viewport """ - self.viewport = self.next_viewport - self.load_viewport() - + """Load and set viewport to next viewport and load viewport """ + self.viewport = ExtensionManager.get_GUI(self.next_viewport, "main") + self.load_viewport() + def load_viewport(self): """Apply current viewport to the central widget and set up proper signal's for communication. """ self.setCentralWidget(self.viewport) @@ -118,7 +121,7 @@ def load_viewport(self): def change_viewport(self, viewport): """Prepare next viewport for loading and start loading process when ready.""" - self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport received.")) + self.log.debug(self.translate("logs", "Request to change viewport received.")) self.next_viewport = viewport if self.viewport.is_dirty: self.viewport.on_stop.connect(self.set_viewport) @@ -141,14 +144,14 @@ def closeEvent(self, event): """ if self.exitOnClose: - self.log.debug(QtCore.QCoreApplication.translate("logs", "Application has received a EXIT close event and will shutdown completely.")) + self.log.debug(self.translate("logs", "Application has received a EXIT close event and will shutdown completely.")) event.accept() elif self.remove_on_close: - self.log.debug(QtCore.QCoreApplication.translate("logs", "Application has received a GUI closing close event and will close its main window.")) + self.log.debug(self.translate("logs", "Application has received a GUI closing close event and will close its main window.")) self.deleteLater() event.accept() else: - self.log.debug(QtCore.QCoreApplication.translate("logs", "Application has received a non-exit close event and will hide its main window.")) + self.log.debug(self.translate("logs", "Application has received a non-exit close event and will hide its main window.")) self.hide() event.setAccepted(True) event.ignore() @@ -190,14 +193,14 @@ def load_settings(self): try: geometry = _settings.value("geometry", defaults['geometry']) except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not load window geometry from settings file or defaults.")) + self.log.critical(self.translate("logs", "Could not load window geometry from settings file or defaults.")) self.log.exception(_excp) raise _settings.endGroup() try: self.setGeometry(geometry) except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Cannot create GUI window.")) + self.log.critical(self.translate("logs", "Cannot create GUI window.")) self.log.exception(_excp) raise @@ -212,7 +215,7 @@ def save_settings(self): try: _settings.setValue("geometry", self.geometry()) except Exception as _excp: - self.log.warn(QtCore.QCoreApplication.translate("logs", "Could not save window geometry. Will continue without saving window geometry.")) + self.log.warn(self.translate("logs", "Could not save window geometry. Will continue without saving window geometry.")) self.log.exception(_excp) _settings.endGroup() diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index c9f0148..4a9d7b8 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -25,6 +25,9 @@ class MenuBar(QtGui.QWidget): + #create signal used to communicate with mainWindow on viewport change + viewport_requested = QtCore.pyqtSignal(str) + def __init__(self, parent=None): super().__init__() @@ -33,8 +36,6 @@ def __init__(self, parent=None): #set function logger self.log = logging.getLogger("commotion_client."+__name__) - #create signal used to communicate with mainWindow on viewport change - self.viewportRequested = QtCore.pyqtSignal(str) try: self.populateMenu() except Exception as e: @@ -43,19 +44,19 @@ def __init__(self, parent=None): self.setLayout(self.layout) - def requestViewport(self, viewport): + def request_viewport(self, viewport): """ When called will emit a request for a viewport change. """ self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport sent")) - self.viewportRequested.emit(viewport) + self.viewport_requested.emit(viewport) def populateMenu(self): """ Clears and re-populates the menu using the loaded extensions. """ menuItems = {} - extensions = list(config.findConfigs("extension")) + extensions = list(config.find_configs("extension")) if extensions: topLevel = self.getParents(extensions) for topLevelItem in topLevel: @@ -112,7 +113,7 @@ def addMenuItem(self, title, extensions): subMenuItem = subMenuWidget(self) subMenuItem.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", ext['menuItem'])) #We use partial here to pass a variable along when we attach the "clicked()" signal to the MenuBars requestViewport function - subMenuItem.clicked.connect(partial(self.requestViewport, ext['name'])) + subMenuItem.clicked.connect(partial(self.request_viewport, ext['name'])) except Exception as e: self.log.error(QtCore.QCoreApplication.translate("logs", "Faile to create sub-menu \"{0}\" object for \"{1}\" object.".format(ext['name'], title))) self.log.debug(e, exc_info=1) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index 97a6041..b5ccaa9 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -133,6 +133,7 @@ def __init__(self, key, status, argv): self.setApplicationName(self.translate("main", "Commotion Client")) #special translation case since we are outside of the main application self.setWindowIcon(QtGui.QIcon(":logo48.png")) self.setApplicationVersion("1.0") #TODO Generate this on build + self.translate = QtCore.QCoreApplication.translate self.status = status self.controller = False self.main = False @@ -156,7 +157,7 @@ def init_client(self): elif self.status == "daemon": self.start_daemon() except Exception as _excp: #log failure here and exit - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not fully initialize applicaiton. Application must be halted.") + _catch_all = self.translate("logs", "Could not fully initialize applicaiton. Application must be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) @@ -169,7 +170,7 @@ def start_full(self): try: self.main = self.create_main_window() except Exception as _excp: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create Main Window. Application must be halted.") + _catch_all = self.translate("logs", "Could not create Main Window. Application must be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) @@ -178,7 +179,7 @@ def start_full(self): try: self.sys_tray = self.create_sys_tray() except Exception as _excp: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not create system tray. Application must be halted.") + _catch_all = self.translate("logs", "Could not create system tray. Application must be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) @@ -194,7 +195,7 @@ def start_daemon(self): if self.main: self.hide_main_window(force=True, errors="strict") except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not close down existing GUI componenets to switch to daemon mode.")) + self.log.critical(self.translate("logs", "Could not close down existing GUI componenets to switch to daemon mode.")) self.log.exception(_excp) raise try: @@ -203,7 +204,7 @@ def start_daemon(self): #if not self.controller: #TODO Actually create a stub controller file # self.controller = create_controller() except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not start daemon. Application must be halted.")) + self.log.critical(self.translate("logs", "Could not start daemon. Application must be halted.")) self.log.exception(_excp) raise else: @@ -221,13 +222,13 @@ def stop_client(self, force_close=None): self.close_controller(force_close) except Exception as _excp: if force_close: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not cleanly close client. Application must be halted.") + _catch_all = self.translate("logs", "Could not cleanly close client. Application must be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be closed.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) + self.log.error(self.translate("logs", "Client could not be closed.")) + self.log.info(self.translate("logs", "It is reccomended that you restart the application.")) self.log.exception(_excp) def restart_client(self, force_close=None): @@ -244,13 +245,13 @@ def restart_client(self, force_close=None): self.init_client() except Exception as _excp: if force_close: - _catch_all = QtCore.QCoreApplication.translate("logs", "Client could not be restarted. Applicaiton will now be halted") + _catch_all = self.translate("logs", "Client could not be restarted. Applicaiton will now be halted") self.log.error(_catch_all) self.log.exception(_excp) self.end(_catch_all) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "Client could not be restarted.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you restart the application.")) + self.log.error(self.translate("logs", "Client could not be restarted.")) + self.log.info(self.translate("logs", "It is reccomended that you restart the application.")) self.log.exception(_excp) raise _restart.end() @@ -264,13 +265,13 @@ def create_main_window(self): Will create a new main window or return existing main window if one is already created. """ if self.main: - self.log.debug(QtCore.QCoreApplication.translate("logs", "New window requested when one already exists. Returning existing main window.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "If you would like to close the main window and re-open it please call close_main_window() first.")) + self.log.debug(self.translate("logs", "New window requested when one already exists. Returning existing main window.")) + self.log.info(self.translate("logs", "If you would like to close the main window and re-open it please call close_main_window() first.")) return self.main try: _main = main_window.MainWindow() except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not create Main Window. Application must be halted.")) + self.log.critical(self.translate("logs", "Could not create Main Window. Application must be halted.")) self.log.exception(_excp) raise else: @@ -286,13 +287,13 @@ def init_main(self): self.sys_tray.exit.triggered.connect(self.main.exitEvent) self.sys_tray.show_main.connect(self.main.bring_front) except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not initialize connections between the main window and other application components.")) + self.log.error(self.translate("logs", "Could not initialize connections between the main window and other application components.")) self.log.exception(_excp) raise try: self.main.show() except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not show the main window.")) + self.log.error(self.translate("logs", "Could not show the main window.")) self.log.exception(_excp) raise @@ -307,7 +308,7 @@ def hide_main_window(self, force=None, errors=None): try: self.main.exit() except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not hide main window. Attempting to close all and only open taskbar.")) + self.log.error(self.translate("logs", "Could not hide main window. Attempting to close all and only open taskbar.")) self.log.exception(_excp) if force: try: @@ -315,7 +316,7 @@ def hide_main_window(self, force=None, errors=None): self.main = None self.main = self.create_main_window() except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not force main window restart.")) + self.log.error(self.translate("logs", "Could not force main window restart.")) self.log.exception(_excp) raise elif errors == "strict": @@ -333,7 +334,7 @@ def hide_main_window(self, force=None, errors=None): self.main = main_window.MainWindow() self.main.app_message.connect(self.process_message) except: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could close and re-open the main window.")) + self.log.error(self.translate("logs", "Could close and re-open the main window.")) self.log.exception(_excp) if errors == "strict": raise @@ -354,20 +355,20 @@ def close_main_window(self, force_close=None): self.main.purge self.main = False except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close main window.")) + self.log.error(self.translate("logs", "Could not close main window.")) if force_close: - self.log.info(QtCore.QCoreApplication.translate("logs", "force_close activated. Closing application.")) + self.log.info(self.translate("logs", "force_close activated. Closing application.")) try: self.main.deleteLater() self.main = False except Exception as _excp: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close main window using its internal mechanisms. Application will be halted.") + _catch_all = self.translate("logs", "Could not close main window using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close main window.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) + self.log.error(self.translate("logs", "Could not close main window.")) + self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) self.log.exception(_excp) raise @@ -384,7 +385,7 @@ def create_controller(self): #self.controller = CommotionController() #TODO Implement controller #self.controller.init() #?????? except Exception as _excp: - self.log.critical(QtCore.QCoreApplication.translate("logs", "Could not create controller. Application must be halted.")) + self.log.critical(self.translate("logs", "Could not create controller. Application must be halted.")) self.log.exception(_excp) raise @@ -402,19 +403,19 @@ def close_controller(self, force_close=None): #if self.controller.close(): # self.controller = None except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close controller.")) + self.log.error(self.translate("logs", "Could not close controller.")) if force_close: - self.log.info(QtCore.QCoreApplication.translate("logs", "force_close activated. Closing application.")) + self.log.info(self.translate("logs", "force_close activated. Closing application.")) try: del self.controller except Exception as _excp: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close controller using its internal mechanisms. Application will be halted.") + _catch_all = self.translate("logs", "Could not close controller using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not cleanly close controller.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) + self.log.error(self.translate("logs", "Could not cleanly close controller.")) + self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) self.log.exception(_excp) raise @@ -432,7 +433,7 @@ def init_sys_tray(self): self.sys_tray.exit.triggered.connect(self.main.exitEvent) self.sys_tray.show_main.connect(self.main.bring_front) except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not initialize connections between the system tray and other application components.")) + self.log.error(self.translate("logs", "Could not initialize connections between the system tray and other application components.")) self.log.exception(_excp) raise @@ -443,7 +444,7 @@ def create_sys_tray(self): try: tray = system_tray.TrayIcon() except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not start system tray.")) + self.log.error(self.translate("logs", "Could not start system tray.")) self.log.exception(_excp) raise else: @@ -460,20 +461,20 @@ def close_sys_tray(self, force_close=None): self.sys_tray.close() self.sys_tray = False except Exception as _excp: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close system tray.")) + self.log.error(self.translate("logs", "Could not close system tray.")) if force_close: - self.log.info(QtCore.QCoreApplication.translate("logs", "force_close activated. Closing application.")) + self.log.info(self.translate("logs", "force_close activated. Closing application.")) try: self.sys_tray.deleteLater() self.sys_tray.close() except: - _catch_all = QtCore.QCoreApplication.translate("logs", "Could not close system tray using its internal mechanisms. Application will be halted.") + _catch_all = self.translate("logs", "Could not close system tray using its internal mechanisms. Application will be halted.") self.log.critical(_catch_all) self.log.exception(_excp) self.end(_catch_all) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not close system tray.")) - self.log.info(QtCore.QCoreApplication.translate("logs", "It is reccomended that you close the entire application.")) + self.log.error(self.translate("logs", "Could not close system tray.")) + self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) self.log.exception(_excp) raise From 303ae05602406298a1d80dfd7f87843a3444aabd Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 17:02:15 -0400 Subject: [PATCH 022/107] added file management needed for extension management --- commotion_client/utils/fs_utils.py | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py index ca62c93..0746d6f 100644 --- a/commotion_client/utils/fs_utils.py +++ b/commotion_client/utils/fs_utils.py @@ -7,6 +7,10 @@ """ +#PyQt imports +from PyQt4 import QtCore + +#Standard Library Imports import os import logging @@ -39,3 +43,60 @@ def walklevel(some_dir, level=1): num_sep_this = root.count(os.path.sep) if num_sep + level <= num_sep_this: del dirs[:] + +def make_temp_dir(new=None): + """Makes a temporary directory and returns the QDir object. + + @param new bool Create a new uniquely named directory within the exiting Commotion temp directory and return the new folder object + """ + temp_path = "/Commotion/" + if new: + unique_dir_name = uuid.uuid4() + temp_path += str( unique_dir_name ) +# temp_dir = QtCore.QDir( QtCore.QDir.tempPath() + temp_path ) + temp_dir = QtCore.QDir( os.path.join(QtCore.QDir.tempPath(), temp_path )) + if QtCore.QDir().mkpath( temp_dir.path() ): + log.debug( QtCore.QCoreApplication.translate( "logs", "Creating main temporary directory" )) + else: + _error = QtCore.QCoreApplication.translate( "logs", "Error creating temporary directory" ) + log.error( _error ) + raise IOError( _error ) + return temp_dir + + +def clean_dir( path=None ): + """ Cleans a directory. If not given a path it will clean the FULL temporary directory""" + + if not path: + path = QtCore.QDir( os.path.join(QtCore.QDir.tempPath(), "Commotion" )) + + path.setFilter( QtCore.QDir.NoSymLinks | QtCore.QDir.Files ) + list_of_files = path.entryList() + + for file_ in list_of_files: + file_ = os.path.join( path.path(), file_ ) + if not QtCore.QFile( file_ ).remove(): + _error = QtCore.QCoreApplication.translate( "logs", "Error saving extension to extensions directory." ) + log.error( _error ) + raise IOError( _error ) + path.rmpath( path.path() ) + return True + +def copy_contents( start, end ): + """ Copies the contents of one directory into another + + @param start QDir A Qdir object for the first directory + @param end QDir A Qdir object for the final directory + """ + + start.setFilter( QtCore.QDir.NoSymLinks | QtCore.QDir.Files ) + list_of_files = path.entryList() + + for file_ in list_of_files: + source = os.path.join( start.path(), file_ ) + dest = os.path.join( end.path(), file_ ) + if not QtCore.QFile( source ).copy( dest ): + _error = QtCore.QCoreApplication.translate( "logs", "Error copying file into extensions directory. File already exists." ) + log.error( _error ) + raise IOError( _error ) + return True From 4dfc0b5b31e00ae913c4d5c6d1ce0e1cd79fbd07 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 17:02:56 -0400 Subject: [PATCH 023/107] fixed a mis-typed variable --- commotion_client/GUI/welcome_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/GUI/welcome_page.py b/commotion_client/GUI/welcome_page.py index 46e8bed..315f6f3 100644 --- a/commotion_client/GUI/welcome_page.py +++ b/commotion_client/GUI/welcome_page.py @@ -39,7 +39,7 @@ def __init__(self, parent=None): @property def is_dirty(self): """The current state of the viewport object """ - return self.dirty + return self._dirty def clean_up(self): self.on_stop.emit() From 597178ed39af8f43363d10476d5140cb5c78af1d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 17:03:37 -0400 Subject: [PATCH 024/107] Cleaned up documentation --- commotion_client/utils/config.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index a7daee5..32f9993 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -21,16 +21,16 @@ def find_configs(config_type, name=None): """ - Function used to obtain the path to a config file. + Function used to obtain a config file. @param config_type The type of configuration file sought. (global, user, extension) @param name optional The name of the configuration file if known - @return list of tuples containing the path and name of found config files or False if a config matching the description cannot be found. + @return list of tuples containing a config name and its config. """ config_files = get_config_paths(config_type) if config_files: - configs = get_config(config_files) + configs = get_configs(config_files) return configs elif name != None: for conf in configs: @@ -70,7 +70,7 @@ def get_config_paths(config_type): return False -def get_config(paths): +def get_configs(paths): """ Generator to retreive config files for the paths passed to it @@ -85,14 +85,12 @@ def get_config(paths): yield config else: log.error(QtCore.QCoreApplication.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) - def load_config(config): """ This function loads a json formatted config file and returns it. - @param fileName the name of the config file - @param path the path to the configuration file in question + @param config the path to a config file @return a dictionary containing the config files values """ #Open the file From 7fa33cb447dcc786b14681012dab8a67bc73ab18 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 18 Mar 2014 17:04:04 -0400 Subject: [PATCH 025/107] started scoping out extension management functionality. --- .../extensions/extension_manager.py | 282 +++++++++++++++++- 1 file changed, 272 insertions(+), 10 deletions(-) diff --git a/commotion_client/extensions/extension_manager.py b/commotion_client/extensions/extension_manager.py index e07c915..122dbbf 100644 --- a/commotion_client/extensions/extension_manager.py +++ b/commotion_client/extensions/extension_manager.py @@ -14,12 +14,15 @@ #Standard Library Imports import logging import importlib +import shutil #PyQt imports from PyQt4 import QtCore #Commotion Client Imports from utils import config +from utils import fs_utils +from utils import validate import extensions class ExtensionManager(): @@ -27,6 +30,7 @@ class ExtensionManager(): def __init__(self, parent=None): self.log = logging.getLogger("commotion_client."+__name__) self.extensions = self.check_installed() + self.translate = QtCore.QCoreApplication.translate def check_installed(self): @@ -50,20 +54,18 @@ def check_installed(self): extensions[ext] = "contrib" return extensions - - def get(self, extension_name, subsection=None): - """Return the full extension object or one of its primary sub-objects (settings or main) + def load_user_interface(self, extension_name, subsection=None): + """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) - @subsection str Name of a objects sub-section. (settings or main) + @subsection str Name of a objects sub-section. (settings, main, or toolbar) """ - config = config.find_configs("user", extension_name) + user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} + settings = self.load_settings(extension_name) if subsection: - subsection = config[subsection] + extension_name = extension_name+"."settings[subsection] + subsection = user_interface_types[subsection] extension = self.import_extension(extension_name, subsection) - if subsection: - return extension.ViewPort() - else: - return extension + return extension def import_extension(self, extension_name, subsection=None): """load extensions by string name.""" @@ -73,3 +75,263 @@ def import_extension(self, extension_name, subsection=None): extension = importlib.import_module("."+extension_name, "extensions") return extension + def load_settings(self, extension_name): + """Gets an extension's settings and returns them as a dict. + + @return dict A dictionary containing an extensions properties. + """ + extension_config = { "name":extension_name } + extension_type = self.extensions[extension_name] + + _settings = QtCore.QSettings() + _settings.beginGroup("extensions") + _settings.beginGroup(extension_type) + extension_config['main'] = _settings.value("main", None) + if not extension_config['main']: + if fs_utils.is_file("extensions/"+extension_type+"/"+extension_name+"/main.py"): + extension_config['main'] = "main" + else: + _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) + self.log.error(_error) + raise IOError(_error) + extension_config['settings'] = _settings.value( "settings", extension_config['main'] ) + extension_config['toolbar'] = _settings.value( "toolbar", extension_config['main'] ) + extension_config['parent'] = _settings.value( "parent", "Add-On's" ) + extension_config['menu_item'] = _settings.value( "menu_item", extension_config['name'] ) + extension_config['menu_level'] = _settings.value( "menu_level", 10 ) + extension_config['tests'] = _settings.value( "tests", None ) + if not extension_config['tests']: + if fs_utils.is_file("extensions/"+extension_type+"/"+extension_name+"/tests.py"): + extension_config['tests'] = "tests" + _settings.endGroup() + _settings.endGroup() + return extension_config + + def save_settings(self, extension_config, extension_type="contrib"): + """Saves an extensions core properties into the applications extension settings + + @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. + @param extension_config dict Dictionary of key-pairs for json config. + @return bool True if successful. It should raise various exceptions if it fails + """ + + _settings = QtCore.QSettings() + _settings.beginGroup( "extensions" ) + #Extension Name + try: + extension_name = extension_config['name'] + except KeyError as _expt: + self.log.error(self.translate("logs", "Could not load unknown extension because it's config file was missing a name value.")) + self.log.exception(_excp) + raise + else: + extension_path = "extensions/"+extension_type+"/"+extension_name+"/" + if fs_utils.is_file( extension_path ): + _settings.beginGroup( extension_name ) + else: + _error = self.translate("logs", "Extension {0} does not exist. Please re-load or remove this extension using the extension menu.".format(extension_config['name'])) + self.log.error(_error) + raise IOError(_error) + #Extension Main + try: + _main = extension_config['main'] + except KeyError: + if fs_utils.is_file(extension_path+"main.py"): + _main = "main" #Set this for later default values + _settings.value( "main", "main" ) + else: + if fs_utils.is_file(extension_path+_main+".py"): + _settings.value( "main", _main ) + else: + _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_config['name'])) + self.log.error(_error) + raise IOError(_error) + #Extension Settings & Toolbar + for val in ["settings", "toolbar"]: + try: + _config_value = extension_config[val] + except KeyError: + #Defaults to main, which was checked and set before + _settings.value( val, _main ) + else: + if fs_utils.is_file(extension_path+_config_value+".py"): + _settings.value( val, _config_value) + else: + _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config. Please either remove the config listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) + self.log.error(_error) + raise IOError(_error) + #Extension Parent + try: + _settings.value( "parent", extension_config["parent"] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) + _settings.value( "parent", "Extensions" ) + #Extension Menu Item + try: + _settings.value( "menu_item", extension_config["menu_item"] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) + _settings.value( "menu_item", extension_name ) + #Extension Menu Level + try: + _settings.value( "menu_level", extension_config["menu_level"] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) + _settings.value( "menu_level", 10 ) + #Extension Tests + try: + _tests = extension_config['tests'] + except KeyError: + if fs_utils.is_file(extension_path+"tests.py"): + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) + _settings.value( "tests", "tests" ) + else: + self.log.debug(self.translate("logs", "{0} does not contain any tests. Shame on you.... SHAME!".format(extension_name))) + else: + if fs_utils.is_file(extension_path+_tests+".py"): + _settings.value( "main", _test ) + else: + _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) + self.log.error(_error) + raise IOError(_error) + _settings.endGroup() + _settings.endGroup() + return True + + def save_extension(self, extension, extension_type="contrib", md5sum=None): + """Unpacks a extension and attempts to add it to the Commotion system. + + @param extension + @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. + @param md5sum + @return bool True if an extension was saved, False if it could not save. + """ + #There can be only two... and I don't trust you. + if extension_type != "contrib": + extension_type = "core" + + if md5sum: + try: + self.verify_extension(md5sum) + except InvalidSignature: + self.log.error(self.translate("logs", "Could not verify extension. Please check that you used the correct key and extension file.")) + return False + try: + unpacked = self.unpack_extension(self, extension, extension_type) + except IOError as _excpt: + self.log.error(self.translate("logs", "Failed to unpack extension.")) + return False + if not self.ensure_minimally_complete(unpacked): + self.log.error(self.translate("logs", "Extension is not complete.")) + return False + + #TODO THIS!!! + + #check if extension with that name already exists + #make sure it has everythign it needs + #move it into the proer folder + #save it into settings + + + def ensure_minimally_complete(self, extension): + """Check that a package has the minimum required functional components""" + files = extension.entryList() + if "config.json" in files: + config = config.load_config( os.path.join( extension.path(), "config.json" )) + else: + self.log.error(self.translate("logs", "Extension does not contain a config file.")) + return + if not config["name"]: + self.log.error(self.translate("logs", "The extension's config file does not contain a name value.")) + return False + if config["main"]: + if not ( str( config["name"] ) + ".py" ) in files: + self.log.error(self.translate("logs", "The extension's config file specifys a 'main' python file that is missing.")) + return False + else: + if not ( "main.py" ) in files: + self.log.error(self.translate("logs", "The extension is missing a 'main' python file.")) + return False + if config["tests"]: + if not ( str( config["tests"] ) + ".py" ) in files: + self.log.error(self.translate("logs", "The extension's config file specifys a 'tests' python file that is missing.")) + return False + for val in ["settings", "toolbar"]: + if config[val]: + if not ( str( config[val] ) + ".py" ) in files: + self.log.error(self.translate("logs", "The extension's config file specifys a '{0}' python file that is missing.".format(val))) + return False + + + + + + + for key, val in config.items(): + + + + + def verify_extension(self, md5sum): + """Verify's a compressed extension against its MD5 sum + + @param md5sum path to file containing the md5 sum of a valid version of this extension + """ + #TODO THIS!!! + #Check valid sum + #Check extension matches sum + else: + _error = QtCore.QCoreApplication.translate("logs", "Bad or modified extension! The current extension does not match the provided signature.") + self.log.critical(_error) + raise InvalidSignature(_error) + + def unpack_extension(self, compressed_extension, extension_type): + """Unpacks an extension into a temporary directory and returns the location. + + @param compressed_extension string Path to the compressed_extension + @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. + @return QDir A QDir object containing the path to the temporary directory + """ + temp_dir = utils.fs_utils.make_temp_dir(new=True) + temp_abs_path = temp_dir.absolutePath() + try: + shutil.unpack_archive(compressed_extension, temp_abs_path, "gztar") + except ReadError as _excpt: + _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was corrupted or mis-packaged.") + self.log.error(_error) + raise IOError(_error) + except FileNotFoundError as _excpt: + _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was not found.") + self.log.error(_error) + raise IOError(_error) + return temp_dir + + def save_unpacked_extension(self, temp_dir, extension_name, extension_type): + """Moves an extension from the temporary directory to the extension directory.""" + extension_path = "extensions/"+extension_type+"/"+extension_name + full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) + if not s_utils.is_file(full_path): + try: + QtCore.QDir.mkpath(full_path) + try: + fs_utils.copy_contents(temp_dir, full_path) + except IOError as _excpt: + raise IOError( _excpt ) + else: + temp_dir.rmpath( temp_dir.path() ) + return True + except IOError: + log.error(QtCore.QCoreApplication.translate( "logs", "Could not save unpacked extension into extensions directory.") + return False + else: + _error = QtCore.QCoreApplication.translate( "logs", "An extension with that name already exists. Please delete the existing extension and try again." ) + log.error( _error ) + raise ValueError( _error ) + return True + +class InvalidSignature(Exception): + """A verification procedure has failed. + + This exception should only be handled by halting the current task. + """ + pass From a8980c4ae8f9e5138122ec24b3f7b38a22fc9643 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 12:15:47 -0400 Subject: [PATCH 026/107] Cleaned up validation and file system utils Moved validation into validate.ClientConfig and moved all file system interaction into QDir's and proper os.path.join's --- .../extensions/extension_manager.py | 263 ++++++++++-------- 1 file changed, 150 insertions(+), 113 deletions(-) diff --git a/commotion_client/extensions/extension_manager.py b/commotion_client/extensions/extension_manager.py index 122dbbf..d1e1986 100644 --- a/commotion_client/extensions/extension_manager.py +++ b/commotion_client/extensions/extension_manager.py @@ -87,8 +87,13 @@ def load_settings(self, extension_name): _settings.beginGroup("extensions") _settings.beginGroup(extension_type) extension_config['main'] = _settings.value("main", None) + #get extension dir + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) + main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) + extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + extension_files = extension_dir.entryList() if not extension_config['main']: - if fs_utils.is_file("extensions/"+extension_type+"/"+extension_name+"/main.py"): + if "main.py" in extension_files: extension_config['main'] = "main" else: _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) @@ -101,99 +106,127 @@ def load_settings(self, extension_name): extension_config['menu_level'] = _settings.value( "menu_level", 10 ) extension_config['tests'] = _settings.value( "tests", None ) if not extension_config['tests']: - if fs_utils.is_file("extensions/"+extension_type+"/"+extension_name+"/tests.py"): + if "tests.py" in extension_files: extension_config['tests'] = "tests" _settings.endGroup() _settings.endGroup() return extension_config + def remove_extension_settings(self, name): + """Removes an extension and its core properties from the applications extension settings. + + @param name str the name of an extension to remove from the extension settings. + """ + if len( str( name )) > 0: + _settings = QtCore.QSettings() + _settings.beginGroup( "extensions" ) + _settings.remove( str( name ) ) + else: + _error = self.translate("logs", "You must specify an extension name greater than 1 char.") + self.log.error( _error ) + raise ValueError( _error ) + def save_settings(self, extension_config, extension_type="contrib"): """Saves an extensions core properties into the applications extension settings @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. @param extension_config dict Dictionary of key-pairs for json config. - @return bool True if successful. It should raise various exceptions if it fails + @return bool True if successful and False on failures """ - _settings = QtCore.QSettings() _settings.beginGroup( "extensions" ) + #get extension dir + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) + main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) + extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + #create validator + config_validator = validate.ClientConfig(extension_config, extension_dir) #Extension Name - try: - extension_name = extension_config['name'] - except KeyError as _expt: - self.log.error(self.translate("logs", "Could not load unknown extension because it's config file was missing a name value.")) - self.log.exception(_excp) - raise - else: - extension_path = "extensions/"+extension_type+"/"+extension_name+"/" - if fs_utils.is_file( extension_path ): + if config_validator.name(): + try: + extension_name = extension_config['name'] _settings.beginGroup( extension_name ) - else: - _error = self.translate("logs", "Extension {0} does not exist. Please re-load or remove this extension using the extension menu.".format(extension_config['name'])) - self.log.error(_error) - raise IOError(_error) + except KeyError: + _error = self.translate("logs", "The extension is missing a name value which is required.") + self.log.error( _error ) + return False + else: + _error = self.translate("logs", "The extension's name is invalid and cannot be saved.") + self.log.error( _error ) + return False #Extension Main - try: - _main = extension_config['main'] - except KeyError: - if fs_utils.is_file(extension_path+"main.py"): - _main = "main" #Set this for later default values - _settings.value( "main", "main" ) + if config_validator.gui("main"): + try: + _main = extension_config['main'] + except KeyError: + if config_validator.main(): + _main = "main" #Set this for later default values + _settings.value( "main", "main" ) + else: + _settings.value( "main", _main ) else: - if fs_utils.is_file(extension_path+_main+".py"): - _settings.value( "main", _main ) - else: - _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_config['name'])) - self.log.error(_error) - raise IOError(_error) + _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") + self.log.error( _error ) + return False #Extension Settings & Toolbar for val in ["settings", "toolbar"]: - try: - _config_value = extension_config[val] - except KeyError: - #Defaults to main, which was checked and set before - _settings.value( val, _main ) - else: - if fs_utils.is_file(extension_path+_config_value+".py"): - _settings.value( val, _config_value) + if config_validator.gui(val): + try: + _config_value = extension_config[val] + except KeyError: + #Defaults to main, which was checked and set before + _settings.value( val, _main ) else: - _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config. Please either remove the config listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) + _settings.value( val, _config_value) + else: + _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.").format(val)) self.log.error(_error) - raise IOError(_error) + return False #Extension Parent - try: - _settings.value( "parent", extension_config["parent"] ) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) - _settings.value( "parent", "Extensions" ) + if config_validator.parent(): + try: + _settings.value( "parent", extension_config["parent"] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) + _settings.value( "parent", "Extensions" ) + else: + _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") + self.log.error( _error ) + return False + #Extension Menu Item - try: - _settings.value( "menu_item", extension_config["menu_item"] ) - except KeyError: + if config_validator.menu_item(): + try: + _settings.value( "menu_item", extension_config["menu_item"] ) + except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) - _settings.value( "menu_item", extension_name ) + _settings.value( "menu_item", extension_name ) + else: + _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") + self.log.error( _error ) + return False #Extension Menu Level - try: - _settings.value( "menu_level", extension_config["menu_level"] ) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) - _settings.value( "menu_level", 10 ) + if config_validator.menu_level(): + try: + _settings.value( "menu_level", extension_config["menu_level"] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) + _settings.value( "menu_level", 10 ) + else: + _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") + self.log.error( _error ) + return False #Extension Tests - try: - _tests = extension_config['tests'] - except KeyError: - if fs_utils.is_file(extension_path+"tests.py"): - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) - _settings.value( "tests", "tests" ) - else: - self.log.debug(self.translate("logs", "{0} does not contain any tests. Shame on you.... SHAME!".format(extension_name))) + if config_validator.tests(): + try: + _settings.value( "main", extension_config['tests'] ) + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) + _settings.value( "tests", "tests" ) else: - if fs_utils.is_file(extension_path+_tests+".py"): - _settings.value( "main", _test ) - else: - _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) - self.log.error(_error) - raise IOError(_error) + _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) + self.log.error(_error) + return False _settings.endGroup() _settings.endGroup() return True @@ -221,56 +254,60 @@ def save_extension(self, extension, extension_type="contrib", md5sum=None): except IOError as _excpt: self.log.error(self.translate("logs", "Failed to unpack extension.")) return False - if not self.ensure_minimally_complete(unpacked): - self.log.error(self.translate("logs", "Extension is not complete.")) + config_validator = validate.ClientConfig() + try: + config_validator.set_extension( unpacked.absolutePath() ) + except IOError: + self.log.error(self.translate("logs", "Extension is invalid and cannot be saved.")) + self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + fs_utils.clean_dir(unpacked) return False - - #TODO THIS!!! - - #check if extension with that name already exists - #make sure it has everythign it needs - #move it into the proer folder - #save it into settings - - - def ensure_minimally_complete(self, extension): - """Check that a package has the minimum required functional components""" - files = extension.entryList() - if "config.json" in files: - config = config.load_config( os.path.join( extension.path(), "config.json" )) + #Name + if config_validator.name(): + config_path = os.path.join( unpacked.absolutePath(), "config.json" ) + config = config.load_config( config_path ) + existing_extensions = config.find_configs("extension") + try: + assert config['name'] not in existing_extensions + except AssertionError: + self.log.error(self.translate("logs", "The name given to this extension is already in use. Each extension must have a unique name.")) + self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + fs_utils.clean_dir(unpacked) + return False else: - self.log.error(self.translate("logs", "Extension does not contain a config file.")) - return - if not config["name"]: - self.log.error(self.translate("logs", "The extension's config file does not contain a name value.")) + self.log.error(self.translate("logs", "The extension name is invalid and cannot be saved.")) + self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + fs_utils.clean_dir(unpacked) + return False + #Check all values + if not config_validator.validate_all(): + self.log.error(self.translate("logs", "The extension's config contains the following invalid value/s: [{0}]".format( ",".join(config_validator.errors) ))) + self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + fs_utils.clean_dir(unpacked) + return False + #make new directory in extensions + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) + main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) + extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + try: + fs_utils.copy_contents( unpacked, extension_dir ) + except: + self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again." ))) + self.log.info(self.translate("logs", "Cleaning extension's temp and main directory." ))) + fs_utils.clean_dir(extension_dir) + fs_utils.clean_dir(unpacked) return False - if config["main"]: - if not ( str( config["name"] ) + ".py" ) in files: - self.log.error(self.translate("logs", "The extension's config file specifys a 'main' python file that is missing.")) - return False else: - if not ( "main.py" ) in files: - self.log.error(self.translate("logs", "The extension is missing a 'main' python file.")) - return False - if config["tests"]: - if not ( str( config["tests"] ) + ".py" ) in files: - self.log.error(self.translate("logs", "The extension's config file specifys a 'tests' python file that is missing.")) - return False - for val in ["settings", "toolbar"]: - if config[val]: - if not ( str( config[val] ) + ".py" ) in files: - self.log.error(self.translate("logs", "The extension's config file specifys a '{0}' python file that is missing.".format(val))) - return False - - - - - - - for key, val in config.items(): - - - + fs_utils.clean_dir(unpacked) + try: + self.save_settings( config, extension_type) + except KeyError: + self.log.error(self.translate("logs", "Could not save the extension because it was missing manditory values. Please check the config and try again." ))) + self.log.info(self.translate("logs", "Cleaning extension directory and settings." ))) + fs_utils.clean_dir(extension_dir) + self.remove_extension_settings(config['name']) + return False + return True def verify_extension(self, md5sum): """Verify's a compressed extension against its MD5 sum From 87f51623cc406cb909c285ad8ea3a24bb5006dd0 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 12:16:13 -0400 Subject: [PATCH 027/107] added basic networking validation class --- commotion_client/utils/validate.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 8a63262..7cf2bd4 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -13,12 +13,14 @@ import logging import sys import re +import ipaddress #PyQt imports from PyQt4 import QtCore class ClientConfig(): + """ """ def __init__(self, config=None, directory=None): if config: @@ -257,3 +259,30 @@ def check_path_length(self, file_name=None): else: self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) return True + +class Networking(): + + def __init__(): + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + + def ipaddr(self, ip, addr_type=None): + """ + Checks if a string is a validly formatted IPv4 or IPv6 address. + + @param ip str A ip address to be checked + @param addr_type int The appropriate version number: 4 for IPv4, 6 for IPv6. + """ + try: + addr = ipaddress.ip_address( str( ip )) + except ValueError: + self.log.warn(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip)) + return False + if addr_type: + if addr.version == addr_type: + return True + else: + self.log.warn(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip, addr_type)) + return False + else: + return True From d14f0ae15ed898d72b6f5122b107ab8adb04d0b4 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:21:03 -0400 Subject: [PATCH 028/107] completed extension manager Moved extension manager into utils because it is logical. Cleaned up code to be more pep8 compliant. --- .../extension_manager.py | 217 ++++++++---------- 1 file changed, 101 insertions(+), 116 deletions(-) rename commotion_client/{extensions => utils}/extension_manager.py (67%) diff --git a/commotion_client/extensions/extension_manager.py b/commotion_client/utils/extension_manager.py similarity index 67% rename from commotion_client/extensions/extension_manager.py rename to commotion_client/utils/extension_manager.py index d1e1986..d348b56 100644 --- a/commotion_client/extensions/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -15,6 +15,7 @@ import logging import importlib import shutil +import os #PyQt imports from PyQt4 import QtCore @@ -25,50 +26,56 @@ from utils import validate import extensions -class ExtensionManager(): - """ """ - def __init__(self, parent=None): +class ExtensionManager(object): + def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.extensions = self.check_installed() self.translate = QtCore.QCoreApplication.translate - - def check_installed(self): + @staticmethod + def check_installed(): """Creates dictionary keyed with the name of installed extensions with each extensions type. @returns dict Dictionary keyed with the name of installed extensions with each extensions type. """ - extensions = {} + installed_extensions = {} _settings = QtCore.QSettings() _settings.beginGroup("extensions") _settings.beginGroup("core") - core = settings.allKeys(); + core = _settings.allKeys() _settings.endGroup() _settings.beginGroup("contrib") - contrib = settings.allKeys(); + contrib = _settings.allKeys() _settings.endGroup() _settings.endGroup() for ext in core: - extensions[ext] = "core" + installed_extensions[ext] = "core" for ext in contrib: - extensions[ext] = "contrib" - return extensions + installed_extensions[ext] = "contrib" + return installed_extensions def load_user_interface(self, extension_name, subsection=None): """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) - - @subsection str Name of a objects sub-section. (settings, main, or toolbar) + + @param extension_name string The extension to load + @subsection string Name of a objects sub-section. (settings, main, or toolbar) """ user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} settings = self.load_settings(extension_name) if subsection: - extension_name = extension_name+"."settings[subsection] + extension_name = extension_name+"."+settings[subsection] subsection = user_interface_types[subsection] extension = self.import_extension(extension_name, subsection) return extension - def import_extension(self, extension_name, subsection=None): - """load extensions by string name.""" + @staticmethod + def import_extension(extension_name, subsection=None): + """ + Load extensions by string name. + + @param extension_name string The extension to load + @param subsection string The module to load from an extension + """ if subsection: extension = importlib.import_module("."+subsection, "extensions."+extension_name) else: @@ -80,7 +87,7 @@ def load_settings(self, extension_name): @return dict A dictionary containing an extensions properties. """ - extension_config = { "name":extension_name } + extension_config = {"name":extension_name} extension_type = self.extensions[extension_name] _settings = QtCore.QSettings() @@ -88,9 +95,9 @@ def load_settings(self, extension_name): _settings.beginGroup(extension_type) extension_config['main'] = _settings.value("main", None) #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) - main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) - extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_type_dir = os.path.join(main_ext_dir, extension_type) + extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) extension_files = extension_dir.entryList() if not extension_config['main']: if "main.py" in extension_files: @@ -99,32 +106,32 @@ def load_settings(self, extension_name): _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) self.log.error(_error) raise IOError(_error) - extension_config['settings'] = _settings.value( "settings", extension_config['main'] ) - extension_config['toolbar'] = _settings.value( "toolbar", extension_config['main'] ) - extension_config['parent'] = _settings.value( "parent", "Add-On's" ) - extension_config['menu_item'] = _settings.value( "menu_item", extension_config['name'] ) - extension_config['menu_level'] = _settings.value( "menu_level", 10 ) - extension_config['tests'] = _settings.value( "tests", None ) + extension_config['settings'] = _settings.value("settings", extension_config['main']) + extension_config['toolbar'] = _settings.value("toolbar", extension_config['main']) + extension_config['parent'] = _settings.value("parent", "Add-On's") + extension_config['menu_item'] = _settings.value("menu_item", extension_config['name']) + extension_config['menu_level'] = _settings.value("menu_level", 10) + extension_config['tests'] = _settings.value("tests", None) if not extension_config['tests']: if "tests.py" in extension_files: extension_config['tests'] = "tests" _settings.endGroup() _settings.endGroup() return extension_config - + def remove_extension_settings(self, name): """Removes an extension and its core properties from the applications extension settings. @param name str the name of an extension to remove from the extension settings. """ - if len( str( name )) > 0: + if len(str(name)) > 0: _settings = QtCore.QSettings() - _settings.beginGroup( "extensions" ) - _settings.remove( str( name ) ) + _settings.beginGroup("extensions") + _settings.remove(str(name)) else: _error = self.translate("logs", "You must specify an extension name greater than 1 char.") - self.log.error( _error ) - raise ValueError( _error ) + self.log.error(_error) + raise ValueError(_error) def save_settings(self, extension_config, extension_type="contrib"): """Saves an extensions core properties into the applications extension settings @@ -134,25 +141,25 @@ def save_settings(self, extension_config, extension_type="contrib"): @return bool True if successful and False on failures """ _settings = QtCore.QSettings() - _settings.beginGroup( "extensions" ) + _settings.beginGroup("extensions") #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) - main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) - extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_type_dir = os.path.join(main_ext_dir, extension_type) + extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) #create validator config_validator = validate.ClientConfig(extension_config, extension_dir) #Extension Name if config_validator.name(): try: extension_name = extension_config['name'] - _settings.beginGroup( extension_name ) + _settings.beginGroup(extension_name) except KeyError: - _error = self.translate("logs", "The extension is missing a name value which is required.") - self.log.error( _error ) - return False + _error = self.translate("logs", "The extension is missing a name value which is required.") + self.log.error(_error) + return False else: _error = self.translate("logs", "The extension's name is invalid and cannot be saved.") - self.log.error( _error ) + self.log.error(_error) return False #Extension Main if config_validator.gui("main"): @@ -161,12 +168,12 @@ def save_settings(self, extension_config, extension_type="contrib"): except KeyError: if config_validator.main(): _main = "main" #Set this for later default values - _settings.value( "main", "main" ) + _settings.value("main", "main") else: - _settings.value( "main", _main ) + _settings.value("main", _main) else: _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") - self.log.error( _error ) + self.log.error(_error) return False #Extension Settings & Toolbar for val in ["settings", "toolbar"]: @@ -175,54 +182,54 @@ def save_settings(self, extension_config, extension_type="contrib"): _config_value = extension_config[val] except KeyError: #Defaults to main, which was checked and set before - _settings.value( val, _main ) + _settings.value(val, _main) else: - _settings.value( val, _config_value) + _settings.value(val, _config_value) else: - _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.").format(val)) + _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.".format(val)) self.log.error(_error) return False #Extension Parent if config_validator.parent(): try: - _settings.value( "parent", extension_config["parent"] ) + _settings.value("parent", extension_config["parent"]) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) - _settings.value( "parent", "Extensions" ) + _settings.value("parent", "Extensions") else: _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") - self.log.error( _error ) + self.log.error(_error) return False #Extension Menu Item if config_validator.menu_item(): try: - _settings.value( "menu_item", extension_config["menu_item"] ) + _settings.value("menu_item", extension_config["menu_item"]) except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) - _settings.value( "menu_item", extension_name ) + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) + _settings.value("menu_item", extension_name) else: _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") - self.log.error( _error ) + self.log.error(_error) return False #Extension Menu Level if config_validator.menu_level(): try: - _settings.value( "menu_level", extension_config["menu_level"] ) + _settings.value("menu_level", extension_config["menu_level"]) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) - _settings.value( "menu_level", 10 ) + _settings.value("menu_level", 10) else: _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") - self.log.error( _error ) + self.log.error(_error) return False #Extension Tests if config_validator.tests(): try: - _settings.value( "main", extension_config['tests'] ) + _settings.value("main", extension_config['tests']) except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) - _settings.value( "tests", "tests" ) + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) + _settings.value("tests", "tests") else: _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) self.log.error(_error) @@ -231,113 +238,92 @@ def save_settings(self, extension_config, extension_type="contrib"): _settings.endGroup() return True - def save_extension(self, extension, extension_type="contrib", md5sum=None): + def save_extension(self, extension, extension_type="contrib"): """Unpacks a extension and attempts to add it to the Commotion system. @param extension @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. - @param md5sum @return bool True if an extension was saved, False if it could not save. """ - #There can be only two... and I don't trust you. + #There can be only two... and I don't trust you. if extension_type != "contrib": extension_type = "core" - if md5sum: - try: - self.verify_extension(md5sum) - except InvalidSignature: - self.log.error(self.translate("logs", "Could not verify extension. Please check that you used the correct key and extension file.")) - return False try: unpacked = self.unpack_extension(self, extension, extension_type) - except IOError as _excpt: + except IOError: self.log.error(self.translate("logs", "Failed to unpack extension.")) return False config_validator = validate.ClientConfig() try: - config_validator.set_extension( unpacked.absolutePath() ) + config_validator.set_extension(unpacked.absolutePath()) except IOError: self.log.error(self.translate("logs", "Extension is invalid and cannot be saved.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) fs_utils.clean_dir(unpacked) return False #Name if config_validator.name(): - config_path = os.path.join( unpacked.absolutePath(), "config.json" ) - config = config.load_config( config_path ) + config_path = os.path.join(unpacked.absolutePath(), "config.json") + _config = config.load_config(config_path) existing_extensions = config.find_configs("extension") try: - assert config['name'] not in existing_extensions + assert _config['name'] not in existing_extensions except AssertionError: self.log.error(self.translate("logs", "The name given to this extension is already in use. Each extension must have a unique name.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) fs_utils.clean_dir(unpacked) return False else: self.log.error(self.translate("logs", "The extension name is invalid and cannot be saved.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) fs_utils.clean_dir(unpacked) return False #Check all values if not config_validator.validate_all(): - self.log.error(self.translate("logs", "The extension's config contains the following invalid value/s: [{0}]".format( ",".join(config_validator.errors) ))) - self.log.info(self.translate("logs", "Cleaning extension's temp directory." ))) + self.log.error(self.translate("logs", "The extension's config contains the following invalid value/s: [{0}]".format(",".join(config_validator.errors)))) + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) fs_utils.clean_dir(unpacked) return False #make new directory in extensions - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions" ) - main_ext_type_dir = os.path.join(main_ext_dir, extension_type ) - extension_dir = QtCore.QtDir.mkpath( os.path.join(main_ext_type_dir, config['name'] )) + main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_type_dir = os.path.join(main_ext_dir, extension_type) + extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, _config['name'])) try: - fs_utils.copy_contents( unpacked, extension_dir ) - except: - self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again." ))) - self.log.info(self.translate("logs", "Cleaning extension's temp and main directory." ))) + fs_utils.copy_contents(unpacked, extension_dir) + except IOError: + self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again.")) + self.log.info(self.translate("logs", "Cleaning extension's temp and main directory.")) fs_utils.clean_dir(extension_dir) fs_utils.clean_dir(unpacked) return False else: fs_utils.clean_dir(unpacked) try: - self.save_settings( config, extension_type) + self.save_settings(_config, extension_type) except KeyError: - self.log.error(self.translate("logs", "Could not save the extension because it was missing manditory values. Please check the config and try again." ))) - self.log.info(self.translate("logs", "Cleaning extension directory and settings." ))) + self.log.error(self.translate("logs", "Could not save the extension because it was missing manditory values. Please check the config and try again.")) + self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) fs_utils.clean_dir(extension_dir) - self.remove_extension_settings(config['name']) + self.remove_extension_settings(_config['name']) return False return True - def verify_extension(self, md5sum): - """Verify's a compressed extension against its MD5 sum - - @param md5sum path to file containing the md5 sum of a valid version of this extension - """ - #TODO THIS!!! - #Check valid sum - #Check extension matches sum - else: - _error = QtCore.QCoreApplication.translate("logs", "Bad or modified extension! The current extension does not match the provided signature.") - self.log.critical(_error) - raise InvalidSignature(_error) - - def unpack_extension(self, compressed_extension, extension_type): + def unpack_extension(self, compressed_extension): """Unpacks an extension into a temporary directory and returns the location. @param compressed_extension string Path to the compressed_extension - @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. @return QDir A QDir object containing the path to the temporary directory """ - temp_dir = utils.fs_utils.make_temp_dir(new=True) + temp_dir = fs_utils.make_temp_dir(new=True) temp_abs_path = temp_dir.absolutePath() try: shutil.unpack_archive(compressed_extension, temp_abs_path, "gztar") - except ReadError as _excpt: + except ReadError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was corrupted or mis-packaged.") self.log.error(_error) raise IOError(_error) - except FileNotFoundError as _excpt: + except FileNotFoundError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was not found.") self.log.error(_error) raise IOError(_error) @@ -347,24 +333,23 @@ def save_unpacked_extension(self, temp_dir, extension_name, extension_type): """Moves an extension from the temporary directory to the extension directory.""" extension_path = "extensions/"+extension_type+"/"+extension_name full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) - if not s_utils.is_file(full_path): + if not fs_utils.is_file(full_path): try: QtCore.QDir.mkpath(full_path) try: fs_utils.copy_contents(temp_dir, full_path) except IOError as _excpt: - raise IOError( _excpt ) + raise IOError(_excpt) else: - temp_dir.rmpath( temp_dir.path() ) + temp_dir.rmpath(temp_dir.path()) return True except IOError: - log.error(QtCore.QCoreApplication.translate( "logs", "Could not save unpacked extension into extensions directory.") + self.log.error(QtCore.QCoreApplication.translate("logs", "Could not save unpacked extension into extensions directory.")) return False else: - _error = QtCore.QCoreApplication.translate( "logs", "An extension with that name already exists. Please delete the existing extension and try again." ) - log.error( _error ) - raise ValueError( _error ) - return True + _error = QtCore.QCoreApplication.translate("logs", "An extension with that name already exists. Please delete the existing extension and try again.") + self.log.error(_error) + raise ValueError(_error) class InvalidSignature(Exception): """A verification procedure has failed. From ee76366884c8ef104ae48f9070f6c115d000a0d9 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:23:00 -0400 Subject: [PATCH 029/107] cleaned up code to be pep8 compliant-ier --- commotion_client/utils/fs_utils.py | 74 +++++++------- commotion_client/utils/validate.py | 150 +++++++++++++++-------------- 2 files changed, 113 insertions(+), 111 deletions(-) diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py index 0746d6f..6925fe4 100644 --- a/commotion_client/utils/fs_utils.py +++ b/commotion_client/utils/fs_utils.py @@ -13,15 +13,15 @@ #Standard Library Imports import os import logging - -#set function logger -log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. +import uuid def is_file(unknown): """ Determines if a file is accessable. It does NOT check to see if the file contains any data. """ -#stolen from https://github.com/isislovecruft/python-gnupg/blob/master/gnupg/_util.py + #stolen from https://github.com/isislovecruft/python-gnupg/blob/master/gnupg/_util.py + #set function logger + log = logging.getLogger("commotion_client."+__name__) try: assert os.lstat(unknown).st_size > 0, "not a file: %s" % unknown except (AssertionError, TypeError, IOError, OSError) as err: @@ -43,60 +43,60 @@ def walklevel(some_dir, level=1): num_sep_this = root.count(os.path.sep) if num_sep + level <= num_sep_this: del dirs[:] - + def make_temp_dir(new=None): """Makes a temporary directory and returns the QDir object. @param new bool Create a new uniquely named directory within the exiting Commotion temp directory and return the new folder object """ + log = logging.getLogger("commotion_client."+__name__) temp_path = "/Commotion/" if new: unique_dir_name = uuid.uuid4() - temp_path += str( unique_dir_name ) -# temp_dir = QtCore.QDir( QtCore.QDir.tempPath() + temp_path ) - temp_dir = QtCore.QDir( os.path.join(QtCore.QDir.tempPath(), temp_path )) - if QtCore.QDir().mkpath( temp_dir.path() ): - log.debug( QtCore.QCoreApplication.translate( "logs", "Creating main temporary directory" )) + temp_path += str(unique_dir_name) +# temp_dir = QtCore.QDir(QtCore.QDir.tempPath() + temp_path) + temp_dir = QtCore.QDir(os.path.join(QtCore.QDir.tempPath(), temp_path)) + if QtCore.QDir().mkpath(temp_dir.path()): + log.debug(QtCore.QCoreApplication.translate("logs", "Creating main temporary directory")) else: - _error = QtCore.QCoreApplication.translate( "logs", "Error creating temporary directory" ) - log.error( _error ) - raise IOError( _error ) + _error = QtCore.QCoreApplication.translate("logs", "Error creating temporary directory") + log.error(_error) + raise IOError(_error) return temp_dir - -def clean_dir( path=None ): - """ Cleans a directory. If not given a path it will clean the FULL temporary directory""" +def clean_dir(path=None): + """ Cleans a directory. If not given a path it will clean the FULL temporary directory""" + log = logging.getLogger("commotion_client."+__name__) if not path: - path = QtCore.QDir( os.path.join(QtCore.QDir.tempPath(), "Commotion" )) - - path.setFilter( QtCore.QDir.NoSymLinks | QtCore.QDir.Files ) + path = QtCore.QDir(os.path.join(QtCore.QDir.tempPath(), "Commotion")) + path.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) list_of_files = path.entryList() - + for file_ in list_of_files: - file_ = os.path.join( path.path(), file_ ) - if not QtCore.QFile( file_ ).remove(): - _error = QtCore.QCoreApplication.translate( "logs", "Error saving extension to extensions directory." ) - log.error( _error ) - raise IOError( _error ) - path.rmpath( path.path() ) + file_ = os.path.join(path.path(), file_) + if not QtCore.QFile(file_).remove(): + _error = QtCore.QCoreApplication.translate("logs", "Error saving extension to extensions directory.") + log.error(_error) + raise IOError(_error) + path.rmpath(path.path()) return True -def copy_contents( start, end ): +def copy_contents(start, end): """ Copies the contents of one directory into another @param start QDir A Qdir object for the first directory @param end QDir A Qdir object for the final directory """ - - start.setFilter( QtCore.QDir.NoSymLinks | QtCore.QDir.Files ) - list_of_files = path.entryList() - + log = logging.getLogger("commotion_client."+__name__) + start.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) + list_of_files = start.entryList() + for file_ in list_of_files: - source = os.path.join( start.path(), file_ ) - dest = os.path.join( end.path(), file_ ) - if not QtCore.QFile( source ).copy( dest ): - _error = QtCore.QCoreApplication.translate( "logs", "Error copying file into extensions directory. File already exists." ) - log.error( _error ) - raise IOError( _error ) + source = os.path.join(start.path(), file_) + dest = os.path.join(end.path(), file_) + if not QtCore.QFile(source).copy(dest): + _error = QtCore.QCoreApplication.translate("logs", "Error copying file into extensions directory. File already exists.") + log.error(_error) + raise IOError(_error) return True diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 7cf2bd4..0a6dde5 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -14,13 +14,15 @@ import sys import re import ipaddress +import os #PyQt imports from PyQt4 import QtCore +#Commotion Client Imports +from utils import fs_utils -class ClientConfig(): - """ """ +class ClientConfig(object): def __init__(self, config=None, directory=None): if config: @@ -28,8 +30,9 @@ def __init__(self, config=None, directory=None): self._directory = directory self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.errors = None - def set_extension(directory, config=None): + def set_extension(self, directory, config=None): """Set the default values @param config string The absolute path to a config file for this extension @@ -39,8 +42,8 @@ def set_extension(directory, config=None): if config: self._config = config.load_config(config) else: - default_config_path = os.path.join( self._directory, "config.json" ) - if fs_utils.is_file( default_config_path ): + default_config_path = os.path.join(self._directory, "config.json") + if fs_utils.is_file(default_config_path): self._config = config.load_config(default_config_path) else: raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) @@ -52,11 +55,11 @@ def validate_all(self): @return bool True if valid, False if invalid. """ self.errors = None - if not directory: - if not self.directory: + if not self._directory: + if not self._directory: raise NameError(self.translate("logs", "ClientConfig validator requires at least an extension directory to be specified")) else: - directory = self.directory + directory = self._directory errors = [] if not self.name(): errors.append("name") @@ -70,8 +73,8 @@ def validate_all(self): errors.append("parent") else: for gui_name in ['main', 'settings', 'toolbar']: - if not self.gui( gui_name ): - errors.append( gui_name ) + if not self.gui(gui_name): + errors.append(gui_name) if errors: self.errors = errors return False @@ -79,108 +82,108 @@ def validate_all(self): return True - def gui( self, gui_name ): + def gui(self, gui_name): """Validate of one of the gui objects config values. (main, settings, or toolbar) @param gui_name string "main", "settings", or "toolbar" """ try: - val = str( self._config[ gui_name ] ) + val = str(self._config[gui_name]) except KeyError: if gui_name != "main": try: - val = str( self._config[ "main" ] ) + val = str(self._config["main"]) except KeyError: - val = str( 'main' ) + val = str('main') else: - val = str( 'main' ) + val = str('main') file_name = val + ".py" - if not self.check_path( file_name ): - self.log.warn(self.translate( "logs", "The extensions {0} file name is invalid for this system.".format( gui_name ) )) + if not self.check_path(file_name): + self.log.warn(self.translate("logs", "The extensions {0} file name is invalid for this system.".format(gui_name))) return False - if not self.check_exists( file_name ): - self.log.warn(self.translate( "logs", "The extensions {0} file does not exist.".format( gui_name ) )) + if not self.check_exists(file_name): + self.log.warn(self.translate("logs", "The extensions {0} file does not exist.".format(gui_name))) return False return True - def name( self ): + def name(self): try: - name_val = str( self._config['name'] ) + name_val = str(self._config['name']) except KeyError: - self.log.warn(self.translate( "logs", "There is no name value in the config file. This value is required." )) + self.log.warn(self.translate("logs", "There is no name value in the config file. This value is required.")) return False - if not self.check_path_length( name_val ): - self.log.warn(self.translate( "logs", "This value is too long for your system." )) + if not self.check_path_length(name_val): + self.log.warn(self.translate("logs", "This value is too long for your system.")) return False - if not self.check_file_chars( name_val ): - self.log.warn(self.translate( "logs", "This value uses invalid characters for your system." )) + if not self.check_path_chars(name_val): + self.log.warn(self.translate("logs", "This value uses invalid characters for your system.")) return False return True - def menu_item( self ): + def menu_item(self): """Validate a menu item value.""" try: - val = str( self._config[ "menu_item" ] ) + val = str(self._config["menu_item"]) except KeyError: if self.name(): - val = str( self._config[ "name" ] ) + val = str(self._config["name"]) else: - self.log.warn(self.translate( "logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid." )) + self.log.warn(self.translate("logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid.")) return False - if not check_menu_text( val ): + if not self.check_menu_text(val): self.log.warn(self.translate("logs", "The menu_item value is invalid")) return False return True - def parent( self ): + def parent(self): """Validate a parent value.""" try: - val = str( self._config[ "parent" ] ) + val = str(self._config["parent"]) except KeyError: - self.log.info(self.translate( "logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used." )) + self.log.info(self.translate("logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used.")) return True - if not self.check_menu_text( val ): + if not self.check_menu_text(val): self.log.warn(self.translate("logs", "The parent value is invalid")) return False return True - def menu_level( self ): + def menu_level(self): """Validate a Menu Level Config item.""" try: - val = int( self._config[ "menu_level" ] ) + val = int(self._config["menu_level"]) except KeyError: - self.log.info(self.translate( "logs", "There is no 'menu_level' value set in the config. As such the default value of 10 will be used." )) + self.log.info(self.translate("logs", "There is no 'menu_level' value set in the config. As such the default value of 10 will be used.")) return True except ValueError: - self.log.info(self.translate( "logs", "The 'menu_level' value set in the config is not a number and is therefore invalid." )) + self.log.info(self.translate("logs", "The 'menu_level' value set in the config is not a number and is therefore invalid.")) return False - if not ( 0 < val > 100 ): + if not 0 < val > 100: self.log.warn(self.translate("logs", "The menu_level is invalid. Choose a number between 1 and 100")) return False return True - def tests( self ): + def tests(self): """Validate a tests config menu item.""" try: - val = str( self._config[ "tests" ] ) + val = str(self._config["tests"]) except KeyError: - val = str( 'tests' ) + val = str('tests') file_name = val + ".py" - if not self.check_path( file_name ): - self.log.warn(self.translate( "logs", "The extensions 'tests' file name is invalid for this system.")) + if not self.check_path(file_name): + self.log.warn(self.translate("logs", "The extensions 'tests' file name is invalid for this system.")) return False - if not self.check_exists( file_name ): - self.log.warn(self.translate( "logs", "The extensions 'tests' file does not exist." )) + if not self.check_exists(file_name): + self.log.warn(self.translate("logs", "The extensions 'tests' file does not exist.")) return False return True - def check_menu_text( self, menu_text ): + def check_menu_text(self, menu_text): """ Checks that menu text fits within the accepted string length bounds. @param menu_text string The text that will appear in the menu. """ - if not ( 3 < len( str( menu_text )) > 40 ): + if not 3 < len(str(menu_text)) > 40: self.log.warn(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) return False @@ -189,8 +192,8 @@ def check_exists(self, file_name): @param file_name string The file name from a config file """ - files = QtCore.QDir(self.directory).entryList() - if not ( str( file_name )) in files: + files = QtCore.QDir(self._directory).entryList() + if not str(file_name) in files: self.log.warn(self.translate("logs", "The specified file does not exist.")) return False else: @@ -201,11 +204,11 @@ def check_path(self, file_name): @param file_name string The string to check for validity. """ - if not check_path_length( file_name ): - self.log.warn(self.translate( "logs", "This value is too long for your system." )) + if not self.check_path_length(file_name): + self.log.warn(self.translate("logs", "This value is too long for your system.")) return False - if not check_file_chars( file_name ): - self.log.warn(self.translate( "logs", "This value uses invalid characters for your system." )) + if not self.check_path_chars(file_name): + self.log.warn(self.translate("logs", "This value uses invalid characters for your system.")) return False return True @@ -215,13 +218,13 @@ def check_path_chars(self, file_name): @param file_name string The string to check for validity """ # file length limit - platform = sys.platform - reserved = { "cygwin" : "[\|\\\?\*<\":>\+\[\]/]", - "win32" : "[\|\\\?\*<\":>\+\[\]/]", - "darwin" : "[:]", - "linux" : "[/\x00]" } + platform = sys.platform + reserved = {"cygwin" : r"[|\?*<\":>+[]/]", + "win32" : r"[|\?*<\":>+[]/]", + "darwin" : "[:]", + "linux" : "[/\x00]"} if platform and reserved[platform]: - if re.search( file_name, reserved[platform] ): + if re.search(file_name, reserved[platform]): self.log.warn(self.translate("logs", "The extension's config file contains an invalid main value.")) return False else: @@ -237,36 +240,35 @@ def check_path_length(self, file_name=None): @param file_name string The string to check for validity. """ # file length limit - platform = sys.platform + platform = sys.platform # OSX(name<=255), linux(name<=255) name_limit = ['linux', 'darwin'] # Win(name+path<=260), path_limit = ['win32', 'cygwin'] if platform in path_limit: if self.name(): #check valid name before using it - extension_path = os.path.join( QtCore.QDir.currentPath(), "extensions" ) - full_path = os.path.join( extension_path, file_name ) + extension_path = os.path.join(QtCore.QDir.currentPath(), "extensions") + full_path = os.path.join(extension_path, file_name) else: self.log.warn(self.translate("logs", "The extension's config file 'main' value requires a valid 'name' value. Which this extension does not have.")) return False - if len( str( full_path )) > 255: + if len(str(full_path)) > 255: self.log.warn(self.translate("logs", "The full extension path cannot be greater than 260 chars")) return False - else if platform in name_limit: - if len( str( file_name ) ) > 260: + elif platform in name_limit: + if len(str(file_name)) >= 260: self.log.warn(self.translate("logs", "File names can not be greater than 260 chars on your system")) return False else: self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) return True -class Networking(): - - def __init__(): +class Networking(object): + def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate - def ipaddr(self, ip, addr_type=None): + def ipaddr(self, ip_addr, addr_type=None): """ Checks if a string is a validly formatted IPv4 or IPv6 address. @@ -274,15 +276,15 @@ def ipaddr(self, ip, addr_type=None): @param addr_type int The appropriate version number: 4 for IPv4, 6 for IPv6. """ try: - addr = ipaddress.ip_address( str( ip )) + addr = ipaddress.ip_address(str(ip_addr)) except ValueError: - self.log.warn(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip)) + self.log.warn(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip_addr)) return False if addr_type: if addr.version == addr_type: return True else: - self.log.warn(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip, addr_type)) + self.log.warn(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip_addr, addr_type)) return False else: return True From cb19959c4fe80c9304895850c47de9907949547f Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:46:04 -0400 Subject: [PATCH 030/107] hooked extension loader to extension manager --- commotion_client/GUI/main_window.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index b65a0d0..cd336e3 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -24,7 +24,7 @@ from GUI.crash_report import CrashReport from GUI import welcome_page from utils import config -from extensions.extension_manager import ExtensionManager +from utils import extension_manager class MainWindow(QtGui.QMainWindow): """ @@ -39,14 +39,14 @@ def __init__(self, parent=None): super().__init__() #Keep track of if the gui needs any clean up / saving. self._dirty = False - self.log = logging.getLogger("commotion_client."+__name__ + self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate self.init_crash_reporter() self.setup_menu_bar() - self.next_viewport = welcome_page.ViewPort(self) - self.set_viewport() + self.viewport = welcome_page.ViewPort(self) + self.load_viewport() #Default Paramiters #TODO to be replaced with paramiters saved between instances later try: @@ -102,8 +102,9 @@ def init_crash_reporter(self): def set_viewport(self): """Load and set viewport to next viewport and load viewport """ - self.viewport = ExtensionManager.get_GUI(self.next_viewport, "main") - self.load_viewport() + ext_manager = extension_manager.ExtensionManager + self.viewport = ext_manager.import_extension(self.next_viewport).ViewPort(self) + self.load_viewport() def load_viewport(self): """Apply current viewport to the central widget and set up proper signal's for communication. """ From 3a48228114b56dcbd0367735de26d45eb50a043d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:46:58 -0400 Subject: [PATCH 031/107] fixed incorrect loading of fs_utils --- commotion_client/utils/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index 32f9993..29cd37b 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - + """ config @@ -14,7 +14,7 @@ from PyQt4 import QtCore -from utils import fsUtils +from utils import fs_utils #set function logger log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. @@ -54,7 +54,7 @@ def get_config_paths(config_type): self.log.exception(_excp) return False try: - for root, dirs, files in fsUtils.walklevel(path): + for root, dirs, files in fs_utils.walklevel(path): for file_name in files: if file_name.endswith(".conf"): config_files.append(os.path.join(root, file_name)) @@ -79,7 +79,7 @@ def get_configs(paths): """ #load config file for path in paths: - if fsUtils.is_file(path): + if fs_utils.is_file(path): config = load_config(path) if config: yield config From fe797baa20b8ef95437197f5deb701c2e12166db Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:54:55 -0400 Subject: [PATCH 032/107] cleaned up some markdown --- docs/extensions/writing_extensions.md | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/docs/extensions/writing_extensions.md b/docs/extensions/writing_extensions.md index 4d418e7..a741ebb 100644 --- a/docs/extensions/writing_extensions.md +++ b/docs/extensions/writing_extensions.md @@ -61,7 +61,7 @@ Taking just one value we can sketch out how the interface will represent it. Beyond the consitancy provided by common terms, common groupings are also important. In order to ensure that a user can easily modify related configurations. We have grouped the configuration values in the following two groups. - +``` security { "announce" "encryption" @@ -87,7 +87,7 @@ networking { "ipgenmask" "dns" } - +``` TODO: * Show process of designing section headers @@ -140,7 +140,7 @@ This object saves as a ui file. If you are developing from within the commotion_ Before the main window will load your application it needs a configuration file to load it from. This config file should be placed in your extensions main directory. For testing, you can place a copy of it in the folder "commotion_client/data/extensions/." The Commotion client will then automatically load your extension from its place in the "commotion_client/extensions/contrib" directory. We will cover how to package your extension for installation in the last section. Create a file in your main extension directory called ```config.json```. In that file place a json structure including the following items. - +``` { "name":"config_manager", "menuItem":"Configuration Editor", @@ -150,24 +150,8 @@ Create a file in your main extension directory called ```config.json```. In that "main":"main", "tests":"test_suite" } - -The "taskbar," "tests," and "settings," values are optional. But we will be making them in this tutorial. Here are explanations of each value. - -name: The name of the extension. This will be the name that the commotion client will use to import the extension after installation, and MUST be unique across the extensions that the user has installed. [from extensions import name] - -menuItem: The name displayed in the sub-menu that will load this extension. - -menuLevel: The level at which this sub-menu item will be displayed in relation to other (Non-Core) sub-menu items. The lower the number the higher up in the sub-menu. Core extension sub-menu items are ranked first, with other extensions being placed below them in order of ranking. - -parent: The top-level menu-item that this extension falls under. If this top-level menu does not exist it will be created. The top-level menu-item is simply a container that when clicked reveals the items below it. - -settings: (optional) The file that contains the settings page for the extension. If this is not included in the config file and a “settings” class is not found in the file listed under the “main” option the extension will not list a settings button in the extension settings page. [self.settings = name.settings.Settings(settingsViewport)] - -taskbar: (optional) The file that contains the function that will return the custom task-bar when run. The implementation of this is still in development. If not set and a “taskbar” class is not found in the file listed under the “main” option the default taskbar will be implemented. [self.taskbar = name.taskbar.TaskBar(mainTaskbar)] - -main: (optional) The file name to use to populate the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. [self.viewport = name.main.ViewPort(mainViewport)] - -tests: (optional, but bad form if missing) The file that contains the unitTests for this extension. This will be run when the main test_suite is called. If missing you will make the Commotion development team cry. [self.viewport = name.main.ViewPort(mainViewport)] +``` +The "taskbar," "tests," and "settings," values are optional. But we will be making them in this tutorial. You can find explanations of each value at https://wiki.commotionwireless.net/doku.php?id=commotion_architecture:commotion_client_architecture#extension_config_properties Once you have a config file in place we can actually create the logic behind our application. From 34c2991d49bbccdd40ea8aba922ada699dfcc940 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 15:59:25 -0400 Subject: [PATCH 033/107] moved config_manager to core From a7b1d3aa56c6b8908172ae8beaecffa16ef189d3 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 16:02:17 -0400 Subject: [PATCH 034/107] fixed some weirdness --- .../extensions/core}/config_manager/config.json | 0 .../extensions/core}/config_manager/main.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {docs/extensions/tutorial => commotion_client/extensions/core}/config_manager/config.json (100%) rename {docs/extensions/tutorial => commotion_client/extensions/core}/config_manager/main.py (100%) diff --git a/docs/extensions/tutorial/config_manager/config.json b/commotion_client/extensions/core/config_manager/config.json similarity index 100% rename from docs/extensions/tutorial/config_manager/config.json rename to commotion_client/extensions/core/config_manager/config.json diff --git a/docs/extensions/tutorial/config_manager/main.py b/commotion_client/extensions/core/config_manager/main.py similarity index 100% rename from docs/extensions/tutorial/config_manager/main.py rename to commotion_client/extensions/core/config_manager/main.py From 915112cb712dd683e1686102e5bfc52fff78056c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 16:05:13 -0400 Subject: [PATCH 035/107] messed up links and did not realize till it was already pushed. --- .../extensions/core}/config_manager/ui/config_manager.ui | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {docs/extensions/tutorial => commotion_client/extensions/core}/config_manager/ui/config_manager.ui (100%) diff --git a/docs/extensions/tutorial/config_manager/ui/config_manager.ui b/commotion_client/extensions/core/config_manager/ui/config_manager.ui similarity index 100% rename from docs/extensions/tutorial/config_manager/ui/config_manager.ui rename to commotion_client/extensions/core/config_manager/ui/config_manager.ui From 844fdeb1cd613017bdfe3fa80d91658ed9357573 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 19 Mar 2014 16:09:39 -0400 Subject: [PATCH 036/107] changed config_manager to config_editor --- .../core/{config_manager => config_editor}/config.json | 6 +++--- .../core/{config_manager => config_editor}/main.py | 0 .../{config_manager => config_editor}/ui/config_manager.ui | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename commotion_client/extensions/core/{config_manager => config_editor}/config.json (50%) rename commotion_client/extensions/core/{config_manager => config_editor}/main.py (100%) rename commotion_client/extensions/core/{config_manager => config_editor}/ui/config_manager.ui (100%) diff --git a/commotion_client/extensions/core/config_manager/config.json b/commotion_client/extensions/core/config_editor/config.json similarity index 50% rename from commotion_client/extensions/core/config_manager/config.json rename to commotion_client/extensions/core/config_editor/config.json index d936546..db8ba0d 100644 --- a/commotion_client/extensions/core/config_manager/config.json +++ b/commotion_client/extensions/core/config_editor/config.json @@ -1,7 +1,7 @@ { -"name":"extension_template", -"menuItem":"Extension Template", -"parent":"Templates", +"name":"config_manager", +"menuItem":"Commotion Config Editor", +"parent":"Advanced", "settings":"settings", "taskbar":"task_bar", "main":"main", diff --git a/commotion_client/extensions/core/config_manager/main.py b/commotion_client/extensions/core/config_editor/main.py similarity index 100% rename from commotion_client/extensions/core/config_manager/main.py rename to commotion_client/extensions/core/config_editor/main.py diff --git a/commotion_client/extensions/core/config_manager/ui/config_manager.ui b/commotion_client/extensions/core/config_editor/ui/config_manager.ui similarity index 100% rename from commotion_client/extensions/core/config_manager/ui/config_manager.ui rename to commotion_client/extensions/core/config_editor/ui/config_manager.ui From 57a0cbb623cf703a3fa9d90784fc92ff8b6c056b Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 10:18:45 -0400 Subject: [PATCH 037/107] removed old todo, as it is no longer needed. --- TODO | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index 7ca67b4..0000000 --- a/TODO +++ /dev/null @@ -1,46 +0,0 @@ -#TODO -# def check_chipset - -# def startCommotion -# def stopCommotion -# Wrappers for standard connect and fallback, and disconnection routine (which should not be in nm-dispatcher, after all) -# Can be the basis of any easy-to-use command line engine -# Couch all subprocess commands in the sort of structure shown for startOlsrd, to allow for proper output -# Decompose fallback routine itself, such that it doesn't even need to be installed by default? -# Replace ifconfig call with ip call? -# See if wpa_cli and wpa_gui can be made to work with version 2.0 -# Add DNS acquisition logic -# Check and refine generate_ip, selectInterface, and GTK profile editor -# Replace subprocess calls with os.lchmod, os.kill -# Should name of profile displayed by mesh applet be name of the file, or name of the SSID specified in the file? -# Refine ip generation logic, such that you'll never end up with a .0 -# Refine wpa_supplicant version check for debian; add to nm-dispatcher-olsrd -# Allow specification/choice of interface, in cases where there is more than one option -# Have getInterface return compatibility flags? -# Thread driver checks through both submodules: force the applet to use the best option, and tell nm-dispatcher to give up if it's using a bad interface (can you get it to switch, perhaps?!? -# Debug Gtk error messages that should show up when mesh isn't connected (dialog box and menu bar) -# Escalate some logging messages to visual notifications -# Add input validation to readProfile -# Add full connection checker. If the connection fails at any point, try various things, including rfkill, different interface, fallback.py, restarting olsrd, etc." - -# Add Dependencies: iw, vi, pkill, gksu -# def write_wpasupplicant_config(self, profile): -# Add logic to intelligently chose the best of multiple interfaces, fall back to other if that doesn't work -# Show Disconnect command even when mesh isn't completely successful. -# Combine the interface selection logic from nm-dispatcher??? and mesh-applet into commotion-linux; have both call commotion-linux -# Replace all gksu calls with gksudo -# Restructure files so that everything is inside of "commotion" directories (in etc, pyshared, share, etc.) -# Makes sure that all file writes are unbuffered (buffer=0 paramater) -# Add autogeneration routine for .wpasupplicant files, or at least a check -# Finish replacing all static mentions of '/etc/nm... with a variable -# Add rfkill block wifi /unblock wifi to all routines -# GTK profile editor? -# IBSS_RSN check, via iw list -# Driver check -# Change install of pyjavaproperties to be done through pipy repo, post install hook -# Add full up/down logic to fallback script, such that it can be called by the mesh applet's disconnect function, and not result in the generation of multiple password prompts -# Make inactive menu items grey, not invisible -# Remove/overhaul debug log call -# Get mesh status buttons to appear conditionally -# NO RELATIVE PATHS IN SUBPROCESS CALLS -# Port applet display logic to fallback routine From a131e9e9c3c9ca40eb9a2c5e386be6434d511e9f Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 12:33:44 -0400 Subject: [PATCH 038/107] moved all config files into config directories --- .../data/extensions/configs/contrib/test_ext001.conf | 8 ++++++++ commotion_client/utils/config.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 commotion_client/data/extensions/configs/contrib/test_ext001.conf diff --git a/commotion_client/data/extensions/configs/contrib/test_ext001.conf b/commotion_client/data/extensions/configs/contrib/test_ext001.conf new file mode 100644 index 0000000..e8daa4d --- /dev/null +++ b/commotion_client/data/extensions/configs/contrib/test_ext001.conf @@ -0,0 +1,8 @@ +{ +"name":"test_ext001", +"menuItem":"Test Extension 001", +"parent":"Test Suite", +"settings":"mySettings", +"taskbar":"myTaskBar", +"main":"myMain" +} \ No newline at end of file diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index 29cd37b..01f2760 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -43,8 +43,12 @@ def find_configs(config_type, name=None): return False def get_config_paths(config_type): + """ + Returns the paths to all config files. - configLocations = {"global":"data/global/", "user":"data/user/", "extension":"data/extensions/"} + @param config_type string The type of config to get [ global|user|extension ] + """ + configLocations = {"global":"data/global/configs", "user":"data/user/configs", "extension":"data/extensions/configs"} config_files = [] try: From 1fcf59465b99912849cc99bba46350fd410b03b4 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 12:34:39 -0400 Subject: [PATCH 039/107] Added proper checking for QVariant type --- commotion_client/GUI/main_window.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index cd336e3..4ca8591 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -191,19 +191,13 @@ def load_settings(self): _settings.beginGroup("MainWindow") #Load settings from saved, or use defaults - try: - geometry = _settings.value("geometry", defaults['geometry']) - except Exception as _excp: - self.log.critical(self.translate("logs", "Could not load window geometry from settings file or defaults.")) - self.log.exception(_excp) - raise + geometry = _settings.value("geometry", defaults['geometry']).toRect() + if geometry.isNull() == True: + _error = self.translate("logs", "Could not load window geometry from settings file or defaults.") + self.log.critical(_error) + raise EnvironmentError(_error) _settings.endGroup() - try: - self.setGeometry(geometry) - except Exception as _excp: - self.log.critical(self.translate("logs", "Cannot create GUI window.")) - self.log.exception(_excp) - raise + self.setGeometry(geometry) def save_settings(self): """ From 6f7f7b32a4ee6fe007730b6630274ba32c5cbc32 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 12:35:32 -0400 Subject: [PATCH 040/107] Changed to .conf naming for configs --- commotion_client/utils/validate.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 0a6dde5..72c6249 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -1,3 +1,4 @@ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- @@ -27,7 +28,7 @@ class ClientConfig(object): def __init__(self, config=None, directory=None): if config: self._config = config.load_config(config) - self._directory = directory + self._directory = QtCore.QDir(directory) self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate self.errors = None @@ -38,11 +39,18 @@ def set_extension(self, directory, config=None): @param config string The absolute path to a config file for this extension @param directory string The absolute path to this extensions directory """ - self._directory = directory + self._directory = QtCore.QDir(directory) if config: self._config = config.load_config(config) else: - default_config_path = os.path.join(self._directory, "config.json") + files = self._directory.entryList() + for file_ in files: + if re.match("^.*\.conf$", file_): + default_config_path = os.path.join(self._directory, file_) + try: + assert default_config_path + except NameError: + raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) if fs_utils.is_file(default_config_path): self._config = config.load_config(default_config_path) else: From 5e8f239c395e94fa309491ec7546c5e057604a0d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 12:36:50 -0400 Subject: [PATCH 041/107] removed old test extension --- commotion_client/data/extensions/test_ext001.conf | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 commotion_client/data/extensions/test_ext001.conf diff --git a/commotion_client/data/extensions/test_ext001.conf b/commotion_client/data/extensions/test_ext001.conf deleted file mode 100644 index e8daa4d..0000000 --- a/commotion_client/data/extensions/test_ext001.conf +++ /dev/null @@ -1,8 +0,0 @@ -{ -"name":"test_ext001", -"menuItem":"Test Extension 001", -"parent":"Test Suite", -"settings":"mySettings", -"taskbar":"myTaskBar", -"main":"myMain" -} \ No newline at end of file From 8ea9efde923c61eb60f69a1f8612adb82f1b21be Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 17:04:15 -0400 Subject: [PATCH 042/107] Moved extension handling to extension manager Moved major extension handling from the menu bar into the extension manager and config modules. Also documented and pep8-ified the old code as I worked thorugh it. --- commotion_client/GUI/menu_bar.py | 183 +++++++++++++++++-------------- 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index 4a9d7b8..f48bf64 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -22,6 +22,7 @@ #Commotion Client Imports from utils import config +from utils.extension_manager import ExtensionManager class MenuBar(QtGui.QWidget): @@ -35,14 +36,13 @@ def __init__(self, parent=None): #set function logger self.log = logging.getLogger("commotion_client."+__name__) - + self.translate = QtCore.QCoreApplication.translate try: - self.populateMenu() - except Exception as e: - self.log.critical(e, exc_info=1) - #TODO RAISE CRITICAL ERROR WINDOW AND CLOSE DOWN THE APPLICATION HERE - self.setLayout(self.layout) - + self.populate_menu() + except (NameError, AttributeError) as _excpt: + self.log.exception(_excpt) + raise + self.log.debug(QtCore.QCoreApplication.translate("logs", "Menu bar has initalized successfully.")) def request_viewport(self, viewport): """ @@ -50,103 +50,116 @@ def request_viewport(self, viewport): """ self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport sent")) self.viewport_requested.emit(viewport) - - def populateMenu(self): - """ - Clears and re-populates the menu using the loaded extensions. - """ - menuItems = {} - extensions = list(config.find_configs("extension")) + + def clear_layout(self, layout): + """Clears a layout of all widgets. + + Args: + layout (QLayout): A QLayout object that needs to be cleared of all objects. + """ + if not layout.isEmpty(): + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + else: + self.clear_layout(item.layout()) + + def populate_menu(self): + """Resets and populates the menu using loaded extensions.""" + if not self.layout.isEmpty(): + self.clear_layout(self.layout) + menu_items = {} + extensions = ExtensionManager.get_installed() + all_extensions = extensions['core'] + extensions['contrib'] if extensions: - topLevel = self.getParents(extensions) - for topLevelItem in topLevel: + top_level = self.get_parents(all_extensions) + for top_level_item in top_level: try: - currentItem = self.addMenuItem(topLevelItem, extensions) - if currentItem: - menuItems[topLevelItem] = currentItem - except Exception as e: - self.log.error(QtCore.QCoreApplication.translate("logs", "Loading extension \"{0}\" failed for an unknown reason.".format(topLevelItem))) - self.log.debug(e, exc_info=1) - if menuItems: - for title, section in menuItems.items(): - try: - #Add top level menu item - self.layout.addWidget(section[0]) - #Add sub-menu layout - self.layout.addWidget(section[1]) - except Exception as e: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not add menu item \"{0}\" to menu layout.".format(title))) - self.log.debug(e, exc_info=1) + current_item = self.add_menu_item(top_level_item) + except NameError as _excpt: + self.log.debug(self.translate("logs", "No extensions found under the parent item {0}. Parent item will not be added to the menu.".format(top_level_item))) + self.log.exception(_excpt) + else: + if current_item: + menu_items[top_level_item] = current_item + if menu_items: + for title, section in menu_items.items(): + #Add top level menu item + self.layout.addWidget(section[0]) + #Add sub-menu layout + self.layout.addWidget(section[1]) else: self.log.error(QtCore.QCoreApplication.translate("logs", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) - raise Exception(QtCore.QCoreApplication.translate("exception", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) + raise AttributeError(QtCore.QCoreApplication.translate("exception", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) else: self.log.error(QtCore.QCoreApplication.translate("logs", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) - raise Exception(QtCore.QCoreApplication.translate("exception", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) - #TODO Add a set of windowed error's for a variety of levels. Fatal err - + raise NameError(QtCore.QCoreApplication.translate("exception", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) + self.setLayout(self.layout) + def get_parents(self, extension_list): + """Gets all unique parents from a list of extensions. - def addMenuItem(self, title, extensions): + This function gets the "parent" menu items from a list of extensions and returns a list of the unique members. + + Args: + extension_list (list): A list containing a set of strings that list the names of extensions. + + Returns: + A list of all the unique parents of the given extensions. + + ['parent item 01', 'parent item 02'] """ - Creates and returns a single top level menu item with cascading sub-menu items from a title and a dictionary of extensions. + parents = [] + for ext in extension_list: + try: + parent = ExtensionManager.get_property(ext, "parent") + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(ext, "parent"))) + parent = "Extensions" + if parent not in parents: + parents.append(parent) + return parents + - @param title the top level menu item to place everything under - @param extensions the set of extensions to populate the menu with - @return tuple containing top level button and hidden sub-menu + def add_menu_item(self, parent): + """Creates and returns a single top level menu item with cascading sub-menu items. + + Args: + parent (string): The "parent" the top level menu item that is being requested. + + Returns: + A tuple containing a top level button and its hidden sub-menu items. """ + extensions = ExtensionManager.get_extension_from_property(parent, 'parent') + if not extensions: + raise NameError(self.translate("logs", "No extensions found under the parent item {0}.".format(parent))) #Create Top level item button - try: - titleButton = QtGui.QPushButton(QtCore.QCoreApplication.translate("Menu Item", title)) - titleButton.setCheckable(True) - except Exception as e: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not create top level menu item {0}.".format(title))) - self.log.debug(e, exc_info=1) - return False + title_button = QtGui.QPushButton(QtCore.QCoreApplication.translate("Menu Item", parent)) + title_button.setCheckable(True) #Create sub-menu - subMenu = QtGui.QFrame() - subMenuItems = QtGui.QVBoxLayout() + sub_menu = QtGui.QFrame() + sub_menu_layout = QtGui.QVBoxLayout() #populate the sub-menu item table. for ext in extensions: - if ext['parent'] and ext['parent'] == title: - try: #Create subMenuWidget - subMenuItem = subMenuWidget(self) - subMenuItem.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", ext['menuItem'])) - #We use partial here to pass a variable along when we attach the "clicked()" signal to the MenuBars requestViewport function - subMenuItem.clicked.connect(partial(self.request_viewport, ext['name'])) - except Exception as e: - self.log.error(QtCore.QCoreApplication.translate("logs", "Faile to create sub-menu \"{0}\" object for \"{1}\" object.".format(ext['name'], title))) - self.log.debug(e, exc_info=1) - return False - try: - subMenuItems.addWidget(subMenuItem) - except Exception as e: - self.log.error(QtCore.QCoreApplication.translate("logs", "Failed to add sub-menu object \"{0}\" to the sub-menu.".format(ext['name']))) - self.log.debug(e, exc_info=1) - return False - subMenu.setLayout(subMenuItems) - subMenu.hide() + sub_menu_item = subMenuWidget(self) + try: + menu_item_title = ExtensionManager.get_property(ext, 'menu_item') + except KeyError: + menu_item_title = ext + subMenuItem.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", menu_item_title)) + #We use partial here to pass a variable along when we attach the "clicked()" signal to the MenuBars requestViewport function + sub_menu_item.clicked.connect(partial(self.request_viewport, ext)) + sub_menu_layout.addWidget(sub_menu_item) + sub_menu.setLayout(sub_menu_layout) + sub_menu.hide() #Connect toggle on out checkable title button to the visability of our subMenu - titleButton.toggled.connect(subMenu.setVisible) + title_button.toggled.connect(sub_menu.setVisible) #package and return top level item and its corresponding subMenu - section = (titleButton, subMenu) + section = (title_button, sub_menu) return section - def getParents(self, extensionList): - parents = [] - - for ext in extensionList: - parent = None - if ext["parent"]: - parent = ext["parent"] - if parent not in parents: - parents.append(parent) - else: - if ext["menuItem"] not in parents: - parents.append(ext["menuItem"]) - return parents - - class subMenuWidget(QtGui.QLabel): """ From d25980a6e0a614669524f618eb4bbde55bc893ae Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 21 Mar 2014 17:07:28 -0400 Subject: [PATCH 043/107] Added more extension identification functions Created functions for extension mangement and searching that only use the installed extensions instead of searching through the extensions in the extensions directory. This should make having downloaded, but uninstalled extensions easier to ignore. Also added the required QVariant type modifications when loading extension values. --- commotion_client/utils/extension_manager.py | 179 +++++++++++++++++--- 1 file changed, 156 insertions(+), 23 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index d348b56..1bdf16d 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -31,12 +31,44 @@ def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.extensions = self.check_installed() self.translate = QtCore.QCoreApplication.translate + self.config_values = ["name", + "main", + "menu_item", + "menu_level", + "parent", + "settings", + "toolbar"] + def check_installed(self, name=None): + """Checks if and extension is installed. + + Args: + name (type): Name of a extension to check. + + Returns: + bool: True if named extension is installed, false, if not. + """ + installed_extensions = self.get_installed() + if name: + if name in installed_extensions["core"]: + return True + elif name in installed_extensions["contrib"]: + return True + else: + return False + @staticmethod - def check_installed(): - """Creates dictionary keyed with the name of installed extensions with each extensions type. + def get_installed(): + """Get all installed extensions seperated by type. + + Pulls the current installed extensions from the application settings and returns a dictionary with the lists of the two extension types. + + Returns: + A dictionary with two keys ['core' and 'contrib'] that both have lists of all extensions of that type as their values. + + {'core':['coreExtensionOne', 'coreExtensionTwo'], + 'contrib':['contribExtension', 'anotherContrib']} - @returns dict Dictionary keyed with the name of installed extensions with each extensions type. """ installed_extensions = {} _settings = QtCore.QSettings() @@ -54,6 +86,104 @@ def check_installed(): installed_extensions[ext] = "contrib" return installed_extensions + def get_extension_from_property(self, key, val): + """Takes a property and returns all extensions who have the passed value set under the passed property. + + Checks all installed extensions and returns the name of all extensions whose config contains the key:val pair passed to this function. + + Args: + key (string): The name of the property to be checked. + val (string): The value that the property must have to be selected + + Returns: + A list of extension names that have the key:val property in their config if they exist. + ['ext01', 'ext02', 'ext03'] + + Raises: + KeyError: If the value requested is non-standard. + """ + matching_extensions = [] + if value not in self.config_values: + _error = self.translate("logs", "That is not a valid extension config value.") + self.log.error(_error) + raise KeyError(_error) + _settings = QtCore.QSettings() + _settings.beginGroup("extensions") + ext_sections = ['core', 'contrib'] + for ext_type in ext_sections: + #enter extension type group + _settings.beginGroup(ext_type) + all_exts = _settings.allKeys() + #QtCore.QStringList from allKeys() is missing the .toList() method from to its QtCore.QVariant.QStringList version. So, we do this horrible while loop instead. + while all_exts.isEmpty() != True: + current_extension = all_exts.takeFirst() + #enter extension settings + _settings.beginGroup(current_extension) + if _settings.value(key).toString() == str(val): + matching_extensions.append(current_extension) + #exit extension + _settings.endGroup() + #exit extension type group + _settings.endGroup() + if matching_extensions: + return matching_extensions + else: + self.log.debug(self.translate("logs", "No extensions had the requested value.")) + + def get_property(self, name, key): + """ + Get a property of an installed extension. + + Args: + name (string): The extension's name. + key (string): The key of the value you are requesting from the extension. + + Returns: + A STRING containing the value associated the extensions key in the applications saved extension settings. + + Raises: + KeyError: If the value requested is non-standard. + """ + if value not in self.config_values: + _error = self.translate("logs", "That is not a valid extension config value.") + self.log.error(_error) + raise KeyError(_error) + _settings = QtCore.QSettings() + _settings.beginGroup("extensions") + ext_type = self.get_type(name) + _settings.beginGroup(ext_type) + _settings.beginGroup(name) + setting_value = _settings.value(key) + if setting_value.isNull(): + _error = self.translate("logs", "The extension config does not contain that value.") + self.log.error(_error) + raise KeyError(_error) + else: + return setting_value.toStr() + + def get_type(self, name): + """Returns the extension type of an installed extension. + + Args: + name (string): the name of the extension + + Returns: + A string with the type of extension. "Core" or "Contrib" + + Raises: + KeyError: If an extension does not exist. + """ + core_ext = _settings.value("core/"+str(name)) + contrib_ext _settings.value("contrib/"+str(name)) + if not core_ext.isNull() and contrib_ext.isNull(): + return "core" + elif not contrib_ext.isNull() and core_ext.isNull(): + return "contrib" + else: + _error = self.translate("logs", "This extension does not exist.") + self.log.error(_error) + raise KeyError(_error) + def load_user_interface(self, extension_name, subsection=None): """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) @@ -93,7 +223,7 @@ def load_settings(self, extension_name): _settings = QtCore.QSettings() _settings.beginGroup("extensions") _settings.beginGroup(extension_type) - extension_config['main'] = _settings.value("main", None) + extension_config['main'] = _settings.value("main").toString() #get extension dir main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) @@ -106,12 +236,12 @@ def load_settings(self, extension_name): _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) self.log.error(_error) raise IOError(_error) - extension_config['settings'] = _settings.value("settings", extension_config['main']) - extension_config['toolbar'] = _settings.value("toolbar", extension_config['main']) - extension_config['parent'] = _settings.value("parent", "Add-On's") - extension_config['menu_item'] = _settings.value("menu_item", extension_config['name']) - extension_config['menu_level'] = _settings.value("menu_level", 10) - extension_config['tests'] = _settings.value("tests", None) + extension_config['settings'] = _settings.value("settings", extension_config['main']).toString() + extension_config['toolbar'] = _settings.value("toolbar", extension_config['main']).toString() + extension_config['parent'] = _settings.value("parent", "Add-On's").toString() + extension_config['menu_item'] = _settings.value("menu_item", extension_config['name']).toString() + extension_config['menu_level'] = _settings.value("menu_level", 10).toInt() + extension_config['tests'] = _settings.value("tests").toString() if not extension_config['tests']: if "tests.py" in extension_files: extension_config['tests'] = "tests" @@ -168,9 +298,9 @@ def save_settings(self, extension_config, extension_type="contrib"): except KeyError: if config_validator.main(): _main = "main" #Set this for later default values - _settings.value("main", "main") + _settings.value("main", "main").toString() else: - _settings.value("main", _main) + _settings.value("main", _main).toString() else: _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") self.log.error(_error) @@ -182,9 +312,9 @@ def save_settings(self, extension_config, extension_type="contrib"): _config_value = extension_config[val] except KeyError: #Defaults to main, which was checked and set before - _settings.value(val, _main) + _settings.value(val, _main).toString() else: - _settings.value(val, _config_value) + _settings.value(val, _config_value).toString() else: _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.".format(val)) self.log.error(_error) @@ -192,10 +322,10 @@ def save_settings(self, extension_config, extension_type="contrib"): #Extension Parent if config_validator.parent(): try: - _settings.value("parent", extension_config["parent"]) + _settings.value("parent", extension_config["parent"]).toString() except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) - _settings.value("parent", "Extensions") + _settings.value("parent", "Extensions").toString() else: _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") self.log.error(_error) @@ -204,10 +334,10 @@ def save_settings(self, extension_config, extension_type="contrib"): #Extension Menu Item if config_validator.menu_item(): try: - _settings.value("menu_item", extension_config["menu_item"]) + _settings.value("menu_item", extension_config["menu_item"]).toString() except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) - _settings.value("menu_item", extension_name) + _settings.value("menu_item", extension_name).toString() else: _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") self.log.error(_error) @@ -215,10 +345,10 @@ def save_settings(self, extension_config, extension_type="contrib"): #Extension Menu Level if config_validator.menu_level(): try: - _settings.value("menu_level", extension_config["menu_level"]) + _settings.value("menu_level", extension_config["menu_level"]).toInt() except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) - _settings.value("menu_level", 10) + _settings.value("menu_level", 10).toInt() else: _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") self.log.error(_error) @@ -226,10 +356,10 @@ def save_settings(self, extension_config, extension_type="contrib"): #Extension Tests if config_validator.tests(): try: - _settings.value("main", extension_config['tests']) + _settings.value("main", extension_config['tests']).toString() except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) - _settings.value("tests", "tests") + _settings.value("tests", "tests").toString() else: _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) self.log.error(_error) @@ -264,7 +394,10 @@ def save_extension(self, extension, extension_type="contrib"): return False #Name if config_validator.name(): - config_path = os.path.join(unpacked.absolutePath(), "config.json") + files = unpacked.entryList() + for file_ in files: + if re.match("^.*\.conf$", file_): + config_path = os.path.join(unpacked.absolutePath(), file_) _config = config.load_config(config_path) existing_extensions = config.find_configs("extension") try: From ea4edbf762bd1f7b55ab4dbee476fc9789636407 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 10:04:29 -0400 Subject: [PATCH 044/107] Added some basic code standards guidance --- .../extensions/core/config_editor/config.json | 4 +- docs/style_standards/README.md | 64 +++++ .../google_docstring_example.py | 223 ++++++++++++++++++ 3 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 docs/style_standards/README.md create mode 100644 docs/style_standards/google_docstring_example.py diff --git a/commotion_client/extensions/core/config_editor/config.json b/commotion_client/extensions/core/config_editor/config.json index db8ba0d..8b1ce87 100644 --- a/commotion_client/extensions/core/config_editor/config.json +++ b/commotion_client/extensions/core/config_editor/config.json @@ -1,6 +1,6 @@ { -"name":"config_manager", -"menuItem":"Commotion Config Editor", +"name":"config_editor", +"menuItem":"Commotion Config File Editor", "parent":"Advanced", "settings":"settings", "taskbar":"task_bar", diff --git a/docs/style_standards/README.md b/docs/style_standards/README.md new file mode 100644 index 0000000..ce80b4c --- /dev/null +++ b/docs/style_standards/README.md @@ -0,0 +1,64 @@ +# Code Standards + +## Style + +The code base should comply with [PEP 8](http://legacy.python.org/dev/peps/pep-0008/) styling. + +## Documentation and Doc-Strings + +Doc Strings should follow the Google style docstrings shown in the google_docstring_example.py file contained in this folder. + +## Logging + +### Code + +#### Proper Logging + +Every functional file should import the "logging" standard library and create a logger that is a decendant of the main commotion_client logger. + +``` +import logging + +... + +self.log = logging.getLogger("commotion_client."+__name__) +``` + +#### Logging and translation + +We use the PyQt translate library to translate text in the Commotion client. The string ``logs``` is used as the "context" for all logging objects. While the translate library will automatically add the class name as the context for most translated strings we would like to seperate out logging strings so that translators working with the project can prioritize it less than critical user facing text. + +``` +_error = QtCore.QCoreApplication.translate("logs", "That is not a valid extension config value.") +self.log.error(_error) +``` + +Due to the long length of the translation call ``QtCore.QCoreApplication.translate``` feel free to set this value to the variable self.translate at the start of any classes. Please refrain from using another variable name to maintain consistancy actoss the code base. + +```self.translate = QtCore.QCoreApplication.translate``` + +### LogLevels + +Logging should correspond to the following levels: + + * critical: The application is going to need to close. There is no possible recovery or alternative behavior. This will generate an error-report (if possible) and is ABSOLUTELY a bug that will need to be addressed if a user reports seeing one of these logs. + + * error & exception: The application is in distress and has visibly failed to do what was requested of it by the user. These do not have to close the application, and may have failsafes or handling, but should be severe enough to be reported to the user. If a user experiences one of these the application has failed in a way that is a programmers fault. These can generate an error-report at the programmers discression. + + * warn: An unexpected event has occured. A user may be affected, but adaquate fallbacks and handling can still provide the user with a smooth experience. These are the issues that need to be tracked, but are not neccesarily a bug, but simply the application handling inconsistant environmental conditions or usage. + + * info: Things you want to see at high volume in case you need to forensically analyze an issue. System lifecycle events (system start, stop) go here. "Session" lifecycle events (login, logout, etc.) go here. Significant boundary events should be considered as well (e.g. database calls, remote API calls). Typical business exceptions can go here (e.g. login failed due to bad credentials). Any other event you think you'll need to see in production at high volume goes here. + + * debug: Just about everything that doesn't make the "info" cut... any message that is helpful in tracking the flow through the system and isolating issues, especially during the development and QA phases. We use "debug" level logs for entry/exit of most non-trivial methods and marking interesting events and decision points inside methods. + +### Logging Exeptions + +Exceptions should be logged using the exception handle at the point where they interfeir with the core task. If you wish to add logging at the point where you raise an exception use a debug or info log level to provide information about context around an exception. + +## Exception Handling + + +## Code + +### Python Version +All code MUST be compatable with Python3. diff --git a/docs/style_standards/google_docstring_example.py b/docs/style_standards/google_docstring_example.py new file mode 100644 index 0000000..c94dcdf --- /dev/null +++ b/docs/style_standards/google_docstring_example.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +"""Example Google style docstrings. + +This module demonstrates documentation as specified by the `Google Python +Style Guide`_. Docstrings may extend over multiple lines. Sections are created +with a section header and a colon followed by a block of indented text. + +Example: + Examples can be given using either the ``Example`` or ``Examples`` + sections. Sections support any reStructuredText formatting, including + literal blocks:: + + $ python example_google.py + +Section breaks are created by simply resuming unindented text. Section breaks +are also implicitly created anytime a new section starts. + +Attributes: + module_level_variable (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +.. _Google Python Style Guide: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html + +""" + +module_level_variable = 12345 + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If the parameter itself is optional, it should be noted by adding + ", optional" to the type. If \*args or \*\*kwargs are accepted, they + should be listed as \*args and \*\*kwargs. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (str, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError('param1 may not be equal to param2') + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Args: + n (int): The upper limit of the range to generate, from 0 to `n` - 1 + + Yields: + int: The next number in the range of 0 to `n` - 1 + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print [i for i in example_generator(4)] + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + msg (str): Human readable string describing the exception. + code (int, optional): Error code, defaults to 2. + + Attributes: + msg (str): Human readable string describing the exception. + code (int): Exception error code. + + """ + def __init__(self, msg, code=2): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they should be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. + + Attributes: + attr1 (str): Description of `attr1`. + attr2 (list of str): Description of `attr2`. + attr3 (int): Description of `attr3`. + + """ + def __init__(self, param1, param2, param3=0): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1 (str): Description of `param1`. + param2 (list of str): Description of `param2`. Multiple + lines are supported. + param3 (int, optional): Description of `param3`, defaults to 0. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output. + + This behavior can be disabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = False + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass From 055da6c254d2bd41904194fd6f061492c7fb4961 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 10:05:43 -0400 Subject: [PATCH 045/107] added missing settings import --- commotion_client/utils/extension_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 1bdf16d..517d2a9 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -173,15 +173,16 @@ def get_type(self, name): Raises: KeyError: If an extension does not exist. """ + _settings = QtCore.QSettings() + _settings.beginGroup("extensions") core_ext = _settings.value("core/"+str(name)) - contrib_ext _settings.value("contrib/"+str(name)) + contrib_ext = _settings.value("contrib/"+str(name)) if not core_ext.isNull() and contrib_ext.isNull(): return "core" elif not contrib_ext.isNull() and core_ext.isNull(): return "contrib" else: _error = self.translate("logs", "This extension does not exist.") - self.log.error(_error) raise KeyError(_error) def load_user_interface(self, extension_name, subsection=None): From 332046442dd408f7116bed20d873a4d73258a638 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 10:57:16 -0400 Subject: [PATCH 046/107] added basic setup.py --- setup.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..39b8b9f --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from distutils.core import setup +import py2exe + +setup(name="Commotion Client", + version="1.0", + url="commotionwireless.net", + license="Affero General Public License V3 (AGPLv3)", + platforms="linux", + packages=['core_extensions', + 'contrib_extensions'], + package_dir={'core_extensions': 'commotion_client/extensions/core', + 'contrib_extensions': 'commotion_client/extensions/contrib'}, + package_data={'core_extensions': ['config_manager'], + 'contrib_extensions': ['test']}, + ) From 4bfda663c19049753a9c62851b0ba6f165aedb86 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 11:39:11 -0400 Subject: [PATCH 047/107] added loaded config files to data directory --- .gitignore | 5 +- commotion_client/utils/extension_manager.py | 62 +++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 20bf2d1..24eb5ee 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ commotion_client/temp/ Ui_*.py # Emacs auto-save cruft because s2e does not want to spend the time debugging his .emacs config right now. -\#.*# \ No newline at end of file +\#.*# + +#Extension Application Data +commotion_client/data/extensions/* diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 517d2a9..100b074 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -43,19 +43,21 @@ def check_installed(self, name=None): """Checks if and extension is installed. Args: - name (type): Name of a extension to check. + name (type): Name of a extension to check. If not specified will check if there are any extensions installed. Returns: bool: True if named extension is installed, false, if not. """ - installed_extensions = self.get_installed() - if name: - if name in installed_extensions["core"]: - return True - elif name in installed_extensions["contrib"]: - return True - else: - return False + installed_extensions = self.get_installed().keys() + if name and name in installed_extensions: + return True + else if not name and installed_extensions: + return True + else: + return False + + def load_all(self): + ERRORS_HERE() @staticmethod def get_installed(): @@ -379,7 +381,6 @@ def save_extension(self, extension, extension_type="contrib"): #There can be only two... and I don't trust you. if extension_type != "contrib": extension_type = "core" - try: unpacked = self.unpack_extension(self, extension, extension_type) except IOError: @@ -398,6 +399,7 @@ def save_extension(self, extension, extension_type="contrib"): files = unpacked.entryList() for file_ in files: if re.match("^.*\.conf$", file_): + config_name = _file config_path = os.path.join(unpacked.absolutePath(), file_) _config = config.load_config(config_path) existing_extensions = config.find_configs("extension") @@ -441,8 +443,48 @@ def save_extension(self, extension, extension_type="contrib"): fs_utils.clean_dir(extension_dir) self.remove_extension_settings(_config['name']) return False + try: + self.add_config(unpacked.absolutePath(), config_name) + except IOError: + self.log.error(self.translate("logs", "Could not add the config to the core config directory.")) + self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) + fs_utils.clean_dir(extension_dir) + self.remove_extension_settings(_config['name']) + return False return True + def add_config(self, extension_dir, name): + """Copies a config file to the "loaded" core extension config data folder. + + Args: + extension_dir (string): The absolute path to the extension directory + name (string): The name of the config file + + Returns: + bool: True if successful + + Raises: + IOError: If a config file of the same name already exists or the extension can not be saved. + """ + data_dir = os.path.join(QtCore.QDir.current(), "data") + config_dir = os.path.join(data_dir, "extensions") + #If the data/extensions folder does not exist, make it. + if not QtCore.Qdir(config_dir).exists(): + QtCore.Qdir(data_dir).mkdir("extensions") + source = QtCore.Qdir(extension_dir) + s_file = os.path.join(source.path(), name) + dest_file = os.path.join(config_dir, name) + if source.exists(name): + if not QtCore.QFile(s_file).copy(dest_file): + _error = QtCore.QCoreApplication.translate("logs", "Error saving extension config. File already exists.") + log.info(_error) + raise IOError(_error) + return True + + + + + def unpack_extension(self, compressed_extension): """Unpacks an extension into a temporary directory and returns the location. From a878f122fa5b4dedbe55af1dff85211ed7b8edbd Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 11:43:48 -0400 Subject: [PATCH 048/107] added extension config removal --- commotion_client/utils/extension_manager.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 100b074..d7ca328 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -481,6 +481,27 @@ def add_config(self, extension_dir, name): raise IOError(_error) return True + def remove_config(self, name): + """Removes a config file from the "loaded" core extension config data folder. + + Args: + name (string): The name of the config file + + Returns: + bool: True if successful + + Raises: + IOError: If a config file does not exist in the extension data folder. + """ + data_dir = os.path.join(QtCore.QDir.current(), "data") + config_dir = os.path.join(data_dir, "extensions") + config = os.path.join(config_dir, name) + if QtCore.QFile(config).exists(): + if not QtCore.QFile(config).remove(): + _error = QtCore.QCoreApplication.translate("logs", "Error deleting file.") + log.info(_error) + raise IOError(_error) + return True From 10e1363330aa32b58f8c5adcaa648ce12a2169ef Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 24 Mar 2014 12:20:57 -0400 Subject: [PATCH 049/107] Made extension configs live in one unified folder under data --- .../configs/contrib/test_ext001.conf | 8 ---- commotion_client/utils/extension_manager.py | 46 +++++++++++++++++-- 2 files changed, 42 insertions(+), 12 deletions(-) delete mode 100644 commotion_client/data/extensions/configs/contrib/test_ext001.conf diff --git a/commotion_client/data/extensions/configs/contrib/test_ext001.conf b/commotion_client/data/extensions/configs/contrib/test_ext001.conf deleted file mode 100644 index e8daa4d..0000000 --- a/commotion_client/data/extensions/configs/contrib/test_ext001.conf +++ /dev/null @@ -1,8 +0,0 @@ -{ -"name":"test_ext001", -"menuItem":"Test Extension 001", -"parent":"Test Suite", -"settings":"mySettings", -"taskbar":"myTaskBar", -"main":"myMain" -} \ No newline at end of file diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index d7ca328..bfafc48 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -57,8 +57,34 @@ def check_installed(self, name=None): return False def load_all(self): - ERRORS_HERE() - + installed = self.get_installed() + saved = config.get_config_paths("extensions") + for saved_path in saved: + _saved_config = config.load_config(saved_path) + _main_ext_dir = os.path.join(QtCore.QDir.current(), "extensions") + if saved['name'] in installed.keys(): + _type = installed[saved_config['name']] + _type_dir = os.path.join(main_ext_dir, _type) + else: + _core_dir = QtCore.QDir(os.path.join(_main_ext_dir, "core")) + if _core_dir.exists() and _core_dir.exists(saved_config['name']): + _type = "core" + _type_dir = _core_dir + else: + _contrib_dir = QtCore.QDir(os.path.join(_main_ext_dir, "contrib")) + if _contrib_dir.exists() and _contrib_dir.exists(saved_config['name']): + _type = "contrib" + _type_dir = _contrib_dir + else: + return False + _extension_dir = os.path.join(_type_dir, saved_config['name']) + _extension = validate.ClientConfig(config, extension_dir) + if not _extension.validate_all(): + self.log.warn(self.translate("logs", "Extension {0}is invalid and cannot be saved.".format(saved_config['name']))) + else: + if not self.save_extension(saved['name'], _type): + self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(saved_config['name']))) + @staticmethod def get_installed(): """Get all installed extensions seperated by type. @@ -527,7 +553,19 @@ def unpack_extension(self, compressed_extension): return temp_dir def save_unpacked_extension(self, temp_dir, extension_name, extension_type): - """Moves an extension from the temporary directory to the extension directory.""" + """Moves an extension from a temporary directory to the extension directory. + + Args: + temp_dir (string): Absolute path to the temporary directory + extension_name (string): The name of the extension + extension_type (string): The type of the extension (core or contrib) + + Returns: + bool True if successful, false if unsuccessful. + + Raises: + ValueError: If an extension with that name already exists. + """ extension_path = "extensions/"+extension_type+"/"+extension_name full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) if not fs_utils.is_file(full_path): @@ -545,7 +583,7 @@ def save_unpacked_extension(self, temp_dir, extension_name, extension_type): return False else: _error = QtCore.QCoreApplication.translate("logs", "An extension with that name already exists. Please delete the existing extension and try again.") - self.log.error(_error) + self.log.info(_error) raise ValueError(_error) class InvalidSignature(Exception): From 4da5a82eef1a0c2e1fb6de70b673ea651120d592 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 25 Mar 2014 11:16:13 -0400 Subject: [PATCH 050/107] removed all logging of exceptions that are raised Logging exceptions that are raised causes unneeded repeats that clutters the logs. --- commotion_client/utils/extension_manager.py | 49 +++++++++------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index bfafc48..08f0cc7 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -51,39 +51,44 @@ def check_installed(self, name=None): installed_extensions = self.get_installed().keys() if name and name in installed_extensions: return True - else if not name and installed_extensions: + elif not name and installed_extensions: return True else: return False def load_all(self): + """Attempts to load all config files saved in the data.extensions directory""" installed = self.get_installed() - saved = config.get_config_paths("extensions") + saved = config.get_config_paths("extension") + if not saved: + self.log.info(self.translate("logs", "No saved config files found.")) + return False for saved_path in saved: _saved_config = config.load_config(saved_path) - _main_ext_dir = os.path.join(QtCore.QDir.current(), "extensions") - if saved['name'] in installed.keys(): - _type = installed[saved_config['name']] + _main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") + if _saved_config['name'] in installed.keys(): + _type = installed[_saved_config['name']] _type_dir = os.path.join(main_ext_dir, _type) else: _core_dir = QtCore.QDir(os.path.join(_main_ext_dir, "core")) - if _core_dir.exists() and _core_dir.exists(saved_config['name']): + if _core_dir.exists() and _core_dir.exists(_saved_config['name']): _type = "core" _type_dir = _core_dir else: _contrib_dir = QtCore.QDir(os.path.join(_main_ext_dir, "contrib")) - if _contrib_dir.exists() and _contrib_dir.exists(saved_config['name']): + if _contrib_dir.exists() and _contrib_dir.exists(_saved_config['name']): _type = "contrib" _type_dir = _contrib_dir else: - return False - _extension_dir = os.path.join(_type_dir, saved_config['name']) + self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_saved_config['name']))) + break + _extension_dir = os.path.join(_type_dir, _saved_config['name']) _extension = validate.ClientConfig(config, extension_dir) if not _extension.validate_all(): - self.log.warn(self.translate("logs", "Extension {0}is invalid and cannot be saved.".format(saved_config['name']))) + self.log.warn(self.translate("logs", "Extension {0}is invalid and cannot be saved.".format(_saved_config['name']))) else: - if not self.save_extension(saved['name'], _type): - self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(saved_config['name']))) + if not self.save_extension(_saved_config['name'], _type): + self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_saved_config['name']))) @staticmethod def get_installed(): @@ -133,7 +138,6 @@ def get_extension_from_property(self, key, val): matching_extensions = [] if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") - self.log.error(_error) raise KeyError(_error) _settings = QtCore.QSettings() _settings.beginGroup("extensions") @@ -174,7 +178,6 @@ def get_property(self, name, key): """ if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") - self.log.error(_error) raise KeyError(_error) _settings = QtCore.QSettings() _settings.beginGroup("extensions") @@ -184,7 +187,6 @@ def get_property(self, name, key): setting_value = _settings.value(key) if setting_value.isNull(): _error = self.translate("logs", "The extension config does not contain that value.") - self.log.error(_error) raise KeyError(_error) else: return setting_value.toStr() @@ -254,7 +256,7 @@ def load_settings(self, extension_name): _settings.beginGroup(extension_type) extension_config['main'] = _settings.value("main").toString() #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) extension_files = extension_dir.entryList() @@ -263,7 +265,6 @@ def load_settings(self, extension_name): extension_config['main'] = "main" else: _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) - self.log.error(_error) raise IOError(_error) extension_config['settings'] = _settings.value("settings", extension_config['main']).toString() extension_config['toolbar'] = _settings.value("toolbar", extension_config['main']).toString() @@ -289,7 +290,6 @@ def remove_extension_settings(self, name): _settings.remove(str(name)) else: _error = self.translate("logs", "You must specify an extension name greater than 1 char.") - self.log.error(_error) raise ValueError(_error) def save_settings(self, extension_config, extension_type="contrib"): @@ -302,7 +302,7 @@ def save_settings(self, extension_config, extension_type="contrib"): _settings = QtCore.QSettings() _settings.beginGroup("extensions") #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) #create validator @@ -448,7 +448,7 @@ def save_extension(self, extension, extension_type="contrib"): fs_utils.clean_dir(unpacked) return False #make new directory in extensions - main_ext_dir = os.path.join(QtCore.QtDir.current(), "extensions") + main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, _config['name'])) try: @@ -492,7 +492,7 @@ def add_config(self, extension_dir, name): Raises: IOError: If a config file of the same name already exists or the extension can not be saved. """ - data_dir = os.path.join(QtCore.QDir.current(), "data") + data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") #If the data/extensions folder does not exist, make it. if not QtCore.Qdir(config_dir).exists(): @@ -503,7 +503,6 @@ def add_config(self, extension_dir, name): if source.exists(name): if not QtCore.QFile(s_file).copy(dest_file): _error = QtCore.QCoreApplication.translate("logs", "Error saving extension config. File already exists.") - log.info(_error) raise IOError(_error) return True @@ -519,13 +518,12 @@ def remove_config(self, name): Raises: IOError: If a config file does not exist in the extension data folder. """ - data_dir = os.path.join(QtCore.QDir.current(), "data") + data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") config = os.path.join(config_dir, name) if QtCore.QFile(config).exists(): if not QtCore.QFile(config).remove(): _error = QtCore.QCoreApplication.translate("logs", "Error deleting file.") - log.info(_error) raise IOError(_error) return True @@ -544,11 +542,9 @@ def unpack_extension(self, compressed_extension): shutil.unpack_archive(compressed_extension, temp_abs_path, "gztar") except ReadError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was corrupted or mis-packaged.") - self.log.error(_error) raise IOError(_error) except FileNotFoundError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was not found.") - self.log.error(_error) raise IOError(_error) return temp_dir @@ -583,7 +579,6 @@ def save_unpacked_extension(self, temp_dir, extension_name, extension_type): return False else: _error = QtCore.QCoreApplication.translate("logs", "An extension with that name already exists. Please delete the existing extension and try again.") - self.log.info(_error) raise ValueError(_error) class InvalidSignature(Exception): From 1bdfb67d28ccaf2814c7d259e76ee42b3502325c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:55:42 -0400 Subject: [PATCH 051/107] added cx_freeze build configurations --- Makefile | 10 ++++++++-- build/README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 29 +++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 build/README.md diff --git a/Makefile b/Makefile index 297a1e7..80ac7b8 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,19 @@ .PHONY: build windows osx debian clean install -all: build windows debian osx +all: build windows debian osx extensions + +extensions: + echo "write the extension config section.... seriously" +# Need to copy all core & listed contrib extension data into commotion_client/data/extensions/. +# cp commotion_client/extensions/core//*.conf commotion_client/data/extensions/. build: clean python3.3 build/build.py build pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o commotion_client/assets/commotion_assets_rc.py + cxfreeze commotion_client.py --base-name=commotion_client test: clean build - cp commotion_client/assets/commotion_assets_rc.py commotion_client/. + @echo "test build complete" windows: @echo "windows compileing is not yet implemented" diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..f4057fe --- /dev/null +++ b/build/README.md @@ -0,0 +1,43 @@ + +# Build Documentation + +## cx_freeze instructions + +### Get cx_freeze + +To build cross platform images we use the [cx_freeze](http://cx-freeze.sourceforge.net/) tool. + +To install cx_freeze you can [download](http://cx-freeze.sourceforge.net/index.html) it or [build it](http://cx-freeze.sourceforge.net/index.html) from source using the README provided upon downloading it. + +#### Fixing Build Errors + + * I can't build cx_freeze. + +If you encounter an error like the one below it could be that the version of python you are using was compiled without a shared library. + +``` +/usr/bin/ld: cannot find -lpython3.3 +collect2: error: ld returned 1 exit status +error: command 'gcc' failed with exit status 1 +``` + +You will want to reconfigure & install your version of python using the following option. + +``` +./configure --enable-shared +``` + + * python3.3 does not work when I re-compile it with enable-shared + +If you are using python3.3, which is the version of python being used for this project you may have some problems with runing python3.3 after compiling it with enable-shared. The solution to this is to edit /etc/ld.so.conf or create something in /etc/ld.so.conf.d/ to add /usr/local/lib and /usr/local/lib64. Then run ldconfig. + +### Preparing the project for building + +#### Adding Extensions to Builds + +Extensions are built-in to the Commotion client by adding them to the extension folder and then adding that folder name to the core_extensions list in the setup.py. + +``` +core_extensions = ["config_editor", "main_window", "your_extension_name"] +``` + diff --git a/setup.py b/setup.py index 39b8b9f..c8a8de2 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from distutils.core import setup +from cx_freeze import setup, Executable import py2exe + +base = "commotion_client" +icon = "assets/images/logo32.png" + +#define core packages +core_pkgs = ["utils", "GUI", "assets"] +core_extensions = ["config_editor"] + +#add core_extensions to core packages +for ext in core_extensions: + core_pkgs.append("extensions."+ext) + + +exe = Executable( + script="commotion_client.py", + packages=core_pkgs + ) + setup(name="Commotion Client", version="1.0", url="commotionwireless.net", license="Affero General Public License V3 (AGPLv3)", - platforms="linux", - packages=['core_extensions', - 'contrib_extensions'], - package_dir={'core_extensions': 'commotion_client/extensions/core', - 'contrib_extensions': 'commotion_client/extensions/contrib'}, - package_data={'core_extensions': ['config_manager'], - 'contrib_extensions': ['test']}, + executables = [exe], + include_files=[os.path.join("assets", "commotion_assets_rc.py")] ) From fe1829468d539a6cfb906c92497acb52d1c5c390 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:56:04 -0400 Subject: [PATCH 052/107] added some style specifications --- docs/style_standards/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/style_standards/README.md b/docs/style_standards/README.md index ce80b4c..3991815 100644 --- a/docs/style_standards/README.md +++ b/docs/style_standards/README.md @@ -55,6 +55,8 @@ Logging should correspond to the following levels: Exceptions should be logged using the exception handle at the point where they interfeir with the core task. If you wish to add logging at the point where you raise an exception use a debug or info log level to provide information about context around an exception. +tldr: If you raise an exception you should not log it. + ## Exception Handling From 086d6b06b09c21f5e77032e47c6ee9bc9969d5ed Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:56:32 -0400 Subject: [PATCH 053/107] removed empty file --- commotion_client/tests.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 commotion_client/tests.py diff --git a/commotion_client/tests.py b/commotion_client/tests.py deleted file mode 100644 index e69de29..0000000 From 9f3637c4e796a0ee87b555b8982fb4bc4cfb093f Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:57:32 -0400 Subject: [PATCH 054/107] removed core and contrib extension differences --- .../config_editor.conf} | 5 +- .../{core => }/config_editor/main.py | 0 .../config_editor/ui/config_manager.ui | 48 +++++++++---------- 3 files changed, 25 insertions(+), 28 deletions(-) rename commotion_client/extensions/{core/config_editor/config.json => config_editor/config_editor.conf} (53%) rename commotion_client/extensions/{core => }/config_editor/main.py (100%) rename commotion_client/extensions/{core => }/config_editor/ui/config_manager.ui (96%) diff --git a/commotion_client/extensions/core/config_editor/config.json b/commotion_client/extensions/config_editor/config_editor.conf similarity index 53% rename from commotion_client/extensions/core/config_editor/config.json rename to commotion_client/extensions/config_editor/config_editor.conf index 8b1ce87..90d574d 100644 --- a/commotion_client/extensions/core/config_editor/config.json +++ b/commotion_client/extensions/config_editor/config_editor.conf @@ -2,8 +2,5 @@ "name":"config_editor", "menuItem":"Commotion Config File Editor", "parent":"Advanced", -"settings":"settings", -"taskbar":"task_bar", -"main":"main", -"tests":"test_suite" +"main":"main" } diff --git a/commotion_client/extensions/core/config_editor/main.py b/commotion_client/extensions/config_editor/main.py similarity index 100% rename from commotion_client/extensions/core/config_editor/main.py rename to commotion_client/extensions/config_editor/main.py diff --git a/commotion_client/extensions/core/config_editor/ui/config_manager.ui b/commotion_client/extensions/config_editor/ui/config_manager.ui similarity index 96% rename from commotion_client/extensions/core/config_editor/ui/config_manager.ui rename to commotion_client/extensions/config_editor/ui/config_manager.ui index cd142e2..3add7de 100644 --- a/commotion_client/extensions/core/config_editor/ui/config_manager.ui +++ b/commotion_client/extensions/config_editor/ui/config_manager.ui @@ -24,7 +24,7 @@ Commotion Configuration Manager - + :/logo16.png:/logo16.png @@ -115,7 +115,7 @@ - + :/filled?20.png:/filled?20.png @@ -217,7 +217,7 @@ - + :/filled?20.png:/filled?20.png @@ -325,7 +325,7 @@ - + :/filled?20.png:/filled?20.png @@ -350,7 +350,7 @@ - + Key @@ -364,14 +364,14 @@ - + Confirm - + @@ -438,7 +438,7 @@ - + :/filled?20.png:/filled?20.png @@ -543,7 +543,7 @@ - + :/filled?20.png:/filled?20.png @@ -646,7 +646,7 @@ - + :/filled?20.png:/filled?20.png @@ -767,7 +767,7 @@ - + :/filled?20.png:/filled?20.png @@ -871,7 +871,7 @@ - + :/filled?20.png:/filled?20.png @@ -957,7 +957,7 @@ - + :/filled?20.png:/filled?20.png @@ -1043,7 +1043,7 @@ - + :/filled?20.png:/filled?20.png @@ -1163,7 +1163,7 @@ - + :/filled?20.png:/filled?20.png @@ -1319,7 +1319,7 @@ - + :/filled?20.png:/filled?20.png @@ -1408,7 +1408,7 @@ - + :/filled?20.png:/filled?20.png @@ -1508,7 +1508,7 @@ - + :/filled?20.png:/filled?20.png @@ -1599,7 +1599,7 @@ - + :/filled?20.png:/filled?20.png @@ -1690,7 +1690,7 @@ - + :/filled?20.png:/filled?20.png @@ -1781,7 +1781,7 @@ - + :/filled?20.png:/filled?20.png @@ -1872,7 +1872,7 @@ - + :/filled?20.png:/filled?20.png @@ -1921,8 +1921,8 @@ - - + + From 28a692d0b30bb22d04c7b388c50308fc5298dff5 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:58:10 -0400 Subject: [PATCH 055/107] added a load_all call in the client --- commotion_client/commotion_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index b5ccaa9..e2e9dfe 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -24,6 +24,8 @@ from utils import logger from utils import thread from utils import single_application +from utils import extension_manager + from GUI import main_window from GUI import system_tray @@ -166,6 +168,9 @@ def start_full(self): """ Start or switch client over to full client. """ + extensions = extension_manager.ExtensionManager() + if not extensions.check_installed(): + extensions.load_all() if not self.main: try: self.main = self.create_main_window() @@ -272,7 +277,6 @@ def create_main_window(self): _main = main_window.MainWindow() except Exception as _excp: self.log.critical(self.translate("logs", "Could not create Main Window. Application must be halted.")) - self.log.exception(_excp) raise else: return _main From 53d3cffadec33693339e78c1cdf42516c5c00682 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:58:46 -0400 Subject: [PATCH 056/107] continued extensive overhaul of extension manager --- commotion_client/GUI/menu_bar.py | 14 +- commotion_client/utils/config.py | 30 +-- commotion_client/utils/extension_manager.py | 241 +++++++++++++------- commotion_client/utils/fs_utils.py | 20 +- commotion_client/utils/validate.py | 57 +++-- 5 files changed, 223 insertions(+), 139 deletions(-) diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index f48bf64..e5b6baf 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -40,7 +40,7 @@ def __init__(self, parent=None): try: self.populate_menu() except (NameError, AttributeError) as _excpt: - self.log.exception(_excpt) + self.log.info(self.translate("logs", "The Menu Bar could not populate the menu")) raise self.log.debug(QtCore.QCoreApplication.translate("logs", "Menu bar has initalized successfully.")) @@ -71,10 +71,14 @@ def populate_menu(self): if not self.layout.isEmpty(): self.clear_layout(self.layout) menu_items = {} - extensions = ExtensionManager.get_installed() - all_extensions = extensions['core'] + extensions['contrib'] + ext_mgr = ExtensionManager() + extensions = ext_mgr.get_installed().keys() + if not extensions: + ext_mgr.load_all() + extensions = ext_mgr.get_installed().keys() + if extensions: - top_level = self.get_parents(all_extensions) + top_level = self.get_parents(extensions) for top_level_item in top_level: try: current_item = self.add_menu_item(top_level_item) @@ -91,10 +95,8 @@ def populate_menu(self): #Add sub-menu layout self.layout.addWidget(section[1]) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) raise AttributeError(QtCore.QCoreApplication.translate("exception", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) else: - self.log.error(QtCore.QCoreApplication.translate("logs", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) raise NameError(QtCore.QCoreApplication.translate("exception", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) self.setLayout(self.layout) diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index 01f2760..d81f226 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -52,10 +52,10 @@ def get_config_paths(config_type): config_files = [] try: - path = configLocations[config_type] + path = os.path.join(QtCore.QDir.currentPath(), configLocations[config_type]) except KeyError as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "Cannot search for config type {0} as it is an unsupported type.".format(config_type))) - self.log.exception(_excp) + log.warn(QtCore.QCoreApplication.translate("logs", "Cannot search for config type {0} as it is an unsupported type.".format(config_type))) + log.exception(_excp) return False try: for root, dirs, files in fs_utils.walklevel(path): @@ -64,10 +64,10 @@ def get_config_paths(config_type): config_files.append(os.path.join(root, file_name)) except AssertionError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "Config file folder at path {0} does not exist. No Config files loaded.".format(path))) - self.log.exception(_excp) + log.exception(_excp) except TypeError as _excp: log.error(QtCore.QCoreApplication.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) - self.log.exception(_excp) + log.exception(_excp) if config_files: return config_files else: @@ -101,24 +101,24 @@ def load_config(config): try: f = open(config, mode='r', encoding="utf-8", errors="strict") except ValueError as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(config))) - self.log.exception(_excp) + log.warn(QtCore.QCoreApplication.translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(config))) + log.exception(_excp) return False - except Exception as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file exists and is not corrupted.".format(config))) - self.log.exception(_excp) + except TypeError as _excp: + log.warn(QtCore.QCoreApplication.translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file is the correct type.".format(config))) + log.exception(_excp) return False else: tmpMsg = f.read() #Parse the JSON try: data = json.loads(tmpMsg) - log.debug(QtCore.QCoreApplication.translate("logs", "Successfully loaded {0}".format(config))) + log.info(QtCore.QCoreApplication.translate("logs", "Successfully loaded {0}".format(config))) return data except ValueError as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(config))) - self.log.exception(_excp) + log.warn(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(config))) + log.exception(_excp) return False except Exception as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to an unknown error.".format(config))) - self.log.exception(_excp) + log.warn(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to an unknown error.".format(config))) + log.exception(_excp) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 08f0cc7..80120a0 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -16,6 +16,7 @@ import importlib import shutil import os +import re #PyQt imports from PyQt4 import QtCore @@ -38,6 +39,10 @@ def __init__(self): "parent", "settings", "toolbar"] + self.extension_dirs = { + "user" : os.path.join(QtCore.QDir.homePath(), ".commotion/extensions"), + "global" :os.path.join(QtCore.QDir.currentPath(), "extensions") + } def check_installed(self, name=None): """Checks if and extension is installed. @@ -57,39 +62,44 @@ def check_installed(self, name=None): return False def load_all(self): - """Attempts to load all config files saved in the data.extensions directory""" + """Loads all extensions in the user and global extension directories. + + This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions that should be loaded for a user and adds them to the settings. NOTE: Does not do any validation as it relies on save_settings to validate all fields. + + Returns: + List of names (strings) of extensions loaded on success. Returns False (bool) on failure. + """ installed = self.get_installed() - saved = config.get_config_paths("extension") - if not saved: - self.log.info(self.translate("logs", "No saved config files found.")) + exist = config.get_config_paths("extension") + if not exist: + self.log.info(self.translate("logs", "No extensions found.")) return False - for saved_path in saved: - _saved_config = config.load_config(saved_path) - _main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") - if _saved_config['name'] in installed.keys(): - _type = installed[_saved_config['name']] - _type_dir = os.path.join(main_ext_dir, _type) + for config_path in exist: + _config = config.load_config(config_path) + if _config['name'] in installed.keys(): + _type = installed[_config['name']] + if _type == "global": + _ext_dir = os.path.join(self.extension_dir['global'], _config['name']) + elif _type == "user": + _ext_dir = os.path.join(self.extension_dir['user'], _config['name']) + else: + self.log.warn(self.translate("logs", "Extension {0} is of an unknown type. It will not be loaded".format(_config['name']))) + continue else: - _core_dir = QtCore.QDir(os.path.join(_main_ext_dir, "core")) - if _core_dir.exists() and _core_dir.exists(_saved_config['name']): - _type = "core" - _type_dir = _core_dir + if QtCore.QDir(self.extension_dir['user']).exists(_config['name']): + _ext_dir = os.path.join(self.extension_dir['user'], _config['name']) + elif QtCore.QDir(self.extension_dir['global']).exists(_config['name']): + _ext_dir = os.path.join(self.extension_dir['global'], _config['name']) else: - _contrib_dir = QtCore.QDir(os.path.join(_main_ext_dir, "contrib")) - if _contrib_dir.exists() and _contrib_dir.exists(_saved_config['name']): - _type = "contrib" - _type_dir = _contrib_dir - else: - self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_saved_config['name']))) - break - _extension_dir = os.path.join(_type_dir, _saved_config['name']) - _extension = validate.ClientConfig(config, extension_dir) - if not _extension.validate_all(): - self.log.warn(self.translate("logs", "Extension {0}is invalid and cannot be saved.".format(_saved_config['name']))) + self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_config['name']))) + continue + if not self.save_settings(_config, _type): + self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) else: - if not self.save_extension(_saved_config['name'], _type): - self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_saved_config['name']))) + saved.append(_config['name']) + return saved or False + @staticmethod def get_installed(): """Get all installed extensions seperated by type. @@ -97,26 +107,20 @@ def get_installed(): Pulls the current installed extensions from the application settings and returns a dictionary with the lists of the two extension types. Returns: - A dictionary with two keys ['core' and 'contrib'] that both have lists of all extensions of that type as their values. + A dictionary keyed by the names of all extensions with the values being if they are a user extension or a global extension. - {'core':['coreExtensionOne', 'coreExtensionTwo'], - 'contrib':['contribExtension', 'anotherContrib']} + {'coreExtensionOne':"user", 'coreExtensionTwo':"global", + 'contribExtension':"global", 'anotherContrib':"global"} """ + WRITE_TESTS_FOR_ME() installed_extensions = {} _settings = QtCore.QSettings() _settings.beginGroup("extensions") - _settings.beginGroup("core") - core = _settings.allKeys() + extensions = _settings.allKeys() + for ext in extensions: + installed_extensions[ext] = _settings.value(ext+"/type").toString() _settings.endGroup() - _settings.beginGroup("contrib") - contrib = _settings.allKeys() - _settings.endGroup() - _settings.endGroup() - for ext in core: - installed_extensions[ext] = "core" - for ext in contrib: - installed_extensions[ext] = "contrib" return installed_extensions def get_extension_from_property(self, key, val): @@ -135,6 +139,8 @@ def get_extension_from_property(self, key, val): Raises: KeyError: If the value requested is non-standard. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() matching_extensions = [] if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") @@ -176,6 +182,8 @@ def get_property(self, name, key): Raises: KeyError: If the value requested is non-standard. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") raise KeyError(_error) @@ -203,6 +211,8 @@ def get_type(self, name): Raises: KeyError: If an extension does not exist. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() _settings = QtCore.QSettings() _settings.beginGroup("extensions") core_ext = _settings.value("core/"+str(name)) @@ -221,6 +231,8 @@ def load_user_interface(self, extension_name, subsection=None): @param extension_name string The extension to load @subsection string Name of a objects sub-section. (settings, main, or toolbar) """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} settings = self.load_settings(extension_name) if subsection: @@ -237,6 +249,8 @@ def import_extension(extension_name, subsection=None): @param extension_name string The extension to load @param subsection string The module to load from an extension """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() if subsection: extension = importlib.import_module("."+subsection, "extensions."+extension_name) else: @@ -248,6 +262,8 @@ def load_settings(self, extension_name): @return dict A dictionary containing an extensions properties. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() extension_config = {"name":extension_name} extension_type = self.extensions[extension_name] @@ -256,9 +272,9 @@ def load_settings(self, extension_name): _settings.beginGroup(extension_type) extension_config['main'] = _settings.value("main").toString() #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") + main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) + extension_dir = QtCore.QDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) extension_files = extension_dir.entryList() if not extension_config['main']: if "main.py" in extension_files: @@ -284,6 +300,8 @@ def remove_extension_settings(self, name): @param name str the name of an extension to remove from the extension settings. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() if len(str(name)) > 0: _settings = QtCore.QSettings() _settings.beginGroup("extensions") @@ -292,19 +310,30 @@ def remove_extension_settings(self, name): _error = self.translate("logs", "You must specify an extension name greater than 1 char.") raise ValueError(_error) - def save_settings(self, extension_config, extension_type="contrib"): - """Saves an extensions core properties into the applications extension settings - - @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. - @param extension_config dict Dictionary of key-pairs for json config. - @return bool True if successful and False on failures + def save_settings(self, extension_config, extension_type="global"): + """Saves an extensions core properties into the applications extension settings. + + long description + + Args: + extension_config (dict) An extension config in dictionary format. + extension_type (string): Type of extension "user" or "global". Defaults to global. + + Returns: + bool: True if successful, False on failures + + Raises: + exception: Description. + """ _settings = QtCore.QSettings() _settings.beginGroup("extensions") #get extension dir - main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") - main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) + try: + extension_dir = self.extension_dirs[extension_type] + except KeyError: + self.log.warn(self.translate("logs", "Invalid extension type. Please check the extension type and try again.")) + return False #create validator config_validator = validate.ClientConfig(extension_config, extension_dir) #Extension Name @@ -393,25 +422,42 @@ def save_settings(self, extension_config, extension_type="contrib"): _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) self.log.error(_error) return False - _settings.endGroup() + #Write extension type + _settings.value("type", extension_type) _settings.endGroup() return True - def save_extension(self, extension, extension_type="contrib"): - """Unpacks a extension and attempts to add it to the Commotion system. + def save_extension(self, extension, extension_type="contrib", unpack=None): + """Attempts to add an extension to the Commotion system. - @param extension - @param extension_type string Type of extension "contrib" or "core". Defaults to contrib. - @return bool True if an extension was saved, False if it could not save. + Args: + extension (string): The name of the extension + extension_type (string): Type of extension "contrib" or "core". Defaults to contrib. + unpack (string or QDir): Path to compressed extension + + Returns: + bool True if an extension was saved, False if it could not save. + + Raises: + exception: Description. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() #There can be only two... and I don't trust you. if extension_type != "contrib": extension_type = "core" - try: - unpacked = self.unpack_extension(self, extension, extension_type) - except IOError: - self.log.error(self.translate("logs", "Failed to unpack extension.")) - return False + if unpack: + try: + unpacked = QtCore.QDir(self.unpack_extension(unpack)) + except IOError: + self.log.error(self.translate("logs", "Failed to unpack extension.")) + return False + else: + self.log.info(self.translate("logs", "Saving non-compressed extension.")) + main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") + main_ext_type_dir = os.path.join(main_ext_dir, extension_type) + unpacked = QtCore.QDir(os.path.join(main_ext_type_dir, extension)) + unpacked.mkpath(unpacked.absolutePath()) config_validator = validate.ClientConfig() try: config_validator.set_extension(unpacked.absolutePath()) @@ -425,7 +471,7 @@ def save_extension(self, extension, extension_type="contrib"): files = unpacked.entryList() for file_ in files: if re.match("^.*\.conf$", file_): - config_name = _file + config_name = file_ config_path = os.path.join(unpacked.absolutePath(), file_) _config = config.load_config(config_path) existing_extensions = config.find_configs("extension") @@ -433,48 +479,59 @@ def save_extension(self, extension, extension_type="contrib"): assert _config['name'] not in existing_extensions except AssertionError: self.log.error(self.translate("logs", "The name given to this extension is already in use. Each extension must have a unique name.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) + fs_utils.clean_dir(unpacked) return False else: self.log.error(self.translate("logs", "The extension name is invalid and cannot be saved.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) + fs_utils.clean_dir(unpacked) return False #Check all values if not config_validator.validate_all(): self.log.error(self.translate("logs", "The extension's config contains the following invalid value/s: [{0}]".format(",".join(config_validator.errors)))) - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) + fs_utils.clean_dir(unpacked) return False #make new directory in extensions - main_ext_dir = os.path.join(QtCore.QtDir.currentPath(), "extensions") + main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QtDir.mkpath(os.path.join(main_ext_type_dir, _config['name'])) - try: - fs_utils.copy_contents(unpacked, extension_dir) - except IOError: - self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again.")) - self.log.info(self.translate("logs", "Cleaning extension's temp and main directory.")) - fs_utils.clean_dir(extension_dir) - fs_utils.clean_dir(unpacked) - return False - else: - fs_utils.clean_dir(unpacked) + extension_dir = QtCore.QDir(os.path.join(main_ext_type_dir, _config['name'])) + extension_dir.mkdir(extension_dir.path()) + if unpack: + try: + fs_utils.copy_contents(unpacked, extension_dir) + except IOError: + self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again.")) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension's temp and main directory.")) + fs_utils.clean_dir(extension_dir) + fs_utils.clean_dir(unpacked) + return False + else: + if unpack: + fs_utils.clean_dir(unpacked) try: self.save_settings(_config, extension_type) except KeyError: self.log.error(self.translate("logs", "Could not save the extension because it was missing manditory values. Please check the config and try again.")) - self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) - fs_utils.clean_dir(extension_dir) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension directory.")) + fs_utils.clean_dir(extension_dir) + self.log.info(self.translate("logs", "Cleaning settings.")) self.remove_extension_settings(_config['name']) return False try: self.add_config(unpacked.absolutePath(), config_name) except IOError: self.log.error(self.translate("logs", "Could not add the config to the core config directory.")) - self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) - fs_utils.clean_dir(extension_dir) + if unpack: + self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) + fs_utils.clean_dir(extension_dir) + self.log.info(self.translate("logs", "Cleaning settings.")) self.remove_extension_settings(_config['name']) return False return True @@ -492,6 +549,8 @@ def add_config(self, extension_dir, name): Raises: IOError: If a config file of the same name already exists or the extension can not be saved. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") #If the data/extensions folder does not exist, make it. @@ -518,6 +577,8 @@ def remove_config(self, name): Raises: IOError: If a config file does not exist in the extension data folder. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") config = os.path.join(config_dir, name) @@ -534,19 +595,21 @@ def unpack_extension(self, compressed_extension): """Unpacks an extension into a temporary directory and returns the location. @param compressed_extension string Path to the compressed_extension - @return QDir A QDir object containing the path to the temporary directory + @return A string object containing the absolute path to the temporary directory """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() temp_dir = fs_utils.make_temp_dir(new=True) temp_abs_path = temp_dir.absolutePath() try: shutil.unpack_archive(compressed_extension, temp_abs_path, "gztar") - except ReadError: + except FileNotFoundError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was corrupted or mis-packaged.") raise IOError(_error) except FileNotFoundError: _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was not found.") raise IOError(_error) - return temp_dir + return temp_dir.absolutePath() def save_unpacked_extension(self, temp_dir, extension_name, extension_type): """Moves an extension from a temporary directory to the extension directory. @@ -562,6 +625,8 @@ def save_unpacked_extension(self, temp_dir, extension_name, extension_type): Raises: ValueError: If an extension with that name already exists. """ + WRITE_TESTS_FOR_ME() + FIX_ME_FOR_NEW_EXTENSION_TYPES() extension_path = "extensions/"+extension_type+"/"+extension_name full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) if not fs_utils.is_file(full_path): diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py index 6925fe4..4ca1b1b 100644 --- a/commotion_client/utils/fs_utils.py +++ b/commotion_client/utils/fs_utils.py @@ -17,10 +17,8 @@ def is_file(unknown): """ - Determines if a file is accessable. It does NOT check to see if the file contains any data. - """ - #stolen from https://github.com/isislovecruft/python-gnupg/blob/master/gnupg/_util.py - #set function logger + Determines if a file is accessable. It does NOT check to see if the file contains any data. + """ log = logging.getLogger("commotion_client."+__name__) try: assert os.lstat(unknown).st_size > 0, "not a file: %s" % unknown @@ -50,19 +48,19 @@ def make_temp_dir(new=None): @param new bool Create a new uniquely named directory within the exiting Commotion temp directory and return the new folder object """ log = logging.getLogger("commotion_client."+__name__) - temp_path = "/Commotion/" + temp_path = "Commotion" + temp_dir = QtCore.QDir.tempPath() if new: unique_dir_name = uuid.uuid4() - temp_path += str(unique_dir_name) -# temp_dir = QtCore.QDir(QtCore.QDir.tempPath() + temp_path) - temp_dir = QtCore.QDir(os.path.join(QtCore.QDir.tempPath(), temp_path)) - if QtCore.QDir().mkpath(temp_dir.path()): + temp_path = os.path.join(temp_path, str(unique_dir_name)) + temp_full = QtCore.QDir(os.path.join(temp_dir, temp_path)) + if temp_full.mkpath(temp_full.path()): log.debug(QtCore.QCoreApplication.translate("logs", "Creating main temporary directory")) else: _error = QtCore.QCoreApplication.translate("logs", "Error creating temporary directory") - log.error(_error) + log.debug(_error) raise IOError(_error) - return temp_dir + return temp_full def clean_dir(path=None): diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 72c6249..761022a 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -22,31 +22,43 @@ #Commotion Client Imports from utils import fs_utils +from utils import config class ClientConfig(object): - def __init__(self, config=None, directory=None): - if config: - self._config = config.load_config(config) + def __init__(self, ext_config=None, directory=None): + """ + Args: + ext_config (string): Absolute Path to the config file. + directory (string): Absolute Path to the directory containing the extension. + """ + if ext_config: + if fs_utils.is_file(ext_config): + self._config = config.load_config(ext_config) + else: + raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) self._directory = QtCore.QDir(directory) self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate self.errors = None - - def set_extension(self, directory, config=None): + + def set_extension(self, directory, ext_config=None): """Set the default values @param config string The absolute path to a config file for this extension @param directory string The absolute path to this extensions directory """ self._directory = QtCore.QDir(directory) - if config: - self._config = config.load_config(config) + if ext_config: + if fs_utils.is_file(ext_config): + self._config = config.load_config(ext_config) + else: + raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) else: files = self._directory.entryList() for file_ in files: if re.match("^.*\.conf$", file_): - default_config_path = os.path.join(self._directory, file_) + default_config_path = os.path.join(self._directory.path(), file_) try: assert default_config_path except NameError: @@ -71,17 +83,23 @@ def validate_all(self): errors = [] if not self.name(): errors.append("name") + self.log.info(self.translate("logs", "The name of extension {0} is invalid.".format(self._config['name']))) if not self.tests(): errors.append("tests") + self.log.info(self.translate("logs", "The extensions {0} tests is invalid.".format(self._config['name']))) if not self.menu_level(): errors.append("menu_level") + self.log.info(self.translate("logs", "The extensions {0} menu_level is invalid.".format(self._config['name']))) if not self.menu_item(): errors.append("menu_item") + self.log.info(self.translate("logs", "The extensions {0} menu_item is invalid.".format(self._config['name']))) if not self.parent(): - errors.append("parent") + errors.append("parent") + self.log.info(self.translate("logs", "The extensions {0} parent is invalid.".format(self._config['name']))) else: for gui_name in ['main', 'settings', 'toolbar']: if not self.gui(gui_name): + self.log.info(self.translate("logs", "The extensions {0} {1} is invalid.".format(self._config['name'], gui_name))) errors.append(gui_name) if errors: self.errors = errors @@ -181,8 +199,7 @@ def tests(self): self.log.warn(self.translate("logs", "The extensions 'tests' file name is invalid for this system.")) return False if not self.check_exists(file_name): - self.log.warn(self.translate("logs", "The extensions 'tests' file does not exist.")) - return False + self.log.info(self.translate("logs", "The extensions 'tests' file does not exist. But tests are not required. Shame on you though, SHAME!.")) return True def check_menu_text(self, menu_text): @@ -191,9 +208,11 @@ def check_menu_text(self, menu_text): @param menu_text string The text that will appear in the menu. """ - if not 3 < len(str(menu_text)) > 40: + if not 3 < len(str(menu_text)) < 40: self.log.warn(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) return False + else: + return True def check_exists(self, file_name): """Checks if a specified file exists within a directory @@ -202,7 +221,7 @@ def check_exists(self, file_name): """ files = QtCore.QDir(self._directory).entryList() if not str(file_name) in files: - self.log.warn(self.translate("logs", "The specified file does not exist.")) + self.log.warn(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) return False else: return True @@ -254,19 +273,19 @@ def check_path_length(self, file_name=None): # Win(name+path<=260), path_limit = ['win32', 'cygwin'] if platform in path_limit: - if self.name(): #check valid name before using it - extension_path = os.path.join(QtCore.QDir.currentPath(), "extensions") - full_path = os.path.join(extension_path, file_name) - else: - self.log.warn(self.translate("logs", "The extension's config file 'main' value requires a valid 'name' value. Which this extension does not have.")) - return False + extension_path = os.path.join(QtCore.QDir.currentPath(), "extensions") + full_path = os.path.join(extension_path, file_name) if len(str(full_path)) > 255: self.log.warn(self.translate("logs", "The full extension path cannot be greater than 260 chars")) return False + else: + return True elif platform in name_limit: if len(str(file_name)) >= 260: self.log.warn(self.translate("logs", "File names can not be greater than 260 chars on your system")) return False + else: + return True else: self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) return True From 8f1e6d6f41d553e1e776a62d39bea000acc5f625 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 11:59:07 -0400 Subject: [PATCH 057/107] added tests for extension manager load_all fucntion --- tests/utils/extension_manager_tests.py | 94 ++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/utils/extension_manager_tests.py diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py new file mode 100644 index 0000000..e1b2e9d --- /dev/null +++ b/tests/utils/extension_manager_tests.py @@ -0,0 +1,94 @@ +from PyQt4 import QtCore +from PyQt4 import QtGui + + +import unittest +import re + +from commotion_client.utils import extension_manager + +class ExtensionSettingsTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication([]) + self.ext_mgr = extension_manager.ExtensionManager() + self.settings = QtCore.QSettings("test_case", "testing_app") + + def tearDown(self): + self.app = None + self.ext_mgr = None + self.settings.clear() + +class CoreExtensionMgrTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication([]) + self.ext_mgr = extension_manager.ExtensionManager() + + def tearDown(self): + self.app = None + self.ext_mgr = None + +class LoadSettings(ExtensionSettingsTestCase): + """ + Functions Covered: + load_all + """ + + def test_load_all_settings(self): + """Test that settings are saved upon running load_all.""" + #Test that there are no extensions loaded + count = self.settings.allKeys() + assertIs(type(count), int) + assertEquals(count, 0) + #Load extensions + loaded = self.ext_mgr.load_all() + #load_all returns extensions that are loaded + assertIsNot(loaded, False) + #Check that some extensions were added to settings + post_count = self.settings.allKeys() + assertIs(type(count), int) + assertGreater(count, 0) + + def test_load_all_core_ext(self): + """Test that all core extension directories are saved upon running load_all.""" + #get all extensions currently loaded + global_ext = QtCore.QDir(self.ext_mgr.extension_dir['global']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) + loaded = self.ext_mgr.load_all() + self.settings.beginGroup("extensions") + k = self.settings.AllKeys() + for ext in global_ext: + assertTrue(k.contains(ext), "Core extension {0} should have been loaded, but was not.".format(ext)) + + def test_load_all_user_ext(self): + """Test that all user extension directories are saved upon running load_all.""" + #get user extensions + user_ext = QtCore.QDir(ext_mgr.extension_dir['user']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) + #If no user extensions exist (which they should not) set the current user directory to the model extension directory. + if not user_ext.count() > 0: + main_dir = dirname(os.path.abspath(QtCore.QDir.currentPath())) + model_directory = os.path.join(main_dir, "tests/models/extensions") + ext_mgr.extension_dirs['user'] = model_directory + #refresh user_ext list + user_ext = QtCore.QDir(ext_mgr.extension_dir['user']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) + loaded = self.ext_mgr.load_all() + self.settings.beginGroup("extensions") + k = self.settings.AllKeys() + for ext in user_ext: + assertTrue(k.contains(ext), "Extension {0} should have been loaded, but was not.".format(ext)) + +class InitTestCase(CoreExtensionMgrTestCase): + + def test_init(self): + #TODO: TEST THESE + fail("test not implemented") + ext_mgr.log = False + ext_mgr.extensions = False + ext_mgr.translate = False + ext_mgr.config_values = False + ext_mgr.extension_dir = False + +class FileSystemManagement(CoreExtensionMgrTestCase): + + + From 4f1062862682fa9b915609856bcf9db947cb75dc Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 12:27:55 -0400 Subject: [PATCH 058/107] making bundling exe's cleaner --- .gitignore | 3 +++ setup.py | 27 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 24eb5ee..60fc156 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ Ui_*.py #Extension Application Data commotion_client/data/extensions/* + +#compiled clients +build/exe* diff --git a/setup.py b/setup.py index c8a8de2..b79ab4f 100644 --- a/setup.py +++ b/setup.py @@ -1,25 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os +import sys +from cx_Freeze import setup, Executable -from cx_freeze import setup, Executable -import py2exe - -base = "commotion_client" icon = "assets/images/logo32.png" + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + + #define core packages core_pkgs = ["utils", "GUI", "assets"] core_extensions = ["config_editor"] +packages = [] +assets_file = os.path.join("commotion_client", "assets", "commotion_assets_rc.py") #add core_extensions to core packages for ext in core_extensions: core_pkgs.append("extensions."+ext) +for pkg in core_pkgs: + packages.append("commotion_client."+pkg) exe = Executable( - script="commotion_client.py", - packages=core_pkgs + script=os.path.join("commotion_client", "commotion_client.py"), + packages=core_pkgs, ) setup(name="Commotion Client", @@ -27,5 +38,5 @@ url="commotionwireless.net", license="Affero General Public License V3 (AGPLv3)", executables = [exe], - include_files=[os.path.join("assets", "commotion_assets_rc.py")] - ) + options = {"build_exe":{ "include_files":[] }} + ) From 50c94cadac5b419bd5b2f44520cae4e82e1ac202 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 16:34:37 -0400 Subject: [PATCH 059/107] modified dependencies to allow bundling --- .gitignore | 1 + commotion_client/GUI/__init__.py | 0 commotion_client/GUI/crash_report.py | 2 +- commotion_client/GUI/main_window.py | 12 ++++++------ commotion_client/GUI/menu_bar.py | 4 ++-- commotion_client/GUI/system_tray.py | 2 +- commotion_client/GUI/ui/__init__.py | 0 commotion_client/GUI/welcome_page.py | 2 +- commotion_client/commotion_client.py | 12 ++++++------ commotion_client/utils/config.py | 2 +- commotion_client/utils/extension_manager.py | 8 ++++---- commotion_client/utils/validate.py | 4 ++-- setup.py | 14 +++++++------- 13 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 commotion_client/GUI/__init__.py create mode 100644 commotion_client/GUI/ui/__init__.py diff --git a/.gitignore b/.gitignore index 60fc156..55b0920 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ commotion_client/data/extensions/* #compiled clients build/exe* +build/lib diff --git a/commotion_client/GUI/__init__.py b/commotion_client/GUI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/GUI/crash_report.py b/commotion_client/GUI/crash_report.py index 0579c78..698eae0 100644 --- a/commotion_client/GUI/crash_report.py +++ b/commotion_client/GUI/crash_report.py @@ -23,7 +23,7 @@ from PyQt4 import QtCore from PyQt4 import QtGui -from GUI.ui import Ui_crash_report_window +from commotion_client.GUI.ui import Ui_crash_report_window class CrashReport(Ui_crash_report_window.crash_window): diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index 4ca8591..2ed9f5f 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -19,12 +19,12 @@ from PyQt4 import QtGui #Commotion Client Imports -from assets import commotion_assets_rc -from GUI.menu_bar import MenuBar -from GUI.crash_report import CrashReport -from GUI import welcome_page -from utils import config -from utils import extension_manager +from commotion_client.assets import commotion_assets_rc +from commotion_client.GUI.menu_bar import MenuBar +from commotion_client.GUI.crash_report import CrashReport +from commotion_client.GUI import welcome_page +from commotion_client.utils import config +from commotion_client.utils import extension_manager class MainWindow(QtGui.QMainWindow): """ diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index e5b6baf..ad9dbdc 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -21,8 +21,8 @@ from PyQt4 import QtGui #Commotion Client Imports -from utils import config -from utils.extension_manager import ExtensionManager +from commotion_client.utils import config +from commotion_client.utils.extension_manager import ExtensionManager class MenuBar(QtGui.QWidget): diff --git a/commotion_client/GUI/system_tray.py b/commotion_client/GUI/system_tray.py index 9d06028..ff12bcf 100644 --- a/commotion_client/GUI/system_tray.py +++ b/commotion_client/GUI/system_tray.py @@ -6,7 +6,7 @@ from PyQt4 import QtGui #Commotion Client Imports -from assets import commotion_assets_rc +from commotion_client.assets import commotion_assets_rc class TrayIcon(QtGui.QWidget): """ diff --git a/commotion_client/GUI/ui/__init__.py b/commotion_client/GUI/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/GUI/welcome_page.py b/commotion_client/GUI/welcome_page.py index 315f6f3..860ec6d 100644 --- a/commotion_client/GUI/welcome_page.py +++ b/commotion_client/GUI/welcome_page.py @@ -19,7 +19,7 @@ from PyQt4 import QtCore from PyQt4 import QtGui -from GUI.ui import Ui_welcome_page +from commotion_client.GUI.ui import Ui_welcome_page class ViewPort(Ui_welcome_page.ViewPort): """ diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index e2e9dfe..e95fdc0 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -21,13 +21,13 @@ from PyQt4 import QtGui from PyQt4 import QtCore -from utils import logger -from utils import thread -from utils import single_application -from utils import extension_manager +from commotion_client.utils import logger +from commotion_client.utils import thread +from commotion_client.utils import single_application +from commotion_client.utils import extension_manager -from GUI import main_window -from GUI import system_tray +from commotion_client.GUI import main_window +from commotion_client.GUI import system_tray #from controller import CommotionController #TODO Create Controller diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py index d81f226..a7afb03 100644 --- a/commotion_client/utils/config.py +++ b/commotion_client/utils/config.py @@ -14,7 +14,7 @@ from PyQt4 import QtCore -from utils import fs_utils +from commotion_client.utils import fs_utils #set function logger log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 80120a0..4c60d92 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -22,10 +22,10 @@ from PyQt4 import QtCore #Commotion Client Imports -from utils import config -from utils import fs_utils -from utils import validate -import extensions +from commotion_client.utils import config +from commotion_client.utils import fs_utils +from commotion_client.utils import validate +from commotion_client import extensions class ExtensionManager(object): def __init__(self): diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 761022a..640f409 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -21,8 +21,8 @@ from PyQt4 import QtCore #Commotion Client Imports -from utils import fs_utils -from utils import config +from commotion_client.utils import fs_utils +from commotion_client.utils import config class ClientConfig(object): diff --git a/setup.py b/setup.py index b79ab4f..c9f31ae 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,8 @@ if sys.platform == "win32": base = "Win32GUI" - #define core packages -core_pkgs = ["utils", "GUI", "assets"] +core_pkgs = ["commotion_client", "utils", "GUI", "assets"] core_extensions = ["config_editor"] packages = [] assets_file = os.path.join("commotion_client", "assets", "commotion_assets_rc.py") @@ -25,18 +24,19 @@ for ext in core_extensions: core_pkgs.append("extensions."+ext) -for pkg in core_pkgs: - packages.append("commotion_client."+pkg) - exe = Executable( - script=os.path.join("commotion_client", "commotion_client.py"), + targetName="Commotion", + script="commotion_client/commotion_client.py", packages=core_pkgs, ) setup(name="Commotion Client", version="1.0", +# packages=["commotion_client"], +# include_dirs=['commotion_client'], +# ext_modules=[Extension("Commotion Client", ['commotion_client'])], url="commotionwireless.net", license="Affero General Public License V3 (AGPLv3)", executables = [exe], - options = {"build_exe":{ "include_files":[] }} + options = {"build_exe":{"include_files": [(assets_file, "commotion_assets_rc.py")]}} ) From 6649cf9f5aae2663fb6a56c8c6861d1666502c41 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 17:14:14 -0400 Subject: [PATCH 060/107] cleaned up code. No functional changes --- setup.py | 60 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index c9f31ae..d5d09a9 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" + +This program is a part of The Commotion Client + +Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +""" +""" +setup.py + +This module includes the cx_freeze functionality for building the bundled extensions. + +You can find further documentation on this in the build/ directory under README.md. +""" import os import sys +#import the setup.py version of setup from cx_Freeze import setup, Executable - -icon = "assets/images/logo32.png" - +#---------- OS Setup -----------# # GUI applications require a different base on Windows (the default is for a # console application). @@ -14,29 +40,39 @@ if sys.platform == "win32": base = "Win32GUI" -#define core packages +# Windows requires the icon to be specified in the setup.py. +icon = "commotion_client/assets/images/logo32.png" + +#---------- Packages -----------# + +# Define core packages. core_pkgs = ["commotion_client", "utils", "GUI", "assets"] +# Define bundled "core" extensions here. core_extensions = ["config_editor"] -packages = [] -assets_file = os.path.join("commotion_client", "assets", "commotion_assets_rc.py") - -#add core_extensions to core packages +# Add core_extensions to core packages. for ext in core_extensions: core_pkgs.append("extensions."+ext) + +# Include compiled assets file. +assets_file = os.path.join("commotion_client", "assets", "commotion_assets_rc.py") +# Place compiled assets file into the root directory. +include_assets = (assets_file, "commotion_assets_rc.py") + +#---------- Executable Setup -----------# + exe = Executable( targetName="Commotion", script="commotion_client/commotion_client.py", packages=core_pkgs, ) +#---------- Core Setup -----------# + setup(name="Commotion Client", version="1.0", -# packages=["commotion_client"], -# include_dirs=['commotion_client'], -# ext_modules=[Extension("Commotion Client", ['commotion_client'])], url="commotionwireless.net", license="Affero General Public License V3 (AGPLv3)", executables = [exe], - options = {"build_exe":{"include_files": [(assets_file, "commotion_assets_rc.py")]}} + options = {"build_exe":{"include_files": [include_assets]}} ) From c4f1bb7bc4521d5185580f0bb97676e0a01de68a Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 27 Mar 2014 17:14:37 -0400 Subject: [PATCH 061/107] Made logging cleaner, and added a debug message. --- commotion_client/commotion_client.py | 29 ++++++++---- commotion_client/utils/logger.py | 66 +++++++++++++++------------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index e95fdc0..777b37c 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -53,8 +53,9 @@ def get_args(): parsed_args['message'] = args.message if args.message else False #TODO getConfig() #actually want to get this from commotion_config parsed_args['logLevel'] = args.verbose if args.verbose else 2 - #TODO change the logfile to be grabbed from the commotion config reader - parsed_args['logFile'] = args.logfile if args.logfile else "temp/logfile.temp" + #TODO change the logfile to be the default logging place for the system + default_logfile = os.path.join(args.logfile QtCore.QDir.currentPath(), "logfile") + parsed_args['logFile'] = args.logfile if args.logfile else default_logfile parsed_args['key'] = ['key'] if args.key else "commotionRocks" #TODO the key is PRIME easter-egg fodder parsed_args['status'] = "daemon" if args.daemon else False return parsed_args @@ -73,7 +74,7 @@ def main(): log = logger.set_logging("commotion_client", args['logLevel'], args['logFile']) #Create Instance of Commotion Application - app = CommotionClientApplication(args['key'], args['status'], sys.argv) + app = CommotionClientApplication(args, sys.argv) #Enable Translations #TODO This code needs to be evaluated to ensure that it is pulling in correct translators locale = QtCore.QLocale.system().name() @@ -90,14 +91,14 @@ def main(): #Checking for custom message msg = args['message'] app.send_message(msg) - log.info(app.translate("logs", "application is already running, sent following message: \n\"{0}\"".format(msg))) + app.log.info(app.translate("logs", "application is already running, sent following message: \n\"{0}\"".format(msg))) else: - log.info(app.translate("logs", "application is already running. Application will be brought to foreground")) + app.log.info(app.translate("logs", "application is already running. Application will be brought to foreground")) app.send_message("showMain") app.end("Only one instance of a commotion application may be running at any time.") sys.exit(app.exec_()) - log.debug(app.translate("logs", "Shutting down")) + app.log.debug(app.translate("logs", "Shutting down")) class HoldStateDuringRestart(thread.GenericThread): """ @@ -107,6 +108,7 @@ class HoldStateDuringRestart(thread.GenericThread): def __init__(self): super().__init__() self.restart_complete = None + self.log = logging.getLogger("commotion_client."+__name__) def end(self): self.restart_complete = True @@ -127,8 +129,12 @@ class CommotionClientApplication(single_application.SingleApplicationWithMessagi restarted = QtCore.pyqtSignal() - def __init__(self, key, status, argv): - super().__init__(key, argv) + def __init__(self, args, argv): + super().__init__(args['key'], argv) + status = args['status'] + self.loglevel = args['logLevel'] + self.logfile = args['logFile'] + self.log = self.init_logging() #Set Application and Organization Information self.setOrganizationName("The Open Technology Institute") self.setOrganizationDomain("commotionwireless.net") @@ -164,6 +170,10 @@ def init_client(self): self.log.exception(_excp) self.end(_catch_all) + def init_logging(self): + log = logger.set_logging("commotion_client", self.loglevel, self.logfile) + return log + def start_full(self): """ Start or switch client over to full client. @@ -497,6 +507,9 @@ def process_message(self, message): elif message == "restart": self.log.info(self.translate("logs", "Received a message to restart. Restarting Now.")) self.restart_client(force_close=True) #TODO, might not want strict here post-development + elif message == "debug": + self.loglevel = 5 + self.log = self.init_logging() else: self.log.info(self.translate("logs", "message \"{0}\" not a supported type.".format(message))) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index 70fff80..97a4e83 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -1,53 +1,65 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# -# Copyright (C) 2014 Seamus Tuohy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . + +""" + +This program is a part of The Commotion Client + +Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +""" """ Main logging controls for Commotion-Client + +Example Use: + + from commotion-client.utils import logger + +#This logger should be the packages __name__ to use inheretance from the main commotion package. This way the code in an indivdual extension will be small and it will use the logging settings that were defined in the main logging function. + + log = logger.getLogger("commotion_client"+__name__) + +Example Main Client Use: + log = logger.set_logging("commotion_client", 2, "/os/specific/logfile/loc") + """ #TODO create seperate levels for the stream, the file, and the full logger import logging -#TODO Create a config parser and uncomment the following line to use it -#from . import config def set_logging(name, verbosity=None, logfile=None): """ Creates a logger object """ - logger = logging.getLogger(name) + logger = logging.getLogger(name) formatter = logging.Formatter('%(asctime)s - %(name)s - %(processName)s:%(lineno)d - %(levelname)s - %(message)s') if logfile: fh = logging.FileHandler(logfile) - else: - #TODO Create a config parser and uncomment the following line to use it - #default_logfile = config.get("logfile") - fh = logging.FileHandler(default_logfile) fh.setFormatter(formatter) stream = logging.StreamHandler() stream.setFormatter(formatter) + #set alternate verbosity if verbosity == None: stream.setLevel(logging.ERROR) fh.setLevel(logging.WARN) elif 1 <= verbosity <= 5: levels = [logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] - print(verbosity) stream.setLevel(levels[(verbosity-1)]) fh.setLevel(levels[(verbosity-1)]) logger.setLevel(levels[(verbosity-1)]) @@ -57,11 +69,3 @@ def set_logging(name, verbosity=None, logfile=None): logger.addHandler(fh) logger.addHandler(stream) return logger - - -# [TO CALL LOGGER] -# -# from commotion-client.utils import logger -# #This logger should be the packages __name__ to use inheretance from the main commotion package. This way the code in an indivdual extension will be small and it will use the logging settings that were defined in the main logging function. -# log = logger.getLogger(__name__) -# #The main function calls log = logger.set_logging("commotion_client", 5, "/os/specific/logfile/loc") From b081a7a041635f5670ca671e1f05f71049acfe03 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 11:51:24 -0400 Subject: [PATCH 062/107] Cleaned up logging Default log locations are now based upon the current operating system defaults. The core logger is also now handled through a class called LogHandler that allows for modifications to the log levels and logfile locations throughout the applications life. This will allow for debugging to be turned on when non-critical issues appear without having to close the application and possibly lose the error state. --- commotion_client/commotion_client.py | 19 ++- commotion_client/utils/logger.py | 176 ++++++++++++++++++++------- 2 files changed, 141 insertions(+), 54 deletions(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index 777b37c..599e4d5 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -69,9 +69,6 @@ def main(): Function that handles command line arguments, translation, and creates the main application. """ args = get_args() - - #Enable Logging - log = logger.set_logging("commotion_client", args['logLevel'], args['logFile']) #Create Instance of Commotion Application app = CommotionClientApplication(args, sys.argv) @@ -132,9 +129,7 @@ class CommotionClientApplication(single_application.SingleApplicationWithMessagi def __init__(self, args, argv): super().__init__(args['key'], argv) status = args['status'] - self.loglevel = args['logLevel'] - self.logfile = args['logFile'] - self.log = self.init_logging() + self.init_logging(args['logLevel'], args['logFile']) #Set Application and Organization Information self.setOrganizationName("The Open Technology Institute") self.setOrganizationDomain("commotionwireless.net") @@ -170,10 +165,11 @@ def init_client(self): self.log.exception(_excp) self.end(_catch_all) - def init_logging(self): - log = logger.set_logging("commotion_client", self.loglevel, self.logfile) - return log - + def init_logging(self, level, logfile): + args['logLevel'], args['logFile'] + self.logger = logger.LogHandler("commotion_client", self.loglevel, self.logfile) + self.log = logger.get_logger() + def start_full(self): """ Start or switch client over to full client. @@ -508,8 +504,7 @@ def process_message(self, message): self.log.info(self.translate("logs", "Received a message to restart. Restarting Now.")) self.restart_client(force_close=True) #TODO, might not want strict here post-development elif message == "debug": - self.loglevel = 5 - self.log = self.init_logging() + self.logger.set_verbosity("DEBUG") else: self.log.info(self.translate("logs", "message \"{0}\" not a supported type.".format(message))) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index 97a4e83..3039478 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -22,50 +22,142 @@ """ -""" -Main logging controls for Commotion-Client - -Example Use: - - from commotion-client.utils import logger - -#This logger should be the packages __name__ to use inheretance from the main commotion package. This way the code in an indivdual extension will be small and it will use the logging settings that were defined in the main logging function. - - log = logger.getLogger("commotion_client"+__name__) - -Example Main Client Use: - log = logger.set_logging("commotion_client", 2, "/os/specific/logfile/loc") - -""" - #TODO create seperate levels for the stream, the file, and the full logger - +from PyQt4 import QtCore import logging +import os -def set_logging(name, verbosity=None, logfile=None): - """ - Creates a logger object +class LogHandler(object): """ - logger = logging.getLogger(name) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(processName)s:%(lineno)d - %(levelname)s - %(message)s') - if logfile: - fh = logging.FileHandler(logfile) - fh.setFormatter(formatter) - stream = logging.StreamHandler() - stream.setFormatter(formatter) + Main logging controls for Commotion-Client. + + This application is ONLY to be called by the main application. This logger sets up the main namespace for all other logging to take place within. All other loggers should be the core string "commotion_client" and the packages __name__ to use inheretance from the main commotion package. This way the code in an indivdual extension will be small and will inheret the logging settings that were defined in the main application. + + Example Use for ALL other modules and packages: - #set alternate verbosity - if verbosity == None: - stream.setLevel(logging.ERROR) - fh.setLevel(logging.WARN) - elif 1 <= verbosity <= 5: - levels = [logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] - stream.setLevel(levels[(verbosity-1)]) - fh.setLevel(levels[(verbosity-1)]) - logger.setLevel(levels[(verbosity-1)]) - else: - raise TypeError("""The Logging level you have defined is not supported please enter a number between 1 and 5""") - #Add handlers to logger - logger.addHandler(fh) - logger.addHandler(stream) - return logger + from commotion-client.utils import logger + log = logger.getLogger("commotion_client"+__name__) + + """ + + + def __init__(self, name, verbosity=None, logfile=None): + self.logger = logger.getLogger(str(name)) + self.stream = None + self.file_handler = None + self.logfile = None + self.formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(lineno)d : %(message)s') + self.set_logfile(logfile) + self.set_verbosity(verbosity) + self.levels = {"CRITICAL":logging.CRITICAL, "ERROR":logging.ERROR, "WARN":logging.WARN, "INFO":logging.INFO, "DEBUG":logging.DEBUG} + + def set_logfile(self, logfile=None): + """Set the file to log to. + + Args: + logfile (string): The absolute path to the file to log to. + optional: defaults to the default system logfile path. + """ + if logfile: + log_dir = QtCore.QDir(os.path.dirname(logfile)) + if not log_dir.exists(): + if log_dir.mkpath(log_dir.absolutePath()): + self.logfile = logfile + platform = sys.platform + if platform == 'darwin': + #Try /Library/Logs first + log_dir = QtCore.QDir(os.path.join(QtCore.QDir.homePath(), "Library", "Logs")) + #if it does not exist try and create it + if not log_dir.exists(): + if log_dir.mkpath(log_dir.absolutePath()): + self.logfile = log_dir.filePath("commotion.log") + else: + #If fail then just write logs in app path + self.logfile = QtCore.QDir.current().filePath("commotion.log") + else: + + self.logfile = log_dir.filePath("commotion.log") + elif platform in ['win32', 'cygwin']: + #Try ../AppData/Local/Commotion first + log_dir = QtCore.QDir(os.path.join(os.getenv('APPDATA'), "Local", "Commotion")) + #if it does not exist try and create it + if not log_dir.exists(): + if log_dir.mkpath(log_dir.absolutePath()): + self.logfile = log_dir.filePath("commotion.log") + else: + #If fail then just write logs in app path + self.logfile = QtCore.QDir.current().filePath("commotion.log") + else: + self.logfile = log_dir.filePath("commotion.log") + elif platform == 'linux': + #Try /var/logs/ + log_dir = QtCore.QDir("/var/logs/") + if not log_dir.exists(): #Seriously! What kind of twisted linux system is this? + if log_dir.mkpath(log_dir.absolutePath()): + self.logfile = log_dir.filePath("commotion.log") + else: + #If fail then just write logs in app path + self.logfile = QtCore.QDir.current().filePath("commotion.log") + else: + self.logfile = log_dir.filePath("commotion.log") + else: + #Fallback is in the core app directory. + self.logfile = QtCore.QDir.current().filePath("commotion.log") + + def set_verbosity(self, verbosity=None, log_type=None): + """Set's the verbosity of the logging for the application. + + Args: + verbosity (string|int): The verbosity level for logging to take place. + optional: Defaults to "Error" level + log_type (string): The type of logging whose verbosity is to be changed. + optional: If not specified ALL logging types will be changed. + + Returns: + bool True if successful, False if failed + + Raises: + exception: Description. + + """ + try: + int_level = int(verbosity) + except ValueError: + if str(verbosity).upper() in self.levels.keys(): + level = self.levels[str(verbosity).upper()] + else: + return False + else: + if 1 <= int_level <= 5: + level = list(self.levels.keys())[int_level-1] + else: + return False + + if log_type == "stream": + set_stream = True + elif log_type == "logfile": + set_logfile = True + else: + set_logfile = True + set_stream = True + + if set_stream == True: + self.logger.removeHandler(self.stream) + self.stream = None + self.stream = logging.StreamHandler() + self.stream.setFormatter(self.formatter) + self.stream.setLevel(level) + self.logger.addHandler(self.stream) + if set_logfile == True: + self.logger.removeHandler(self.file_handler) + self.file_handler = None + self.file_handler = logging.RotatingFileHandler(self.logfile, + maxBytes=5000000, + backupCount=5) + self.file_handler.setFormatter(self.formatter) + self.file_handler.setLevel(level) + self.logger.addHandler(self.file_handler) + return True + + def get_logger(self): + return self.logger From 1c7e6f02f6e696d70ea94d0b3c79fcf99e069f40 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 15:37:47 -0400 Subject: [PATCH 063/107] added an in-line-todo. --- commotion_client/utils/logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index 3039478..ab26094 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -17,8 +17,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ @@ -97,6 +96,7 @@ def set_logfile(self, logfile=None): self.logfile = log_dir.filePath("commotion.log") else: #If fail then just write logs in app path + #TODO check if this is appropriate... its not. self.logfile = QtCore.QDir.current().filePath("commotion.log") else: self.logfile = log_dir.filePath("commotion.log") From fc175ffb46b85cff89534f7cff30040feab980fa Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 15:41:58 -0400 Subject: [PATCH 064/107] added default extension directories Created a function that populates the default extension directories based upon operating system defaults. Bundling the application data made using the repository directory structure unwealdy as a store for application extensions. As such, application data will be stored in the folders specified within this function. --- commotion_client/utils/extension_manager.py | 77 ++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 4c60d92..91a4b4b 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -30,8 +30,8 @@ class ExtensionManager(object): def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) - self.extensions = self.check_installed() self.translate = QtCore.QCoreApplication.translate + self.set_extension_dir_defaults() self.config_values = ["name", "main", "menu_item", @@ -39,11 +39,69 @@ def __init__(self): "parent", "settings", "toolbar"] - self.extension_dirs = { - "user" : os.path.join(QtCore.QDir.homePath(), ".commotion/extensions"), - "global" :os.path.join(QtCore.QDir.currentPath(), "extensions") - } + self.extensions = self.check_installed() + + def set_extension_dir_defaults(self): + """Sets self.extension_dirs dictionary for user and global extension directories to system defaults. + + Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. Then sets the extension managers extension_dirs dictionary to point to those directories. + OS Defaults: + + OSX: + user: $HOME/Library/Commotion/extension_data/ + global: /Library/Application Support /Commotion/extension_data/ + + Windows: + user: %APPDATA%\Local\Commotion\extension_data\. + global: %COMMON_APPDATA%\Local\Commotion\extension_data\. + The %APPDATA% path is usually C:\Documents and Settings\User Name\Application Data; the %COMMON_APPDATA% path is usually C:\Documents and Settings\All Users\Application Data. + + Linux: + user: $HOME/.Commotion/extension_data/ + global: /usr/share/Commotion/extension_data/ + + Raises: + IOError: If the application does not have permission to create ANY of the extension directories. + + """ + self.log.debug(self.translate("logs", "Setting the default extension directory defaults.")) + platform = sys.platform + #Default global and user extension directories per platform. + #win23, darwin, and linux supported. + platform_dirs = { + 'darwin': { + 'user' : os.path.join("Library", "Commotion", "extension_data"), + 'user_root': QtCore.QDir.home(), + 'global' : os.path.join("Library", "Application Support", "Commotion", "extension_data") + 'global_root' : QtCore.QDir.root()}, + 'win32' : { + 'user':os.path.join("Local", "Commotion", "extension_data"), + 'user_root': QtCore.QDir(os.getenv('APPDATA')), + 'global':os.path.join("Local", "Commotion", "extension_data"), + 'global_root' : QtCore.QDir(os.getenv('COMMON_APPDATA'))}, + 'linux': { + 'user':os.path.join(".Commotion", "extension_data"), + 'user_root': QtCore.QDir.home(), + 'global':os.path.join("usr", "share", "Commotion", "extension_data"), + 'global_root' : QtCore.QDir.root()}} + + #User Path Settings + for path_type in ['user', 'global']: + ext_dir = platform_dirs[platform][path_type+'_root'] + ext_path = platform_dirs[platform][path_type] + if not ext_dir.exists(): + if ext_dir.mkpath(ext_path.absolutePath()): + self.log.debug(self.translate("logs", "Created the {0} extension directory at {1}".format(path_type, str(ext_path.absolutePath())))) + ext_dir.setPath(ext_path) + self.extension_dirs[path_type] = ext_dir.absolutePath() + self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_path.absolutePath())))) + else: + raise IOError(self.translate("logs", "Could not create the user extension directory.")) + else: + self.extension_dirs[path_type] = ext_dir.absolutePath() + self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_path.absolutePath())))) + def check_installed(self, name=None): """Checks if and extension is installed. @@ -61,13 +119,16 @@ def check_installed(self, name=None): else: return False - def load_all(self): - """Loads all extensions in the user and global extension directories. + def load_core(self): + """Loads all core extensions. - This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions that should be loaded for a user and adds them to the settings. NOTE: Does not do any validation as it relies on save_settings to validate all fields. + This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them. + + NOTE: Relies on save_settings to validate all fields. Returns: List of names (strings) of extensions loaded on success. Returns False (bool) on failure. + """ installed = self.get_installed() exist = config.get_config_paths("extension") From 2ca6a3e5137980875b8b0cdad7b6f133a20285ac Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 15:43:23 -0400 Subject: [PATCH 065/107] changed load_all call to new load_core name --- commotion_client/commotion_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index 599e4d5..f602439 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -176,7 +176,7 @@ def start_full(self): """ extensions = extension_manager.ExtensionManager() if not extensions.check_installed(): - extensions.load_all() + extensions.load_core() if not self.main: try: self.main = self.create_main_window() From 017afd52a42708c00467871bf5c190a28d9bcd45 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 16:28:02 -0400 Subject: [PATCH 066/107] added easy linux client cx_freeze compilation and creation --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 80ac7b8..9647a05 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ extensions: build: clean python3.3 build/build.py build pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o commotion_client/assets/commotion_assets_rc.py - cxfreeze commotion_client.py --base-name=commotion_client test: clean build @echo "test build complete" @@ -21,10 +20,16 @@ windows: osx: @echo "macintosh saddening is not yet implemented" +linux: clean_linux build + python3.3 setup.py build + +clean_linux: + rm -fr build/exe.linux-x86_64-3.3/ || true + debian: @echo "debian packaging is not yet implemented" -clean: +clean: clean_linux python3.3 build/build.py clean rm commotion_client/assets/commotion_assets_rc.py || true rm commotion_client/commotion_assets_rc.py || true From e0cd65de1a876e4ccae72c713671fac6cfdea1d8 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 16:28:34 -0400 Subject: [PATCH 067/107] fixed logger creation functions --- commotion_client/commotion_client.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index f602439..6d52a9d 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -53,9 +53,7 @@ def get_args(): parsed_args['message'] = args.message if args.message else False #TODO getConfig() #actually want to get this from commotion_config parsed_args['logLevel'] = args.verbose if args.verbose else 2 - #TODO change the logfile to be the default logging place for the system - default_logfile = os.path.join(args.logfile QtCore.QDir.currentPath(), "logfile") - parsed_args['logFile'] = args.logfile if args.logfile else default_logfile + parsed_args['logFile'] = args.logfile if args.logfile else None parsed_args['key'] = ['key'] if args.key else "commotionRocks" #TODO the key is PRIME easter-egg fodder parsed_args['status'] = "daemon" if args.daemon else False return parsed_args @@ -69,7 +67,6 @@ def main(): Function that handles command line arguments, translation, and creates the main application. """ args = get_args() - #Create Instance of Commotion Application app = CommotionClientApplication(args, sys.argv) @@ -129,7 +126,9 @@ class CommotionClientApplication(single_application.SingleApplicationWithMessagi def __init__(self, args, argv): super().__init__(args['key'], argv) status = args['status'] - self.init_logging(args['logLevel'], args['logFile']) + _logfile = args['logFile'] + _loglevel = args['logLevel'] + self.init_logging(_loglevel, _logfile) #Set Application and Organization Information self.setOrganizationName("The Open Technology Institute") self.setOrganizationDomain("commotionwireless.net") @@ -165,10 +164,9 @@ def init_client(self): self.log.exception(_excp) self.end(_catch_all) - def init_logging(self, level, logfile): - args['logLevel'], args['logFile'] - self.logger = logger.LogHandler("commotion_client", self.loglevel, self.logfile) - self.log = logger.get_logger() + def init_logging(self, level=None, logfile=None): + self.logger = logger.LogHandler("commotion_client", level, logfile) + self.log = self.logger.get_logger() def start_full(self): """ From 40164ea64c7fd6fe236e787a0d25191b8c5f0182 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 16:29:27 -0400 Subject: [PATCH 068/107] fixed some improper logging --- commotion_client/utils/extension_manager.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 91a4b4b..74fc2d6 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -17,6 +17,7 @@ import shutil import os import re +import sys #PyQt imports from PyQt4 import QtCore @@ -31,6 +32,7 @@ class ExtensionManager(object): def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.extension_dirs = {} self.set_extension_dir_defaults() self.config_values = ["name", "main", @@ -42,7 +44,7 @@ def __init__(self): self.extensions = self.check_installed() def set_extension_dir_defaults(self): - """Sets self.extension_dirs dictionary for user and global extension directories to system defaults. + r"""Sets self.extension_dirs dictionary for user and global extension directories to system defaults. Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. Then sets the extension managers extension_dirs dictionary to point to those directories. @@ -73,7 +75,7 @@ def set_extension_dir_defaults(self): 'darwin': { 'user' : os.path.join("Library", "Commotion", "extension_data"), 'user_root': QtCore.QDir.home(), - 'global' : os.path.join("Library", "Application Support", "Commotion", "extension_data") + 'global' : os.path.join("Library", "Application Support", "Commotion", "extension_data"), 'global_root' : QtCore.QDir.root()}, 'win32' : { 'user':os.path.join("Local", "Commotion", "extension_data"), @@ -92,15 +94,15 @@ def set_extension_dir_defaults(self): ext_path = platform_dirs[platform][path_type] if not ext_dir.exists(): if ext_dir.mkpath(ext_path.absolutePath()): - self.log.debug(self.translate("logs", "Created the {0} extension directory at {1}".format(path_type, str(ext_path.absolutePath())))) ext_dir.setPath(ext_path) + self.log.debug(self.translate("logs", "Created the {0} extension directory at {1}".format(path_type, str(ext_dir.absolutePath())))) self.extension_dirs[path_type] = ext_dir.absolutePath() - self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_path.absolutePath())))) + self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_dir.absolutePath())))) else: raise IOError(self.translate("logs", "Could not create the user extension directory.")) else: self.extension_dirs[path_type] = ext_dir.absolutePath() - self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_path.absolutePath())))) + self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_dir.absolutePath())))) def check_installed(self, name=None): """Checks if and extension is installed. @@ -130,6 +132,15 @@ def load_core(self): List of names (strings) of extensions loaded on success. Returns False (bool) on failure. """ + package = extensions + prefix = package.__name__ + "." + for importer, modname, ispkg in pkgutil.iter_modules(package.__path__, prefix): + self.log.debug("logs", "Core package {0} found in extensions.") + module = __import__(modname, fromlist="dummy") + self.log.debug("logs", "Core package {0} imported.") + CONTINUE_THIS_HERE() + #---------TODO START HERE -------------------- + installed = self.get_installed() exist = config.get_config_paths("extension") if not exist: From f1ba211682268d8b7e444f0eefb3886bc8805c3c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 16:30:02 -0400 Subject: [PATCH 069/107] fixed variable creation order and added needed imports. --- commotion_client/utils/logger.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index ab26094..c763d4b 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -24,7 +24,9 @@ #TODO create seperate levels for the stream, the file, and the full logger from PyQt4 import QtCore import logging +from logging import handlers import os +import sys class LogHandler(object): """ @@ -38,17 +40,19 @@ class LogHandler(object): log = logger.getLogger("commotion_client"+__name__) """ - - + def __init__(self, name, verbosity=None, logfile=None): - self.logger = logger.getLogger(str(name)) + #set core logger + self.logger = logging.getLogger(str(name)) + #set defaults + self.levels = {"CRITICAL":logging.CRITICAL, "ERROR":logging.ERROR, "WARN":logging.WARN, "INFO":logging.INFO, "DEBUG":logging.DEBUG} + self.formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(lineno)d : %(message)s') self.stream = None self.file_handler = None self.logfile = None - self.formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(lineno)d : %(message)s') + #setup logger self.set_logfile(logfile) self.set_verbosity(verbosity) - self.levels = {"CRITICAL":logging.CRITICAL, "ERROR":logging.ERROR, "WARN":logging.WARN, "INFO":logging.INFO, "DEBUG":logging.DEBUG} def set_logfile(self, logfile=None): """Set the file to log to. @@ -151,7 +155,7 @@ def set_verbosity(self, verbosity=None, log_type=None): if set_logfile == True: self.logger.removeHandler(self.file_handler) self.file_handler = None - self.file_handler = logging.RotatingFileHandler(self.logfile, + self.file_handler = handlers.RotatingFileHandler(self.logfile, maxBytes=5000000, backupCount=5) self.file_handler.setFormatter(self.formatter) From 2f675b1735b15bb316f2fc3025481ade52d7b8d5 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 28 Mar 2014 16:30:22 -0400 Subject: [PATCH 070/107] added the last-ditch log location to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 55b0920..3bfb552 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ commotion_client/data/extensions/* #compiled clients build/exe* build/lib + +#auto-created commotion on failure of everywhere else +commotion.log From 2d381e3d80386e3a7b1112081fafb1978ecfb0bd Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 31 Mar 2014 12:03:25 -0400 Subject: [PATCH 071/107] merged config handling into extension manager Config's only exist for extensions currently. As such, they have been moved into the extension manager. The file system level JSON loading function was moved into fs_utils to logically seperate it from the config_loader in case other JSON files need to be opened in the future. --- commotion_client/utils/config.py | 124 ------------ commotion_client/utils/extension_manager.py | 199 +++++++++++++++----- commotion_client/utils/fs_utils.py | 63 +++++-- 3 files changed, 206 insertions(+), 180 deletions(-) delete mode 100644 commotion_client/utils/config.py diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py deleted file mode 100644 index a7afb03..0000000 --- a/commotion_client/utils/config.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -""" -config - -The configuration manager. -""" -import sys -import os -import json -import logging - -from PyQt4 import QtCore - -from commotion_client.utils import fs_utils - -#set function logger -log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. - -def find_configs(config_type, name=None): - """ - Function used to obtain a config file. - - @param config_type The type of configuration file sought. (global, user, extension) - @param name optional The name of the configuration file if known - - @return list of tuples containing a config name and its config. - """ - config_files = get_config_paths(config_type) - if config_files: - configs = get_configs(config_files) - return configs - elif name != None: - for conf in configs: - if conf["name"] and conf["name"] == name: - return conf - log.error(QtCore.QCoreApplication.translate("logs", "No config of the chosed type named {0} found".format(name))) - return False - else: - log.error(QtCore.QCoreApplication.translate("logs", "No Configs of the chosed type found")) - return False - -def get_config_paths(config_type): - """ - Returns the paths to all config files. - - @param config_type string The type of config to get [ global|user|extension ] - """ - configLocations = {"global":"data/global/configs", "user":"data/user/configs", "extension":"data/extensions/configs"} - config_files = [] - - try: - path = os.path.join(QtCore.QDir.currentPath(), configLocations[config_type]) - except KeyError as _excp: - log.warn(QtCore.QCoreApplication.translate("logs", "Cannot search for config type {0} as it is an unsupported type.".format(config_type))) - log.exception(_excp) - return False - try: - for root, dirs, files in fs_utils.walklevel(path): - for file_name in files: - if file_name.endswith(".conf"): - config_files.append(os.path.join(root, file_name)) - except AssertionError as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "Config file folder at path {0} does not exist. No Config files loaded.".format(path))) - log.exception(_excp) - except TypeError as _excp: - log.error(QtCore.QCoreApplication.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) - log.exception(_excp) - if config_files: - return config_files - else: - return False - - -def get_configs(paths): - """ - Generator to retreive config files for the paths passed to it - - @param a list of paths of the configuration file to retreive - @return config file as a dictionary - """ - #load config file - for path in paths: - if fs_utils.is_file(path): - config = load_config(path) - if config: - yield config - else: - log.error(QtCore.QCoreApplication.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) - -def load_config(config): - """ - This function loads a json formatted config file and returns it. - - @param config the path to a config file - @return a dictionary containing the config files values - """ - #Open the file - try: - f = open(config, mode='r', encoding="utf-8", errors="strict") - except ValueError as _excp: - log.warn(QtCore.QCoreApplication.translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(config))) - log.exception(_excp) - return False - except TypeError as _excp: - log.warn(QtCore.QCoreApplication.translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file is the correct type.".format(config))) - log.exception(_excp) - return False - else: - tmpMsg = f.read() - #Parse the JSON - try: - data = json.loads(tmpMsg) - log.info(QtCore.QCoreApplication.translate("logs", "Successfully loaded {0}".format(config))) - return data - except ValueError as _excp: - log.warn(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(config))) - log.exception(_excp) - return False - except Exception as _excp: - log.warn(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to an unknown error.".format(config))) - log.exception(_excp) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 74fc2d6..0c3d55f 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -18,12 +18,13 @@ import os import re import sys +import pkgutil +import json #PyQt imports from PyQt4 import QtCore #Commotion Client Imports -from commotion_client.utils import config from commotion_client.utils import fs_utils from commotion_client.utils import validate from commotion_client import extensions @@ -42,9 +43,11 @@ def __init__(self): "settings", "toolbar"] self.extensions = self.check_installed() + _core = os.path.join(QtCore.QDir.currentPath(), "core_extensions") + self.core = ConfigManager(_core) def set_extension_dir_defaults(self): - r"""Sets self.extension_dirs dictionary for user and global extension directories to system defaults. + """Sets self.extension_dirs dictionary for user and global extension directories to system defaults. Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. Then sets the extension managers extension_dirs dictionary to point to those directories. @@ -55,9 +58,9 @@ def set_extension_dir_defaults(self): global: /Library/Application Support /Commotion/extension_data/ Windows: - user: %APPDATA%\Local\Commotion\extension_data\. - global: %COMMON_APPDATA%\Local\Commotion\extension_data\. - The %APPDATA% path is usually C:\Documents and Settings\User Name\Application Data; the %COMMON_APPDATA% path is usually C:\Documents and Settings\All Users\Application Data. + user: %APPDATA%\\Local\\Commotion\\extension_data\\. + global: %COMMON_APPDATA%\\Local\\Commotion\extension_data\\. + The %APPDATA% path is usually C:\\Documents and Settings\\User Name\\Application Data; the %COMMON_APPDATA% path is usually C:\\Documents and Settings\\All Users\\Application Data. Linux: user: $HOME/.Commotion/extension_data/ @@ -65,8 +68,8 @@ def set_extension_dir_defaults(self): Raises: IOError: If the application does not have permission to create ANY of the extension directories. - """ + self.log.debug(self.translate("logs", "Setting the default extension directory defaults.")) platform = sys.platform #Default global and user extension directories per platform. @@ -132,22 +135,13 @@ def load_core(self): List of names (strings) of extensions loaded on success. Returns False (bool) on failure. """ - package = extensions - prefix = package.__name__ + "." - for importer, modname, ispkg in pkgutil.iter_modules(package.__path__, prefix): - self.log.debug("logs", "Core package {0} found in extensions.") - module = __import__(modname, fromlist="dummy") - self.log.debug("logs", "Core package {0} imported.") - CONTINUE_THIS_HERE() - #---------TODO START HERE -------------------- - installed = self.get_installed() - exist = config.get_config_paths("extension") + exist = self.core.configs if not exist: self.log.info(self.translate("logs", "No extensions found.")) return False for config_path in exist: - _config = config.load_config(config_path) + _config = self.config.load_config(config_path) if _config['name'] in installed.keys(): _type = installed[_config['name']] if _type == "global": @@ -185,7 +179,7 @@ def get_installed(): 'contribExtension':"global", 'anotherContrib':"global"} """ - WRITE_TESTS_FOR_ME() +# WRITE_TESTS_FOR_ME() installed_extensions = {} _settings = QtCore.QSettings() _settings.beginGroup("extensions") @@ -211,8 +205,8 @@ def get_extension_from_property(self, key, val): Raises: KeyError: If the value requested is non-standard. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() matching_extensions = [] if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") @@ -254,8 +248,8 @@ def get_property(self, name, key): Raises: KeyError: If the value requested is non-standard. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() if value not in self.config_values: _error = self.translate("logs", "That is not a valid extension config value.") raise KeyError(_error) @@ -283,8 +277,8 @@ def get_type(self, name): Raises: KeyError: If an extension does not exist. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() _settings = QtCore.QSettings() _settings.beginGroup("extensions") core_ext = _settings.value("core/"+str(name)) @@ -303,8 +297,8 @@ def load_user_interface(self, extension_name, subsection=None): @param extension_name string The extension to load @subsection string Name of a objects sub-section. (settings, main, or toolbar) """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} settings = self.load_settings(extension_name) if subsection: @@ -321,8 +315,8 @@ def import_extension(extension_name, subsection=None): @param extension_name string The extension to load @param subsection string The module to load from an extension """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() if subsection: extension = importlib.import_module("."+subsection, "extensions."+extension_name) else: @@ -334,8 +328,8 @@ def load_settings(self, extension_name): @return dict A dictionary containing an extensions properties. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() extension_config = {"name":extension_name} extension_type = self.extensions[extension_name] @@ -346,7 +340,7 @@ def load_settings(self, extension_name): #get extension dir main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QDir.mkpath(os.path.join(main_ext_type_dir, config['name'])) + extension_dir = QtCore.QDir.mkpath(os.path.join(main_ext_type_dir, extension_config['name'])) extension_files = extension_dir.entryList() if not extension_config['main']: if "main.py" in extension_files: @@ -372,8 +366,8 @@ def remove_extension_settings(self, name): @param name str the name of an extension to remove from the extension settings. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() if len(str(name)) > 0: _settings = QtCore.QSettings() _settings.beginGroup("extensions") @@ -513,8 +507,8 @@ def save_extension(self, extension, extension_type="contrib", unpack=None): Raises: exception: Description. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() #There can be only two... and I don't trust you. if extension_type != "contrib": extension_type = "core" @@ -545,8 +539,8 @@ def save_extension(self, extension, extension_type="contrib", unpack=None): if re.match("^.*\.conf$", file_): config_name = file_ config_path = os.path.join(unpacked.absolutePath(), file_) - _config = config.load_config(config_path) - existing_extensions = config.find_configs("extension") + _config = self.config.load_config(config_path) + existing_extensions = self.config.find_configs("extension") try: assert _config['name'] not in existing_extensions except AssertionError: @@ -621,8 +615,8 @@ def add_config(self, extension_dir, name): Raises: IOError: If a config file of the same name already exists or the extension can not be saved. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") #If the data/extensions folder does not exist, make it. @@ -649,8 +643,8 @@ def remove_config(self, name): Raises: IOError: If a config file does not exist in the extension data folder. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() data_dir = os.path.join(QtCore.QDir.currentPath(), "data") config_dir = os.path.join(data_dir, "extensions") config = os.path.join(config_dir, name) @@ -669,8 +663,8 @@ def unpack_extension(self, compressed_extension): @param compressed_extension string Path to the compressed_extension @return A string object containing the absolute path to the temporary directory """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() temp_dir = fs_utils.make_temp_dir(new=True) temp_abs_path = temp_dir.absolutePath() try: @@ -697,8 +691,8 @@ def save_unpacked_extension(self, temp_dir, extension_name, extension_type): Raises: ValueError: If an extension with that name already exists. """ - WRITE_TESTS_FOR_ME() - FIX_ME_FOR_NEW_EXTENSION_TYPES() +# WRITE_TESTS_FOR_ME() +# FIX_ME_FOR_NEW_EXTENSION_TYPES() extension_path = "extensions/"+extension_type+"/"+extension_name full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) if not fs_utils.is_file(full_path): @@ -724,3 +718,118 @@ class InvalidSignature(Exception): This exception should only be handled by halting the current task. """ pass + + +class ConfigManager(object): + + def __init__(self, path=None): + #set function logger + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + if path: + self.paths = self.get_paths(path) + self.configs = [] + self.configs = self.get() + + def find(self, name=None): + """ + Function used to obtain a config file from the ConfigManager. + + @param name optional The name of the configuration file if known + @param path string The absolute path to the folder to check for extension configs. + + @return list of tuples containing a config name and its config. + """ + if not self.configs: + self.log.warn(self.translate("logs", "No configs have been loaded. Please load configs first.".format(name))) + return False + if not name: + return self.configs + elif name != None: + for conf in self.configs: + if conf["name"] and conf["name"] == name: + return conf + self.log.error(self.translate("logs", "No config of the chosed type named {0} found".format(name))) + return False + + def get_paths(self, directory): + """Returns the paths to all config files within a directory. + + Args: + directory (string): The path to the folder that extension's are within. Extensions can be up to one level below the directory given. + + Returns: + config_files (array): An array of paths to all config files found in the directory given. + + Raises: + TypeError: If no extensions exist within the directory requested. + AssertionError: If the directory path does not exist. + + """ + #Check the directory and + dir_obj = QtCore.QDir(str(directory)) + if not dir_obj.exists(dir_obj.path()): + self.log.warn(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) + return False + else: + path = dir_obj.absolutePath() + + config_files = [] + try: + for root, dirs, files in fs_utils.walklevel(path): + for file_name in files: + if file_name.endswith(".conf"): + config_files.append(os.path.join(root, file_name)) + except AssertionError: + self.log.warn(self.translate("logs", "Config file folder at path {0} does not exist. No Config files loaded.".format(path))) + raise + except TypeError: + self.log.warn(self.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) + raise + if config_files: + return config_files + else: + raise TypeError(self.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) + + def get(self, paths): + """ + Generator to retreive config files for the paths passed to it + + @param a list of paths of the configuration file to retreive + @return config file as a dictionary + """ + #load config file + if not paths: + paths = self.paths + for path in paths: + if fs_utils.is_file(path): + config = self.load(path) + if config: + yield config + else: + self.log.warn(self.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) + + def load(self, path): + """This function loads the formatted config file and returns it. + + long description + + Args: + path (string): The path to a config file + + Returns: + (dictionary) On success returns a dictionary containing the config file values. + (bool): On failure returns False + + """ + myfile = QtCore.QFile(str(path)) + if not myfile.exists(myfile.absolutePath()): + return False + try: + config = fs_utils.json_load(path) + except ValueError, TypeError as _excpt: + self.log.warn(self.translate("logs", "Could not load the config file {0}".format(str(path)))) + self.log.debug(_excpt) + return False + else: + return config diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py index 4ca1b1b..1a6bc55 100644 --- a/commotion_client/utils/fs_utils.py +++ b/commotion_client/utils/fs_utils.py @@ -16,21 +16,25 @@ import uuid def is_file(unknown): + """Determines if a file is accessable. It does NOT check to see if the file contains any data. + + Args: + unknown (string): The path to check for a accessable file. + + Returns: + bool True if a file is accessable and readable, False if a file is unreadable, or unaccessable. + """ - Determines if a file is accessable. It does NOT check to see if the file contains any data. - """ + translate = QtCore.QCoreApplication.translate log = logging.getLogger("commotion_client."+__name__) - try: - assert os.lstat(unknown).st_size > 0, "not a file: %s" % unknown - except (AssertionError, TypeError, IOError, OSError) as err: -#end stolen <3 - log.debug("is_file():"+err.strerror) + this_file = QtCore.QFile(str(unknown)) + if not this_file.exists(): + log.warn(translate("logs","The file {0} does not exist.".format(str(unknown)))) return False - if os.access(unknown, os.R_OK): - return True - else: - log.warn("is_file():You do not have permission to access that file") + if not os.access(unknown, os.R_OK): + log.warn(translate("logs","You do not have permission to access the file {0}".format(str(unknown)))) return False + return True def walklevel(some_dir, level=1): some_dir = some_dir.rstrip(os.path.sep) @@ -98,3 +102,40 @@ def copy_contents(start, end): log.error(_error) raise IOError(_error) return True + +def json_load(path): + """This function loads a JSON file and returns a formatted dictionary. + + Args: + path (string): The path to a json formatted file. + + Returns: + The JSON data from the file formatted as a dictionary. + + Raises: + TypeError: The file could not be opened due to an unknown error. + ValueError: The file was of an invalid type (eg. not in utf-8 format, etc.) + + """ + translate = QtCore.QCoreApplication.translate + log = logging.getLogger("commotion_client."+__name__) + + #Open the file + try: + f = open(string, mode='r', encoding="utf-8", errors="strict") + except ValueError: + log.warn(translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(path))) + raise + except TypeError: + log.warn(translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file is the correct type.".format(path))) + raise + else: + tmpMsg = f.read() + #Parse the JSON + try: + data = json.loads(tmpMsg) + log.info(translate("logs", "Successfully loaded {0}".format(path))) + return data + except ValueError: + log.warn(translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(path))) + raise From c7c093c3c0cbc411e49d69a945349acfc3240b9c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 31 Mar 2014 13:56:40 -0400 Subject: [PATCH 072/107] added build instructions for executable --- build/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build/README.md b/build/README.md index f4057fe..8a70b68 100644 --- a/build/README.md +++ b/build/README.md @@ -41,3 +41,11 @@ Extensions are built-in to the Commotion client by adding them to the extension core_extensions = ["config_editor", "main_window", "your_extension_name"] ``` +### Creating an executable + +Linux: + * go to the root directory of the project. + * type ```make linux``` + * The executables folder will be created in the build directory. + * run the ```Commotion``` executable in the executables folder. + \ No newline at end of file From 000d8a955cf670fa534fdb06eb4ef4516f487669 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 1 Apr 2014 14:54:36 -0400 Subject: [PATCH 073/107] Created Zipped extension loader Extensions are now zipped up during the build process to allow the build client to utilize them easier. Also cleaned up the build tree and its documentation. Now when any extension is built it is addded to the resources folder in the build tree. --- Makefile | 34 +++--- build/README.md | 36 ++++++ build/{ => scripts}/build.py | 4 +- .../compile_ui.py} | 7 +- build/scripts/zip_extensions.py | 103 ++++++++++++++++++ setup.py | 19 ++-- 6 files changed, 177 insertions(+), 26 deletions(-) rename build/{ => scripts}/build.py (92%) rename build/{compileUiFiles.py => scripts/compile_ui.py} (95%) create mode 100644 build/scripts/zip_extensions.py diff --git a/Makefile b/Makefile index 9647a05..5850f52 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,15 @@ .PHONY: build windows osx debian clean install -all: build windows debian osx extensions +all: build windows debian osx -extensions: - echo "write the extension config section.... seriously" -# Need to copy all core & listed contrib extension data into commotion_client/data/extensions/. -# cp commotion_client/extensions/core//*.conf commotion_client/data/extensions/. +extensions: build_tree + python3.3 build/scripts/zip_extensions.py -build: clean - python3.3 build/build.py build - pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o commotion_client/assets/commotion_assets_rc.py +build: clean extensions assets + python3.3 build/scripts/build.py build + +assets: build_tree + pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py test: clean build @echo "test build complete" @@ -20,16 +20,18 @@ windows: osx: @echo "macintosh saddening is not yet implemented" -linux: clean_linux build +linux: build python3.3 setup.py build -clean_linux: - rm -fr build/exe.linux-x86_64-3.3/ || true - debian: @echo "debian packaging is not yet implemented" -clean: clean_linux - python3.3 build/build.py clean - rm commotion_client/assets/commotion_assets_rc.py || true - rm commotion_client/commotion_assets_rc.py || true +build_tree: + mkdir build/resources || true + mkdir build/exe || true + +clean: + python3.3 build/scripts/build.py clean + rm -fr build/resources/* + rm -fr build/exe.* || true + diff --git a/build/README.md b/build/README.md index 8a70b68..27af79e 100644 --- a/build/README.md +++ b/build/README.md @@ -1,6 +1,42 @@ # Build Documentation +## Build Folder Structure + +build/ + ├── exe.-/ + ├── README.md <- The file you are reading. + ├── resources/ + └── scripts/ + └──compile_ui.py + + + +### exe.-/ + +A set of folders containing all final bundled executables created by cx_freeze in the build process. + +Built for a 64 bit linux machine with python3.3 this will look like: + +```exe.linux-x86_64-3.3``` + +These folders are not tracked by version control + +### scripts/ + +All scripts used by the build process + +### resources/ + +All resources created during the build process. + +This includes: + * All bundled extensions + * Compiled assets file ( commotion_assets_rc.py ) + +This folder is not tracked by version control + + ## cx_freeze instructions ### Get cx_freeze diff --git a/build/build.py b/build/scripts/build.py similarity index 92% rename from build/build.py rename to build/scripts/build.py index d9d535d..a07b130 100644 --- a/build/build.py +++ b/build/scripts/build.py @@ -3,7 +3,7 @@ import os import sys -import compileUiFiles +import compile_ui import fnmatch def clean(): @@ -22,7 +22,7 @@ def clean(): def build(): #compile the forms try: - compileUiFiles.compileUiFiles() + compile_ui.compileUiFiles() except Exception as e: sys.exit(e) diff --git a/build/compileUiFiles.py b/build/scripts/compile_ui.py similarity index 95% rename from build/compileUiFiles.py rename to build/scripts/compile_ui.py index ab120aa..421f176 100644 --- a/build/compileUiFiles.py +++ b/build/scripts/compile_ui.py @@ -3,9 +3,14 @@ # Copyright (c) 2009 - 2014 Detlev Offenbach # +# Per Eric Project April 1, 2014 Licensed GPLV 3 +# GNU GENERAL PUBLIC LICENSE +# Version 3, 29 June 2007 """ -Script for Commotion to compile all .ui files to Python source. Taken from the eric5 code base. +Script for Commotion to compile all .ui files to Python source. + +From the eric5 projects code base. """ import sys diff --git a/build/scripts/zip_extensions.py b/build/scripts/zip_extensions.py new file mode 100644 index 0000000..e10489c --- /dev/null +++ b/build/scripts/zip_extensions.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + +This program is a part of The Commotion Client + +Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +""" +""" +zip_extensions.py + +This module takes all extensions in the commotion_client/extension/ directory and prepares them as commotion packages. +""" + +import zipfile +import os + +def get_extensions(main_directory): + """Gets all extension sub-directories within a given directory. + + Args: + main_directory (string): The path to the main extension directory to check for extensions within. + + Returns: + A list containing of all of the extension directories within the main_directory. + ['path/to/extension01', 'path/to/extension02', 'path/to/extension03'] + """ + #if not a directory... shame on them + if not os.path.isdir(main_directory): + raise NotADirectoryError("{0} is not a directory.".format(main_directory)) + extensions = [] + #walk the directory and add all sub-directories as extensions. + for dirpath, dirnames, filenames in os.walk(main_directory): + for directory in dirnames: + #don't add pycache if it exists + if directory != "__pycache__": + extensions.append(os.path.join(dirpath, directory)) + break + return extensions + +def zip_extension(source, destination): + """short description + + long description + + Args: + source (string): The relative path to the source directory which contains the extension files. + destination (string): The relative path to the destination directory where the zipfile will be placed. + + """ + #if extension is not a directory then this won't work + if not os.path.isdir(source): + raise NotADirectoryError("{0} is not a directory.".format(main_directory)) + extension_name = os.path.basename(os.path.normpath(source)) + to_zip = [] + #walk the full extension directory. + for dirpath, dirnames, filenames in os.walk(source): + if "__init__.py" not in filenames: + touch_init(dirpath) + to_zip.append(os.path.join(dirpath, "__init__.py")) + for zip_file in filenames: + to_zip.append(os.path.join(dirpath, zip_file)) + #create and populate zipfile + with zipfile.ZipFile(os.path.join(destination, extension_name), 'a') as compressed_extension: + for ready_file in to_zip: + extension_path = os.path.relpath(ready_file, source) + compressed_extension.write(ready_file, extension_path) + + +def touch_init(extension_dir): + """ Touches the init file in each directory of an extension to make sure it exists. + + Args: + extension_dir (string): The path to a directory an __init__.py file should exist within. + """ + with open(os.path.join(extension_dir, "__init__.py"), 'a') as f: + os.utime("__init__.py") + +def zip_all(): + """Zip's all extensions in the main commotion_client directory and moves them into the build directories resources folder. + """ + main_directory = os.path.join("commotion_client", "extensions") + zip_directory = os.path.join("build", "resources") + extension_paths = get_extensions(main_directory) + for extension_directory in extension_paths: + zip_extension(extension_directory, zip_directory) + +if __name__ == "__main__": + zip_all() diff --git a/setup.py b/setup.py index d5d09a9..2ec4e07 100644 --- a/setup.py +++ b/setup.py @@ -47,22 +47,27 @@ # Define core packages. core_pkgs = ["commotion_client", "utils", "GUI", "assets"] -# Define bundled "core" extensions here. -core_extensions = ["config_editor"] -# Add core_extensions to core packages. -for ext in core_extensions: - core_pkgs.append("extensions."+ext) - # Include compiled assets file. assets_file = os.path.join("commotion_client", "assets", "commotion_assets_rc.py") # Place compiled assets file into the root directory. include_assets = (assets_file, "commotion_assets_rc.py") +all_assets = [include_assets] + +# Define bundled "core" extensions here. +core_extensions = ["config_editor"] +# Add core_extensions to core packages. +for ext in core_extensions: + ext_loc = os.path.join("build", "resources", ext) + asset_loc = os.path.join("extensions", ext) + all_assets.append((ext_loc, asset_loc)) + #---------- Executable Setup -----------# exe = Executable( targetName="Commotion", + targetDir="build/exe", script="commotion_client/commotion_client.py", packages=core_pkgs, ) @@ -74,5 +79,5 @@ url="commotionwireless.net", license="Affero General Public License V3 (AGPLv3)", executables = [exe], - options = {"build_exe":{"include_files": [include_assets]}} + options = {"build_exe":{"include_files": all_assets}} ) From 3fc9910c8b0bfff22b62a273129ca4f859cb7c6d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 2 Apr 2014 15:56:44 -0400 Subject: [PATCH 074/107] removed all current() and currentPath() calls. current() points to the current directory, but the application can be called from a variety of directories, and not just its root directory. As such, all uses of current will be replaced with calls to /.Commotion instead. --- commotion_client/utils/logger.py | 40 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index c763d4b..c9c4e2a 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -44,6 +44,7 @@ class LogHandler(object): def __init__(self, name, verbosity=None, logfile=None): #set core logger self.logger = logging.getLogger(str(name)) + self.logger.setLevel('DEBUG') #set defaults self.levels = {"CRITICAL":logging.CRITICAL, "ERROR":logging.ERROR, "WARN":logging.WARN, "INFO":logging.INFO, "DEBUG":logging.DEBUG} self.formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(lineno)d : %(message)s') @@ -72,26 +73,17 @@ def set_logfile(self, logfile=None): log_dir = QtCore.QDir(os.path.join(QtCore.QDir.homePath(), "Library", "Logs")) #if it does not exist try and create it if not log_dir.exists(): - if log_dir.mkpath(log_dir.absolutePath()): - self.logfile = log_dir.filePath("commotion.log") - else: - #If fail then just write logs in app path - self.logfile = QtCore.QDir.current().filePath("commotion.log") - else: - - self.logfile = log_dir.filePath("commotion.log") + if not log_dir.mkpath(log_dir.absolutePath()): + raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + self.logfile = log_dir.filePath("commotion.log") elif platform in ['win32', 'cygwin']: #Try ../AppData/Local/Commotion first log_dir = QtCore.QDir(os.path.join(os.getenv('APPDATA'), "Local", "Commotion")) #if it does not exist try and create it if not log_dir.exists(): - if log_dir.mkpath(log_dir.absolutePath()): - self.logfile = log_dir.filePath("commotion.log") - else: - #If fail then just write logs in app path - self.logfile = QtCore.QDir.current().filePath("commotion.log") - else: - self.logfile = log_dir.filePath("commotion.log") + if not log_dir.mkpath(log_dir.absolutePath()): + raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + self.logfile = log_dir.filePath("commotion.log") elif platform == 'linux': #Try /var/logs/ log_dir = QtCore.QDir("/var/logs/") @@ -99,14 +91,19 @@ def set_logfile(self, logfile=None): if log_dir.mkpath(log_dir.absolutePath()): self.logfile = log_dir.filePath("commotion.log") else: - #If fail then just write logs in app path + #If fail then just write logs in home directory #TODO check if this is appropriate... its not. - self.logfile = QtCore.QDir.current().filePath("commotion.log") + home = QtCore.QDir.home() + if not home.mkdir(".Commotion"): + raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + else: + home.cd(".Commotion") + self.logfile = home.filePath("commotion.log") else: self.logfile = log_dir.filePath("commotion.log") else: - #Fallback is in the core app directory. - self.logfile = QtCore.QDir.current().filePath("commotion.log") + #I'm out! + raise OSError(self.translate("logs", "Could not create a logfile.")) def set_verbosity(self, verbosity=None, log_type=None): """Set's the verbosity of the logging for the application. @@ -133,7 +130,8 @@ def set_verbosity(self, verbosity=None, log_type=None): return False else: if 1 <= int_level <= 5: - level = list(self.levels.keys())[int_level-1] + _levels = [ 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG'] + level = self.levels[_levels[int_level-1]] else: return False @@ -144,7 +142,7 @@ def set_verbosity(self, verbosity=None, log_type=None): else: set_logfile = True set_stream = True - + if set_stream == True: self.logger.removeHandler(self.stream) self.stream = None From 2bd13449dc344f6d26fe89784626c4a5f9d36fab Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 2 Apr 2014 16:13:01 -0400 Subject: [PATCH 075/107] removed exe directory in favor of auto-built exe. dirs --- Makefile | 2 -- setup.py | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 5850f52..5fee2e3 100644 --- a/Makefile +++ b/Makefile @@ -28,10 +28,8 @@ debian: build_tree: mkdir build/resources || true - mkdir build/exe || true clean: python3.3 build/scripts/build.py clean rm -fr build/resources/* rm -fr build/exe.* || true - diff --git a/setup.py b/setup.py index 2ec4e07..a5d456f 100644 --- a/setup.py +++ b/setup.py @@ -54,8 +54,14 @@ include_assets = (assets_file, "commotion_assets_rc.py") all_assets = [include_assets] + +#======== ADD EXTENSIONS HERE ==============# + # Define bundled "core" extensions here. core_extensions = ["config_editor"] + +#===========================================# + # Add core_extensions to core packages. for ext in core_extensions: ext_loc = os.path.join("build", "resources", ext) @@ -67,7 +73,6 @@ exe = Executable( targetName="Commotion", - targetDir="build/exe", script="commotion_client/commotion_client.py", packages=core_pkgs, ) From 3e651bcca397dd2bd52ac96583d463aa9469f092 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 4 Apr 2014 16:27:39 -0400 Subject: [PATCH 076/107] added a test runner to the project --- Makefile | 11 ++++++++--- tests/run_tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 tests/run_tests.py diff --git a/Makefile b/Makefile index 5fee2e3..d98ba9b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build windows osx debian clean install +.PHONY: build windows osx debian clean install tests all: build windows debian osx @@ -11,8 +11,6 @@ build: clean extensions assets assets: build_tree pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py -test: clean build - @echo "test build complete" windows: @echo "windows compileing is not yet implemented" @@ -29,7 +27,14 @@ debian: build_tree: mkdir build/resources || true +test: tests + @echo "test build complete" + +tests: clean build + python3.3 tests/run_tests.py + clean: python3.3 build/scripts/build.py clean rm -fr build/resources/* rm -fr build/exe.* || true + rm -fr tests/temp/* diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..111ddfc --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,32 @@ +""" +Evaluation tests for Commotion Networks. + +""" +import unittest +import importlib +import time +import sys +import os +import faulthandler + +def create_runner(verbosity_level=None): + """creates a testing runner. + + suite_type: (string) suites to run [acceptable values = suite_types in build_suite()] + """ + faulthandler.enable() + loader = unittest.TestLoader() + tests = loader.discover('.', '*_tests.py') + testRunner = unittest.runner.TextTestRunner(verbosity=verbosity_level, warnings="always") + testRunner.run(tests) + + +if __name__ == '__main__': + """Creates argument parser for required arguments and calls test runner""" + import argparse + parser = argparse.ArgumentParser(description='openThreads test suite') + parser.add_argument("-v", "--verbosity", nargs="?", default=2, const=2, dest="verbosity_level", metavar="VERBOSITY", help="make test_suite verbose") + + args = parser.parse_args() + create_runner(args.verbosity_level) + From 70e78530747420fa3a19eca5606a9e027c763fc5 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 4 Apr 2014 16:32:31 -0400 Subject: [PATCH 077/107] added temporary build and testing objects to gitignore --- .gitignore | 4 ++++ Makefile | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3bfb552..9cc7a63 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ commotion_client/data/extensions/* #compiled clients build/exe* build/lib +build/resources + +#testing objects +tests/temp #auto-created commotion on failure of everywhere else commotion.log diff --git a/Makefile b/Makefile index d98ba9b..45fb3a0 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,12 @@ all: build windows debian osx extensions: build_tree python3.3 build/scripts/zip_extensions.py -build: clean extensions assets +build: clean extensions assets python3.3 build/scripts/build.py build assets: build_tree pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py - windows: @echo "windows compileing is not yet implemented" @@ -31,10 +30,11 @@ test: tests @echo "test build complete" tests: clean build + mkdir tests/temp || true python3.3 tests/run_tests.py clean: python3.3 build/scripts/build.py clean - rm -fr build/resources/* + rm -fr build/resources/* || true rm -fr build/exe.* || true - rm -fr tests/temp/* + rm -fr tests/temp/* || true From 7319b187a400a785a73836387efc43f069f9da60 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 4 Apr 2014 16:32:52 -0400 Subject: [PATCH 078/107] updated the readme's --- build/README.md | 6 ++++-- docs/style_standards/README.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build/README.md b/build/README.md index 27af79e..51911d0 100644 --- a/build/README.md +++ b/build/README.md @@ -1,4 +1,3 @@ - # Build Documentation ## Build Folder Structure @@ -84,4 +83,7 @@ Linux: * type ```make linux``` * The executables folder will be created in the build directory. * run the ```Commotion``` executable in the executables folder. - \ No newline at end of file + +### The setup script + +The setup.py script in the root directory is not a traditional distutils setup.py script. It is actually a customized cx_freeze setup script. You can find documentation for it on the [cx_freeze docs site](http://cx-freeze.readthedocs.org/en/latest/distutils.html) and a searchable mailing list on [sourceforge](http://sourceforge.net/p/cx-freeze/mailman/cx-freeze-users/). \ No newline at end of file diff --git a/docs/style_standards/README.md b/docs/style_standards/README.md index 3991815..f622ce0 100644 --- a/docs/style_standards/README.md +++ b/docs/style_standards/README.md @@ -53,9 +53,9 @@ Logging should correspond to the following levels: ### Logging Exeptions -Exceptions should be logged using the exception handle at the point where they interfeir with the core task. If you wish to add logging at the point where you raise an exception use a debug or info log level to provide information about context around an exception. +Exceptions should be logged using the exception handle at the point where they interfeir with the core task. When an exception is handled in the program it should be logged on the debug level. A short description of why an exception is raised should be logged at the debug level wherever an excetion is first raised in the program. -tldr: If you raise an exception you should not log it. +tldr: If you raise an exception, log what happened, but let whomever handles it log the traceback as debug. ## Exception Handling From 04def439edd69fd3e28c2162d3c451d8ea92495c Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 4 Apr 2014 16:34:21 -0400 Subject: [PATCH 079/107] added some missing module __init__ files --- commotion_client/extensions/config_editor/__init__.py | 0 commotion_client/extensions/config_editor/ui/__init__.py | 0 tests/__init__.py | 0 tests/utils/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 commotion_client/extensions/config_editor/__init__.py create mode 100644 commotion_client/extensions/config_editor/ui/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/utils/__init__.py diff --git a/commotion_client/extensions/config_editor/__init__.py b/commotion_client/extensions/config_editor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/extensions/config_editor/ui/__init__.py b/commotion_client/extensions/config_editor/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 From 1699f84efc5f87ff26b20925cc2c09890b96a86e Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 4 Apr 2014 16:34:41 -0400 Subject: [PATCH 080/107] added a mock extension object for testing. --- tests/mock/extensions/config_editor | Bin 0 -> 67159 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/mock/extensions/config_editor diff --git a/tests/mock/extensions/config_editor b/tests/mock/extensions/config_editor new file mode 100644 index 0000000000000000000000000000000000000000..c1cd7452caa34ff49a942ceb7f8d70ab993651f5 GIT binary patch literal 67159 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 W$EbB> Date: Fri, 4 Apr 2014 16:49:55 -0400 Subject: [PATCH 081/107] Made Extension Manager use zipped extensions Added unit-tests, ConfigManager, extension loading functions, and fs_utils that work with zipped files> --- commotion_client/utils/extension_manager.py | 267 ++++++++++++++------ commotion_client/utils/fs_utils.py | 26 +- commotion_client/utils/validate.py | 129 ++++++---- tests/utils/extension_manager_tests.py | 218 +++++++++++----- 4 files changed, 427 insertions(+), 213 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 0c3d55f..ded7895 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -18,7 +18,7 @@ import os import re import sys -import pkgutil +import zipfile import json #PyQt imports @@ -30,27 +30,31 @@ from commotion_client import extensions class ExtensionManager(object): + def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.extensions = {} self.extension_dirs = {} - self.set_extension_dir_defaults() self.config_values = ["name", "main", "menu_item", "menu_level", "parent", "settings", - "toolbar"] - self.extensions = self.check_installed() - _core = os.path.join(QtCore.QDir.currentPath(), "core_extensions") - self.core = ConfigManager(_core) + "toolbar", + "tests"] - def set_extension_dir_defaults(self): - """Sets self.extension_dirs dictionary for user and global extension directories to system defaults. - - Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. Then sets the extension managers extension_dirs dictionary to point to those directories. + def init_extension_libraries(self): + self.set_library_defaults() + self.init_libraries() + self.load_core() + self.init_extension_config() + self.load_libraries() + def set_library_defaults(self): + """Sets the default directories for core, user, and global extensions. + OS Defaults: OSX: @@ -69,8 +73,15 @@ def set_extension_dir_defaults(self): Raises: IOError: If the application does not have permission to create ANY of the extension directories. """ - - self.log.debug(self.translate("logs", "Setting the default extension directory defaults.")) + #==== Core ====# + _app_path = QtCore.QDir(QtCore.QCoreApplication.applicationDirPath()) + _app_path.setPath("extensions") + #set the core extension directory + self.extension_dirs['core'] = _app_path.absolutePath() + self.log.debug(self.translate("logs", "Core extension directory succesfully set.")) + + #==== SYSTEM DEFAULTS =====# + self.log.debug(self.translate("logs", "Setting the default extension directory defaults.")) platform = sys.platform #Default global and user extension directories per platform. #win23, darwin, and linux supported. @@ -90,23 +101,52 @@ def set_extension_dir_defaults(self): 'user_root': QtCore.QDir.home(), 'global':os.path.join("usr", "share", "Commotion", "extension_data"), 'global_root' : QtCore.QDir.root()}} - - #User Path Settings for path_type in ['user', 'global']: ext_dir = platform_dirs[platform][path_type+'_root'] ext_path = platform_dirs[platform][path_type] + #move the root directory to the correct sub-path. + ext_dir.setPath(ext_path) + #Set the extension directory. + self.extension_dirs[path_type] = ext_dir.absolutePath() + + def init_libraries(self): + """Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. """ + #==== USER & GLOBAL =====# + for path_type in ['user', 'global']: + try: + ext_dir = QtCore.QDir(self.extension_dirs[path_type]) + except KeyError: + self.log.warn(self.translate("logs", "No directory is specified for the {0} library. Try running set_library_defaults to initalize the default libraries.".format(path_type))) + #If the directories are not yet created. We are not going to have this fail. + continue if not ext_dir.exists(): - if ext_dir.mkpath(ext_path.absolutePath()): - ext_dir.setPath(ext_path) - self.log.debug(self.translate("logs", "Created the {0} extension directory at {1}".format(path_type, str(ext_dir.absolutePath())))) - self.extension_dirs[path_type] = ext_dir.absolutePath() - self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_dir.absolutePath())))) + if ext_dir.mkpath(ext_dir.absolutePath()): + self.log.debug(self.translate("logs", "Created the {0} extension library at {1}".format(path_type, str(ext_dir.absolutePath())))) else: - raise IOError(self.translate("logs", "Could not create the user extension directory.")) + raise IOError(self.translate("logs", "Could not create the user extension library for {0}.".format(path_type))) else: - self.extension_dirs[path_type] = ext_dir.absolutePath() - self.log.debug(self.translate("logs", "Set the {0} extension directory to {1}".format(path_type, str(ext_dir.absolutePath())))) - + self.log.debug(self.translate("logs", "The extension library at {0} already existed for {1}".format(str(ext_dir.absolutePath()), path_type))) + + def init_extension_config(self, ext_type=None): + """ Initializes config objects for the path of extensions. + + Args: + ext_type (string): A specific extension type to load/reload a config object from. [global, user, or core]. If not provided, defaults to all. + + Raises: + ValueError: If the extension type passed is not either [core, global, or user] + """ + self.log.debug(self.translate("logs", "Initializing {0} extension configs..".format(ext_type))) + extension_types = ['user', 'global', 'core'] + if ext_type: + if str(ext_type) in extension_types: + extension_types = [ext_type] + else: + raise ValueError(self.translate("logs", "{0} is not an acceptable extension type.".format(ext_type))) + for type_ in extension_types: + self.extensions[type_] = ConfigManager(self.extension_dirs[type_]) + self.log.debug(self.translate("logs", "Configs for {0} extension library loaded..".format(type_))) + def check_installed(self, name=None): """Checks if and extension is installed. @@ -116,58 +156,82 @@ def check_installed(self, name=None): Returns: bool: True if named extension is installed, false, if not. """ - installed_extensions = self.get_installed().keys() + installed_extensions = list(self.get_installed().keys()) if name and name in installed_extensions: + self.log.debug(self.translate("logs", "Extension {0} found in installed extensions.".format(name))) return True elif not name and installed_extensions: + self.log.debug(self.translate("logs", "Installed extensions found.")) return True else: + self.log.debug(self.translate("logs", "Extension/s NOT found.")) return False def load_core(self): - """Loads all core extensions. + """Loads all core extensions into the globals library and global settings. This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them. + """ + #Core extensions are loaded from the global directory. + #If a core extension has been deleted from the global directory it will be replaced from the core directory. + self.init_extension_config('core') + _core_dir = QtCore.QDir(self.extension_dirs['core']) + _global_dir = QtCore.QDir(self.extension_dirs['global']) + for ext in self.extensions['core'].configs: + try: + #Check if the extension is in the globals + global_extensions = list(self.extensions['global'].configs.keys()) + if ext['name'] in global_extensions: + continue + except KeyError: + #If extension not loaded in globals it will raise a KeyError + _core_ext_path = _core_dir.absoluteFilePath(ext['name']) + _global_ext_path = _global_dir.absoluteFilePath(ext['name']) + self.log.info(self.translate("logs", "Core extension {0} was missing from the global extension directory. Copying it into the global extension directory from the core now.".format(ext['name']))) + #Copy extension into global directory + if QtCore.QFile(_core_ext_path).copy(_global_ext_path): + self.log.debug(self.translate("logs", "Extension config successfully copied.")) + else: + self.log.debug(self.translate("logs", "Extension config was not copied.")) + + def load_libraries(self, ext_type=None): + """Loads the currently installed libraries into the users settings. + + Args: + ext_type (string): A specific extension type [global or user] to load extensions from. If not provided, defaults to both. + NOTE: Relies on save_settings to validate all fields. Returns: List of names (strings) of extensions loaded on success. Returns False (bool) on failure. - """ - installed = self.get_installed() - exist = self.core.configs - if not exist: - self.log.info(self.translate("logs", "No extensions found.")) - return False - for config_path in exist: - _config = self.config.load_config(config_path) - if _config['name'] in installed.keys(): - _type = installed[_config['name']] - if _type == "global": - _ext_dir = os.path.join(self.extension_dir['global'], _config['name']) - elif _type == "user": - _ext_dir = os.path.join(self.extension_dir['user'], _config['name']) + extension_types = ['user', 'global'] + if str(ext_type) in extension_types: + extension_types = [ext_type] + for type_ in extension_types: + saved = [] + ext_configs = self.extensions[type_].configs + if not ext_configs: + self.log.info(self.translate("logs", "No extensions of type {0} found.".format(type_))) + return False + for _config in global_ext: + if _config['name'] in installed.keys(): + _type = installed[_config['name']] + _ext_dir = os.path.join(self.extension_dirs[type_], _config['name']) else: - self.log.warn(self.translate("logs", "Extension {0} is of an unknown type. It will not be loaded".format(_config['name']))) - continue - else: - if QtCore.QDir(self.extension_dir['user']).exists(_config['name']): - _ext_dir = os.path.join(self.extension_dir['user'], _config['name']) - elif QtCore.QDir(self.extension_dir['global']).exists(_config['name']): - _ext_dir = os.path.join(self.extension_dir['global'], _config['name']) + if QtCore.QDir(self.extension_dirs[type_]).exists(_config['name']): + _ext_dir = os.path.join(self.extension_dirs[type_], _config['name']) + else: + self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_config['name']))) + continue + if not self.save_settings(_config, _type): + self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) else: - self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_config['name']))) - continue - if not self.save_settings(_config, _type): - self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) - else: - saved.append(_config['name']) - return saved or False - + saved.append(_config['name']) + return saved or False - @staticmethod - def get_installed(): + def get_installed(self): """Get all installed extensions seperated by type. Pulls the current installed extensions from the application settings and returns a dictionary with the lists of the two extension types. @@ -179,14 +243,15 @@ def get_installed(): 'contribExtension':"global", 'anotherContrib':"global"} """ -# WRITE_TESTS_FOR_ME() + self.log.debug(self.translate("logs", "Getting installed extensions.")) installed_extensions = {} _settings = QtCore.QSettings() _settings.beginGroup("extensions") - extensions = _settings.allKeys() + extensions = _settings.childGroups() for ext in extensions: - installed_extensions[ext] = _settings.value(ext+"/type").toString() + installed_extensions[ext] = _settings.value(ext+"/type") _settings.endGroup() + self.log.debug(self.translate("logs", "The following extensions are installed: [{0}].".format(extensions))) return installed_extensions def get_extension_from_property(self, key, val): @@ -341,7 +406,7 @@ def load_settings(self, extension_name): main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") main_ext_type_dir = os.path.join(main_ext_dir, extension_type) extension_dir = QtCore.QDir.mkpath(os.path.join(main_ext_type_dir, extension_config['name'])) - extension_files = extension_dir.entryList() + extension_files = extension_dirs.entryList() if not extension_config['main']: if "main.py" in extension_files: extension_config['main'] = "main" @@ -534,11 +599,11 @@ def save_extension(self, extension, extension_type="contrib", unpack=None): return False #Name if config_validator.name(): - files = unpacked.entryList() + files = unpacked.entryInfoList() for file_ in files: - if re.match("^.*\.conf$", file_): - config_name = file_ - config_path = os.path.join(unpacked.absolutePath(), file_) + if file_.suffix() == ".conf": + config_name = file_.fileName() + config_path = file_.absolutePath() _config = self.config.load_config(config_path) existing_extensions = self.config.find_configs("extension") try: @@ -653,8 +718,6 @@ def remove_config(self, name): _error = QtCore.QCoreApplication.translate("logs", "Error deleting file.") raise IOError(_error) return True - - def unpack_extension(self, compressed_extension): @@ -726,10 +789,30 @@ def __init__(self, path=None): #set function logger self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.log.debug(self.translate("logs", "Initalizing ConfigManager")) + self.configs = [] + self.directory = None + self.paths = [] if path: - self.paths = self.get_paths(path) - self.configs = [] - self.configs = self.get() + self.directory = path + try: + self.paths = self.get_paths(path) + except TypeError: + self.log.info(self.translate("logs", "No extensions found in the {0} directory".format(path))) + else: + self.log.info(self.translate("logs", "Extensions found in the {0} directory. Attempting to load extension configs.".format(path))) + self.configs = list(self.get()) + + def has_configs(self): + """Provides the status of a ConfigManagers config files. + + Returns: + bool: True, if there are configs. False, if there are no configs currently. + """ + if self.configs: + return True + else: + return False def find(self, name=None): """ @@ -753,13 +836,13 @@ def find(self, name=None): return False def get_paths(self, directory): - """Returns the paths to all config files within a directory. + """Returns the paths to all extensions with config files within a directory. Args: directory (string): The path to the folder that extension's are within. Extensions can be up to one level below the directory given. Returns: - config_files (array): An array of paths to all config files found in the directory given. + config_files (array): An array of paths to all extension objects withj config files that were found. Raises: TypeError: If no extensions exist within the directory requested. @@ -778,20 +861,24 @@ def get_paths(self, directory): try: for root, dirs, files in fs_utils.walklevel(path): for file_name in files: - if file_name.endswith(".conf"): - config_files.append(os.path.join(root, file_name)) + if zipfile.is_zipfile(os.path.join(root, file_name)): + ext_zip = zipfile.ZipFile(os.path.join(root, file_name), 'r') + ext_names = ext_zip.namelist() + for member_name in ext_names: + if member_name.endswith(".conf"): + config_files.append(os.path.join(root, file_name)) except AssertionError: - self.log.warn(self.translate("logs", "Config file folder at path {0} does not exist. No Config files loaded.".format(path))) + self.log.warn(self.translate("logs", "Extension library at path {0} does not exist. No Config files identified.".format(path))) raise except TypeError: - self.log.warn(self.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) + self.log.warn(self.translate("logs", "No extensions found at path {0}. No Config files identified.".format(path))) raise if config_files: return config_files else: raise TypeError(self.translate("logs", "No config files found at path {0}. No Config files loaded.".format(path))) - def get(self, paths): + def get(self, paths=None): """ Generator to retreive config files for the paths passed to it @@ -822,14 +909,28 @@ def load(self, path): (bool): On failure returns False """ + config = None + data = None myfile = QtCore.QFile(str(path)) - if not myfile.exists(myfile.absolutePath()): + if not myfile.exists(): return False - try: - config = fs_utils.json_load(path) - except ValueError, TypeError as _excpt: - self.log.warn(self.translate("logs", "Could not load the config file {0}".format(str(path)))) - self.log.debug(_excpt) + if not zipfile.is_zipfile(str(path)): return False + with zipfile.ZipFile(path, 'r') as zip_ext: + for file_name in zip_ext.namelist(): + if file_name.endswith(".conf"): + config = zip_ext.read(file_name) + self.log.debug(self.translate("logs", "Config found in extension {0}.".format(path))) + if config: + try: + data = json.loads(config.decode('utf-8')) + self.log.info(self.translate("logs", "Successfully loaded {0}'s config file.".format(path))) + except ValueError: + self.log.warn(self.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(path))) + return False + if data: + self.log.debug(self.translate("logs", "Config file loaded.".format(path))) + return data else: - return config + self.log.debug(self.translate("logs", "Failed to load config file.".format(path))) + return False diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py index 1a6bc55..f886ccb 100644 --- a/commotion_client/utils/fs_utils.py +++ b/commotion_client/utils/fs_utils.py @@ -14,6 +14,11 @@ import os import logging import uuid +import json + +translate = QtCore.QCoreApplication.translate +log = logging.getLogger("commotion_client."+__name__) + def is_file(unknown): """Determines if a file is accessable. It does NOT check to see if the file contains any data. @@ -26,7 +31,6 @@ def is_file(unknown): """ translate = QtCore.QCoreApplication.translate - log = logging.getLogger("commotion_client."+__name__) this_file = QtCore.QFile(str(unknown)) if not this_file.exists(): log.warn(translate("logs","The file {0} does not exist.".format(str(unknown)))) @@ -38,7 +42,9 @@ def is_file(unknown): def walklevel(some_dir, level=1): some_dir = some_dir.rstrip(os.path.sep) - assert os.path.isdir(some_dir) + log.debug(translate("logs", "attempting to walk directory {0}".format(some_dir))) + if not os.path.isdir(some_dir): + raise NotADirectoryError(translate("logs", "{0} is not a directory. Can only 'walk' down through directories.".format(some_dir))) num_sep = some_dir.count(os.path.sep) for root, dirs, files in os.walk(some_dir): yield root, dirs, files @@ -73,11 +79,11 @@ def clean_dir(path=None): if not path: path = QtCore.QDir(os.path.join(QtCore.QDir.tempPath(), "Commotion")) path.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) - list_of_files = path.entryList() + list_of_files = path.entryInfoList() - for file_ in list_of_files: - file_ = os.path.join(path.path(), file_) - if not QtCore.QFile(file_).remove(): + for file_info in list_of_files: + file_path = file_info.absoluteFilePath() + if not QtCore.QFile(file_path).remove(): _error = QtCore.QCoreApplication.translate("logs", "Error saving extension to extensions directory.") log.error(_error) raise IOError(_error) @@ -92,11 +98,11 @@ def copy_contents(start, end): """ log = logging.getLogger("commotion_client."+__name__) start.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) - list_of_files = start.entryList() + list_of_files = start.entryInfoList() - for file_ in list_of_files: - source = os.path.join(start.path(), file_) - dest = os.path.join(end.path(), file_) + for file_info in list_of_files: + source = file_info.absoluteFilePath() + dest = os.path.join(end.path(), file_info.fileName()) if not QtCore.QFile(source).copy(dest): _error = QtCore.QCoreApplication.translate("logs", "Error copying file into extensions directory. File already exists.") log.error(_error) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 640f409..e87c271 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -22,52 +22,66 @@ #Commotion Client Imports from commotion_client.utils import fs_utils -from commotion_client.utils import config class ClientConfig(object): - def __init__(self, ext_config=None, directory=None): + def __init__(self, config, directory=None): """ Args: - ext_config (string): Absolute Path to the config file. - directory (string): Absolute Path to the directory containing the extension. + config (dictionary): The config for the extension. + directory (string): Absolute Path to the directory containing the extension. If not specified the validator will ONLY check the validity of the config passed to it. """ - if ext_config: - if fs_utils.is_file(ext_config): - self._config = config.load_config(ext_config) - else: - raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) - self._directory = QtCore.QDir(directory) + self.config_values = ["name", + "main", + "menu_item", + "menu_level", + "parent", + "settings", + "toolbar"] self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.config = config + if directory: + self.directory = directory self.errors = None - - def set_extension(self, directory, ext_config=None): - """Set the default values - @param config string The absolute path to a config file for this extension - @param directory string The absolute path to this extensions directory - """ - self._directory = QtCore.QDir(directory) - if ext_config: - if fs_utils.is_file(ext_config): - self._config = config.load_config(ext_config) - else: - raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) - else: - files = self._directory.entryList() - for file_ in files: - if re.match("^.*\.conf$", file_): - default_config_path = os.path.join(self._directory.path(), file_) - try: - assert default_config_path - except NameError: - raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) - if fs_utils.is_file(default_config_path): - self._config = config.load_config(default_config_path) - else: - raise IOError(self.translate("logs", "Extension does not contain a config file and is therefore invalid.")) - + @property + def config(self): + """Return the config value.""" + return self.config + + @config.setter + def config(self, value): + """Check for valid values before allowing them to be set.""" + if 'name' not in value: + raise KeyError(self.translate("logs", "The config file must contain at least a name value.")) + for val in value.keys(): + if val not in self.config_values: + raise KeyError(self.translate("logs", "The config file specified has values within it that are not allowed.")) + self.config = value + + + @property + def directory(self): + return self.directory + + @directory.setter + def directory(self, value): + value_dir = QtCore.QDir(value) + #Check that the directory in fact exists. + if not value_dir.exists(): + raise NotADirectoryError(self.translate("logs", "The directory should, by definition, actually be a directory. What was submitted was not a directory. Please specify the directory of an existing extension to continue.")) + #Check that there are files in the directory provided + value_dir.setFilter(QtCore.QDir.NotDotAndDotDot|QtCore.QDir.NoSymLinks) + if not value_dir.entryList(): + raise FileNotFoundError(self.translate("logs", "There are no files in the extension directory provided. Is an extension directory without any files an extension directory at all? We will ponder these mysteries while you check to see if the extension directory provided is correct." )) + #Check that we can read the directory and its files. Sadly, QDir.isReadable() is broken on a few platforms so we check that and use the file filter to check each file. + value_dir.setFilter(QtCore.QDir.Executable|QtCore.QDir.Files) + file_list = value_dir.entryInfoList() + if not file_list or not value_dir.isReadable(): + raise PermissionError(self.translate("logs", "The application does not have permission to read any files within this directory. How is it supposed to validate the extension then? You ask. It can't. Please modify the permissions on the directory to allow the application to read the files within.")) + self.directory = value + def validate_all(self): """Run all validation functions on an uncompressed extension. @@ -75,31 +89,28 @@ def validate_all(self): @return bool True if valid, False if invalid. """ self.errors = None - if not self._directory: - if not self._directory: - raise NameError(self.translate("logs", "ClientConfig validator requires at least an extension directory to be specified")) - else: - directory = self._directory + if not self.config: + raise NameError(self.translate("logs", "ClientConfig validator requires at least a config has been specified")) errors = [] if not self.name(): errors.append("name") - self.log.info(self.translate("logs", "The name of extension {0} is invalid.".format(self._config['name']))) + self.log.info(self.translate("logs", "The name of extension {0} is invalid.".format(self.config['name']))) if not self.tests(): errors.append("tests") - self.log.info(self.translate("logs", "The extensions {0} tests is invalid.".format(self._config['name']))) + self.log.info(self.translate("logs", "The extension {0}'s tests is invalid.".format(self.config['name']))) if not self.menu_level(): errors.append("menu_level") - self.log.info(self.translate("logs", "The extensions {0} menu_level is invalid.".format(self._config['name']))) + self.log.info(self.translate("logs", "The extension {0}'s menu_level is invalid.".format(self.config['name']))) if not self.menu_item(): errors.append("menu_item") - self.log.info(self.translate("logs", "The extensions {0} menu_item is invalid.".format(self._config['name']))) + self.log.info(self.translate("logs", "The extension {0}'s menu_item is invalid.".format(self.config['name']))) if not self.parent(): errors.append("parent") - self.log.info(self.translate("logs", "The extensions {0} parent is invalid.".format(self._config['name']))) + self.log.info(self.translate("logs", "The extension {0}'s parent is invalid.".format(self.config['name']))) else: for gui_name in ['main', 'settings', 'toolbar']: if not self.gui(gui_name): - self.log.info(self.translate("logs", "The extensions {0} {1} is invalid.".format(self._config['name'], gui_name))) + self.log.info(self.translate("logs", "The extension {0}'s {1} is invalid.".format(self.config['name'], gui_name))) errors.append(gui_name) if errors: self.errors = errors @@ -114,11 +125,11 @@ def gui(self, gui_name): @param gui_name string "main", "settings", or "toolbar" """ try: - val = str(self._config[gui_name]) + val = str(self.config[gui_name]) except KeyError: if gui_name != "main": try: - val = str(self._config["main"]) + val = str(self.config["main"]) except KeyError: val = str('main') else: @@ -134,7 +145,7 @@ def gui(self, gui_name): def name(self): try: - name_val = str(self._config['name']) + name_val = str(self.config['name']) except KeyError: self.log.warn(self.translate("logs", "There is no name value in the config file. This value is required.")) return False @@ -149,10 +160,10 @@ def name(self): def menu_item(self): """Validate a menu item value.""" try: - val = str(self._config["menu_item"]) + val = str(self.config["menu_item"]) except KeyError: if self.name(): - val = str(self._config["name"]) + val = str(self.config["name"]) else: self.log.warn(self.translate("logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid.")) return False @@ -164,7 +175,7 @@ def menu_item(self): def parent(self): """Validate a parent value.""" try: - val = str(self._config["parent"]) + val = str(self.config["parent"]) except KeyError: self.log.info(self.translate("logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used.")) return True @@ -176,7 +187,7 @@ def parent(self): def menu_level(self): """Validate a Menu Level Config item.""" try: - val = int(self._config["menu_level"]) + val = int(self.config["menu_level"]) except KeyError: self.log.info(self.translate("logs", "There is no 'menu_level' value set in the config. As such the default value of 10 will be used.")) return True @@ -191,7 +202,7 @@ def menu_level(self): def tests(self): """Validate a tests config menu item.""" try: - val = str(self._config["tests"]) + val = str(self.config["tests"]) except KeyError: val = str('tests') file_name = val + ".py" @@ -219,7 +230,10 @@ def check_exists(self, file_name): @param file_name string The file name from a config file """ - files = QtCore.QDir(self._directory).entryList() + if not self.directory: + self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) + return True + files = QtCore.QDir(self.directory).entryList() if not str(file_name) in files: self.log.warn(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) return False @@ -266,6 +280,9 @@ def check_path_length(self, file_name=None): @param file_name string The string to check for validity. """ + if not self.directory: + self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) + return True # file length limit platform = sys.platform # OSX(name<=255), linux(name<=255) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index e1b2e9d..5c1e0b3 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -4,6 +4,7 @@ import unittest import re +import os from commotion_client.utils import extension_manager @@ -11,84 +12,173 @@ class ExtensionSettingsTestCase(unittest.TestCase): def setUp(self): self.app = QtGui.QApplication([]) + self.app.setOrganizationName("test_case"); + self.app.setApplicationName("testing_app"); + self.settings = QtCore.QSettings() self.ext_mgr = extension_manager.ExtensionManager() - self.settings = QtCore.QSettings("test_case", "testing_app") - def tearDown(self): - self.app = None - self.ext_mgr = None - self.settings.clear() - -class CoreExtensionMgrTestCase(unittest.TestCase): - - def setUp(self): - self.app = QtGui.QApplication([]) - self.ext_mgr = extension_manager.ExtensionManager() def tearDown(self): + self.app.deleteLater() + del self.app self.app = None self.ext_mgr = None + self.settings.clear() + #Delete everything under tests/temp + for root, dirs, files in os.walk(os.path.abspath("tests/temp/"), topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) -class LoadSettings(ExtensionSettingsTestCase): +class LoadConfigSettings(ExtensionSettingsTestCase): """ Functions Covered: load_all + init_extension_config + """ - - def test_load_all_settings(self): - """Test that settings are saved upon running load_all.""" - #Test that there are no extensions loaded - count = self.settings.allKeys() - assertIs(type(count), int) - assertEquals(count, 0) - #Load extensions - loaded = self.ext_mgr.load_all() - #load_all returns extensions that are loaded - assertIsNot(loaded, False) - #Check that some extensions were added to settings - post_count = self.settings.allKeys() - assertIs(type(count), int) - assertGreater(count, 0) - - def test_load_all_core_ext(self): + def test_load_core_ext(self): """Test that all core extension directories are saved upon running load_all.""" #get all extensions currently loaded - global_ext = QtCore.QDir(self.ext_mgr.extension_dir['global']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) - loaded = self.ext_mgr.load_all() - self.settings.beginGroup("extensions") - k = self.settings.AllKeys() - for ext in global_ext: - assertTrue(k.contains(ext), "Core extension {0} should have been loaded, but was not.".format(ext)) - - def test_load_all_user_ext(self): - """Test that all user extension directories are saved upon running load_all.""" - #get user extensions - user_ext = QtCore.QDir(ext_mgr.extension_dir['user']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) - #If no user extensions exist (which they should not) set the current user directory to the model extension directory. - if not user_ext.count() > 0: - main_dir = dirname(os.path.abspath(QtCore.QDir.currentPath())) - model_directory = os.path.join(main_dir, "tests/models/extensions") - ext_mgr.extension_dirs['user'] = model_directory - #refresh user_ext list - user_ext = QtCore.QDir(ext_mgr.extension_dir['user']).files(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) - loaded = self.ext_mgr.load_all() + self.ext_mgr.extension_dirs['core'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/temp/") + global_dir = QtCore.QDir(self.ext_mgr.extension_dirs['global']) + global_exts = global_dir.entryList(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) + loaded = self.ext_mgr.load_core() self.settings.beginGroup("extensions") - k = self.settings.AllKeys() - for ext in user_ext: - assertTrue(k.contains(ext), "Extension {0} should have been loaded, but was not.".format(ext)) + k = self.settings.allKeys() + for ext in global_exts: + self.assertTrue(k.contains(ext), "Core extension {0} should have been loaded, but was not.".format(ext)) + + def test_init_extension_config(self): + """Test that init extension config properly handles the various use cases.""" + #ext_type MUST be core|global|user + with self.assertRaises(ValueError): + self.ext_mgr.init_extension_config('pineapple') + + #Check for an empty directory. + self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/temp/") + self.ext_mgr.init_extension_config('user') + self.assertFalse(self.ext_mgr.extensions['user'].has_configs()) + self.ext_mgr.extensions['user'] = None + + #populate with populated directory + self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.init_extension_config('user') + self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) + self.ext_mgr.extensions['user'] = None + + #check all types on default call + self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.extension_dirs['core'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.init_extension_config() + self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) + self.assertTrue(self.ext_mgr.extensions['global'].has_configs()) + self.assertTrue(self.ext_mgr.extensions['core'].has_configs()) + self.ext_mgr.extensions['user'] = None + self.ext_mgr.extensions['global'] = None + self.ext_mgr.extensions['core'] = None + +class GetConfigSettings(ExtensionSettingsTestCase): + + def test_get_installed(self): + """Test that get_installed function properly checks & returns installed extensions.""" + empty_inst = self.ext_mgr.get_installed() + self.assertEqual(empty_inst, {}) + #add a value to settings + self.settings.setValue("extensions/test/type", "global") + self.settings.sync() + one_item = self.ext_mgr.get_installed() + self.assertEqual(len(one_item), 1) + self.assertIn("test", one_item) + self.assertEqual(one_item['test'], 'global') + + def test_check_installed(self): + """Test that get_installed function properly checks & returns if extensions are installed.""" + #test empty first + self.assertFalse(self.ext_mgr.check_installed()) + self.assertFalse(self.ext_mgr.check_installed("test")) + #add a value to settings + self.settings.setValue("extensions/test/type", "global") + self.settings.sync() + self.assertTrue(self.ext_mgr.check_installed()) + self.assertTrue(self.ext_mgr.check_installed("test")) + self.assertFalse(self.ext_mgr.check_installed("pineapple")) + +class ExtensionLibraries(ExtensionSettingsTestCase): + + def test_init_libraries(self): + """Tests that the extension_dir libraries are created when provided and fail gracefully when not. """ + self.ext_mgr.init_libraries() + self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/temp/oneLevel/") + self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/temp/oneLevel/twoLevel/") + self.ext_mgr.init_libraries() + self.assertTrue(os.path.isdir(os.path.abspath("tests/temp/oneLevel/twoLevel/"))) + self.assertTrue(os.path.isdir(os.path.abspath("tests/temp/oneLevel/"))) -class InitTestCase(CoreExtensionMgrTestCase): + def test_load_librarys(self): + self.fail("This test for function load_libraries (line 198) needs to be implemented") + + def test_get_extension_from_property(self): + self.fail("This test for function get_extension_from_property (line 257) needs to be implemented") + + def test_get_property(self): + self.fail("This test for function get_property (line 302) needs to be implemented") + + def test_get_type(self): + self.fail("This test for function get_type (line 333) needs to be implemented... but this function may actually need to be removed too.") + + def test_load_user_interface(self): + self.fail("This test for function load_user_interface (line 359) needs to be implemented") + + def test_import_extension(self): + self.fail("This test for function import_extension (line 376) needs to be implemented") + + def test_load_settings(self): + self.fail("This test for function load_settings (line 391) needs to be implemented") + + def test_remove_extension_settings(self): + self.fail("This test for function remove_extension_settings (line 429) needs to be implemented... or moved to a settings module that will eventually handle all the encrypted stuff... but that actually might be better done outside of this.") + + def test_save_settings(self): + self.fail("This test for function save_settings (line 444) needs to be implemented") + + def test_save_extension(self): + self.fail("This test for function save_extension (line 561) needs to be implemented") + + def test_add_config(self): + self.fail("This test for function add_config (line 670) needs to be implemented... but the function should actually just be removed.") + + def test_remove_config(self): + self.fail("This test for function remove_config (line 699) needs to be implemented... but the function should actually just be removed.") + + def test_unpack_extension(self): + self.fail("This test for function unpack_extension (line 723) needs to be implemented... but that function actually MUST be removed since we are not unpacking extension objects") + + def test_save_unpacked_extension(self): + self.fail("This test for function save_unpacked_extension (line 743) needs to be implemented... but that function actually MUST be removed since we are not unpacking extension objects") + +class ConfigManagerTests(unittest.TestCase): + + #Create a new setUp and CleanUP set of functions for these configManager tests def test_init(self): - #TODO: TEST THESE - fail("test not implemented") - ext_mgr.log = False - ext_mgr.extensions = False - ext_mgr.translate = False - ext_mgr.config_values = False - ext_mgr.extension_dir = False - -class FileSystemManagement(CoreExtensionMgrTestCase): - - - + self.fail("This test for the init function (line 788) needs to be implemented") + + def test_has_configs(self): + self.fail("This test for function has_configs (line 806) needs to be implemented") + + def test_find(self): + self.fail("This test for function find (line 817) needs to be implemented") + + def test_get_path(self): + self.fail("This test for function get_paths (line 838) needs to be implemented") + + def test_get(self): + self.fail("This test for function get (line 881) needs to be implemented") + + def test_load(self): + self.fail("This test for function load (line 899) needs to be implemented") + From a9c910be2f31285c46750bdc06639dd0d5c86b58 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 8 Apr 2014 15:33:08 -0400 Subject: [PATCH 082/107] Config properties no longer recursive. Due to config properties setting themselves they were fully recursive. To stop them from turtle-ing all the way down I changed them to set local variables, like they should have in the first place. --- commotion_client/utils/validate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index e87c271..077bf09 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -1,4 +1,5 @@ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- @@ -48,7 +49,7 @@ def __init__(self, config, directory=None): @property def config(self): """Return the config value.""" - return self.config + return self._config @config.setter def config(self, value): @@ -57,13 +58,13 @@ def config(self, value): raise KeyError(self.translate("logs", "The config file must contain at least a name value.")) for val in value.keys(): if val not in self.config_values: - raise KeyError(self.translate("logs", "The config file specified has values within it that are not allowed.")) - self.config = value + raise KeyError(self.translate("logs", "The config file specified has the value {0} within it which is not a valid value.".format(val))) + self._config = value @property def directory(self): - return self.directory + return self._directory @directory.setter def directory(self, value): @@ -80,7 +81,7 @@ def directory(self, value): file_list = value_dir.entryInfoList() if not file_list or not value_dir.isReadable(): raise PermissionError(self.translate("logs", "The application does not have permission to read any files within this directory. How is it supposed to validate the extension then? You ask. It can't. Please modify the permissions on the directory to allow the application to read the files within.")) - self.directory = value + self._directory = value def validate_all(self): """Run all validation functions on an uncompressed extension. From 74667144bf5839d0f539d3eaf0cb00836fbdd82e Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 8 Apr 2014 15:34:51 -0400 Subject: [PATCH 083/107] Removed mistyped filter --- commotion_client/utils/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 077bf09..aebd8f1 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -73,7 +73,7 @@ def directory(self, value): if not value_dir.exists(): raise NotADirectoryError(self.translate("logs", "The directory should, by definition, actually be a directory. What was submitted was not a directory. Please specify the directory of an existing extension to continue.")) #Check that there are files in the directory provided - value_dir.setFilter(QtCore.QDir.NotDotAndDotDot|QtCore.QDir.NoSymLinks) + value_dir.setFilter(QtCore.QDir.NoDotAndDotDot|QtCore.QDir.NoSymLinks) if not value_dir.entryList(): raise FileNotFoundError(self.translate("logs", "There are no files in the extension directory provided. Is an extension directory without any files an extension directory at all? We will ponder these mysteries while you check to see if the extension directory provided is correct." )) #Check that we can read the directory and its files. Sadly, QDir.isReadable() is broken on a few platforms so we check that and use the file filter to check each file. From 9eba7a0529054d101fac60a4de00b8ad00bfa849 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 8 Apr 2014 16:46:38 -0400 Subject: [PATCH 084/107] fixed dependencies to make install_loaded work --- commotion_client/utils/extension_manager.py | 245 +++++++++++--------- 1 file changed, 138 insertions(+), 107 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index ded7895..6fc84ff 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -10,6 +10,28 @@ * finding, loading, and unloading extensions * installing extensions +----------- +Definitions: +----------- + +Library: The [core, user, & global] folders that hold the extension zip archives. + +Loaded: An extension (Library) that has been found by the ExtensionManager and has had its config loaded into one of the ConfigManagers [core, global, user]. + +Installed: An extension that has been saved into the [user or global] applications settings. On initial installation the extension will be set to initialized and show up in the main extension settings menu. + +Initialized: An installed extension that has had its "initialized" flag set to true. Initalized applications show up in the menu and have a personal settings page if enabled. + +Disabled: An installed extension that has had its "initialized" flag set to false. Disabled applications will not show up in the menu or have their personal settings page enabled, but will still show up in the main extension settings menu. + +Uninstalled: An extension that has been removed from the application settings and also had its library deleted from all extension directories [user global] + +Core Extensions: Core extensions are extensions that are loaded along with the application. On restart these extensions are checked against the global extensions, and if missing copied into the global extension directory and re-installed. + +Global Extensions: Global extensions are extensions that are available for all logged in users. + +User Extensions: User extensions are extensions that are only installed for the current user. + """ #Standard Library Imports import logging @@ -27,6 +49,7 @@ #Commotion Client Imports from commotion_client.utils import fs_utils from commotion_client.utils import validate +from commotion_client.utils import settings from commotion_client import extensions class ExtensionManager(object): @@ -35,22 +58,42 @@ def __init__(self): self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate self.extensions = {} - self.extension_dirs = {} - self.config_values = ["name", + self.libraries = {} + self.user_settings = self.get_user_settings() + self.config_keys = ["name", "main", "menu_item", "menu_level", "parent", "settings", "toolbar", - "tests"] + "tests", + "initialized",] + + def get_user_settings(self): + """Get the currently logged in user settings object.""" + settings_manager = settings.UserSettingsManager() + _settings = settings_manager.get() + if _settings.Scope() == 0: + _settings.beginGroup("extensions") + return _settings + else: + raise TypeError(self.translate("logs", "User settings has a global scope and will not be loaded. Because, security.")) + def init_extension_libraries(self): + """This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them.""" + + #set default library paths self.set_library_defaults() + #create directory structures if needed self.init_libraries() - self.load_core() + #load core and move to global if needed + self.load_install_core() + #Load all extension configs found in libraries self.init_extension_config() - self.load_libraries() + #install all loaded config's + self.install_loaded() def set_library_defaults(self): """Sets the default directories for core, user, and global extensions. @@ -77,7 +120,7 @@ def set_library_defaults(self): _app_path = QtCore.QDir(QtCore.QCoreApplication.applicationDirPath()) _app_path.setPath("extensions") #set the core extension directory - self.extension_dirs['core'] = _app_path.absolutePath() + self.libraries['core'] = _app_path.absolutePath() self.log.debug(self.translate("logs", "Core extension directory succesfully set.")) #==== SYSTEM DEFAULTS =====# @@ -107,16 +150,16 @@ def set_library_defaults(self): #move the root directory to the correct sub-path. ext_dir.setPath(ext_path) #Set the extension directory. - self.extension_dirs[path_type] = ext_dir.absolutePath() + self.libraries[path_type] = ext_dir.absolutePath() def init_libraries(self): - """Creates an extension folder, if it does not exit, in the operating systems default application data directories for the current user and for the global application. """ + """Creates a library folder, if it does not exit, in the directories specified for the current user and for the global application. """ #==== USER & GLOBAL =====# for path_type in ['user', 'global']: try: - ext_dir = QtCore.QDir(self.extension_dirs[path_type]) + ext_dir = QtCore.QDir(self.libraries[path_type]) except KeyError: - self.log.warn(self.translate("logs", "No directory is specified for the {0} library. Try running set_library_defaults to initalize the default libraries.".format(path_type))) + self.log.warning(self.translate("logs", "No directory is specified for the {0} library. Try running set_library_defaults to initalize the default libraries.".format(path_type))) #If the directories are not yet created. We are not going to have this fail. continue if not ext_dir.exists(): @@ -144,7 +187,7 @@ def init_extension_config(self, ext_type=None): else: raise ValueError(self.translate("logs", "{0} is not an acceptable extension type.".format(ext_type))) for type_ in extension_types: - self.extensions[type_] = ConfigManager(self.extension_dirs[type_]) + self.extensions[type_] = ConfigManager(self.libraries[type_]) self.log.debug(self.translate("logs", "Configs for {0} extension library loaded..".format(type_))) def check_installed(self, name=None): @@ -167,17 +210,39 @@ def check_installed(self, name=None): self.log.debug(self.translate("logs", "Extension/s NOT found.")) return False + def get_installed(self): + """Get all installed extensions seperated by type. + + Pulls the current installed extensions from the application settings and returns a dictionary with the lists of the two extension types. + + Returns: + A dictionary keyed by the names of all extensions with the values being if they are a user extension or a global extension. + + {'coreExtensionOne':"user", 'coreExtensionTwo':"global", + 'contribExtension':"global", 'anotherContrib':"global"} + + """ + self.log.debug(self.translate("logs", "Getting installed extensions.")) + installed_extensions = {} + _settings = self.user_settings + extensions = _settings.childGroups() + for ext in extensions: + installed_extensions[ext] = _settings.value(ext+"/type") + self.log.debug(self.translate("logs", "The following extensions are installed: [{0}].".format(extensions))) + return installed_extensions + def load_core(self): - """Loads all core extensions into the globals library and global settings. + """Loads all core extensions into the globals library and re-initialized the global config. - This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them. + This function bootstraps global library from the core library. It iterates through all extensions in the core library and populates the global config with any extensions it does not already contain and then loads them into the global config. """ #Core extensions are loaded from the global directory. #If a core extension has been deleted from the global directory it will be replaced from the core directory. self.init_extension_config('core') - _core_dir = QtCore.QDir(self.extension_dirs['core']) - _global_dir = QtCore.QDir(self.extension_dirs['global']) + _core_dir = QtCore.QDir(self.libraries['core']) + _global_dir = QtCore.QDir(self.libraries['global']) + _reload_globals = False for ext in self.extensions['core'].configs: try: #Check if the extension is in the globals @@ -194,65 +259,42 @@ def load_core(self): self.log.debug(self.translate("logs", "Extension config successfully copied.")) else: self.log.debug(self.translate("logs", "Extension config was not copied.")) + _reload_globals = True + if _reload_globals == True: + self.init_extension_config("global") - def load_libraries(self, ext_type=None): - """Loads the currently installed libraries into the users settings. + def install_loaded(self, ext_type=None): + """Installs loaded libraries by saving their settings into the application settings. Args: ext_type (string): A specific extension type [global or user] to load extensions from. If not provided, defaults to both. - - NOTE: Relies on save_settings to validate all fields. Returns: - List of names (strings) of extensions loaded on success. Returns False (bool) on failure. + List of names (strings) of extensions loaded on success. Returns and empty list [] on failure. + + Note on validation: Relies on save_settings to validate all fields. + Note on core: Core extensions are never "installed" they are used to populate the global library and then installed under global settings. + """ + _keys = self.user_settings.allKeys() extension_types = ['user', 'global'] - if str(ext_type) in extension_types: + if ext_type and str(ext_type) in extension_types: extension_types = [ext_type] for type_ in extension_types: saved = [] ext_configs = self.extensions[type_].configs if not ext_configs: - self.log.info(self.translate("logs", "No extensions of type {0} found.".format(type_))) - return False - for _config in global_ext: - if _config['name'] in installed.keys(): - _type = installed[_config['name']] - _ext_dir = os.path.join(self.extension_dirs[type_], _config['name']) - else: - if QtCore.QDir(self.extension_dirs[type_]).exists(_config['name']): - _ext_dir = os.path.join(self.extension_dirs[type_], _config['name']) + self.log.info(self.translate("logs", "No extensions of type {0} are currently loaded.".format(type_))) + return [] + for _config in ext_configs: + #Only install if not already installed in this section. + if _config['name'] not in _keys: + #Attempt to save the extension. + if not self.save_settings(_config, type_): + self.log.warning(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) else: - self.log.warn(self.translate("logs", "There is no corresponding data to accompany the config for extension {0}. It will not be loaded".format(_config['name']))) - continue - if not self.save_settings(_config, _type): - self.log.warn(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) - else: - saved.append(_config['name']) - return saved or False - - def get_installed(self): - """Get all installed extensions seperated by type. - - Pulls the current installed extensions from the application settings and returns a dictionary with the lists of the two extension types. - - Returns: - A dictionary keyed by the names of all extensions with the values being if they are a user extension or a global extension. - - {'coreExtensionOne':"user", 'coreExtensionTwo':"global", - 'contribExtension':"global", 'anotherContrib':"global"} - - """ - self.log.debug(self.translate("logs", "Getting installed extensions.")) - installed_extensions = {} - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") - extensions = _settings.childGroups() - for ext in extensions: - installed_extensions[ext] = _settings.value(ext+"/type") - _settings.endGroup() - self.log.debug(self.translate("logs", "The following extensions are installed: [{0}].".format(extensions))) - return installed_extensions + saved.append(_config['name']) + return saved or [] def get_extension_from_property(self, key, val): """Takes a property and returns all extensions who have the passed value set under the passed property. @@ -273,31 +315,23 @@ def get_extension_from_property(self, key, val): # WRITE_TESTS_FOR_ME() # FIX_ME_FOR_NEW_EXTENSION_TYPES() matching_extensions = [] - if value not in self.config_values: - _error = self.translate("logs", "That is not a valid extension config value.") + if key not in self.config_keys: + _error = self.translate("logs", "{0} is not a valid extension config value.".format(key)) raise KeyError(_error) - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") - ext_sections = ['core', 'contrib'] - for ext_type in ext_sections: - #enter extension type group - _settings.beginGroup(ext_type) - all_exts = _settings.allKeys() - #QtCore.QStringList from allKeys() is missing the .toList() method from to its QtCore.QVariant.QStringList version. So, we do this horrible while loop instead. - while all_exts.isEmpty() != True: - current_extension = all_exts.takeFirst() - #enter extension settings - _settings.beginGroup(current_extension) - if _settings.value(key).toString() == str(val): - matching_extensions.append(current_extension) - #exit extension - _settings.endGroup() - #exit extension type group + _settings = self.user_settings + all_exts = _settings.childGroups() + for current_extension in all_exts: + #enter extension settings + _settings.beginGroup(current_extension) + if _settings.value(key) == val: + matching_extensions.append(current_extension) + #exit extension _settings.endGroup() if matching_extensions: return matching_extensions else: - self.log.debug(self.translate("logs", "No extensions had the requested value.")) + self.log.info(self.translate("logs", "No extensions had the requested value.")) + return [] def get_property(self, name, key): """ @@ -315,17 +349,18 @@ def get_property(self, name, key): """ # WRITE_TESTS_FOR_ME() # FIX_ME_FOR_NEW_EXTENSION_TYPES() - if value not in self.config_values: + if key not in self.config_keys: _error = self.translate("logs", "That is not a valid extension config value.") raise KeyError(_error) - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") + _settings = self.user_settings ext_type = self.get_type(name) _settings.beginGroup(ext_type) _settings.beginGroup(name) setting_value = _settings.value(key) if setting_value.isNull(): _error = self.translate("logs", "The extension config does not contain that value.") + _settings.endGroup() + _settings.endGroup() raise KeyError(_error) else: return setting_value.toStr() @@ -344,8 +379,7 @@ def get_type(self, name): """ # WRITE_TESTS_FOR_ME() # FIX_ME_FOR_NEW_EXTENSION_TYPES() - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") + _settings = self.user_settings core_ext = _settings.value("core/"+str(name)) contrib_ext = _settings.value("contrib/"+str(name)) if not core_ext.isNull() and contrib_ext.isNull(): @@ -398,8 +432,7 @@ def load_settings(self, extension_name): extension_config = {"name":extension_name} extension_type = self.extensions[extension_name] - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") + _settings = self.user_settings _settings.beginGroup(extension_type) extension_config['main'] = _settings.value("main").toString() #get extension dir @@ -423,7 +456,6 @@ def load_settings(self, extension_name): if "tests.py" in extension_files: extension_config['tests'] = "tests" _settings.endGroup() - _settings.endGroup() return extension_config def remove_extension_settings(self, name): @@ -434,8 +466,7 @@ def remove_extension_settings(self, name): # WRITE_TESTS_FOR_ME() # FIX_ME_FOR_NEW_EXTENSION_TYPES() if len(str(name)) > 0: - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") + _settings = self.user_settings _settings.remove(str(name)) else: _error = self.translate("logs", "You must specify an extension name greater than 1 char.") @@ -457,11 +488,10 @@ def save_settings(self, extension_config, extension_type="global"): exception: Description. """ - _settings = QtCore.QSettings() - _settings.beginGroup("extensions") + _settings = self.user_settings #get extension dir try: - extension_dir = self.extension_dirs[extension_type] + extension_dir = self.libraries[extension_type] except KeyError: self.log.warn(self.translate("logs", "Invalid extension type. Please check the extension type and try again.")) return False @@ -473,7 +503,7 @@ def save_settings(self, extension_config, extension_type="global"): extension_name = extension_config['name'] _settings.beginGroup(extension_name) except KeyError: - _error = self.translate("logs", "The extension is missing a name value which is required.") + _error = self.translate("logs", "The extension is missing a name value which is required.") self.log.error(_error) return False else: @@ -487,9 +517,9 @@ def save_settings(self, extension_config, extension_type="global"): except KeyError: if config_validator.main(): _main = "main" #Set this for later default values - _settings.value("main", "main").toString() + _settings.setValue("main", "main") else: - _settings.value("main", _main).toString() + _settings.setValue("main", _main) else: _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") self.log.error(_error) @@ -501,9 +531,9 @@ def save_settings(self, extension_config, extension_type="global"): _config_value = extension_config[val] except KeyError: #Defaults to main, which was checked and set before - _settings.value(val, _main).toString() + _settings.setValue(val, _main) else: - _settings.value(val, _config_value).toString() + _settings.setValue(val, _config_value) else: _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.".format(val)) self.log.error(_error) @@ -511,10 +541,10 @@ def save_settings(self, extension_config, extension_type="global"): #Extension Parent if config_validator.parent(): try: - _settings.value("parent", extension_config["parent"]).toString() + _settings.setValue("parent", extension_config["parent"]) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) - _settings.value("parent", "Extensions").toString() + _settings.setValue("parent", "Extensions") else: _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") self.log.error(_error) @@ -523,10 +553,10 @@ def save_settings(self, extension_config, extension_type="global"): #Extension Menu Item if config_validator.menu_item(): try: - _settings.value("menu_item", extension_config["menu_item"]).toString() + _settings.setValue("menu_item", extension_config["menu_item"]) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) - _settings.value("menu_item", extension_name).toString() + _settings.setValue("menu_item", extension_name) else: _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") self.log.error(_error) @@ -534,10 +564,10 @@ def save_settings(self, extension_config, extension_type="global"): #Extension Menu Level if config_validator.menu_level(): try: - _settings.value("menu_level", extension_config["menu_level"]).toInt() + _settings.setValue("menu_level", extension_config["menu_level"]) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) - _settings.value("menu_level", 10).toInt() + _settings.setValue("menu_level", 10) else: _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") self.log.error(_error) @@ -545,16 +575,17 @@ def save_settings(self, extension_config, extension_type="global"): #Extension Tests if config_validator.tests(): try: - _settings.value("main", extension_config['tests']).toString() + _settings.setValue("main", extension_config['tests']) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) - _settings.value("tests", "tests").toString() + _settings.setValue("tests", "tests") else: _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) self.log.error(_error) return False #Write extension type - _settings.value("type", extension_type) + _settings.setValue("type", extension_type) + _settings.setValue("initialized", True) _settings.endGroup() return True From df8939f28790b9d47fd5d9dfb018f521e3bbe4d6 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 8 Apr 2014 16:47:41 -0400 Subject: [PATCH 085/107] Added test for install loaded function --- tests/utils/extension_manager_tests.py | 78 ++++++++++++++++++-------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index 5c1e0b3..e4853ed 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -14,7 +14,6 @@ def setUp(self): self.app = QtGui.QApplication([]) self.app.setOrganizationName("test_case"); self.app.setApplicationName("testing_app"); - self.settings = QtCore.QSettings() self.ext_mgr = extension_manager.ExtensionManager() @@ -22,8 +21,8 @@ def tearDown(self): self.app.deleteLater() del self.app self.app = None + self.ext_mgr.user_settings.clear() self.ext_mgr = None - self.settings.clear() #Delete everything under tests/temp for root, dirs, files in os.walk(os.path.abspath("tests/temp/"), topdown=False): for name in files: @@ -39,17 +38,18 @@ class LoadConfigSettings(ExtensionSettingsTestCase): """ def test_load_core_ext(self): - """Test that all core extension directories are saved upon running load_all.""" + """Test that all core extension directories are loaded upon running load_core.""" #get all extensions currently loaded - self.ext_mgr.extension_dirs['core'] = os.path.abspath("tests/mock/extensions/") - self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/temp/") - global_dir = QtCore.QDir(self.ext_mgr.extension_dirs['global']) + self.ext_mgr.libraries['core'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['global'] = os.path.abspath("tests/temp/") + global_dir = QtCore.QDir(self.ext_mgr.libraries['global']) global_exts = global_dir.entryList(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot) loaded = self.ext_mgr.load_core() - self.settings.beginGroup("extensions") - k = self.settings.allKeys() + self.ext_mgr.user_settings.beginGroup("extensions") + k = self.ext_mgr.user_settings.allKeys() for ext in global_exts: - self.assertTrue(k.contains(ext), "Core extension {0} should have been loaded, but was not.".format(ext)) + contains = (ext in k) + self.assertTrue(contains, "Core extension {0} should have been loaded, but was not.".format(ext)) def test_init_extension_config(self): """Test that init extension config properly handles the various use cases.""" @@ -58,21 +58,21 @@ def test_init_extension_config(self): self.ext_mgr.init_extension_config('pineapple') #Check for an empty directory. - self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/temp/") + self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/") self.ext_mgr.init_extension_config('user') self.assertFalse(self.ext_mgr.extensions['user'].has_configs()) self.ext_mgr.extensions['user'] = None #populate with populated directory - self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") self.ext_mgr.init_extension_config('user') self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) self.ext_mgr.extensions['user'] = None #check all types on default call - self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/mock/extensions/") - self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/mock/extensions/") - self.ext_mgr.extension_dirs['core'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['global'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['core'] = os.path.abspath("tests/mock/extensions/") self.ext_mgr.init_extension_config() self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) self.assertTrue(self.ext_mgr.extensions['global'].has_configs()) @@ -88,8 +88,8 @@ def test_get_installed(self): empty_inst = self.ext_mgr.get_installed() self.assertEqual(empty_inst, {}) #add a value to settings - self.settings.setValue("extensions/test/type", "global") - self.settings.sync() + self.ext_mgr.user_settings.setValue("test/type", "global") + self.ext_mgr.user_settings.sync() one_item = self.ext_mgr.get_installed() self.assertEqual(len(one_item), 1) self.assertIn("test", one_item) @@ -101,8 +101,8 @@ def test_check_installed(self): self.assertFalse(self.ext_mgr.check_installed()) self.assertFalse(self.ext_mgr.check_installed("test")) #add a value to settings - self.settings.setValue("extensions/test/type", "global") - self.settings.sync() + self.ext_mgr.user_settings.setValue("test/type", "global") + self.ext_mgr.user_settings.sync() self.assertTrue(self.ext_mgr.check_installed()) self.assertTrue(self.ext_mgr.check_installed("test")) self.assertFalse(self.ext_mgr.check_installed("pineapple")) @@ -110,16 +110,48 @@ def test_check_installed(self): class ExtensionLibraries(ExtensionSettingsTestCase): def test_init_libraries(self): - """Tests that the extension_dir libraries are created when provided and fail gracefully when not. """ + """Tests that the library are created when provided and fail gracefully when not. """ + + #init libraries from library defaults + self.ext_mgr.set_library_defaults() + user_dir = self.ext_mgr.libraries['user'] + global_dir = self.ext_mgr.libraries['global'] self.ext_mgr.init_libraries() - self.ext_mgr.extension_dirs['user'] = os.path.abspath("tests/temp/oneLevel/") - self.ext_mgr.extension_dirs['global'] = os.path.abspath("tests/temp/oneLevel/twoLevel/") + self.assertTrue(os.path.isdir(os.path.abspath(user_dir))) + self.assertTrue(os.path.isdir(os.path.abspath(global_dir))) + #assert that init libraries works with non-default paths. + + self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/oneLevel/") + self.ext_mgr.libraries['global'] = os.path.abspath("tests/temp/oneLevel/twoLevel/") self.ext_mgr.init_libraries() self.assertTrue(os.path.isdir(os.path.abspath("tests/temp/oneLevel/twoLevel/"))) self.assertTrue(os.path.isdir(os.path.abspath("tests/temp/oneLevel/"))) - def test_load_librarys(self): - self.fail("This test for function load_libraries (line 198) needs to be implemented") + def test_install_loaded(self): + """ Tests that all loaded, and currently uninstalled, libraries are installed""" + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup empty directory + self.ext_mgr.libraries['global'] = os.path.abspath("tests/temp/global/") + #setup paths and configs + self.ext_mgr.init_libraries() + self.ext_mgr.init_extension_config("user") + self.ext_mgr.init_extension_config("global") + #run function + user_installed = self.ext_mgr.install_loaded() + self.assertEqual(user_installed, ["config_editor"]) + + #Test that the mock extension was loaded + self.assertTrue(self.ext_mgr.check_installed("config_editor")) + #Test that ONLY the mock extension was loaded and in the user section + one_item_only = self.ext_mgr.get_installed() + self.assertEqual(len(one_item_only), 1) + self.assertIn("config_editor", one_item_only) + self.assertEqual(one_item_only['config_editor'], 'user') + #Test that the config_manager was "initialized". + initialized = self.ext_mgr.get_extension_from_property("initialized", True) + self.assertIn("config_editor", initialized) + #test not initialize an existing, intialized, extension def test_get_extension_from_property(self): self.fail("This test for function get_extension_from_property (line 257) needs to be implemented") From d2f41cca3178d2e67bc870c1d1e1436dd06f2d75 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Tue, 8 Apr 2014 17:10:57 -0400 Subject: [PATCH 086/107] added tests for getting an installed extension from a property. --- commotion_client/utils/extension_manager.py | 4 +- commotion_client/utils/validate.py | 82 +++++++++++---------- tests/utils/extension_manager_tests.py | 18 ++++- 3 files changed, 62 insertions(+), 42 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 6fc84ff..486d170 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -297,7 +297,7 @@ def install_loaded(self, ext_type=None): return saved or [] def get_extension_from_property(self, key, val): - """Takes a property and returns all extensions who have the passed value set under the passed property. + """Takes a property and returns all INSTALLED extensions who have the passed value set under the passed property. Checks all installed extensions and returns the name of all extensions whose config contains the key:val pair passed to this function. @@ -312,8 +312,6 @@ def get_extension_from_property(self, key, val): Raises: KeyError: If the value requested is non-standard. """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() matching_extensions = [] if key not in self.config_keys: _error = self.translate("logs", "{0} is not a valid extension config value.".format(key)) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index aebd8f1..8193be7 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -17,6 +17,7 @@ import re import ipaddress import os +import zipfile #PyQt imports from PyQt4 import QtCore @@ -30,7 +31,7 @@ def __init__(self, config, directory=None): """ Args: config (dictionary): The config for the extension. - directory (string): Absolute Path to the directory containing the extension. If not specified the validator will ONLY check the validity of the config passed to it. + directory (string): Absolute Path to the directory containing the extension zipfile. If not specified the validator will ONLY check the validity of the config passed to it. """ self.config_values = ["name", "main", @@ -38,12 +39,14 @@ def __init__(self, config, directory=None): "menu_level", "parent", "settings", - "toolbar"] + "toolbar", + "initialized",] self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate self.config = config if directory: - self.directory = directory + #set extension directory to point at config zipfile in that directory + self.extension_path = directory self.errors = None @property @@ -63,25 +66,27 @@ def config(self, value): @property - def directory(self): - return self._directory + def extension_path(self): + return self._extension_path - @directory.setter - def directory(self, value): + @extension_path.setter + def extension_path(self, value): + """Takes any directory passed to it and specifies the config file """ value_dir = QtCore.QDir(value) #Check that the directory in fact exists. if not value_dir.exists(): raise NotADirectoryError(self.translate("logs", "The directory should, by definition, actually be a directory. What was submitted was not a directory. Please specify the directory of an existing extension to continue.")) #Check that there are files in the directory provided - value_dir.setFilter(QtCore.QDir.NoDotAndDotDot|QtCore.QDir.NoSymLinks) - if not value_dir.entryList(): - raise FileNotFoundError(self.translate("logs", "There are no files in the extension directory provided. Is an extension directory without any files an extension directory at all? We will ponder these mysteries while you check to see if the extension directory provided is correct." )) + if not value_dir.exists(self.config['name']): + raise FileNotFoundError(self.translate("logs", "The extension is not in the extension directory provided. Is an extension directory without an extension an extension directory at all? We will ponder these mysteries while you check to see if the extension directory provided is correct." )) #Check that we can read the directory and its files. Sadly, QDir.isReadable() is broken on a few platforms so we check that and use the file filter to check each file. - value_dir.setFilter(QtCore.QDir.Executable|QtCore.QDir.Files) + value_dir.setFilter(QtCore.QDir.Readable|QtCore.QDir.Files) file_list = value_dir.entryInfoList() if not file_list or not value_dir.isReadable(): - raise PermissionError(self.translate("logs", "The application does not have permission to read any files within this directory. How is it supposed to validate the extension then? You ask. It can't. Please modify the permissions on the directory to allow the application to read the files within.")) - self._directory = value + raise PermissionError(self.translate("logs", "The application does not have permission to read any files within this directory. How is it supposed to validate the extension within then? You ask. It can't. Please modify the permissions on the directory and files within to allow the application to read the extension file.")) + #Set the extension "directory" to point at the extension zipfile + path = os.path.join(value, self.config['name']) + self._extension_path = path def validate_all(self): """Run all validation functions on an uncompressed extension. @@ -137,10 +142,10 @@ def gui(self, gui_name): val = str('main') file_name = val + ".py" if not self.check_path(file_name): - self.log.warn(self.translate("logs", "The extensions {0} file name is invalid for this system.".format(gui_name))) + self.log.warning(self.translate("logs", "The extensions {0} file name is invalid for this system.".format(gui_name))) return False if not self.check_exists(file_name): - self.log.warn(self.translate("logs", "The extensions {0} file does not exist.".format(gui_name))) + self.log.warning(self.translate("logs", "The extensions {0} file does not exist.".format(gui_name))) return False return True @@ -148,13 +153,13 @@ def name(self): try: name_val = str(self.config['name']) except KeyError: - self.log.warn(self.translate("logs", "There is no name value in the config file. This value is required.")) + self.log.warning(self.translate("logs", "There is no name value in the config file. This value is required.")) return False if not self.check_path_length(name_val): - self.log.warn(self.translate("logs", "This value is too long for your system.")) + self.log.warning(self.translate("logs", "This value is too long for your system.")) return False if not self.check_path_chars(name_val): - self.log.warn(self.translate("logs", "This value uses invalid characters for your system.")) + self.log.warning(self.translate("logs", "This value uses invalid characters for your system.")) return False return True @@ -166,10 +171,10 @@ def menu_item(self): if self.name(): val = str(self.config["name"]) else: - self.log.warn(self.translate("logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid.")) + self.log.warning(self.translate("logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid.")) return False if not self.check_menu_text(val): - self.log.warn(self.translate("logs", "The menu_item value is invalid")) + self.log.warning(self.translate("logs", "The menu_item value is invalid")) return False return True @@ -181,7 +186,7 @@ def parent(self): self.log.info(self.translate("logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used.")) return True if not self.check_menu_text(val): - self.log.warn(self.translate("logs", "The parent value is invalid")) + self.log.warning(self.translate("logs", "The parent value is invalid")) return False return True @@ -196,7 +201,7 @@ def menu_level(self): self.log.info(self.translate("logs", "The 'menu_level' value set in the config is not a number and is therefore invalid.")) return False if not 0 < val > 100: - self.log.warn(self.translate("logs", "The menu_level is invalid. Choose a number between 1 and 100")) + self.log.warning(self.translate("logs", "The menu_level is invalid. Choose a number between 1 and 100")) return False return True @@ -208,7 +213,7 @@ def tests(self): val = str('tests') file_name = val + ".py" if not self.check_path(file_name): - self.log.warn(self.translate("logs", "The extensions 'tests' file name is invalid for this system.")) + self.log.warning(self.translate("logs", "The extensions 'tests' file name is invalid for this system.")) return False if not self.check_exists(file_name): self.log.info(self.translate("logs", "The extensions 'tests' file does not exist. But tests are not required. Shame on you though, SHAME!.")) @@ -221,22 +226,23 @@ def check_menu_text(self, menu_text): @param menu_text string The text that will appear in the menu. """ if not 3 < len(str(menu_text)) < 40: - self.log.warn(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) + self.log.warning(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) return False else: return True def check_exists(self, file_name): - """Checks if a specified file exists within a directory + """Checks if a specified file exists within an extension. @param file_name string The file name from a config file """ - if not self.directory: + if not self.extension_path: self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) return True - files = QtCore.QDir(self.directory).entryList() + ext_zip = zipfile.ZipFile(self.extension_path, 'r') + files = ext_zip.namelist() if not str(file_name) in files: - self.log.warn(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) + self.log.warning(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) return False else: return True @@ -247,10 +253,10 @@ def check_path(self, file_name): @param file_name string The string to check for validity. """ if not self.check_path_length(file_name): - self.log.warn(self.translate("logs", "This value is too long for your system.")) + self.log.warning(self.translate("logs", "This value is too long for your system.")) return False if not self.check_path_chars(file_name): - self.log.warn(self.translate("logs", "This value uses invalid characters for your system.")) + self.log.warning(self.translate("logs", "This value uses invalid characters for your system.")) return False return True @@ -267,12 +273,12 @@ def check_path_chars(self, file_name): "linux" : "[/\x00]"} if platform and reserved[platform]: if re.search(file_name, reserved[platform]): - self.log.warn(self.translate("logs", "The extension's config file contains an invalid main value.")) + self.log.warning(self.translate("logs", "The extension's config file contains an invalid main value.")) return False else: return True else: - self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file uses chars that your system does not allow.").format(platform)) + self.log.warning(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file uses chars that your system does not allow.").format(platform)) return True @@ -281,7 +287,7 @@ def check_path_length(self, file_name=None): @param file_name string The string to check for validity. """ - if not self.directory: + if not self.extension_path: self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) return True # file length limit @@ -294,18 +300,18 @@ def check_path_length(self, file_name=None): extension_path = os.path.join(QtCore.QDir.currentPath(), "extensions") full_path = os.path.join(extension_path, file_name) if len(str(full_path)) > 255: - self.log.warn(self.translate("logs", "The full extension path cannot be greater than 260 chars")) + self.log.warning(self.translate("logs", "The full extension path cannot be greater than 260 chars")) return False else: return True elif platform in name_limit: if len(str(file_name)) >= 260: - self.log.warn(self.translate("logs", "File names can not be greater than 260 chars on your system")) + self.log.warning(self.translate("logs", "File names can not be greater than 260 chars on your system")) return False else: return True else: - self.log.warn(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) + self.log.warning(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) return True class Networking(object): @@ -323,13 +329,13 @@ def ipaddr(self, ip_addr, addr_type=None): try: addr = ipaddress.ip_address(str(ip_addr)) except ValueError: - self.log.warn(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip_addr)) + self.log.warning(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip_addr)) return False if addr_type: if addr.version == addr_type: return True else: - self.log.warn(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip_addr, addr_type)) + self.log.warning(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip_addr, addr_type)) return False else: return True diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index e4853ed..e76b2d1 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -154,7 +154,23 @@ def test_install_loaded(self): #test not initialize an existing, intialized, extension def test_get_extension_from_property(self): - self.fail("This test for function get_extension_from_property (line 257) needs to be implemented") + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Install loaded configs + self.ext_mgr.install_loaded() + + has_value = self.ext_mgr.get_extension_from_property("menu_item", "Commotion Config File Editor") + self.assertIn("config_editor", has_value) + #key MUST be one of the approved keys + with self.assertRaises(KeyError): + self.ext_mgr.get_extension_from_property('pineapple', "made_of_fire") + does_not_have = self.ext_mgr.get_extension_from_property("menu_item", "I am not called this") + self.assertNotIn("config_editor", does_not_have) + + + def test_get_property(self): self.fail("This test for function get_property (line 302) needs to be implemented") From c353b40cc2c32931f20dcd8dc1e6f9441faad8a9 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 10 Apr 2014 11:18:18 -0400 Subject: [PATCH 087/107] Made Readme valuable for this project --- README.md | 194 +++++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index a0d616f..c3322c2 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,103 @@ -##Commotion Linux - Python Implementation - -##INTRODUCTION -This is an initial implementation of core Commotion functionality in the form of a python module (commotionc) and related scripts. None of the code in this bundle is intended to be called directly, but rather serves as a backend for commotion-mesh-applet and nm-applet-olsrd (although if you really want to you can use fallback.py as a basic command-line interface to bring Commotion up and down). Future versions of this code will allow for full command-line control of the Commotion software stack via one unified binary. - -##PRE-REQUISITES -1. Confirm that you have a wireless adapter whose driver supports both the cfg80211 kernel interface and ibss mode. A list of drivers that support these features can be found at http://wireless.kernel.org/en/users/Drivers. -2. Ensure that you are running an up-to-date OS and kernel. Where possible, download and install the newest kernel your OS supports (optimally 3.5 or higher). Many wireless drivers are embedded in the kernel, and some of these have only recently gotten full cfg80211 support. -3. If you are running an OS that uses a version of wpasupplicant older than v. 1.0, you will either need to recompile wpasupplicant for your platform with support for IBSS\_RSN, or install the commotion-wpasupplicant package. If you opt to use commotion-wpasupplicant, your system will only be able connect in "fallback" mode, which entirely bypasses network manager. - -##INSTALLATION ----------- -If you are using Debian, Ubuntu, Mint, or any other Debian-derivative, you can -install Commotion by downloading all .deb packages located at -https://downloads.commotionwireless.net/linux, and installing them with: - -sudo dpkg -i \*.deb - -If you encounter any dependency errors during this process, simply run: - -apt-get install -f - -to resolve the problems, and then run the original dpkg command once again. - -##USAGE -###Step 1 -Define a new mesh network profile or modify the default profile in `/etc/commotion/profiles.d/`. You can define as many different network profiles as you wish, one per file. .profile files consist of simple parameter=value pairs, one per line. The default `commotionwireless.net.profile` file installed by the commotion-mesh-applet package shows all available parameters: - -``` -ssid=commotionwireless.net -#Network name (REQUIRED) -bssid=02:CA:FF:EE:BA:BE -#IBSS cell ID, which takes the form of a fake mac address. If this field is omitted, it will be automatically generated via an md4 hash of the ssid and channel. -channel=5 -#2.4 GHz Channel (REQUIRED) -ip=5.0.0.0 -#When ipgenerate=true, ip holds the base address from which the actual ip will be generated. When ipgenerate=false, ip holds the actual ip that will be used for the connection (REQUIRED) -ipgenerate=true -#See note for ip parameter. ipgenerate is automatically set to false once a permanent ip has been generated (REQUIRED) -netmask=255.0.0.0 -#The subnet mask of the network (REQUIRED) -dns=8.8.8.8 -#DNS server (REQUIRED) -psk=meshpassword -#The password required to connect to an IBSS-RSN encrypted mesh network. When connecting to a network with an encrypted backhaul, this parameter is required. When connecting to a networking without encryption, the parameter should be omitted entirely. -``` - -###Step 2 -Once you have either modified the default profile or installed a new one, you will need to force the various Commotion helper applets to reparse the profiles.d directory, like so: -* **commotion-mesh-applet:** Restart commotion-mesh-applet either by logging out of your current user session and logging in again, or exiting the applet and then running it directly from the command line: `/usr/bin/gnome-applets/commotion-mesh-applet`. -* **nm-dispatcher-olsrd:** Have network manager connect or disconnect to a network - but *NOT* the mesh network you're trying to connect to. This will force the dispatcher script to run, which will pull the updates to the `profiles.d` directory into the appropriate connection files in `/etc/NetworkManager/system-connections/`. You can can confirm that the new Commotion settings have been accepted by looking at the appropriate network profile in the nm-applet interface, and/or the contents of `/etc/NetworkManager/system-connections/` - -###Step 3 -Click on the mesh profile you wish to connect to in the list of networks shown by commotion-mesh-applet. If your system is capable of using the "network manager" connection path, Network Manager will activate the specified connection, and nm-applet will display an ad-hoc icon once you are connected. If your system relies on the "fallback" connection path, Network Manager will be put to sleep when the mesh network is activated, and will remain so until the mesh connection is deactivated. In the fallback case, all networking mechanics are handled directly by wpasupplicant and calls to ifconfig. - -###Step 4 -When you wish to restore normal networking functionality, click in commotion-mesh-applet. - -##TROUBLESHOOTING NOTES -* Logging for all commotion modules is handled by syslog, with each message prefixed by the module that generated it (ie, `nm-dispatcher-olsrd.log`). -* Hence, a useful command to run while trying to connect to a mesh might be: *tail -f `/var/log/syslog` | grep -e commotion -e nm-dispatcher* -* Some additional function failure information may be dumped to standard out by commotion-mesh-applet, so if you're having trouble connecting you should close the applet and restart it from a command line, so that you can see its output. -* If you are using the "network manager" connection path, you might want to launch and keep *wpa_cli* open while you're trying to connect. This will allow you to see whether or not the Linux client is able to successfully complete the authentication handshake with the rest of the network. -* When connecting to encrypted mesh networks, you want to see output that says "Key negotiation completed with " immediately after you connect. In the fallback case, this output should be dumped to stdout by commotion-mesh-applet. In the "network manager" connection process, this output should be shown by *wpa_cli*. -* Some drivers much more likely to properly connect to a mesh after being unloaded from and then reloaded into the kernel. Weirdly, this also applies to olsrd: If everything else seems to be working, but olsrd refuses to get routes, try unloading/reloading the driver, and reconnecting. -* iwconfig and iw both lie horribly about data such as active channel and authentication status. Don't necessarily believe what they tell you. - -##BUGS ----- -If you encounter any problems or wish to request features, please add them to -our issue tracker: - -https://github.com/opentechinstitute/nm-dispatcher-olsrd - -##BUILDING --------- -If you are on a non-Debian-derivitive GNU/Linux distro, then you'll need to -install this manually. We are looking for contributions of packaging to make -this easy for people to do. - -Check the `debian/control` file for a list of standard libraries that are -required. Here are the other libraries needed: - -* https://github.com/opentechinstitute/commotion-linux-py -* https://pypi.python.org/pypi/python-networkmanager - -This project relies heavily on the NetworkManager 0.9.x dbus API, currently -via the python-networkmanager library available on pypi: - -http://people.redhat.com/dcbw/NetworkManager/NetworkManager%20DBUS%20API.txt -http://projects.gnome.org/NetworkManager/developers/api/09/spec.html +##Commotion Client (UNSTABLE) + +The Commotion Wireless desktop/laptop client. + +To allow desktop clients to create, connect to, and configure Commotion wireless mesh networks. + +This repository is in active development. IT DOES NOT WORK! Please look at the roadmap below to see where the project is currently at. + +###FUTURE Features: + + * A graphical user interface with: + * A "setup wizard" for quickly creating/connecting to a Commotion mesh. + * Mesh network advances settings configuration tools + * Commotion mesh config customizer + * Application system with: + * Mesh network application viewer + * Client application advertisement + * Multiple user accounts with: + * Seperate "Serval Keychains" + * Custom Network & Application Settings + * A status bar icon for selecting, connecting to, and disconnecting from ad-hoc networks + * A robust extension system that allows for easy customization and extension of the core platform + * Full string translation & internationalization support + * Built in accessability support + +###Requirements: ( To run ) + + * Python 3 or higher + +###Requirements: ( To build from source ) + + * Python 3.3 or higher + * cx_freeze (See: build/README.md for instructions) + +###Current Roadmap: + + * Core application + * Single application support + * Cross-application instance messaging + * Crash reporting + * With PGP encryption to the Commotion Team (planned) + * unit tests (planned) + * Main Window + * unit tests (planned) + * Menu Bar + * Automatically displays all core and user loaded extensions (planned) + * Unit Tests (planned) + * Task Bar + * unit tests (planned) + * Extension Manager + * unit tests (planned) + * Core Extensions + * Network vizualizer (planned) + * unit tests (planned) + * Commotion Config File Editor (planned) + * unit tests (planned) + * Setup Wizard (planned) + * unit tests (planned) + * User Settings [applications] (planned) + * unit tests (planned) + * User Settings [Serval & Security] (planned) + * unit tests (planned) + * Application Viewer (planned) + * unit tests (planned) + * Application Advertiser (planned) + * unit tests (planned) + * Welcome Page (planned) + * Crash Window + * unit tests (planned) + * Network Status overview (planned) + * unit tests (planned) + * Setting menu + * unit tests (planned) + * Core application settings + * unit tests (planned) + * User settings + * unit tests (planned) + * Extension settings menu + * unit tests (planned) + * Settings for any extensions with custom settings pages + * Commotion Service Manager integration + * unit tests (planned) + * CSM python bindings + * Threaded messaging to CSM (planned) + * Application viewer (planned) + * Application advertiser (planned) + * Commotion Controller + * unit tests (planned) + * Threaded messaging (planned) + * Messaging objects to pass to extensions (planned) + * Network agent interceptor [for extending commotiond functionality across platforms] (planned) + * Commotiond integration (planned) + * Control Panel settings menu + * A client agnostic control panel tool for mesh-network settings in an operating systems generic control panel. (planned) + * unit tests (planned) + * Linux Support (planned) + * Windows Support (planned LONGTERM) + * OSX Support (planned LONGTERM) + * Commotion Human Interface Guidelines compliant interface (planned) + * In-Line Documentation tranlation into developer API (planned) + + + From 979d0efa23f5119701c2973b7955b0756edd0505 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Thu, 10 Apr 2014 14:33:10 -0400 Subject: [PATCH 088/107] updated extension manager installing and uninstalling. --- commotion_client/utils/extension_manager.py | 125 ++++++++++++-------- 1 file changed, 76 insertions(+), 49 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 486d170..928b875 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -89,10 +89,10 @@ def init_extension_libraries(self): #create directory structures if needed self.init_libraries() #load core and move to global if needed - self.load_install_core() + self.load_core() #Load all extension configs found in libraries self.init_extension_config() - #install all loaded config's + #install all loaded config's with the existing settings self.install_loaded() def set_library_defaults(self): @@ -265,7 +265,9 @@ def load_core(self): def install_loaded(self, ext_type=None): """Installs loaded libraries by saving their settings into the application settings. - + + This function will install all loaded libraries into the users settings. It will add any missing configs and values that are not found. If a value exists install loaded will not change it. + Args: ext_type (string): A specific extension type [global or user] to load extensions from. If not provided, defaults to both. @@ -276,16 +278,17 @@ def install_loaded(self, ext_type=None): Note on core: Core extensions are never "installed" they are used to populate the global library and then installed under global settings. """ - _keys = self.user_settings.allKeys() + _settings = self.user_settings + _keys = _settings.childKeys() extension_types = ['user', 'global'] if ext_type and str(ext_type) in extension_types: extension_types = [ext_type] + saved = [] for type_ in extension_types: - saved = [] ext_configs = self.extensions[type_].configs if not ext_configs: self.log.info(self.translate("logs", "No extensions of type {0} are currently loaded.".format(type_))) - return [] + continue for _config in ext_configs: #Only install if not already installed in this section. if _config['name'] not in _keys: @@ -294,7 +297,17 @@ def install_loaded(self, ext_type=None): self.log.warning(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) else: saved.append(_config['name']) - return saved or [] + else: + modified_config = False + _settings.beginGroup(_config['name']) + for key in _config.keys(): + if not _settings.value(key): + modified_config = True + _settings.setValue(key, _config[key]) + _settings.endGroup() + if modified_config: + saved.append(_config['name']) + return saved def get_extension_from_property(self, key, val): """Takes a property and returns all INSTALLED extensions who have the passed value set under the passed property. @@ -333,77 +346,84 @@ def get_extension_from_property(self, key, val): def get_property(self, name, key): """ - Get a property of an installed extension. + Get a property of an installed extension from the user settings. + Args: name (string): The extension's name. key (string): The key of the value you are requesting from the extension. Returns: - A STRING containing the value associated the extensions key in the applications saved extension settings. + A the (string) value associated the extensions key in the applications saved extension settings. Raises: KeyError: If the value requested is non-standard. """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() if key not in self.config_keys: _error = self.translate("logs", "That is not a valid extension config value.") raise KeyError(_error) _settings = self.user_settings - ext_type = self.get_type(name) - _settings.beginGroup(ext_type) _settings.beginGroup(name) setting_value = _settings.value(key) - if setting_value.isNull(): + if not setting_value: _error = self.translate("logs", "The extension config does not contain that value.") _settings.endGroup() - _settings.endGroup() raise KeyError(_error) else: - return setting_value.toStr() + _settings.endGroup() + return setting_value + + def load_user_interface(self, extension_name, subsection=None): + """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) - def get_type(self, name): - """Returns the extension type of an installed extension. - Args: - name (string): the name of the extension - + extension_name (string): The extension to load + subsection (string): Name of a objects sub-section. (settings, main, or toolbar) + Returns: - A string with the type of extension. "Core" or "Contrib" - - Raises: - KeyError: If an extension does not exist. + The ( class) contained within the module. """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - _settings = self.user_settings - core_ext = _settings.value("core/"+str(name)) - contrib_ext = _settings.value("contrib/"+str(name)) - if not core_ext.isNull() and contrib_ext.isNull(): - return "core" - elif not contrib_ext.isNull() and core_ext.isNull(): - return "contrib" - else: - _error = self.translate("logs", "This extension does not exist.") - raise KeyError(_error) - - def load_user_interface(self, extension_name, subsection=None): - """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) - @param extension_name string The extension to load - @subsection string Name of a objects sub-section. (settings, main, or toolbar) - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} - settings = self.load_settings(extension_name) + _config = self.get_config(extension_name) + try: + if _config['initialized'] != True: + self.log.debug(self.translate("logs", "Extension manager attempted to load a user interface from uninitalized extension {0}. Uninitialized extensions cannot be loaded. Try installing/initalizing the extension first.".format(extension_name))) + raise AttributeError(self.translate("logs", "Attempted to load a user interface from an uninitialized extension.")) + except KeyError: + self.log.debug(self.translate("logs", "Extension manager attempted to load a user interface from uninitalized extension {0}. Uninitialized extensions cannot be loaded. Try installing/initalizing the extension first.".format(extension_name))) + raise AttributeError(self.translate("logs", "Attempted to load a user interface from an uninitialized extension.")) if subsection: extension_name = extension_name+"."+settings[subsection] subsection = user_interface_types[subsection] extension = self.import_extension(extension_name, subsection) return extension + def get_config(self, name): + """Returns a config from an installed extension. + + Args: + name (string): An extension name. + + Returns: + A config (dictionary) for an extension. + + Raises: + KeyError: If an installed extension of the specified name does not exist. + """ + config = {} + _settings = self.user_settings + extensions = _settings.childKeys() + if name not in extensions: + raise KeyError(self.translate("logs", "No installed extension with the name {0} exists.".format(name))) + _settings.beginGroup(name) + extension_config = _settings.childKeys() + for key in extension_config: + config[key] = _settings.value(key) + _settings.endGroup() + return config + + @staticmethod def import_extension(extension_name, subsection=None): """ @@ -425,8 +445,6 @@ def load_settings(self, extension_name): @return dict A dictionary containing an extensions properties. """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() extension_config = {"name":extension_name} extension_type = self.extensions[extension_name] @@ -453,6 +471,7 @@ def load_settings(self, extension_name): if not extension_config['tests']: if "tests.py" in extension_files: extension_config['tests'] = "tests" + extension_config["initialized"] = _settings.value("initialized", True) _settings.endGroup() return extension_config @@ -698,7 +717,7 @@ def save_extension(self, extension, extension_type="contrib", unpack=None): def add_config(self, extension_dir, name): """Copies a config file to the "loaded" core extension config data folder. - + Args: extension_dir (string): The absolute path to the extension directory name (string): The name of the config file @@ -813,8 +832,16 @@ class InvalidSignature(Exception): class ConfigManager(object): + """A object for loading config data from a library. + + This object should only be used to load configs and saving/checking those values against the users settings. Any value checking should take place in the users settings. + """ def __init__(self, path=None): + """ + Args: + path (string): The path to an extension library. + """ #set function logger self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate From a76979a76927956a16eb764dc3fd60b631d16148 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 11 Apr 2014 10:53:26 -0400 Subject: [PATCH 089/107] Replaced load_settings with simpler get_config --- commotion_client/utils/extension_manager.py | 123 ++++++-------------- 1 file changed, 38 insertions(+), 85 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 928b875..6a0c6ea 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -42,6 +42,7 @@ import sys import zipfile import json +import zipimport #PyQt imports from PyQt4 import QtCore @@ -61,14 +62,15 @@ def __init__(self): self.libraries = {} self.user_settings = self.get_user_settings() self.config_keys = ["name", - "main", - "menu_item", - "menu_level", - "parent", - "settings", - "toolbar", - "tests", - "initialized",] + "main", + "menu_item", + "menu_level", + "parent", + "settings", + "toolbar", + "tests", + "initialized", + "type",] def get_user_settings(self): """Get the currently logged in user settings object.""" @@ -297,16 +299,6 @@ def install_loaded(self, ext_type=None): self.log.warning(self.translate("logs", "Extension {0} could not be saved.".format(_config['name']))) else: saved.append(_config['name']) - else: - modified_config = False - _settings.beginGroup(_config['name']) - for key in _config.keys(): - if not _settings.value(key): - modified_config = True - _settings.setValue(key, _config[key]) - _settings.endGroup() - if modified_config: - saved.append(_config['name']) return saved def get_extension_from_property(self, key, val): @@ -373,18 +365,21 @@ def get_property(self, name, key): _settings.endGroup() return setting_value - def load_user_interface(self, extension_name, subsection=None): - """Return the full extension object or one of its primary sub-objects (settings, main, toolbar) + def load_user_interface(self, extension_name, gui): + """Return the graphical user interface (settings, main, toolbar) from an initialized extension. Args: extension_name (string): The extension to load - subsection (string): Name of a objects sub-section. (settings, main, or toolbar) + gui (string): Name of a objects sub-section. (settings, main, or toolbar) Returns: The ( class) contained within the module. + Raise: + AttributeError: If an invalid gui type is requested or an uninitialized extension gui is requested. """ - - user_interface_types = {'main': "ViewPort", "setttings":"SettingsMenu", "toolbar":"ToolBar"} + if str(gui) not in ["settings", "main", "toolbar"]: + self.log.debug(self.translate("logs", "{0} is not a supported user interface type.".format(str(gui)))) + raise AttributeError(self.translate("logs", "Attempted to get a user interface of an invalid type.")) _config = self.get_config(extension_name) try: if _config['initialized'] != True: @@ -393,11 +388,21 @@ def load_user_interface(self, extension_name, subsection=None): except KeyError: self.log.debug(self.translate("logs", "Extension manager attempted to load a user interface from uninitalized extension {0}. Uninitialized extensions cannot be loaded. Try installing/initalizing the extension first.".format(extension_name))) raise AttributeError(self.translate("logs", "Attempted to load a user interface from an uninitialized extension.")) - if subsection: - extension_name = extension_name+"."+settings[subsection] - subsection = user_interface_types[subsection] - extension = self.import_extension(extension_name, subsection) - return extension + #Get ui file name and location of the extension from the settings. + ui_file = _config[gui] + _type = self.get_property(extension_name, "type") + extension_path = os.path.join(self.libraries[_type], extension_name) + #Get the extension + extension = zipimport.zipimporter(extension_path) + #add extension to sys path so imported modules can access other modules in the extension. + sys.path.append(extension_path) + user_interface = extension.load_module(ui_file) + if gui == "toolbar": + return user_interface.ToolBar() + elif gui == "main": + return user_interface.ViewPort() + elif gui == "settings": + return user_interface.SettingsMenu() def get_config(self, name): """Returns a config from an installed extension. @@ -413,7 +418,7 @@ def get_config(self, name): """ config = {} _settings = self.user_settings - extensions = _settings.childKeys() + extensions = _settings.childGroups() if name not in extensions: raise KeyError(self.translate("logs", "No installed extension with the name {0} exists.".format(name))) _settings.beginGroup(name) @@ -423,58 +428,6 @@ def get_config(self, name): _settings.endGroup() return config - - @staticmethod - def import_extension(extension_name, subsection=None): - """ - Load extensions by string name. - - @param extension_name string The extension to load - @param subsection string The module to load from an extension - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - if subsection: - extension = importlib.import_module("."+subsection, "extensions."+extension_name) - else: - extension = importlib.import_module("."+extension_name, "extensions") - return extension - - def load_settings(self, extension_name): - """Gets an extension's settings and returns them as a dict. - - @return dict A dictionary containing an extensions properties. - """ - extension_config = {"name":extension_name} - extension_type = self.extensions[extension_name] - - _settings = self.user_settings - _settings.beginGroup(extension_type) - extension_config['main'] = _settings.value("main").toString() - #get extension dir - main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") - main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QDir.mkpath(os.path.join(main_ext_type_dir, extension_config['name'])) - extension_files = extension_dirs.entryList() - if not extension_config['main']: - if "main.py" in extension_files: - extension_config['main'] = "main" - else: - _error = self.translate("logs", "Extension {0} does not contain a \"main\" extension file. Please re-load or remove this extension.".format(extension_name)) - raise IOError(_error) - extension_config['settings'] = _settings.value("settings", extension_config['main']).toString() - extension_config['toolbar'] = _settings.value("toolbar", extension_config['main']).toString() - extension_config['parent'] = _settings.value("parent", "Add-On's").toString() - extension_config['menu_item'] = _settings.value("menu_item", extension_config['name']).toString() - extension_config['menu_level'] = _settings.value("menu_level", 10).toInt() - extension_config['tests'] = _settings.value("tests").toString() - if not extension_config['tests']: - if "tests.py" in extension_files: - extension_config['tests'] = "tests" - extension_config["initialized"] = _settings.value("initialized", True) - _settings.endGroup() - return extension_config - def remove_extension_settings(self, name): """Removes an extension and its core properties from the applications extension settings. @@ -532,11 +485,11 @@ def save_settings(self, extension_config, extension_type="global"): try: _main = extension_config['main'] except KeyError: - if config_validator.main(): + if not config_validator.main(): _main = "main" #Set this for later default values - _settings.setValue("main", "main") - else: _settings.setValue("main", _main) + else: + _settings.setValue("main", _main) else: _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") self.log.error(_error) @@ -592,7 +545,7 @@ def save_settings(self, extension_config, extension_type="global"): #Extension Tests if config_validator.tests(): try: - _settings.setValue("main", extension_config['tests']) + _settings.setValue("tests", extension_config['tests']) except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) _settings.setValue("tests", "tests") From 66c26cc8933e2a2f927f193344d20e5fd7bf35ea Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 11 Apr 2014 14:06:00 -0400 Subject: [PATCH 090/107] updated the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3322c2..d5d8e19 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Commotion Wireless desktop/laptop client. To allow desktop clients to create, connect to, and configure Commotion wireless mesh networks. -This repository is in active development. IT DOES NOT WORK! Please look at the roadmap below to see where the project is currently at. +This repository is in active development. **IT DOES NOT WORK!** Please look at the roadmap below to see where the project is currently at. ###FUTURE Features: @@ -66,7 +66,7 @@ This repository is in active development. IT DOES NOT WORK! Please look at the r * unit tests (planned) * Welcome Page (planned) * Crash Window - * unit tests (planned) + * unit tests (planned) * Network Status overview (planned) * unit tests (planned) * Setting menu From b317b2e3a74de55e9ac6e4f30065303d7be40ea4 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 11 Apr 2014 14:30:47 -0400 Subject: [PATCH 091/107] specified a clearer set of test ignores. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9cc7a63..86613e4 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ build/lib build/resources #testing objects -tests/temp +tests/temp/* #auto-created commotion on failure of everywhere else commotion.log From 2fe78a742c8db9b9a3a4f1321e9f5371eb802c6d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 11 Apr 2014 15:50:27 -0400 Subject: [PATCH 092/107] Added built assets to mock dir for tests --- Makefile | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 45fb3a0..c496781 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,13 @@ .PHONY: build windows osx debian clean install tests -all: build windows debian osx +all: build -extensions: build_tree - python3.3 build/scripts/zip_extensions.py - -build: clean extensions assets +build: clean assets python3.3 build/scripts/build.py build + python3.3 build/scripts/zip_extensions.py -assets: build_tree +assets: + mkdir build/resources || true pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py windows: @@ -23,17 +22,16 @@ linux: build debian: @echo "debian packaging is not yet implemented" -build_tree: - mkdir build/resources || true - test: tests @echo "test build complete" -tests: clean build +tests: build mkdir tests/temp || true + mkdir tests/mock/assets || true + cp build/resources/commotion_assets_rc.py tests/mock/assets/. || true python3.3 tests/run_tests.py -clean: +clean: python3.3 build/scripts/build.py clean rm -fr build/resources/* || true rm -fr build/exe.* || true From 6cefb8c44bcbcd1bf4d52e0f9d24a80af97d221f Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Fri, 11 Apr 2014 17:18:38 -0400 Subject: [PATCH 093/107] added more test functions and fixed code --- commotion_client/utils/extension_manager.py | 190 ++++++++------- tests/utils/extension_manager_tests.py | 246 ++++++++++++++++++-- 2 files changed, 331 insertions(+), 105 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 6a0c6ea..da30263 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -82,6 +82,16 @@ def get_user_settings(self): else: raise TypeError(self.translate("logs", "User settings has a global scope and will not be loaded. Because, security.")) + def reset_settings_group(self): + """Resets the user_settings group to be at the top of the extensions group. + + Some functions modify the user_settings location to point at indiviudal extensions or other sub-groups. This function resets the settings to point at the top of the extensions group. + + NOTE: This should not be seen as a way to avoid doing clean up on functions you initiate. It is merely a way to ensure that on critical functions (deletions or modifications of existing settings) that errors do not cause data loss for users.. + """ + while self.user_settings.group(): + self.user_settings.endGroup() + self.user_settings.beginGroup("extensions") def init_extension_libraries(self): """This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them.""" @@ -430,17 +440,28 @@ def get_config(self, name): def remove_extension_settings(self, name): """Removes an extension and its core properties from the applications extension settings. + + long description + + Args: + name (str): the name of an extension to remove from the extension settings. - @param name str the name of an extension to remove from the extension settings. + Returns: + bool: True if extension is removed, false if it is not. + + Raises: + ValueError: When an empty string is passed as an argument. """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() + #make sure that a string of "" is not passed to this function because that would remove all keys. + self.reset_settings_group() if len(str(name)) > 0: _settings = self.user_settings _settings.remove(str(name)) + return True else: - _error = self.translate("logs", "You must specify an extension name greater than 1 char.") - raise ValueError(_error) + self.log.debug(self.translate("logs", "A zero length string was passed as the name of an extension to be removed. This would delete all the extensions if it was allowed to succeed.")) + raise ValueError(self.translate("logs", "You must specify an extension name greater than 1 char.")) + return False def save_settings(self, extension_config, extension_type="global"): """Saves an extensions core properties into the applications extension settings. @@ -452,107 +473,114 @@ def save_settings(self, extension_config, extension_type="global"): extension_type (string): Type of extension "user" or "global". Defaults to global. Returns: - bool: True if successful, False on failures - - Raises: - exception: Description. - + bool: True if successful, False on any failures """ _settings = self.user_settings #get extension dir try: extension_dir = self.libraries[extension_type] except KeyError: - self.log.warn(self.translate("logs", "Invalid extension type. Please check the extension type and try again.")) + self.log.warning(self.translate("logs", "Invalid extension type. Please check the extension type and try again.")) return False #create validator - config_validator = validate.ClientConfig(extension_config, extension_dir) + try: + config_validator = validate.ClientConfig(extension_config, extension_dir) + except KeyError as _excp: + self.log.warning(self.translate("logs", "The extension is missing a name value which is required.")) + self.log.debug(_excp) + return False + except FileNotFoundError as _excp: + self.log.warning(self.translate("logs", "The extension was not found on the system and therefore cannot be saved.")) + self.log.debug(_excp) + return False #Extension Name - if config_validator.name(): - try: - extension_name = extension_config['name'] + try: + extension_name = extension_config['name'] + if config_validator.name(): _settings.beginGroup(extension_name) - except KeyError: - _error = self.translate("logs", "The extension is missing a name value which is required.") + _settings.setValue("name", extension_name) + else: + _error = self.translate("logs", "The extension's name is invalid and cannot be saved.") self.log.error(_error) return False - else: - _error = self.translate("logs", "The extension's name is invalid and cannot be saved.") + except KeyError: + _error = self.translate("logs", "The extension is missing a name value which is required.") self.log.error(_error) return False #Extension Main - if config_validator.gui("main"): - try: - _main = extension_config['main'] - except KeyError: - if not config_validator.main(): - _main = "main" #Set this for later default values - _settings.setValue("main", _main) - else: + try: + _main = extension_config['main'] + if not config_validator.gui(_main): + _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") + self.log.error(_error) + return False + except KeyError: + _main = "main" #Set this for later default values + if not config_validator.gui(_main): _settings.setValue("main", _main) else: - _error = self.translate("logs", "The config's main value is invalid and cannot be saved.") - self.log.error(_error) - return False + _settings.setValue("main", _main) #Extension Settings & Toolbar for val in ["settings", "toolbar"]: - if config_validator.gui(val): - try: - _config_value = extension_config[val] - except KeyError: - #Defaults to main, which was checked and set before - _settings.setValue(val, _main) - else: - _settings.setValue(val, _config_value) + try: + _config_value = extension_config[val] + if not config_validator.gui(val): + _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.".format(val)) + self.log.error(_error) + return False + except KeyError: + #Defaults to main, which was checked and set before + _settings.setValue(val, _main) else: - _error = self.translate("logs", "The config's {0} value is invalid and cannot be saved.".format(val)) + _settings.setValue(val, _config_value) + #Extension Parent + try: + _parent = extension_config["parent"] + if config_validator.parent(): + _settings.setValue("parent", _parent) + else: + _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") self.log.error(_error) return False - #Extension Parent - if config_validator.parent(): - try: - _settings.setValue("parent", extension_config["parent"]) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) - _settings.setValue("parent", "Extensions") - else: - _error = self.translate("logs", "The config's parent value is invalid and cannot be saved.") - self.log.error(_error) - return False - + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "parent"))) + _settings.setValue("parent", "Extensions") #Extension Menu Item - if config_validator.menu_item(): - try: - _settings.setValue("menu_item", extension_config["menu_item"]) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) - _settings.setValue("menu_item", extension_name) - else: - _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") - self.log.error(_error) - return False + try: + _menu_item = extension_config["menu_item"] + if config_validator.menu_item(): + _settings.setValue("menu_item", _menu_item) + else: + _error = self.translate("logs", "The config's menu_item value is invalid and cannot be saved.") + self.log.error(_error) + return False + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_item"))) + _settings.setValue("menu_item", extension_name) #Extension Menu Level - if config_validator.menu_level(): - try: - _settings.setValue("menu_level", extension_config["menu_level"]) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) - _settings.setValue("menu_level", 10) - else: - _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") - self.log.error(_error) - return False + try: + _menu_level = extension_config["menu_level"] + if config_validator.menu_level(): + _settings.setValue("menu_level", _menu_level) + else: + _error = self.translate("logs", "The config's menu_level value is invalid and cannot be saved.") + self.log.error(_error) + return False + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "menu_level"))) + _settings.setValue("menu_level", 10) #Extension Tests - if config_validator.tests(): - try: - _settings.setValue("tests", extension_config['tests']) - except KeyError: - self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) - _settings.setValue("tests", "tests") - else: - _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_config['name'], _config_value)) - self.log.error(_error) - return False + try: + _tests = extension_config['tests'] + if config_validator.tests(): + _settings.setValue("tests", _tests) + else: + _error = self.translate("logs", "Extension {0} does not contain the {1} file listed in the config for its tests. Please either remove the listing to allow for the default value, or add the appropriate file.".format(extension_name, _config_value)) + self.log.error(_error) + return False + except KeyError: + self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(extension_name, "tests"))) + _settings.setValue("tests", "tests") #Write extension type _settings.setValue("type", extension_type) _settings.setValue("initialized", True) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index e76b2d1..b3d6924 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -1,3 +1,56 @@ +""" + +This program is a part of The Commotion Client + +Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +""" + + +""" +Unit Tests for commotion_client/utils/extension_manager.py + + +=== Mock Extension === +This set of tests uses a mock extension with the following properties. + +location: tests/mock/extensions/unit_test_mock + +---files in extension archive:--- + * main.py + * units.py + * test_bar.py + * __init__.py + * test.conf + * ui/Ui_test.py + * ui/test.ui + +---Config Values--- +"name":"mock_test_extension", +"menu_item":"A Mock Testing Object", +"parent":"Testing", +"main":"main", +"settings":"main", +"toolbar":"test_bar", +"tests":"units" + + +""" + + from PyQt4 import QtCore from PyQt4 import QtGui @@ -5,6 +58,8 @@ import unittest import re import os +import sys +import copy from commotion_client.utils import extension_manager @@ -139,19 +194,18 @@ def test_install_loaded(self): self.ext_mgr.init_extension_config("global") #run function user_installed = self.ext_mgr.install_loaded() - self.assertEqual(user_installed, ["config_editor"]) + self.assertEqual(user_installed, ["unit_test_mock"]) #Test that the mock extension was loaded - self.assertTrue(self.ext_mgr.check_installed("config_editor")) + self.assertTrue(self.ext_mgr.check_installed("unit_test_mock")) #Test that ONLY the mock extension was loaded and in the user section one_item_only = self.ext_mgr.get_installed() self.assertEqual(len(one_item_only), 1) - self.assertIn("config_editor", one_item_only) - self.assertEqual(one_item_only['config_editor'], 'user') + self.assertIn("unit_test_mock", one_item_only) + self.assertEqual(one_item_only['unit_test_mock'], 'user') #Test that the config_manager was "initialized". initialized = self.ext_mgr.get_extension_from_property("initialized", True) - self.assertIn("config_editor", initialized) - #test not initialize an existing, intialized, extension + self.assertIn("unit_test_mock", initialized) def test_get_extension_from_property(self): #setup directory with extension @@ -159,39 +213,183 @@ def test_get_extension_from_property(self): #setup config self.ext_mgr.init_extension_config("user") #Install loaded configs - self.ext_mgr.install_loaded() + self.ext_mgr.install_loaded("user") - has_value = self.ext_mgr.get_extension_from_property("menu_item", "Commotion Config File Editor") - self.assertIn("config_editor", has_value) + has_value = self.ext_mgr.get_extension_from_property("menu_item", "A Mock Testing Object") + self.assertIn("unit_test_mock", has_value) #key MUST be one of the approved keys with self.assertRaises(KeyError): self.ext_mgr.get_extension_from_property('pineapple', "made_of_fire") does_not_have = self.ext_mgr.get_extension_from_property("menu_item", "I am not called this") - self.assertNotIn("config_editor", does_not_have) - - + self.assertNotIn("unit_test_mock", does_not_have) def test_get_property(self): - self.fail("This test for function get_property (line 302) needs to be implemented") - - def test_get_type(self): - self.fail("This test for function get_type (line 333) needs to be implemented... but this function may actually need to be removed too.") + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Install loaded configs + self.ext_mgr.install_loaded("user") + #test that values which do exist are correct + menu_item = self.ext_mgr.get_property("unit_test_mock", "menu_item") + self.assertEqual(menu_item, "A Mock Testing Object") + #test that invalid keys are correct + with self.assertRaises(KeyError): + self.ext_mgr.get_property("unit_test_mock", "bunnies_per_second") + #test that valid keys, which don't exist in this extension settings are correct + #add a false value to the values checked against. + self.ext_mgr.config_keys.append('pineapple') + with self.assertRaises(KeyError): + self.ext_mgr.get_property("unit_test_mock", "pineapple") def test_load_user_interface(self): - self.fail("This test for function load_user_interface (line 359) needs to be implemented") + #Add required extension resources file from mock to path since we are not running it in the bundled state. + sys.path.append("tests/mock/assets") + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Install loaded configs + self.ext_mgr.install_loaded("user") + #test main viewport + main = self.ext_mgr.load_user_interface("unit_test_mock", "main") + self.assertTrue(main.is_loaded()) + #test pulling object from "main" file + settings = self.ext_mgr.load_user_interface("unit_test_mock", "settings") + self.assertTrue(settings.is_loaded()) + #test pulling object from another file + toolbar = self.ext_mgr.load_user_interface("unit_test_mock", "toolbar") + self.assertTrue(settings.is_loaded()) + #test invalid user interface type + with self.assertRaises(AttributeError): + self.ext_mgr.load_user_interface("unit_test_mock", "pineapple") + #reject uninitialized extensions + self.ext_mgr.user_settings.setValue("unit_test_mock/initialized", False) + with self.assertRaises(AttributeError): + self.ext_mgr.load_user_interface("unit_test_mock", "toolbar") - def test_import_extension(self): - self.fail("This test for function import_extension (line 376) needs to be implemented") + def test_get_config(self): + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Install loaded configs + self.ext_mgr.install_loaded("user") + #test that a full config is returned from a extension + config = self.ext_mgr.get_config("unit_test_mock") + correct_vals = {"menu_item":"A Mock Testing Object", + "parent":"Testing", + "main":"main", + 'name': 'unit_test_mock', + "settings":"main", + "toolbar":"test_bar", + "tests":"units", + "type":"user", + "menu_level":10, + "initialized":True} + self.assertDictEqual(config, correct_vals) + #test that a key error is raised on un-implemented extensions + with self.assertRaises(KeyError): + self.ext_mgr.get_config("pineapple") - def test_load_settings(self): - self.fail("This test for function load_settings (line 391) needs to be implemented") + def test_reset_settings_group(self): + #ensure that default is set to extensions + default = self.ext_mgr.user_settings.group() + self.assertEqual(default, "extensions") + #test it works when already in proper group + self.ext_mgr.reset_settings_group() + already_there = self.ext_mgr.user_settings.group() + self.assertEqual(already_there, "extensions") + + #create a set of groups nested down a few levels + self.ext_mgr.user_settings.setValue("one/two/three/four", True) + #move a level and ensure that .group() shows NOT in extensions + self.ext_mgr.user_settings.beginGroup("one") + one_lev = self.ext_mgr.user_settings.group() + self.assertNotEqual(one_lev, "extensions") + #Test that it works one group down + self.ext_mgr.reset_settings_group() + one_lev_up = self.ext_mgr.user_settings.group() + self.assertEqual(one_lev_up, "extensions") + #Move the rest of the way down. + self.ext_mgr.user_settings.beginGroup("one") + self.ext_mgr.user_settings.beginGroup("two") + self.ext_mgr.user_settings.beginGroup("three") + multi_lev = self.ext_mgr.user_settings.group() + self.assertNotEqual(multi_lev, "extensions") + #test it works multiple levels down. + self.ext_mgr.reset_settings_group() + multi_lev_up = self.ext_mgr.user_settings.group() + self.assertEqual(multi_lev_up, "extensions") def test_remove_extension_settings(self): - self.fail("This test for function remove_extension_settings (line 429) needs to be implemented... or moved to a settings module that will eventually handle all the encrypted stuff... but that actually might be better done outside of this.") - + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Install loaded configs + self.ext_mgr.install_loaded("user") + #test that a '' string raises an error + with self.assertRaises(ValueError): + self.ext_mgr.remove_extension_settings("") + #remove the "unit_test_mock" extension + self.ext_mgr.remove_extension_settings("unit_test_mock") + #test that it no longer exists. + with self.assertRaises(KeyError): + self.ext_mgr.get_config("unit_test_mock") + def test_save_settings(self): - self.fail("This test for function save_settings (line 444) needs to be implemented") + #setup directory with extension + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + #setup config + self.ext_mgr.init_extension_config("user") + #Test config added with proper values + config = self.ext_mgr.extensions["user"].find("unit_test_mock") + self.ext_mgr.save_settings(config, "user") + #Show that the extension group was created + name = self.ext_mgr.user_settings.childGroups() + self.assertEqual(name[0], "unit_test_mock") + #enter group and check values + self.ext_mgr.user_settings.beginGroup("unit_test_mock") + keys = self.ext_mgr.user_settings.childKeys() + for _k in list(config.keys()): + self.assertIn(_k, keys) + self.assertEqual(config[_k], self.ext_mgr.user_settings.value(_k)) + #check for type and initialization + self.assertIn('type', keys) + self.assertIn("initialized", keys) + self.ext_mgr.user_settings.endGroup() + #Check an invalid extension type + self.assertFalse(self.ext_mgr.save_settings(config, "pinapple")) + #Check an empty config fails + self.assertFalse(self.ext_mgr.save_settings({}, "user")) + #check a incorrect name fails (using longer string than all system's support) + name_conf = copy.deepcopy(config) + name_conf['name'] = "s2e" * 250 + self.assertFalse(self.ext_mgr.save_settings(name_conf, "user")) + #check that an empty name fails. + emp_name_conf = copy.deepcopy(config) + emp_name_conf['name'] = "" + self.assertFalse(self.ext_mgr.save_settings(emp_name_conf, "user")) + settings = {'toolbar':'main', + 'main':None, + 'settings':'main', + 'parent':'Extensions', + 'menu_item':'unit_test_mock', + 'menu_level':10, + 'tests':'tests'} + + for key in settings.keys(): + conf = copy.deepcopy(config) + #test invalid value + conf[key] = "s2e" * 250 + self.assertFalse(self.ext_mgr.save_settings(conf, "user")) + #test empty + del(conf[key]) + self.ext_mgr.save_settings(conf, "user") + self.assertEqual(self.ext_mgr.user_settings.value('unit_test_mock/'+key), settings[key]) + def test_save_extension(self): self.fail("This test for function save_extension (line 561) needs to be implemented") From fd4a089f283b14ad6f85e9d87869f399e1bb15ec Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 15:14:25 -0400 Subject: [PATCH 094/107] finished extension manager tests --- commotion_client/utils/extension_manager.py | 253 ++------------------ tests/utils/extension_manager_tests.py | 181 +++++++++++--- 2 files changed, 170 insertions(+), 264 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index da30263..c7de312 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -189,7 +189,7 @@ def init_extension_config(self, ext_type=None): ext_type (string): A specific extension type to load/reload a config object from. [global, user, or core]. If not provided, defaults to all. Raises: - ValueError: If the extension type passed is not either [core, global, or user] + ValueError: If the extension type passed is not either [core, global, or user] or is to an empty or invalid path. """ self.log.debug(self.translate("logs", "Initializing {0} extension configs..".format(ext_type))) extension_types = ['user', 'global', 'core'] @@ -199,7 +199,10 @@ def init_extension_config(self, ext_type=None): else: raise ValueError(self.translate("logs", "{0} is not an acceptable extension type.".format(ext_type))) for type_ in extension_types: - self.extensions[type_] = ConfigManager(self.libraries[type_]) + try: + self.extensions[type_] = ConfigManager(self.libraries[type_]) + except ValueError: + raise self.log.debug(self.translate("logs", "Configs for {0} extension library loaded..".format(type_))) def check_installed(self, name=None): @@ -297,8 +300,12 @@ def install_loaded(self, ext_type=None): extension_types = [ext_type] saved = [] for type_ in extension_types: - ext_configs = self.extensions[type_].configs - if not ext_configs: + try: + ext_configs = self.extensions[type_].configs + except KeyError: #Check if type has not been set yet + self.log.info(self.translate("logs", "No extensions of type {0} are currently loaded.".format(type_))) + continue + if not ext_configs: #Check if the type has been created and then emptied self.log.info(self.translate("logs", "No extensions of type {0} are currently loaded.".format(type_))) continue for _config in ext_configs: @@ -587,231 +594,6 @@ def save_settings(self, extension_config, extension_type="global"): _settings.endGroup() return True - def save_extension(self, extension, extension_type="contrib", unpack=None): - """Attempts to add an extension to the Commotion system. - - Args: - extension (string): The name of the extension - extension_type (string): Type of extension "contrib" or "core". Defaults to contrib. - unpack (string or QDir): Path to compressed extension - - Returns: - bool True if an extension was saved, False if it could not save. - - Raises: - exception: Description. - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - #There can be only two... and I don't trust you. - if extension_type != "contrib": - extension_type = "core" - if unpack: - try: - unpacked = QtCore.QDir(self.unpack_extension(unpack)) - except IOError: - self.log.error(self.translate("logs", "Failed to unpack extension.")) - return False - else: - self.log.info(self.translate("logs", "Saving non-compressed extension.")) - main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") - main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - unpacked = QtCore.QDir(os.path.join(main_ext_type_dir, extension)) - unpacked.mkpath(unpacked.absolutePath()) - config_validator = validate.ClientConfig() - try: - config_validator.set_extension(unpacked.absolutePath()) - except IOError: - self.log.error(self.translate("logs", "Extension is invalid and cannot be saved.")) - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) - return False - #Name - if config_validator.name(): - files = unpacked.entryInfoList() - for file_ in files: - if file_.suffix() == ".conf": - config_name = file_.fileName() - config_path = file_.absolutePath() - _config = self.config.load_config(config_path) - existing_extensions = self.config.find_configs("extension") - try: - assert _config['name'] not in existing_extensions - except AssertionError: - self.log.error(self.translate("logs", "The name given to this extension is already in use. Each extension must have a unique name.")) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) - return False - else: - self.log.error(self.translate("logs", "The extension name is invalid and cannot be saved.")) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) - return False - #Check all values - if not config_validator.validate_all(): - self.log.error(self.translate("logs", "The extension's config contains the following invalid value/s: [{0}]".format(",".join(config_validator.errors)))) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension's temp directory.")) - fs_utils.clean_dir(unpacked) - return False - #make new directory in extensions - main_ext_dir = os.path.join(QtCore.QDir.currentPath(), "extensions") - main_ext_type_dir = os.path.join(main_ext_dir, extension_type) - extension_dir = QtCore.QDir(os.path.join(main_ext_type_dir, _config['name'])) - extension_dir.mkdir(extension_dir.path()) - if unpack: - try: - fs_utils.copy_contents(unpacked, extension_dir) - except IOError: - self.log.error(self.translate("logs", "Could not move extension into main extensions directory from temporary storage. Please try again.")) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension's temp and main directory.")) - fs_utils.clean_dir(extension_dir) - fs_utils.clean_dir(unpacked) - return False - else: - if unpack: - fs_utils.clean_dir(unpacked) - try: - self.save_settings(_config, extension_type) - except KeyError: - self.log.error(self.translate("logs", "Could not save the extension because it was missing manditory values. Please check the config and try again.")) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension directory.")) - fs_utils.clean_dir(extension_dir) - self.log.info(self.translate("logs", "Cleaning settings.")) - self.remove_extension_settings(_config['name']) - return False - try: - self.add_config(unpacked.absolutePath(), config_name) - except IOError: - self.log.error(self.translate("logs", "Could not add the config to the core config directory.")) - if unpack: - self.log.info(self.translate("logs", "Cleaning extension directory and settings.")) - fs_utils.clean_dir(extension_dir) - self.log.info(self.translate("logs", "Cleaning settings.")) - self.remove_extension_settings(_config['name']) - return False - return True - - def add_config(self, extension_dir, name): - """Copies a config file to the "loaded" core extension config data folder. - - Args: - extension_dir (string): The absolute path to the extension directory - name (string): The name of the config file - - Returns: - bool: True if successful - - Raises: - IOError: If a config file of the same name already exists or the extension can not be saved. - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - data_dir = os.path.join(QtCore.QDir.currentPath(), "data") - config_dir = os.path.join(data_dir, "extensions") - #If the data/extensions folder does not exist, make it. - if not QtCore.Qdir(config_dir).exists(): - QtCore.Qdir(data_dir).mkdir("extensions") - source = QtCore.Qdir(extension_dir) - s_file = os.path.join(source.path(), name) - dest_file = os.path.join(config_dir, name) - if source.exists(name): - if not QtCore.QFile(s_file).copy(dest_file): - _error = QtCore.QCoreApplication.translate("logs", "Error saving extension config. File already exists.") - raise IOError(_error) - return True - - def remove_config(self, name): - """Removes a config file from the "loaded" core extension config data folder. - - Args: - name (string): The name of the config file - - Returns: - bool: True if successful - - Raises: - IOError: If a config file does not exist in the extension data folder. - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - data_dir = os.path.join(QtCore.QDir.currentPath(), "data") - config_dir = os.path.join(data_dir, "extensions") - config = os.path.join(config_dir, name) - if QtCore.QFile(config).exists(): - if not QtCore.QFile(config).remove(): - _error = QtCore.QCoreApplication.translate("logs", "Error deleting file.") - raise IOError(_error) - return True - - - def unpack_extension(self, compressed_extension): - """Unpacks an extension into a temporary directory and returns the location. - - @param compressed_extension string Path to the compressed_extension - @return A string object containing the absolute path to the temporary directory - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - temp_dir = fs_utils.make_temp_dir(new=True) - temp_abs_path = temp_dir.absolutePath() - try: - shutil.unpack_archive(compressed_extension, temp_abs_path, "gztar") - except FileNotFoundError: - _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was corrupted or mis-packaged.") - raise IOError(_error) - except FileNotFoundError: - _error = QtCore.QCoreApplication.translate("logs", "Could not load Commotion extension because it was not found.") - raise IOError(_error) - return temp_dir.absolutePath() - - def save_unpacked_extension(self, temp_dir, extension_name, extension_type): - """Moves an extension from a temporary directory to the extension directory. - - Args: - temp_dir (string): Absolute path to the temporary directory - extension_name (string): The name of the extension - extension_type (string): The type of the extension (core or contrib) - - Returns: - bool True if successful, false if unsuccessful. - - Raises: - ValueError: If an extension with that name already exists. - """ -# WRITE_TESTS_FOR_ME() -# FIX_ME_FOR_NEW_EXTENSION_TYPES() - extension_path = "extensions/"+extension_type+"/"+extension_name - full_path = os.path.join(QtCore.QDir.currentPath(), extension_path) - if not fs_utils.is_file(full_path): - try: - QtCore.QDir.mkpath(full_path) - try: - fs_utils.copy_contents(temp_dir, full_path) - except IOError as _excpt: - raise IOError(_excpt) - else: - temp_dir.rmpath(temp_dir.path()) - return True - except IOError: - self.log.error(QtCore.QCoreApplication.translate("logs", "Could not save unpacked extension into extensions directory.")) - return False - else: - _error = QtCore.QCoreApplication.translate("logs", "An extension with that name already exists. Please delete the existing extension and try again.") - raise ValueError(_error) - -class InvalidSignature(Exception): - """A verification procedure has failed. - - This exception should only be handled by halting the current task. - """ - pass - - class ConfigManager(object): """A object for loading config data from a library. @@ -835,7 +617,8 @@ def __init__(self, path=None): try: self.paths = self.get_paths(path) except TypeError: - self.log.info(self.translate("logs", "No extensions found in the {0} directory".format(path))) + self.log.debug(self.translate("logs", "No extensions found in the {0} directory. You must first populate the folder with extensions to init a ConfigManager in that folder. You can create a ConfigManager without a location specified, but you will have to add extensions before getting paths.".format(path))) + raise ValueError(self.translate("logs", "The path {0} is empty. ConfigManager could not be created".format(path))) else: self.log.info(self.translate("logs", "Extensions found in the {0} directory. Attempting to load extension configs.".format(path))) self.configs = list(self.get()) @@ -861,7 +644,7 @@ def find(self, name=None): @return list of tuples containing a config name and its config. """ if not self.configs: - self.log.warn(self.translate("logs", "No configs have been loaded. Please load configs first.".format(name))) + self.log.warning(self.translate("logs", "No configs have been loaded. Please load configs first.".format(name))) return False if not name: return self.configs @@ -888,8 +671,8 @@ def get_paths(self, directory): """ #Check the directory and dir_obj = QtCore.QDir(str(directory)) - if not dir_obj.exists(dir_obj.path()): - self.log.warn(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) + if not dir_obj.exists(dir_obj.absolutePath()): + self.log.warning(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) return False else: path = dir_obj.absolutePath() @@ -931,7 +714,7 @@ def get(self, paths=None): if config: yield config else: - self.log.warn(self.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) + self.log.warning(self.translate("logs", "Config file {0} does not exist and therefore cannot be loaded.".format(path))) def load(self, path): """This function loads the formatted config file and returns it. @@ -963,7 +746,7 @@ def load(self, path): data = json.loads(config.decode('utf-8')) self.log.info(self.translate("logs", "Successfully loaded {0}'s config file.".format(path))) except ValueError: - self.log.warn(self.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(path))) + self.log.warning(self.translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(path))) return False if data: self.log.debug(self.translate("logs", "Config file loaded.".format(path))) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index b3d6924..45df505 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -60,6 +60,8 @@ import os import sys import copy +import types + from commotion_client.utils import extension_manager @@ -114,9 +116,10 @@ def test_init_extension_config(self): #Check for an empty directory. self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/") - self.ext_mgr.init_extension_config('user') - self.assertFalse(self.ext_mgr.extensions['user'].has_configs()) - self.ext_mgr.extensions['user'] = None + with self.assertRaises(ValueError): + self.ext_mgr.init_extension_config('user') + with self.assertRaises(KeyError): + self.ext_mgr.extensions['user'].has_configs() #populate with populated directory self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") @@ -191,7 +194,8 @@ def test_install_loaded(self): #setup paths and configs self.ext_mgr.init_libraries() self.ext_mgr.init_extension_config("user") - self.ext_mgr.init_extension_config("global") + with self.assertRaises(ValueError): + self.ext_mgr.init_extension_config("global") #run function user_installed = self.ext_mgr.install_loaded() self.assertEqual(user_installed, ["unit_test_mock"]) @@ -390,41 +394,160 @@ def test_save_settings(self): self.ext_mgr.save_settings(conf, "user") self.assertEqual(self.ext_mgr.user_settings.value('unit_test_mock/'+key), settings[key]) - - def test_save_extension(self): - self.fail("This test for function save_extension (line 561) needs to be implemented") - - def test_add_config(self): - self.fail("This test for function add_config (line 670) needs to be implemented... but the function should actually just be removed.") - - def test_remove_config(self): - self.fail("This test for function remove_config (line 699) needs to be implemented... but the function should actually just be removed.") - - def test_unpack_extension(self): - self.fail("This test for function unpack_extension (line 723) needs to be implemented... but that function actually MUST be removed since we are not unpacking extension objects") - - def test_save_unpacked_extension(self): - self.fail("This test for function save_unpacked_extension (line 743) needs to be implemented... but that function actually MUST be removed since we are not unpacking extension objects") - class ConfigManagerTests(unittest.TestCase): - #Create a new setUp and CleanUP set of functions for these configManager tests + def setUp(self): + self.app = QtGui.QApplication([]) + self.app.setOrganizationName("test_case"); + self.app.setApplicationName("testing_app"); + + def tearDown(self): + self.app.deleteLater() + del self.app + self.app = None + #Delete everything under tests/temp + for root, dirs, files in os.walk(os.path.abspath("tests/temp/"), topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) def test_init(self): - self.fail("This test for the init function (line 788) needs to be implemented") - + #init config without any paths and ensure that it does not error out and creates the appropriate empty items + self.econfig = extension_manager.ConfigManager() + self.assertEqual(self.econfig.configs, []) + self.assertEqual(self.econfig.paths, []) + self.assertEqual(self.econfig.directory, None) + + #show creation with an empty directory raises the proper error. + with self.assertRaises(ValueError): + self.full_config = extension_manager.ConfigManager("tests/temp/") + + #init config with a working directory and ensure that everything loads appropriately. + self.full_config = extension_manager.ConfigManager("tests/mock/extensions") + self.assertEqual(self.full_config.configs, [ {'parent': 'Testing', + 'name': 'unit_test_mock', + 'tests': 'units', + 'settings': 'main', + 'toolbar': 'test_bar', + 'menu_item': 'A Mock Testing Object', + 'main': 'main'} ]) + self.assertEqual(self.full_config.paths, [os.path.abspath("tests/mock/extensions/unit_test_mock")]) + self.assertEqual(self.full_config.directory, "tests/mock/extensions") + def test_has_configs(self): - self.fail("This test for function has_configs (line 806) needs to be implemented") + #test without configs + self.empty_config = extension_manager.ConfigManager() + self.assertFalse(self.empty_config.has_configs()) + #test with configs + self.full_config = extension_manager.ConfigManager("tests/mock/extensions") + self.assertTrue(self.full_config.has_configs()) def test_find(self): - self.fail("This test for function find (line 817) needs to be implemented") + #test without configs + self.empty_config = extension_manager.ConfigManager() + #test returns False when configs are empty + self.assertFalse(self.empty_config.find()) + #ttest returns False on bad value with no configs + self.assertFalse(self.empty_config.find(), "NONE") + #test with configs + self.full_config = extension_manager.ConfigManager("tests/mock/extensions") + #test returns config list on empty args and empty configs + self.assertEqual(self.full_config.find(), [{'parent': 'Testing', + 'name': 'unit_test_mock', + 'tests': 'units', + 'settings': 'main', + 'toolbar': 'test_bar', + 'menu_item': 'A Mock Testing Object', + 'main': 'main'} ]) + #ttest returns False on bad value with no configs + self.assertFalse(self.full_config.find("NONE")) + #test returns corrent config when specified + dict_list = self.full_config.find("unit_test_mock") + self.assertDictEqual(dict_list, {'parent': 'Testing', + 'name': 'unit_test_mock', + 'tests': 'units', + 'settings': 'main', + 'toolbar': 'test_bar', + 'menu_item': 'A Mock Testing Object', + 'main': 'main'} ) def test_get_path(self): - self.fail("This test for function get_paths (line 838) needs to be implemented") + self.empty_config = extension_manager.ConfigManager() + #an empty path should raise an error + with self.assertRaises(TypeError): + self.empty_config.get_paths("tests/temp/") + #correct path should return the extensions absolute paths. + paths = self.empty_config.get_paths("tests/mock/extensions") + self.assertEqual(paths, [os.path.abspath("tests/mock/extensions/unit_test_mock")]) def test_get(self): - self.fail("This test for function get (line 881) needs to be implemented") - + self.empty_config = extension_manager.ConfigManager() + #a config that does not exist should return an empty list + self.assertEqual(list(self.empty_config.get(["tests/temp/i_dont_exist"])), []) + #correct path should return a generator with the extensions config file + config_path = os.path.abspath("tests/mock/extensions/unit_test_mock") + self.assertEqual(list(self.empty_config.get(["tests/mock/extensions/unit_test_mock"])), + [{'parent': 'Testing', + 'name': 'unit_test_mock', + 'tests': 'units', + 'settings': 'main', + 'toolbar': 'test_bar', + 'menu_item': 'A Mock Testing Object', + 'main': 'main'}]) + self.assertIs(type(self.empty_config.get(["tests/mock/extensions/unit_test_mock"])), types.GeneratorType) + def test_load(self): - self.fail("This test for function load (line 899) needs to be implemented") + self.empty_config = extension_manager.ConfigManager() + #a config that does not exist should return false + self.assertFalse(self.empty_config.load("tests/temp/i_dont_exist")) + #a object that is not a zipfile should return false as well + self.assertFalse(self.empty_config.load("tests/mock/assets/commotion_assets_rc.py")) + #correct path should return the extensions config file + config_path = os.path.abspath("tests/mock/extensions/unit_test_mock") + self.assertEqual(self.empty_config.load("tests/mock/extensions/unit_test_mock"), + {'parent': 'Testing', + 'name': 'unit_test_mock', + 'tests': 'units', + 'settings': 'main', + 'toolbar': 'test_bar', + 'menu_item': 'A Mock Testing Object', + 'main': 'main'}) + + self.fail("A broken extension with an invalid config needs to be added to make this set complete. Test commented out below.") + # with self.assertRaises(ValueError): + # self.empty_config.load("tests/mock/broken_extensions/non_json_config") + self.fail("A broken extension with a config file without the .conf name needs to be added. Test commented out below.") + #self.assertFalse(self.empty_config.load("tests/mock/broken_extensions/no_conf_prefix_config")) + + +def test_init_extension_config(self): + """Test that init extension config properly handles the various use cases.""" + #ext_type MUST be core|global|user + with self.assertRaises(ValueError): + self.ext_mgr.init_extension_config('pineapple') + + #Check for an empty directory. + self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/") + self.ext_mgr.init_extension_config('user') + self.assertFalse(self.ext_mgr.extensions['user'].has_configs()) + self.ext_mgr.extensions['user'] = None + + #populate with populated directory + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.init_extension_config('user') + self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) + self.ext_mgr.extensions['user'] = None + + #check all types on default call + self.ext_mgr.libraries['user'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['global'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.libraries['core'] = os.path.abspath("tests/mock/extensions/") + self.ext_mgr.init_extension_config() + self.assertTrue(self.ext_mgr.extensions['user'].has_configs()) + self.assertTrue(self.ext_mgr.extensions['global'].has_configs()) + self.assertTrue(self.ext_mgr.extensions['core'].has_configs()) + self.ext_mgr.extensions['user'] = None + self.ext_mgr.extensions['global'] = None + self.ext_mgr.extensions['core'] = None From efda8011c6ac99041cf724a4933c89d25bf5cd48 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 15:17:06 -0400 Subject: [PATCH 095/107] removed old config utility. Bundled the utility with the extension manager where it belongs. --- commotion_client/GUI/main_window.py | 1 - commotion_client/GUI/menu_bar.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index 2ed9f5f..ec46c44 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -23,7 +23,6 @@ from commotion_client.GUI.menu_bar import MenuBar from commotion_client.GUI.crash_report import CrashReport from commotion_client.GUI import welcome_page -from commotion_client.utils import config from commotion_client.utils import extension_manager class MainWindow(QtGui.QMainWindow): diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index ad9dbdc..ee4060b 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -21,7 +21,6 @@ from PyQt4 import QtGui #Commotion Client Imports -from commotion_client.utils import config from commotion_client.utils.extension_manager import ExtensionManager class MenuBar(QtGui.QWidget): @@ -72,9 +71,11 @@ def populate_menu(self): self.clear_layout(self.layout) menu_items = {} ext_mgr = ExtensionManager() - extensions = ext_mgr.get_installed().keys() + #extensions = ext_mgr.get_installed().keys() + extensions = None + ext_mgr.load_core() if not extensions: - ext_mgr.load_all() + ext_mgr.load_core() extensions = ext_mgr.get_installed().keys() if extensions: From a79d05ff5058592f465a34ee228faa4b8432b834 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 15:17:54 -0400 Subject: [PATCH 096/107] added missing tests value to config checks --- commotion_client/utils/validate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/commotion_client/utils/validate.py b/commotion_client/utils/validate.py index 8193be7..fc41ec4 100644 --- a/commotion_client/utils/validate.py +++ b/commotion_client/utils/validate.py @@ -40,6 +40,7 @@ def __init__(self, config, directory=None): "parent", "settings", "toolbar", + "tests", "initialized",] self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate From 858bcd6f4aca03c0fd5bf7a077801d58d94dc00f Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 15:41:03 -0400 Subject: [PATCH 097/107] fix translation and home directory creation errors Removed attempts at translation before application initializes. Added a check for the home directory before attempting to create it so that if it already exists the logger does not fail. --- commotion_client/utils/logger.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index c9c4e2a..3dc9658 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -38,7 +38,9 @@ class LogHandler(object): from commotion-client.utils import logger log = logger.getLogger("commotion_client"+__name__) - + + + NOTE: The exceptions in this function do not have translation implemented. This is that they are called before the QT application and, as such, are not pushed through QT's translation tools. This could be a mistake on the developers side, as he is a bit foggy on the specifics of QT translation. You can access the feature request at https://github.com/opentechinstitute/commotion-client/issues/24 """ def __init__(self, name, verbosity=None, logfile=None): @@ -74,7 +76,7 @@ def set_logfile(self, logfile=None): #if it does not exist try and create it if not log_dir.exists(): if not log_dir.mkpath(log_dir.absolutePath()): - raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.") self.logfile = log_dir.filePath("commotion.log") elif platform in ['win32', 'cygwin']: #Try ../AppData/Local/Commotion first @@ -82,7 +84,7 @@ def set_logfile(self, logfile=None): #if it does not exist try and create it if not log_dir.exists(): if not log_dir.mkpath(log_dir.absolutePath()): - raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.") self.logfile = log_dir.filePath("commotion.log") elif platform == 'linux': #Try /var/logs/ @@ -94,8 +96,8 @@ def set_logfile(self, logfile=None): #If fail then just write logs in home directory #TODO check if this is appropriate... its not. home = QtCore.QDir.home() - if not home.mkdir(".Commotion"): - raise NotADirectoryError(self.translate("logs", "Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.")) + if not home.exists(".Commotion") and not home.mkdir(".Commotion"): + raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '{0}/.Commotion' does not exist and could not be created.".format(home.absolutePath())) else: home.cd(".Commotion") self.logfile = home.filePath("commotion.log") @@ -103,7 +105,7 @@ def set_logfile(self, logfile=None): self.logfile = log_dir.filePath("commotion.log") else: #I'm out! - raise OSError(self.translate("logs", "Could not create a logfile.")) + raise OSError("Could not create a logfile.") def set_verbosity(self, verbosity=None, log_type=None): """Set's the verbosity of the logging for the application. From ef656ed057c8fd8b867983080aee95f9e369e3d7 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 16:06:12 -0400 Subject: [PATCH 098/107] Fixed pathing issues for changing dirs. QDir.setPath(\'x\') will change the directory using the current directory as a guide. QDir.cd(\'x\') will change the directory with THAT QDir's current directory as a guide. e.g. when calling code from /home/s2e/ where you create the QDir mydir = QtCore.QDir(\'/tmp/'). mydir.setPath(\'new\') == /home/s2e/new, mydir.cd(\'new\') == /tmp/new. --- commotion_client/utils/extension_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index c7de312..e0cc3c1 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -130,7 +130,7 @@ def set_library_defaults(self): """ #==== Core ====# _app_path = QtCore.QDir(QtCore.QCoreApplication.applicationDirPath()) - _app_path.setPath("extensions") + _app_path.cd("extensions") #set the core extension directory self.libraries['core'] = _app_path.absolutePath() self.log.debug(self.translate("logs", "Core extension directory succesfully set.")) @@ -160,7 +160,7 @@ def set_library_defaults(self): ext_dir = platform_dirs[platform][path_type+'_root'] ext_path = platform_dirs[platform][path_type] #move the root directory to the correct sub-path. - ext_dir.setPath(ext_path) + ext_dir.cd(ext_path) #Set the extension directory. self.libraries[path_type] = ext_dir.absolutePath() From bf883a0be503099e376be3bb45ee98fe9f54db23 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Mon, 14 Apr 2014 16:06:44 -0400 Subject: [PATCH 099/107] Added loggging and changed a warning to a raise --- commotion_client/utils/extension_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index e0cc3c1..4641997 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -662,18 +662,17 @@ def get_paths(self, directory): directory (string): The path to the folder that extension's are within. Extensions can be up to one level below the directory given. Returns: - config_files (array): An array of paths to all extension objects withj config files that were found. + config_files (array): An array of paths to all extension objects with config files that were found. Raises: TypeError: If no extensions exist within the directory requested. AssertionError: If the directory path does not exist. """ - #Check the directory and + #Check the directory and raise value error if not there dir_obj = QtCore.QDir(str(directory)) if not dir_obj.exists(dir_obj.absolutePath()): - self.log.warning(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) - return False + raise ValueError(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) else: path = dir_obj.absolutePath() @@ -707,7 +706,9 @@ def get(self, paths=None): """ #load config file if not paths: + self.log.debug(self.translate("logs", "No paths found. Attempting to load all extension manager paths list.")) paths = self.paths + self.log.debug(self.translate("logs", "Found paths:{0}.".format(paths))) for path in paths: if fs_utils.is_file(path): config = self.load(path) From c3c1584316638f6fe5f01718100319b2cbc1005d Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:19:19 -0400 Subject: [PATCH 100/107] removed unneeded type assignment. --- commotion_client/GUI/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index ec46c44..b686928 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -190,7 +190,7 @@ def load_settings(self): _settings.beginGroup("MainWindow") #Load settings from saved, or use defaults - geometry = _settings.value("geometry", defaults['geometry']).toRect() + geometry = _settings.value("geometry", defaults['geometry']) if geometry.isNull() == True: _error = self.translate("logs", "Could not load window geometry from settings file or defaults.") self.log.critical(_error) From 77428da4d1d1c0c69a319c3e69d4c94c19cfbdf1 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:19:56 -0400 Subject: [PATCH 101/107] re-connected the menu-bar to new extensions --- commotion_client/GUI/menu_bar.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index ee4060b..bfd74ea 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -36,6 +36,7 @@ def __init__(self, parent=None): #set function logger self.log = logging.getLogger("commotion_client."+__name__) self.translate = QtCore.QCoreApplication.translate + self.ext_mgr = ExtensionManager() try: self.populate_menu() except (NameError, AttributeError) as _excpt: @@ -70,14 +71,9 @@ def populate_menu(self): if not self.layout.isEmpty(): self.clear_layout(self.layout) menu_items = {} - ext_mgr = ExtensionManager() - #extensions = ext_mgr.get_installed().keys() - extensions = None - ext_mgr.load_core() - if not extensions: - ext_mgr.load_core() - extensions = ext_mgr.get_installed().keys() - + if not self.ext_mgr.check_installed(): + self.ext_mgr.init_extension_libraries() + extensions = self.ext_mgr.get_installed().keys() if extensions: top_level = self.get_parents(extensions) for top_level_item in top_level: @@ -117,7 +113,7 @@ def get_parents(self, extension_list): parents = [] for ext in extension_list: try: - parent = ExtensionManager.get_property(ext, "parent") + parent = self.ext_mgr.get_property(ext, "parent") except KeyError: self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(ext, "parent"))) parent = "Extensions" @@ -135,7 +131,7 @@ def add_menu_item(self, parent): Returns: A tuple containing a top level button and its hidden sub-menu items. """ - extensions = ExtensionManager.get_extension_from_property(parent, 'parent') + extensions = self.ext_mgr.get_extension_from_property('parent', parent) if not extensions: raise NameError(self.translate("logs", "No extensions found under the parent item {0}.".format(parent))) #Create Top level item button @@ -148,10 +144,10 @@ def add_menu_item(self, parent): for ext in extensions: sub_menu_item = subMenuWidget(self) try: - menu_item_title = ExtensionManager.get_property(ext, 'menu_item') + menu_item_title = self.ext_mgr.get_property(ext, 'menu_item') except KeyError: menu_item_title = ext - subMenuItem.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", menu_item_title)) + sub_menu_item.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", menu_item_title)) #We use partial here to pass a variable along when we attach the "clicked()" signal to the MenuBars requestViewport function sub_menu_item.clicked.connect(partial(self.request_viewport, ext)) sub_menu_layout.addWidget(sub_menu_item) From 1f29f2777d9d658cd2e1a728545070970c59c33a Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:21:30 -0400 Subject: [PATCH 102/107] merged core and global paths Core and global extensions will now both live in the application path to get around root permission issues. --- commotion_client/utils/extension_manager.py | 31 +++++++++++++++------ setup.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 4641997..0760748 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -101,9 +101,14 @@ def init_extension_libraries(self): #create directory structures if needed self.init_libraries() #load core and move to global if needed + self.log.debug(self.libraries) self.load_core() #Load all extension configs found in libraries - self.init_extension_config() + for name, path in self.libraries.items(): + self.log(path) + self.log(QtCore.QDir(path).entryInfoList()) + if QtCore.QDir(path).entryInfoList() != []: + self.init_extension_config(name) #install all loaded config's with the existing settings self.install_loaded() @@ -131,6 +136,7 @@ def set_library_defaults(self): #==== Core ====# _app_path = QtCore.QDir(QtCore.QCoreApplication.applicationDirPath()) _app_path.cd("extensions") + _app_path.cd("core") #set the core extension directory self.libraries['core'] = _app_path.absolutePath() self.log.debug(self.translate("logs", "Core extension directory succesfully set.")) @@ -154,15 +160,17 @@ def set_library_defaults(self): 'linux': { 'user':os.path.join(".Commotion", "extension_data"), 'user_root': QtCore.QDir.home(), - 'global':os.path.join("usr", "share", "Commotion", "extension_data"), - 'global_root' : QtCore.QDir.root()}} + 'global':os.path.join("extensions", "global"), + 'global_root' : QtCore.QDir(QtCore.QCoreApplication.applicationDirPath())}} for path_type in ['user', 'global']: ext_dir = platform_dirs[platform][path_type+'_root'] ext_path = platform_dirs[platform][path_type] + self.log.debug(self.translate("logs", "The root directory of {0} is {1}.".format(path_type, ext_dir.path()))) #move the root directory to the correct sub-path. - ext_dir.cd(ext_path) + lib_path = ext_dir.filePath(ext_path) + self.log.debug(self.translate("logs", "The extension directory has been set to {0}..".format(lib_path))) #Set the extension directory. - self.libraries[path_type] = ext_dir.absolutePath() + self.libraries[path_type] = lib_path def init_libraries(self): """Creates a library folder, if it does not exit, in the directories specified for the current user and for the global application. """ @@ -178,7 +186,9 @@ def init_libraries(self): if ext_dir.mkpath(ext_dir.absolutePath()): self.log.debug(self.translate("logs", "Created the {0} extension library at {1}".format(path_type, str(ext_dir.absolutePath())))) else: - raise IOError(self.translate("logs", "Could not create the user extension library for {0}.".format(path_type))) + self.log.debug(ext_dir.mkpath(ext_dir.absolutePath())) + self.log.debug(ext_dir.exists(ext_dir.absolutePath())) + raise IOError(self.translate("logs", "Could not create the extension library for {0}.".format(path_type))) else: self.log.debug(self.translate("logs", "The extension library at {0} already existed for {1}".format(str(ext_dir.absolutePath()), path_type))) @@ -189,7 +199,7 @@ def init_extension_config(self, ext_type=None): ext_type (string): A specific extension type to load/reload a config object from. [global, user, or core]. If not provided, defaults to all. Raises: - ValueError: If the extension type passed is not either [core, global, or user] or is to an empty or invalid path. + ValueError: If the extension type passed is not either [core, global, or user] """ self.log.debug(self.translate("logs", "Initializing {0} extension configs..".format(ext_type))) extension_types = ['user', 'global', 'core'] @@ -200,9 +210,14 @@ def init_extension_config(self, ext_type=None): raise ValueError(self.translate("logs", "{0} is not an acceptable extension type.".format(ext_type))) for type_ in extension_types: try: + self.log.debug(self.translate("logs", "Creating {0} config manager".format(type_))) self.extensions[type_] = ConfigManager(self.libraries[type_]) except ValueError: - raise + self.log.debug(self.translate("logs", "There were no extensions found for the {0} library.".format(type_))) + continue + except KeyError: + self.log.debug(self.translate("logs", "There were no library path found for the {0} library.".format(type_))) + continue self.log.debug(self.translate("logs", "Configs for {0} extension library loaded..".format(type_))) def check_installed(self, name=None): diff --git a/setup.py b/setup.py index a5d456f..a72c3b3 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ # Add core_extensions to core packages. for ext in core_extensions: ext_loc = os.path.join("build", "resources", ext) - asset_loc = os.path.join("extensions", ext) + asset_loc = os.path.join("extensions", "core", ext) all_assets.append((ext_loc, asset_loc)) From a151dba8235914c1dcede0288a07b8bfbbfa394a Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:22:20 -0400 Subject: [PATCH 103/107] Fixed extension initialization in client main --- commotion_client/commotion_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index 6d52a9d..c36f369 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -174,7 +174,7 @@ def start_full(self): """ extensions = extension_manager.ExtensionManager() if not extensions.check_installed(): - extensions.load_core() + extensions.init_extension_libraries() if not self.main: try: self.main = self.create_main_window() From 88c4cd8887b84c9e44d5b872d58944738af80833 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:25:44 -0400 Subject: [PATCH 104/107] Created a working mock extension --- .../config_editor/config_editor.conf | 2 +- .../extensions/config_editor/main.py | 12 +-- .../extensions/config_editor/test.py | 3 + .../extensions/unit_test_mock/__init__.py | 0 .../extensions/unit_test_mock/main.py | 70 +++++++++++++++++ .../extensions/unit_test_mock/test.conf | 9 +++ .../extensions/unit_test_mock/test.py | 5 ++ .../extensions/unit_test_mock/test_bar.py | 43 +++++++++++ .../extensions/unit_test_mock/ui/__init__.py | 0 .../extensions/unit_test_mock/ui/test.ui | Bin 67159 -> 65156 bytes .../extensions/unit_test_mock/units.py | 0 commotion_client/utils/settings.py | 73 ++++++++++++++++++ tests/mock/extensions/unit_test_mock | Bin 0 -> 136809 bytes 13 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 commotion_client/extensions/config_editor/test.py create mode 100644 commotion_client/extensions/unit_test_mock/__init__.py create mode 100644 commotion_client/extensions/unit_test_mock/main.py create mode 100644 commotion_client/extensions/unit_test_mock/test.conf create mode 100644 commotion_client/extensions/unit_test_mock/test.py create mode 100644 commotion_client/extensions/unit_test_mock/test_bar.py create mode 100644 commotion_client/extensions/unit_test_mock/ui/__init__.py rename tests/mock/extensions/config_editor => commotion_client/extensions/unit_test_mock/ui/test.ui (96%) create mode 100644 commotion_client/extensions/unit_test_mock/units.py create mode 100644 commotion_client/utils/settings.py create mode 100644 tests/mock/extensions/unit_test_mock diff --git a/commotion_client/extensions/config_editor/config_editor.conf b/commotion_client/extensions/config_editor/config_editor.conf index 90d574d..efcb25a 100644 --- a/commotion_client/extensions/config_editor/config_editor.conf +++ b/commotion_client/extensions/config_editor/config_editor.conf @@ -1,6 +1,6 @@ { "name":"config_editor", -"menuItem":"Commotion Config File Editor", +"menu_item":"Commotion Config File Editor", "parent":"Advanced", "main":"main" } diff --git a/commotion_client/extensions/config_editor/main.py b/commotion_client/extensions/config_editor/main.py index 63ed6cf..7d5f494 100644 --- a/commotion_client/extensions/config_editor/main.py +++ b/commotion_client/extensions/config_editor/main.py @@ -14,18 +14,18 @@ #Standard Library Imports import logging - +import sys #PyQt imports from PyQt4 import QtCore from PyQt4 import QtGui #import python modules created by qtDesigner and converted using pyuic4 #from extensions.core.config_manager.ui import Ui_config_manager.py -from docs.extensions.tutorial.config_manager.ui import Ui_config_manager.py +from ui import Ui_config_manager -class ViewPort(Ui_main.ViewPort): +class ViewPort(Ui_config_manager.ViewPort): """ - + pineapple """ start_report_collection = QtCore.pyqtSignal() @@ -37,10 +37,10 @@ def __init__(self, parent=None): self.setupUi(self) self.start_report_collection.connect(self.send_signal) - def send_signal(self): self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) def send_error(self): + """HI""" self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") - + pass diff --git a/commotion_client/extensions/config_editor/test.py b/commotion_client/extensions/config_editor/test.py new file mode 100644 index 0000000..7847780 --- /dev/null +++ b/commotion_client/extensions/config_editor/test.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + diff --git a/commotion_client/extensions/unit_test_mock/__init__.py b/commotion_client/extensions/unit_test_mock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/extensions/unit_test_mock/main.py b/commotion_client/extensions/unit_test_mock/main.py new file mode 100644 index 0000000..801e9fb --- /dev/null +++ b/commotion_client/extensions/unit_test_mock/main.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +main + +A unit test extension. Not for production. + +""" + +#Standard Library Imports +import logging +import sys +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +from ui import Ui_test + +class ViewPort(Ui_test.ViewPort): + """ + This is a mock extension and should not be used for ANYTHING user facing! + """ + + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__() + self.setupUi(self) + self.start_report_collection.connect(self.send_signal) + + def send_signal(self): + self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) + + def send_error(self): + """HI""" + self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") + pass + + def is_loaded(self): + return True + + +class SettingsMenu(Ui_test.ViewPort): + """ + This is a mock extension and should not be used for ANYTHING user facing! + """ + + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__() + self.setupUi(self) + self.start_report_collection.connect(self.send_signal) + + def send_signal(self): + self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) + + def send_error(self): + """HI""" + self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") + pass + + def is_loaded(self): + return True diff --git a/commotion_client/extensions/unit_test_mock/test.conf b/commotion_client/extensions/unit_test_mock/test.conf new file mode 100644 index 0000000..902b226 --- /dev/null +++ b/commotion_client/extensions/unit_test_mock/test.conf @@ -0,0 +1,9 @@ +{ +"name":"unit_test_mock", +"menu_item":"A Mock Testing Object", +"parent":"Testing", +"main":"main", +"settings":"main", +"toolbar":"test_bar", +"tests":"units" +} diff --git a/commotion_client/extensions/unit_test_mock/test.py b/commotion_client/extensions/unit_test_mock/test.py new file mode 100644 index 0000000..7e67062 --- /dev/null +++ b/commotion_client/extensions/unit_test_mock/test.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +def hello(): + a = 5 diff --git a/commotion_client/extensions/unit_test_mock/test_bar.py b/commotion_client/extensions/unit_test_mock/test_bar.py new file mode 100644 index 0000000..6ca343a --- /dev/null +++ b/commotion_client/extensions/unit_test_mock/test_bar.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_bar + +A unit test extension. Not for production. +""" + +#Standard Library Imports +import logging +import sys +#PyQt imports +from PyQt4 import QtCore +from PyQt4 import QtGui + +#import python modules created by qtDesigner and converted using pyuic4 +from ui import Ui_test + +class ToolBar(Ui_test.ViewPort): + """ + This is a mock extension and should not be used for ANYTHING user facing! + """ + + start_report_collection = QtCore.pyqtSignal() + data_report = QtCore.pyqtSignal(str, dict) + error_report = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__() + self.setupUi(self) + self.start_report_collection.connect(self.send_signal) + + def send_signal(self): + self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) + + def send_error(self): + """HI""" + self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") + pass + + def is_loaded(self): + return True diff --git a/commotion_client/extensions/unit_test_mock/ui/__init__.py b/commotion_client/extensions/unit_test_mock/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock/extensions/config_editor b/commotion_client/extensions/unit_test_mock/ui/test.ui similarity index 96% rename from tests/mock/extensions/config_editor rename to commotion_client/extensions/unit_test_mock/ui/test.ui index c1cd7452caa34ff49a942ceb7f8d70ab993651f5..3add7de86a7852153ebc9a7b82c4b9bf22dfd3f9 100644 GIT binary patch delta 11 ScmccK!_xAXd2=dT%Rc}iECq}J delta 2043 zcma)7-EJH;6rPX-q(Q0>DqNpTZZ>GMDWn%wL{y?AEg@~XOOP+x1yZ|r21vfkaLgEd00wg3VK6_^W!h-Pb?0SyR&v!nbW54-p`Pc7Os_yaf z%FV!yTfK{epX>+SXPCR^Lp-sPsTlNlBCOIK_jVCRlI1+wiRRX#b{OScmPeN7Sld^5 zuB=d!_PqzXCo)cl-i=c&nC8+#vD!<9OvXI%4w#UTW?3PJ6;U}_q+#a zPODRl*4MU5qqq7(ZgDxJVr0*i+z!`i^OH@ARU+hIhf14n-f@yJjG`!XreU}zNytiA z!7>^O{<2Wo65tmZvz)A=oIU51@FCAsf!r){W(3!rF#Mvg1y8A~iqa_?vge%mE6b%p z&IW#`Za1CZ4m~*+hGHhE58u!>Adym0ffiQV8*VMo8e%db%gpmW({3$Oa!~6q3kw*4 zd5aVb8D4Ph(59t~9o>dfrGYa(C)Iz(VxxI-)z)I5kuijPhnd{Y5yoRvwFLv2)h zaVYD(K)}3E(ovw&`CKQ2xePfe80o3{B;!<-NJ>a&EHr6u3V;(sULn1hdFVjtHYZ#^ zvtWSfgdT{#W_m&^qS_$t7fqn)i4tko6d|QTcYt$q5sR-)K?g1%ozF_W!(jJEkyMF zG8Sy?goF`ARpckl8`yVZW0|%+%^f#Dnq_XU(963GqiDMq_6)4BthM1S5@uOruu#ib z-KIpuwpQm_EA90w^xPu1FxaTUBxtl=&vjI<*D^dy+f<((yC*2gCJ~RiL^HHDIuqqO zmS2Kd2mMtRPlfj}S*p-OZf$YKj&+ITy$u}5r00r(Wx!VF4RAB!+IP+EB<>719i9u@ zit^DzUujXBs*7mIvXbBW6bGN5LD+~s`=@AoB6>YTk4NA+T?k;xHV&d1!ILkK&IsSV z>VytYPamDq!^5+)z59o2YikiaPmkV@t8Wi*+D^v%n2i}m9ZVJ7_3`J=cYk^FHVE9~ zE|pSjjSGF!1fSo1mE*wW+8vd. + +""" + +""" +CURRENTLY A DEVELOPMENT STUB! + + +setting.py + +The Settings Manager + +Key componenets handled within: + * Loading and Unloading User Settings Files + * Validating the scope of settings + +""" +#Standard Library Imports +import logging + +#PyQt imports +from PyQt4 import QtCore + +#Commotion Client Imports + + +class UserSettingsManager(object): + + def __init__(self): + """Create a settings object that is tied to a specific scope. + CURRENTLY A DEVELOPMENT STUB! + """ + self.settings = QtCore.QSettings() + + def save(self): + """CURRENTLY A DEVELOPMENT STUB!""" + #call PGP to save temporary file to correct encrypted file + pass + + def load(self): + """CURRENTLY A DEVELOPMENT STUB!""" + + #call pgp to get location of decrypted user file, if any + #load global settings file. + # QSettings.setUserIniPath (QString dir) + #get + pass + + def get(self): + """CURRENTLY A DEVELOPMENT STUB!""" + return self.settings + + + diff --git a/tests/mock/extensions/unit_test_mock b/tests/mock/extensions/unit_test_mock new file mode 100644 index 0000000000000000000000000000000000000000..7fe9a228acab988f54d7cad2747d550bb5ac954e GIT binary patch literal 136809 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 bw=HS>Pap^=dEa09AKzYD`ae)&Y3ct1&zk#2 literal 0 HcmV?d00001 From 43b449c167934be62d4c4fd4ce4cfa4de9c46eb0 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:26:41 -0400 Subject: [PATCH 105/107] Changed tests to reflect new global path --- tests/utils/extension_manager_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index 45df505..4723e48 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -173,6 +173,8 @@ def test_init_libraries(self): #init libraries from library defaults self.ext_mgr.set_library_defaults() user_dir = self.ext_mgr.libraries['user'] + #Set global path to be a temporary path because it pulls the application path, which is pythons /usr/local/bin path which we don't have permissions for. + self.ext_mgr.libraries['global'] = os.path.abspath("tests/temp/") global_dir = self.ext_mgr.libraries['global'] self.ext_mgr.init_libraries() self.assertTrue(os.path.isdir(os.path.abspath(user_dir))) @@ -477,6 +479,9 @@ def test_get_path(self): #an empty path should raise an error with self.assertRaises(TypeError): self.empty_config.get_paths("tests/temp/") + # a false path should raise an error + with self.assertRaises(ValueError): + self.empty_config.get_paths("tests/temp/pineapple") #correct path should return the extensions absolute paths. paths = self.empty_config.get_paths("tests/mock/extensions") self.assertEqual(paths, [os.path.abspath("tests/mock/extensions/unit_test_mock")]) From 867cb731af9385b9c43f4ef142a5bb2f2092b5ae Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 13:31:37 -0400 Subject: [PATCH 106/107] removed assertions for bygone exceptions --- tests/utils/extension_manager_tests.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py index 4723e48..fc362cd 100644 --- a/tests/utils/extension_manager_tests.py +++ b/tests/utils/extension_manager_tests.py @@ -114,10 +114,9 @@ def test_init_extension_config(self): with self.assertRaises(ValueError): self.ext_mgr.init_extension_config('pineapple') - #Check for an empty directory. + #Check that an empty directory does nothing. self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/") - with self.assertRaises(ValueError): - self.ext_mgr.init_extension_config('user') + self.ext_mgr.init_extension_config('user') with self.assertRaises(KeyError): self.ext_mgr.extensions['user'].has_configs() @@ -196,8 +195,10 @@ def test_install_loaded(self): #setup paths and configs self.ext_mgr.init_libraries() self.ext_mgr.init_extension_config("user") - with self.assertRaises(ValueError): - self.ext_mgr.init_extension_config("global") + self.ext_mgr.init_extension_config("global") + #Global is empty, so make sure it is not filled. + with self.assertRaises(KeyError): + self.ext_mgr.extensions['global'].has_configs() #run function user_installed = self.ext_mgr.install_loaded() self.assertEqual(user_installed, ["unit_test_mock"]) From a018595c9cd469574a594f459c519b4d072aa4a2 Mon Sep 17 00:00:00 2001 From: Seamus Tuohy Date: Wed, 16 Apr 2014 15:54:59 -0400 Subject: [PATCH 107/107] made extensions fit into the actual main window --- commotion_client/GUI/main_window.py | 23 ++++++++----- .../extensions/config_editor/main.py | 14 ++++++++ .../config_editor/ui/config_manager.ui | 2 +- commotion_client/utils/extension_manager.py | 32 +++++++++++-------- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/commotion_client/GUI/main_window.py b/commotion_client/GUI/main_window.py index b686928..df2c67f 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -43,9 +43,10 @@ def __init__(self, parent=None): self.init_crash_reporter() self.setup_menu_bar() - + #Setup extension manager for viewports + self.ext_manager = extension_manager.ExtensionManager() self.viewport = welcome_page.ViewPort(self) - self.load_viewport() + self.load_viewport(self.viewport) #Default Paramiters #TODO to be replaced with paramiters saved between instances later try: @@ -101,13 +102,19 @@ def init_crash_reporter(self): def set_viewport(self): """Load and set viewport to next viewport and load viewport """ - ext_manager = extension_manager.ExtensionManager - self.viewport = ext_manager.import_extension(self.next_viewport).ViewPort(self) - self.load_viewport() + self.log.info(self.next_extension) + next_view = self.next_extension + next_viewport = self.ext_manager.load_user_interface(str(next_view), "main") + viewport_object = next_viewport(self) + self.load_viewport(viewport_object) - def load_viewport(self): + def load_viewport(self, viewport): """Apply current viewport to the central widget and set up proper signal's for communication. """ - self.setCentralWidget(self.viewport) + testme = self.setCentralWidget(viewport) + self.viewport = viewport + self.viewport.show() + self.log.info(testme) + self.log.info(self.centralWidget()) #connect viewport extension to crash reporter self.viewport.data_report.connect(self.crash_report.crash_info) @@ -122,7 +129,7 @@ def load_viewport(self): def change_viewport(self, viewport): """Prepare next viewport for loading and start loading process when ready.""" self.log.debug(self.translate("logs", "Request to change viewport received.")) - self.next_viewport = viewport + self.next_extension = viewport if self.viewport.is_dirty: self.viewport.on_stop.connect(self.set_viewport) self.clean_up.emit() diff --git a/commotion_client/extensions/config_editor/main.py b/commotion_client/extensions/config_editor/main.py index 7d5f494..13d5cbf 100644 --- a/commotion_client/extensions/config_editor/main.py +++ b/commotion_client/extensions/config_editor/main.py @@ -31,11 +31,25 @@ class ViewPort(Ui_config_manager.ViewPort): start_report_collection = QtCore.pyqtSignal() data_report = QtCore.pyqtSignal(str, dict) error_report = QtCore.pyqtSignal(str) + clean_up = QtCore.pyqtSignal() + on_stop = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__() + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate self.setupUi(self) self.start_report_collection.connect(self.send_signal) + self._dirty = False + + + @property + def is_dirty(self): + """The current state of the viewport object """ + return self._dirty + + def clean_up(self): + self.on_stop.emit() def send_signal(self): self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) diff --git a/commotion_client/extensions/config_editor/ui/config_manager.ui b/commotion_client/extensions/config_editor/ui/config_manager.ui index 3add7de..826e15d 100644 --- a/commotion_client/extensions/config_editor/ui/config_manager.ui +++ b/commotion_client/extensions/config_editor/ui/config_manager.ui @@ -1,7 +1,7 @@ ViewPort - + 0 diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py index 0760748..a41032b 100644 --- a/commotion_client/utils/extension_manager.py +++ b/commotion_client/utils/extension_manager.py @@ -60,6 +60,7 @@ def __init__(self): self.translate = QtCore.QCoreApplication.translate self.extensions = {} self.libraries = {} + self.set_library_defaults() self.user_settings = self.get_user_settings() self.config_keys = ["name", "main", @@ -96,8 +97,6 @@ def reset_settings_group(self): def init_extension_libraries(self): """This function bootstraps the Commotion client when the settings are not populated on first boot or due to error. It iterates through all extensions in the core client and loads them.""" - #set default library paths - self.set_library_defaults() #create directory structures if needed self.init_libraries() #load core and move to global if needed @@ -105,8 +104,6 @@ def init_extension_libraries(self): self.load_core() #Load all extension configs found in libraries for name, path in self.libraries.items(): - self.log(path) - self.log(QtCore.QDir(path).entryInfoList()) if QtCore.QDir(path).entryInfoList() != []: self.init_extension_config(name) #install all loaded config's with the existing settings @@ -257,7 +254,10 @@ def get_installed(self): _settings = self.user_settings extensions = _settings.childGroups() for ext in extensions: - installed_extensions[ext] = _settings.value(ext+"/type") + _type = _settings.value(ext+"/type") + ext_dir = QtCore.QDir(self.libraries[_type]) + if ext_dir.exists(ext): + installed_extensions[ext] = _type self.log.debug(self.translate("logs", "The following extensions are installed: [{0}].".format(extensions))) return installed_extensions @@ -278,6 +278,9 @@ def load_core(self): #Check if the extension is in the globals global_extensions = list(self.extensions['global'].configs.keys()) if ext['name'] in global_extensions: + self.log.debug(self.translate("logs", "Core extension {0} was found in the global extension list.".format(ext['name']))) + if not _global_dir.exists(ext['name']): + raise KeyError(self.translate("Extension {0} was found in the extension list, but it did not exist in the actual library. Loading it to global.".format(ext['name']))) continue except KeyError: #If extension not loaded in globals it will raise a KeyError @@ -286,9 +289,9 @@ def load_core(self): self.log.info(self.translate("logs", "Core extension {0} was missing from the global extension directory. Copying it into the global extension directory from the core now.".format(ext['name']))) #Copy extension into global directory if QtCore.QFile(_core_ext_path).copy(_global_ext_path): - self.log.debug(self.translate("logs", "Extension config successfully copied.")) + self.log.debug(self.translate("logs", "Extension successfully copied.")) else: - self.log.debug(self.translate("logs", "Extension config was not copied.")) + self.log.debug(self.translate("logs", "Extension was not copied.")) _reload_globals = True if _reload_globals == True: self.init_extension_config("global") @@ -414,7 +417,7 @@ def load_user_interface(self, extension_name, gui): raise AttributeError(self.translate("logs", "Attempted to get a user interface of an invalid type.")) _config = self.get_config(extension_name) try: - if _config['initialized'] != True: + if _config['initialized'] != 'true': self.log.debug(self.translate("logs", "Extension manager attempted to load a user interface from uninitalized extension {0}. Uninitialized extensions cannot be loaded. Try installing/initalizing the extension first.".format(extension_name))) raise AttributeError(self.translate("logs", "Attempted to load a user interface from an uninitialized extension.")) except KeyError: @@ -424,17 +427,18 @@ def load_user_interface(self, extension_name, gui): ui_file = _config[gui] _type = self.get_property(extension_name, "type") extension_path = os.path.join(self.libraries[_type], extension_name) - #Get the extension + self.log.debug(extension_path) + #Get the extension extension = zipimport.zipimporter(extension_path) #add extension to sys path so imported modules can access other modules in the extension. sys.path.append(extension_path) user_interface = extension.load_module(ui_file) if gui == "toolbar": - return user_interface.ToolBar() + return user_interface.ToolBar elif gui == "main": - return user_interface.ViewPort() + return user_interface.ViewPort elif gui == "settings": - return user_interface.SettingsMenu() + return user_interface.SettingsMenu def get_config(self, name): """Returns a config from an installed extension. @@ -443,7 +447,7 @@ def get_config(self, name): name (string): An extension name. Returns: - A config (dictionary) for an extension. + A config (dictionary)for an extension. Raises: KeyError: If an installed extension of the specified name does not exist. @@ -605,7 +609,7 @@ def save_settings(self, extension_config, extension_type="global"): _settings.setValue("tests", "tests") #Write extension type _settings.setValue("type", extension_type) - _settings.setValue("initialized", True) + _settings.setValue("initialized", 'true') _settings.endGroup() return True