diff --git a/.gitignore b/.gitignore index 20bf2d1..86613e4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,18 @@ 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/* + +#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 297a1e7..c496781 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,14 @@ -.PHONY: build windows osx debian clean install +.PHONY: build windows osx debian clean install tests -all: build windows debian osx +all: build -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 assets + python3.3 build/scripts/build.py build + python3.3 build/scripts/zip_extensions.py -test: clean build - cp commotion_client/assets/commotion_assets_rc.py commotion_client/. +assets: + mkdir build/resources || true + pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py windows: @echo "windows compileing is not yet implemented" @@ -15,10 +16,23 @@ windows: osx: @echo "macintosh saddening is not yet implemented" +linux: build + python3.3 setup.py build + debian: @echo "debian packaging is not yet implemented" +test: tests + @echo "test build complete" + +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: - python3.3 build/build.py clean - rm commotion_client/assets/commotion_assets_rc.py || true - rm commotion_client/commotion_assets_rc.py || true + python3.3 build/scripts/build.py clean + rm -fr build/resources/* || true + rm -fr build/exe.* || true + rm -fr tests/temp/* || true diff --git a/README.md b/README.md index a0d616f..d5d8e19 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) + + + 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 diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..51911d0 --- /dev/null +++ b/build/README.md @@ -0,0 +1,89 @@ +# 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 + +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"] +``` + +### 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. + +### 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/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/commotion_client/tests.py b/commotion_client/GUI/__init__.py similarity index 100% rename from commotion_client/tests.py rename to commotion_client/GUI/__init__.py 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 25b4242..b686928 100644 --- a/commotion_client/GUI/main_window.py +++ b/commotion_client/GUI/main_window.py @@ -19,72 +19,59 @@ 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 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 extension_manager 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__() - 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__) + self.translate = QtCore.QCoreApplication.translate - 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) + self.init_crash_reporter() + self.setup_menu_bar() + + self.viewport = welcome_page.ViewPort(self) + self.load_viewport() #Default Paramiters #TODO to be replaced with paramiters saved between instances later try: 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.critical(self.translate("logs", "Failed to load window settings.")) + self.log.exception(_excp) 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 @@ -99,20 +86,48 @@ def __init__(self, parent=None): 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.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(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): + """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() - 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 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 + self.viewport.data_report.connect(self.crash_report.crash_info) + self.crash_report.crash_override.connect(self.viewport.start_report_collection) - def changeViewport(self, viewport): - self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport received.")) - self.viewport.setViewport(viewport) + #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) + + 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 + if self.viewport.is_dirty: + self.viewport.on_stop.connect(self.set_viewport) + self.clean_up.emit() + else: + self.set_viewport() def purge(self): """ @@ -129,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() @@ -150,8 +165,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() @@ -175,19 +190,13 @@ def load_settings(self): _settings.beginGroup("MainWindow") #Load settings from saved, or use defaults - try: - 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) - raise + 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) + raise EnvironmentError(_error) _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) - raise + self.setGeometry(geometry) def save_settings(self): """ @@ -200,8 +209,8 @@ 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.debug(_excp, exc_info=1) + self.log.warn(self.translate("logs", "Could not save window geometry. Will continue without saving window geometry.")) + self.log.exception(_excp) _settings.endGroup() @@ -209,10 +218,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 + diff --git a/commotion_client/GUI/menu_bar.py b/commotion_client/GUI/menu_bar.py index c9f0148..bfd74ea 100644 --- a/commotion_client/GUI/menu_bar.py +++ b/commotion_client/GUI/menu_bar.py @@ -21,10 +21,13 @@ from PyQt4 import QtGui #Commotion Client Imports -from utils import config +from commotion_client.utils.extension_manager import ExtensionManager 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__() @@ -32,120 +35,130 @@ 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) + self.translate = QtCore.QCoreApplication.translate + self.ext_mgr = ExtensionManager() 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.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.")) - 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) - - def populateMenu(self): - """ - Clears and re-populates the menu using the loaded extensions. - """ - menuItems = {} - extensions = list(config.findConfigs("extension")) + self.viewport_requested.emit(viewport) + + 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 = {} + if not self.ext_mgr.check_installed(): + self.ext_mgr.init_extension_libraries() + extensions = self.ext_mgr.get_installed().keys() if extensions: - topLevel = self.getParents(extensions) - for topLevelItem in topLevel: + top_level = self.get_parents(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 = 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" + 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 = 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 - 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.requestViewport, 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 = self.ext_mgr.get_property(ext, 'menu_item') + except KeyError: + menu_item_title = ext + 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) + 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): """ 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/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..860ec6d --- /dev/null +++ b/commotion_client/GUI/welcome_page.py @@ -0,0 +1,46 @@ +#!/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 commotion_client.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) + 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() + 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 } diff --git a/commotion_client/commotion_client.py b/commotion_client/commotion_client.py index d8f5516..c36f369 100644 --- a/commotion_client/commotion_client.py +++ b/commotion_client/commotion_client.py @@ -21,11 +21,13 @@ from PyQt4 import QtGui from PyQt4 import QtCore -from utils import logger -from utils import thread -from utils import single_application -from GUI import main_window -from GUI import system_tray +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 commotion_client.GUI import main_window +from commotion_client.GUI import system_tray #from controller import CommotionController #TODO Create Controller @@ -51,8 +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 grabbed from the commotion config reader - parsed_args['logFile'] = args.logfile if args.logfile else "temp/logfile.temp" + 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 @@ -66,12 +67,8 @@ 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['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() @@ -88,17 +85,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.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")) + app.log.debug(app.translate("logs", "Shutting down")) class HoldStateDuringRestart(thread.GenericThread): """ @@ -108,6 +102,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 @@ -119,7 +114,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): """ @@ -128,19 +123,28 @@ 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'] + _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") 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 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 #================================================= @@ -155,32 +159,39 @@ 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.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.log.exception(_excp) + self.end(_catch_all) + 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): """ Start or switch client over to full client. """ + extensions = extension_manager.ExtensionManager() + if not extensions.check_installed(): + extensions.init_extension_libraries() if not self.main: 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.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.log.exception(_excp) + self.end(_catch_all) else: self.init_main() 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.debug(_excp, exc_info=1) - self.exit(_catch_all) + self.log.exception(_excp) + self.end(_catch_all) else: self.init_sys_tray() @@ -193,8 +204,8 @@ 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.debug(_excp, exc_info=1) + self.log.critical(self.translate("logs", "Could not close down existing GUI componenets to switch to daemon mode.")) + self.log.exception(_excp) raise try: #create controller and sys tray @@ -202,8 +213,8 @@ 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.debug(_excp, exc_info=1) + self.log.critical(self.translate("logs", "Could not start daemon. Application must be halted.")) + self.log.exception(_excp) raise else: self.init_sys_tray() @@ -220,14 +231,14 @@ 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.debug(_excp, exc_info=1) - self.exit(_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.debug(_excp, exc_info=1) + 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): """ @@ -243,14 +254,14 @@ 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.debug(_excp, exc_info=1) - self.exit(_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.debug(_excp, exc_info=1) + 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() @@ -263,14 +274,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.debug(_excp, exc_info=1) + self.log.critical(self.translate("logs", "Could not create Main Window. Application must be halted.")) raise else: return _main @@ -285,14 +295,14 @@ 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.debug(_excp, exc_info=1) + 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.debug(_excp, exc_info=1) + self.log.error(self.translate("logs", "Could not show the main window.")) + self.log.exception(_excp) raise def hide_main_window(self, force=None, errors=None): @@ -306,16 +316,16 @@ 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.debug(_excp, exc_info=1) + 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: self.main.purge() 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.debug(_excp, exc_info=1) + self.log.error(self.translate("logs", "Could not force main window restart.")) + self.log.exception(_excp) raise elif errors == "strict": raise @@ -332,8 +342,8 @@ 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.debug(_excp, exc_info=1) + self.log.error(self.translate("logs", "Could close and re-open the main window.")) + self.log.exception(_excp) if errors == "strict": raise else: @@ -353,21 +363,21 @@ 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.debug(_excp, exc_info=1) - self.exit(_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.debug(_excp, exc_info=1) + 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 #================================================= @@ -383,8 +393,8 @@ 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.debug(_excp, exc_info=1) + self.log.critical(self.translate("logs", "Could not create controller. Application must be halted.")) + self.log.exception(_excp) raise def init_controller(self): @@ -401,20 +411,20 @@ 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.debug(_excp, exc_info=1) - self.exit(_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.debug(_excp, exc_info=1) + 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 @@ -431,8 +441,8 @@ 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.debug(_excp, exc_info=1) + self.log.error(self.translate("logs", "Could not initialize connections between the system tray and other application components.")) + self.log.exception(_excp) raise def create_sys_tray(self): @@ -442,8 +452,8 @@ 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.debug(_excp, exc_info=1) + self.log.error(self.translate("logs", "Could not start system tray.")) + self.log.exception(_excp) raise else: return tray @@ -459,21 +469,21 @@ 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.debug(_excp, exc_info=1) - self.exit(_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.debug(_excp, exc_info=1) + 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 #================================================= @@ -491,17 +501,20 @@ 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.logger.set_verbosity("DEBUG") 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() 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 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/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/config_editor.conf b/commotion_client/extensions/config_editor/config_editor.conf new file mode 100644 index 0000000..efcb25a --- /dev/null +++ b/commotion_client/extensions/config_editor/config_editor.conf @@ -0,0 +1,6 @@ +{ +"name":"config_editor", +"menu_item":"Commotion Config File Editor", +"parent":"Advanced", +"main":"main" +} diff --git a/docs/extensions/tutorial/config_manager/main.py b/commotion_client/extensions/config_editor/main.py similarity index 59% rename from docs/extensions/tutorial/config_manager/main.py rename to commotion_client/extensions/config_editor/main.py index d377113..7d5f494 100644 --- a/docs/extensions/tutorial/config_manager/main.py +++ b/commotion_client/extensions/config_editor/main.py @@ -14,21 +14,33 @@ #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() + 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 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/config_editor/ui/__init__.py b/commotion_client/extensions/config_editor/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/extensions/tutorial/config_manager/ui/config_manager.ui b/commotion_client/extensions/config_editor/ui/config_manager.ui similarity index 96% rename from docs/extensions/tutorial/config_manager/ui/config_manager.ui rename to commotion_client/extensions/config_editor/ui/config_manager.ui index cd142e2..3add7de 100644 --- a/docs/extensions/tutorial/config_manager/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 @@ - - + + 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/commotion_client/extensions/unit_test_mock/ui/test.ui b/commotion_client/extensions/unit_test_mock/ui/test.ui new file mode 100644 index 0000000..3add7de --- /dev/null +++ b/commotion_client/extensions/unit_test_mock/ui/test.ui @@ -0,0 +1,1928 @@ + + + ViewPort + + + + 0 + 0 + 822 + 2413 + + + + + 50 + false + false + + + + Qt::NoContextMenu + + + Commotion Configuration Manager + + + + :/logo16.png:/logo16.png + + + THIS NEEDS NEW POP UP TEXT!!! + + + + + 30 + 50 + 746 + 2336 + + + + + + + + + + + + 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 + + + + + + + + + + + + + + + + + + 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 + + + + + + + + + + + + + + + + diff --git a/commotion_client/extensions/unit_test_mock/units.py b/commotion_client/extensions/unit_test_mock/units.py new file mode 100644 index 0000000..e69de29 diff --git a/commotion_client/utils/config.py b/commotion_client/utils/config.py deleted file mode 100644 index 49b4403..0000000 --- a/commotion_client/utils/config.py +++ /dev/null @@ -1,126 +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 utils import fsUtils - -#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): - """ - Function used to obtain the path to a config file. - - @param configType 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) - 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 getConfigPaths(configType): - - configLocations = {"global":"data/global/", "user":"data/user/", "extension":"data/extensions/"} - configFiles = [] - - 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) - 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: - 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: - 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 - else: - return False - - -def getConfig(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: - 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) - - -def loadConfig(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 - @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 e: - 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) - return False - except Exception as e: - 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) - 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))) - return data - except ValueError as e: - 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) - return False - except Exception as e: - log.error(QtCore.QCoreApplication.translate("logs", "Failed to load {0} due to an unknown error.".format(config))) - self.log.debug(e, exc_info=1) diff --git a/commotion_client/utils/extension_manager.py b/commotion_client/utils/extension_manager.py new file mode 100644 index 0000000..0760748 --- /dev/null +++ b/commotion_client/utils/extension_manager.py @@ -0,0 +1,772 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +extension_manager + +The extension management object. + +Key componenets handled within: + * 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 +import importlib +import shutil +import os +import re +import sys +import zipfile +import json +import zipimport + +#PyQt imports +from PyQt4 import QtCore + +#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): + + def __init__(self): + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + self.extensions = {} + self.libraries = {} + self.user_settings = self.get_user_settings() + self.config_keys = ["name", + "main", + "menu_item", + "menu_level", + "parent", + "settings", + "toolbar", + "tests", + "initialized", + "type",] + + 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 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.""" + + #set default library paths + self.set_library_defaults() + #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 + 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() + + def set_library_defaults(self): + """Sets the default directories for core, user, and global extensions. + + 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. + """ + #==== 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.")) + + #==== 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. + 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("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. + 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] = 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. """ + #==== USER & GLOBAL =====# + for path_type in ['user', 'global']: + try: + ext_dir = QtCore.QDir(self.libraries[path_type]) + except KeyError: + 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(): + 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: + 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))) + + 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: + try: + self.log.debug(self.translate("logs", "Creating {0} config manager".format(type_))) + self.extensions[type_] = ConfigManager(self.libraries[type_]) + except ValueError: + 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): + """Checks if and extension is installed. + + Args: + 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 = 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 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 re-initialized the global config. + + 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.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 + 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.")) + _reload_globals = True + if _reload_globals == True: + self.init_extension_config("global") + + 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. + + Returns: + 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. + + """ + _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: + 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: + #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: + 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. + + 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 key not in self.config_keys: + _error = self.translate("logs", "{0} is not a valid extension config value.".format(key)) + raise KeyError(_error) + _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.info(self.translate("logs", "No extensions had the requested value.")) + return [] + + def get_property(self, name, key): + """ + 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 the (string) value associated the extensions key in the applications saved extension settings. + + Raises: + KeyError: If the value requested is non-standard. + """ + 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 + _settings.beginGroup(name) + setting_value = _settings.value(key) + if not setting_value: + _error = self.translate("logs", "The extension config does not contain that value.") + _settings.endGroup() + raise KeyError(_error) + else: + _settings.endGroup() + return setting_value + + 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 + 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. + """ + 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: + 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.")) + #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. + + 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.childGroups() + 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 + + 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. + + Returns: + bool: True if extension is removed, false if it is not. + + Raises: + ValueError: When an empty string is passed as an argument. + """ + #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: + 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. + + 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 any failures + """ + _settings = self.user_settings + #get extension dir + try: + extension_dir = self.libraries[extension_type] + except KeyError: + self.log.warning(self.translate("logs", "Invalid extension type. Please check the extension type and try again.")) + return False + #create validator + 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 + try: + extension_name = extension_config['name'] + if config_validator.name(): + _settings.beginGroup(extension_name) + _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 + except KeyError: + _error = self.translate("logs", "The extension is missing a name value which is required.") + self.log.error(_error) + return False + #Extension Main + 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: + _settings.setValue("main", _main) + #Extension Settings & Toolbar + for val in ["settings", "toolbar"]: + 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: + _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 + 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 + 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 + 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 + 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) + _settings.endGroup() + return True + +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 + self.log.debug(self.translate("logs", "Initalizing ConfigManager")) + self.configs = [] + self.directory = None + self.paths = [] + if path: + self.directory = path + try: + self.paths = self.get_paths(path) + except TypeError: + 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()) + + 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): + """ + 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.warning(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 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 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 raise value error if not there + dir_obj = QtCore.QDir(str(directory)) + if not dir_obj.exists(dir_obj.absolutePath()): + raise ValueError(self.translate("logs", "Folder at path {0} does not exist. No Config files loaded.".format(str(directory)))) + else: + path = dir_obj.absolutePath() + + config_files = [] + try: + for root, dirs, files in fs_utils.walklevel(path): + for file_name in files: + 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", "Extension library at path {0} does not exist. No Config files identified.".format(path))) + raise + except TypeError: + 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=None): + """ + 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: + 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) + if config: + yield config + else: + 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. + + 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 + + """ + config = None + data = None + myfile = QtCore.QFile(str(path)) + if not myfile.exists(): + return False + 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.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))) + return data + else: + self.log.debug(self.translate("logs", "Failed to load config file.".format(path))) + return False diff --git a/commotion_client/utils/fsUtils.py b/commotion_client/utils/fsUtils.py deleted file mode 100644 index fa1ad24..0000000 --- a/commotion_client/utils/fsUtils.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -fs_utils - - -""" - -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. - - -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 - 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) - 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") - return False - -def walklevel(some_dir, level=1): - some_dir = some_dir.rstrip(os.path.sep) - assert os.path.isdir(some_dir) - num_sep = some_dir.count(os.path.sep) - for root, dirs, files in os.walk(some_dir): - yield root, dirs, files - num_sep_this = root.count(os.path.sep) - if num_sep + level <= num_sep_this: - del dirs[:] diff --git a/commotion_client/utils/fs_utils.py b/commotion_client/utils/fs_utils.py new file mode 100644 index 0000000..f886ccb --- /dev/null +++ b/commotion_client/utils/fs_utils.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +fs_utils + + +""" + +#PyQt imports +from PyQt4 import QtCore + +#Standard Library Imports +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. + + 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. + + """ + translate = QtCore.QCoreApplication.translate + 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 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) + 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 + 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" + temp_dir = QtCore.QDir.tempPath() + if new: + unique_dir_name = uuid.uuid4() + 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.debug(_error) + raise IOError(_error) + return temp_full + + +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) + list_of_files = path.entryInfoList() + + 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) + 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 + """ + log = logging.getLogger("commotion_client."+__name__) + start.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) + list_of_files = start.entryInfoList() + + 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) + 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 diff --git a/commotion_client/utils/logger.py b/commotion_client/utils/logger.py index 70fff80..3dc9658 100644 --- a/commotion_client/utils/logger.py +++ b/commotion_client/utils/logger.py @@ -1,67 +1,167 @@ #!/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 . """ -Main logging controls for Commotion-Client + +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 . + """ #TODO create seperate levels for the stream, the file, and the full logger - +from PyQt4 import QtCore import logging -#TODO Create a config parser and uncomment the following line to use it -#from . import config +from logging import handlers +import os +import sys -def set_logging(name, verbosity=None, logfile=None): +class LogHandler(object): """ - Creates a logger object + 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: + + 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 """ - 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)]) - 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 + + 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') + self.stream = None + self.file_handler = None + self.logfile = None + #setup logger + self.set_logfile(logfile) + self.set_verbosity(verbosity) + + 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 not log_dir.mkpath(log_dir.absolutePath()): + 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 + 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 not log_dir.mkpath(log_dir.absolutePath()): + 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/ + 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 home directory + #TODO check if this is appropriate... its not. + home = QtCore.QDir.home() + 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") + else: + self.logfile = log_dir.filePath("commotion.log") + else: + #I'm out! + 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. + + 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: + _levels = [ 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG'] + level = self.levels[_levels[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 = handlers.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 -# [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") + def get_logger(self): + return self.logger diff --git a/commotion_client/utils/settings.py b/commotion_client/utils/settings.py new file mode 100644 index 0000000..b85a18a --- /dev/null +++ b/commotion_client/utils/settings.py @@ -0,0 +1,73 @@ +#!/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 . + +""" + +""" +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/commotion_client/utils/validate.py b/commotion_client/utils/validate.py new file mode 100644 index 0000000..fc41ec4 --- /dev/null +++ b/commotion_client/utils/validate.py @@ -0,0 +1,342 @@ + + +#!/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 +import ipaddress +import os +import zipfile + +#PyQt imports +from PyQt4 import QtCore + +#Commotion Client Imports +from commotion_client.utils import fs_utils + +class ClientConfig(object): + + def __init__(self, config, directory=None): + """ + Args: + config (dictionary): The config for the extension. + 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", + "menu_item", + "menu_level", + "parent", + "settings", + "toolbar", + "tests", + "initialized",] + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + self.config = config + if directory: + #set extension directory to point at config zipfile in that directory + self.extension_path = directory + self.errors = None + + @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 the value {0} within it which is not a valid value.".format(val))) + self._config = value + + + @property + def extension_path(self): + return self._extension_path + + @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 + 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.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 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. + + @brief Will set self.errors if any errors are found. + @return bool True if valid, False if invalid. + """ + self.errors = None + 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']))) + if not self.tests(): + errors.append("tests") + 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 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 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 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 extension {0}'s {1} is invalid.".format(self.config['name'], 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.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.warning(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.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.warning(self.translate("logs", "This value is too long for your system.")) + return False + if not self.check_path_chars(name_val): + self.log.warning(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.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.warning(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.warning(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.warning(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.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!.")) + 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.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 an extension. + + @param file_name string The file name from a config file + """ + if not self.extension_path: + self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) + return True + ext_zip = zipfile.ZipFile(self.extension_path, 'r') + files = ext_zip.namelist() + if not str(file_name) in files: + self.log.warning(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) + 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 self.check_path_length(file_name): + 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.warning(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" : r"[|\?*<\":>+[]/]", + "win32" : r"[|\?*<\":>+[]/]", + "darwin" : "[:]", + "linux" : "[/\x00]"} + if platform and reserved[platform]: + if re.search(file_name, reserved[platform]): + self.log.warning(self.translate("logs", "The extension's config file contains an invalid main value.")) + return False + else: + return True + else: + 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 + + + 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. + """ + 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 + 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: + 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.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.warning(self.translate("logs", "File names can not be greater than 260 chars on your system")) + return False + else: + return True + else: + 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): + def __init__(self): + self.log = logging.getLogger("commotion_client."+__name__) + self.translate = QtCore.QCoreApplication.translate + + def ipaddr(self, ip_addr, 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_addr)) + except ValueError: + 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.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/docs/extensions/extension_template/main.py b/docs/extensions/extension_template/main.py index b7dad62..b8e19da 100644 --- a/docs/extensions/extension_template/main.py +++ b/docs/extensions/extension_template/main.py @@ -26,9 +26,28 @@ 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) + 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"}) + 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/config.json b/docs/extensions/tutorial/config_manager/config.json deleted file mode 100644 index d936546..0000000 --- a/docs/extensions/tutorial/config_manager/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ -"name":"extension_template", -"menuItem":"Extension Template", -"parent":"Templates", -"settings":"settings", -"taskbar":"task_bar", -"main":"main", -"tests":"test_suite" -} 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. diff --git a/docs/style_standards/README.md b/docs/style_standards/README.md new file mode 100644 index 0000000..f622ce0 --- /dev/null +++ b/docs/style_standards/README.md @@ -0,0 +1,66 @@ +# 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. 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, log what happened, but let whomever handles it log the traceback as debug. + +## 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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a72c3b3 --- /dev/null +++ b/setup.py @@ -0,0 +1,88 @@ +#!/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 + +#---------- OS Setup -----------# + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + +# 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"] + +# 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] + + +#======== 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) + asset_loc = os.path.join("extensions", "core", ext) + all_assets.append((ext_loc, asset_loc)) + + +#---------- Executable Setup -----------# + +exe = Executable( + targetName="Commotion", + script="commotion_client/commotion_client.py", + packages=core_pkgs, + ) + +#---------- Core Setup -----------# + +setup(name="Commotion Client", + version="1.0", + url="commotionwireless.net", + license="Affero General Public License V3 (AGPLv3)", + executables = [exe], + options = {"build_exe":{"include_files": all_assets}} + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock/extensions/unit_test_mock b/tests/mock/extensions/unit_test_mock new file mode 100644 index 0000000..7fe9a22 Binary files /dev/null and b/tests/mock/extensions/unit_test_mock differ 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) + diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/extension_manager_tests.py b/tests/utils/extension_manager_tests.py new file mode 100644 index 0000000..fc362cd --- /dev/null +++ b/tests/utils/extension_manager_tests.py @@ -0,0 +1,559 @@ +""" + +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 + + +import unittest +import re +import os +import sys +import copy +import types + + +from commotion_client.utils import extension_manager + +class ExtensionSettingsTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication([]) + self.app.setOrganizationName("test_case"); + self.app.setApplicationName("testing_app"); + self.ext_mgr = extension_manager.ExtensionManager() + + + def tearDown(self): + self.app.deleteLater() + del self.app + self.app = None + self.ext_mgr.user_settings.clear() + self.ext_mgr = 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)) + +class LoadConfigSettings(ExtensionSettingsTestCase): + """ + Functions Covered: + load_all + init_extension_config + + """ + def test_load_core_ext(self): + """Test that all core extension directories are loaded upon running load_core.""" + #get all extensions currently loaded + 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.ext_mgr.user_settings.beginGroup("extensions") + k = self.ext_mgr.user_settings.allKeys() + for ext in global_exts: + 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.""" + #ext_type MUST be core|global|user + with self.assertRaises(ValueError): + self.ext_mgr.init_extension_config('pineapple') + + #Check that an empty directory does nothing. + self.ext_mgr.libraries['user'] = os.path.abspath("tests/temp/") + 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/") + 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 + +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.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) + 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.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")) + +class ExtensionLibraries(ExtensionSettingsTestCase): + + def test_init_libraries(self): + """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'] + #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))) + 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_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") + #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"]) + + #Test that the mock extension was loaded + 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("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("unit_test_mock", initialized) + + def test_get_extension_from_property(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") + + 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("unit_test_mock", does_not_have) + + + def test_get_property(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 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): + #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_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_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): + #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): + #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]) + +class ConfigManagerTests(unittest.TestCase): + + 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): + #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): + #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): + #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.empty_config = extension_manager.ConfigManager() + #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")]) + + def test_get(self): + 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.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