├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── ci ├── dockerfile └── run_docker.sh ├── docs ├── Makefile ├── authors.rst ├── build_process.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── modules.rst ├── packaging.rst ├── readme.rst └── usage.rst ├── hamster_gtk ├── __init__.py ├── hamster_gtk.py ├── helpers.py ├── misc │ ├── __init__.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── date_range_select_dialog.py │ │ ├── edit_fact_dialog.py │ │ ├── error_dialog.py │ │ └── hamster_about_dialog.py │ └── widgets │ │ ├── __init__.py │ │ ├── labelled_widgets_grid.py │ │ └── raw_fact_entry.py ├── overview │ ├── __init__.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── export_dialog.py │ │ └── overview_dialog.py │ └── widgets │ │ ├── __init__.py │ │ ├── charts.py │ │ ├── fact_grid.py │ │ └── misc.py ├── preferences │ ├── __init__.py │ ├── preferences_dialog.py │ └── widgets │ │ ├── __init__.py │ │ ├── combo_file_chooser.py │ │ ├── config_widget.py │ │ ├── hamster_combo_box_text.py │ │ ├── hamster_spin_button.py │ │ ├── hamster_switch.py │ │ └── time_entry.py ├── resources │ ├── css │ │ └── hamster-gtk.css │ ├── gtk │ │ └── menus.ui │ └── hamster-gtk.gresource.xml └── tracking │ ├── __init__.py │ └── screens.py ├── misc └── org.projecthamster.hamster-gtk.desktop ├── requirements ├── dev.pip ├── docs.pip └── test.pip ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── factories.py ├── misc │ ├── __init__.py │ ├── conftest.py │ ├── test_dialogs.py │ └── widgets │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_labelled_widgets_grid.py │ │ ├── test_raw_fact_completion.py │ │ └── test_raw_fact_entry.py ├── overview │ ├── __init__.py │ ├── conftest.py │ ├── test_dialogs.py │ └── test_widgets.py ├── preferences │ ├── __init__.py │ ├── conftest.py │ ├── test_preferences_dialog.py │ └── widgets │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_combo_file_chooser.py │ │ ├── test_config_widget.py │ │ ├── test_hamster_combo_box_text.py │ │ ├── test_hamster_spin_button.py │ │ └── test_time_entry.py ├── test_hamster-gtk.py ├── test_helpers.py ├── test_minimal.py └── tracking │ ├── __init__.py │ ├── conftest.py │ └── test_screens.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | docs/hamster_gtk.* 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # GTK resource file 63 | hamster_gtk/hamster-gtk.gresource 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | os: 4 | - linux 5 | 6 | language: 7 | - python 8 | 9 | python: 10 | - "2.7" 11 | 12 | services: 13 | - docker 14 | 15 | before_script: 16 | - docker build -t elbenfreund:hamster-gtk-devel -f ./ci/dockerfile . 17 | 18 | script: 19 | # See: https://docs.codecov.io/docs/testing-with-docker for details 20 | - ci_env=`bash <(curl -s https://codecov.io/env)` 21 | - docker run -v /home/travis/build/projecthamster/hamster-gtk:/hamster-gtk $ci_env elbenfreund:hamster-gtk-devel ./ci/run_docker.sh 22 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Eric Goller 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/projecthamster/hamster-gtk/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | hamster-gtk could always use more documentation, whether as part of the 40 | official hamster-gtk docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/projecthamster/hamster-gtk/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `hamster-gtk` for local development. 59 | 60 | 1. Fork the `hamster-gtk` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/hamster-gtk.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv hamster-gtk 68 | $ cd hamster-gtk/ 69 | $ make develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass the testsuite:: 78 | 79 | $ make test-all 80 | 81 | 6. Commit your changes and push your branch to GitHub:: 82 | 83 | $ git add . 84 | $ git commit -m "Your detailed description of your changes." 85 | $ git push origin name-of-your-bugfix-or-feature 86 | 87 | 7. Submit a pull request through the GitHub website. 88 | 89 | Pull Request Guidelines 90 | ----------------------- 91 | 92 | Before you submit a pull request, check that it meets these guidelines: 93 | 94 | 1. The pull request should include tests. 95 | 2. If the pull request adds functionality, the docs should be updated. Put 96 | your new functionality into a function with a docstring, and add the 97 | feature to the list in README.rst. 98 | 3. The pull request should work for Python 2.7 and 3.4. Check 99 | https://travis-ci.org/projecthamster/hamster-gtk/pull_requests 100 | and make sure that the tests pass for all supported Python versions. 101 | 102 | Tips 103 | ---- 104 | 105 | To have a quick and dirty run of your tests without using tox and additional linters etc...:: 106 | 107 | $ make test 108 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ======== 5 | 6 | 0.11.0 (2016-10-03) 7 | -------------------- 8 | - Config changes are now applied at runtime (Fixes: #57). 9 | - ``PreferencesDialog`` and its widgets moved to separate subpackage (PR: #83). 10 | - ``facts_changed`` Signal has been renamed to ``facts-changed`` (Fixes: #51). 11 | - ``Overview._charts`` gets destroyed on ``refresh`` (Closes: #106). 12 | - Overview: *Show more* button is inactive if there are no facts (Fixes: #105). 13 | - Overview: Improve ``date colour`` contrast with dark themes (Closes: #93). 14 | - Split ``hamster_gtk.misc.dialogs`` into multiple sub-modules (Closes: #96). 15 | - Test setup makes use of ``xvfb`` (Closes: #95). 16 | - Test ``tox`` against ``python3`` instead of more specific versions (PR: #92). 17 | - Add new helper function ``get_parent_window`` (Closes: #60). 18 | - Fix ``EditDialog`` for uncategoriezed facts (Closes: #59). 19 | - Replace GTK stock buttons with generic label buttons (Closes: #46). 20 | - Escape values inserted as markup (Closes: #78). 21 | - Move CSS into seperate file (Closes: #4). 22 | - Add new function ``hamster_gtk.helpers.get_resource_path`` (PR: #81). 23 | - Add basic ``AboutDialog`` (Closes: #17). 24 | - Minor fixes and refinements. 25 | 26 | 27 | 0.10.0 (2016-07-21) 28 | --------------------- 29 | * First release on PyPI. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include Makefile 7 | include tox.ini 8 | include .editorconfig 9 | 10 | exclude docs/hamster_gtk.* 11 | 12 | recursive-include tests * 13 | recursive-include hamster_gtk *.py 14 | recursive-include hamster_gtk/resources * 15 | recursive-include requirements *.pip 16 | recursive-include docs *.rst conf.py Makefile make.bat 17 | 18 | recursive-exclude * __pycache__ 19 | recursive-exclude * *.py[co] 20 | recursive-exclude * *.sw? 21 | 22 | exclude ci 23 | recursive-exclude ci * 24 | 25 | # Files in ``misc`` should not be part of the sdist as they are not part of the 26 | # actual package but just a help for desktop integration and such. 27 | exclude misc 28 | recursive-exclude misc * 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDDIR = _build 2 | RESOURCESDIR = hamster_gtk/resources 3 | GRESOURCEFILENAME = hamster-gtk.gresource 4 | 5 | .PHONY: clean-pyc clean-build docs clean resources 6 | 7 | define BROWSER_PYSCRIPT 8 | import os, webbrowser, sys 9 | try: 10 | from urllib import pathname2url 11 | except: 12 | from urllib.request import pathname2url 13 | 14 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 15 | endef 16 | export BROWSER_PYSCRIPT 17 | 18 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 19 | 20 | help: 21 | @echo "Please use 'make ' where is one of" 22 | @echo " clean to remove all build, test, coverage and Python artifacts" 23 | @echo " clean-build to remove build artifacts" 24 | @echo " clean-pyc to remove Python file artifacts" 25 | @echo " clean-docs" 26 | @echo " clean-test to remove test and coverage artifacts" 27 | @echo " lint to check style with flake8" 28 | @echo " test to run tests quickly with the default Python" 29 | @echo " test-all to run tests on every Python version with tox" 30 | @echo " coverage to check code coverage quickly with the default Python" 31 | @echo " coverage-html" 32 | @echo " codecov" 33 | @echo " develop to install (or update) all packages required for development" 34 | @echo " docs to generate Sphinx HTML documentation, including API docs" 35 | @echo " isort to run isort on the whole project." 36 | @echo " resources to generate GTK resources" 37 | @echo " release to package and upload a release" 38 | @echo " dist to package" 39 | @echo " install to install the package to the active Python's site-packages" 40 | @echo " register-gnome to register the package as a GNOME Application." 41 | 42 | clean: clean-build clean-pyc clean-test 43 | 44 | clean-build: 45 | rm -fr build/ 46 | rm -f $(RESOURCESDIR)/$(GRESOURCEFILENAME) 47 | rm -fr dist/ 48 | rm -fr .eggs/ 49 | find . -name '*.egg-info' -exec rm -fr {} + 50 | find . -name '*.egg' -exec rm -f {} + 51 | 52 | clean-pyc: 53 | find . -name '*.pyc' -exec rm -f {} + 54 | find . -name '*.pyo' -exec rm -f {} + 55 | find . -name '*~' -exec rm -f {} + 56 | find . -name '__pycache__' -exec rm -fr {} + 57 | 58 | clean-docs: 59 | $(MAKE) -C docs clean BUILDDIR=$(BUILDDIR) 60 | 61 | clean-test: 62 | rm -fr .tox/ 63 | rm -f .coverage 64 | rm -fr htmlcov/ 65 | 66 | develop: 67 | pip install -U pip setuptools wheel 68 | pip install -U -e . 69 | pip install -U -r requirements/dev.pip 70 | 71 | lint: 72 | flake8 hamster-dbus tests 73 | 74 | test: 75 | py.test $(TEST_ARGS) tests/ 76 | 77 | test-all: 78 | tox 79 | 80 | coverage: 81 | coverage run -m pytest $(TEST_ARGS) tests 82 | coverage report 83 | 84 | coverage-html: coverage 85 | coverage html 86 | $(BROWSER) htmlcov/index.html 87 | 88 | codecov: coverage 89 | codecov --token=96b66aeb-8d82-4d44-8ff7-93ac5d5305b9 90 | 91 | docs: 92 | rm -f docs/hamster_gtk.rst 93 | rm -f docs/modules.rst 94 | sphinx-apidoc -o docs/ hamster_gtk 95 | $(MAKE) -C docs clean 96 | $(MAKE) -C docs html 97 | $(BROWSER) docs/_build/html/index.html 98 | 99 | isort: 100 | isort --recursive setup.py hamster_gtk/ tests/ 101 | 102 | 103 | servedocs: docs 104 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 105 | 106 | resources: 107 | glib-compile-resources --sourcedir=$(RESOURCESDIR) --target $(RESOURCESDIR)/$(GRESOURCEFILENAME) $(RESOURCESDIR)/hamster-gtk.gresource.xml 108 | 109 | release: 110 | python setup.py sdist bdist_wheel 111 | twine upload -r pypi -s dist/* 112 | 113 | dist: resources 114 | python setup.py sdist 115 | python setup.py bdist_wheel 116 | ls -l dist 117 | 118 | install: clean resources 119 | python setup.py install 120 | 121 | register-gnome: 122 | desktop-file-install misc/org.projecthamster.hamster-gtk.desktop 123 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | hamster-gtk 3 | =============================== 4 | 5 | .. image:: https://img.shields.io/pypi/v/hamster-gtk.svg 6 | :target: https://pypi.python.org/pypi/hamster-gtk 7 | 8 | .. image:: https://img.shields.io/travis/projecthamster/hamster-gtk/master.svg 9 | :target: https://travis-ci.org/projecthamster/hamster-gtk 10 | 11 | .. .. image:: https://readthedocs.org/projects/hamster-gtk/badge/?version=latest 12 | :target: https://readthedocs.org/projects/hamster-gtk/?badge=latest 13 | :alt: Documentation Status 14 | 15 | 16 | A GTK interface to the hamster time tracker. 17 | 18 | **IMPORTANT** 19 | At this early stage ``hamster-gtk`` is pre-alpha software. As such you are very 20 | welcome to take it our for a spin and submit feedback, however you should not 21 | rely on it to work properly and most certainly you should not use it in a 22 | production environment! 23 | You have been warned. 24 | 25 | Dependencies 26 | ------------- 27 | 28 | If you want to use the ``make register-gnome`` target ``desktop-file-install`` 29 | is required. On debian derivates this is provided by ``desktop-file-utils``. 30 | 31 | Installing from sources: 32 | ~~~~~~~~~~~~~~~~~~~~~~~~ 33 | - xmllint (Needed in order to call ``resources`` make target. On debian this is 34 | part of the ``libxml2-utils`` package. 35 | 36 | To Run the Testsuite 37 | ~~~~~~~~~~~~~~~~~~~~~ 38 | - make 39 | - xvfb 40 | 41 | First Steps 42 | ------------ 43 | * Install dependencies (on debian if using virtualenvwrapper): 44 | install ``virtualenvwrapper python-gi gir1.2-gtk-3.0 libglib2.0-dev 45 | libgtk-3-dev``. 46 | If you use python 3, you will need ``python3-gi`` instead. 47 | * Create new virtual env: ``mkvirtualenv hamster-gtk`` 48 | * Activate env: ``workon hamster-gtk`` 49 | * Activate system site dirs: ``toggleglobalsitepackages``. Otherwise you will 50 | have no access to Gtk. 51 | * Install ``hamster-gtk``: ``pip install hamster-gtk``. 52 | * Run the little furball: ``hamster-gtk`` 53 | 54 | Some notes: 55 | 56 | * Preference changes will only be applied at the next start right now. 57 | * Exported data is tab separated. 58 | * This is pre-alpha software! 59 | 60 | How to run the testsuite 61 | ------------------------- 62 | - Create a virtual environment ``mkvirtualenv hamster-gtk`` (python 2) or 63 | ``mkvirtualenv -p python3 hamster-gtk`` (python 3). Whilst those instructions 64 | do not reflect best practices (which would make use of python 3's built in 65 | venv) it does provide a better handling of ``system-site-packages``. 66 | `This issue `_ provides some context for 67 | the problems one may run into using ``system-site-packages`` with python3 68 | venvs. It is our hope that python 3.7 will fix this. 69 | - enable access to system-site-packages for our virtual environment: 70 | ``$ toggleglobalsitepackages``. This is needed to access our global GTK 71 | related packages. 72 | - Install development environment: ``make develop``. 73 | - To run the actual testsuite: ``make test``. 74 | - To run tests and some auxiliary style checks (flake8, pep257, etc): 75 | ``make test-all``. 76 | 77 | Right now, our actual code testing does not utilize ``tox`` as we keep running 78 | into segfaults (which does not happen without ``tox``). 79 | For this same reason we are currently unable to run our code tests on Travis 80 | as well (we still run the 'style checks' at least). 81 | We hope to get to the bottom of this at some point and would be most grateful 82 | if you have any hint or pointer that may help tracking down this issue. 83 | 84 | Migrating from 'legacy hamster' 85 | --------------------------------- 86 | In case you are wondering “Will I be able to continue using my ‘legacy 87 | hamster’ database with this rewrite?” the answer is “yes and no.” This new 88 | version of hamster significantly raises the standard in terms of data 89 | consistency. Unlike before, it will not be possible to have “Facts” without 90 | an end time specified, nor to have multiple facts overlapping. 91 | 92 | There will be a way to import data that still constitute valid “facts” (having 93 | both a start and an end time). We have, however, not decided on how this will 94 | be implemented, nor what to do with the legacy “facts” that do not have 95 | an end time. 96 | 97 | The general timeline for addressing the actual implementation is: once we are 98 | feature freezing in preparation of release 1.0.0 as part of a more general 99 | pre-release cleanup effort. 100 | 101 | Whilst possible, it is unlikely we will have the resources to provide a fancy 102 | looking GUI to resolve migration conflicts (unless someone new pitches in of 103 | course) so the result will most likely be a migration script of some sort. 104 | 105 | If you are interested in this general issue, please feel free to watch the 106 | `epic issue for 107 | "hamster-lib" `_ that 108 | covers all things relevant. 109 | 110 | News: Version 0.11.0 111 | ---------------------- 112 | This release introduces refines various aspects of your *Hamster-GTK* 113 | experience. Whilst we introduce no new major dialogs (just a simple 114 | about-dialog). We catch up with the lastest version of ``hamster-lib``, 115 | ``0.12.0``. The most noteworthy change for user is probably the ability to use 116 | whitespaces with your ``Activity.name``. Besides that we fixed some rather 117 | anoying bugs as well as continued to refine the codebase. All in all, while 118 | still not big on features, this release should feel much more stable and 119 | reliable. This is not the least due to multiple contributions by ``jtojnar``, 120 | thanks for that! As ususal, for more changes and details, please refer to the 121 | changelog. Happy tracking; Eric. 122 | -------------------------------------------------------------------------------- /ci/dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch-slim 2 | 3 | LABEL maintainer="Eric Goller" 4 | 5 | RUN apt-get update -qq 6 | RUN apt-get install --no-install-recommends -y\ 7 | git\ 8 | python-setuptools\ 9 | python-pip\ 10 | python-dev\ 11 | python3-dev\ 12 | build-essential\ 13 | python-wheel\ 14 | xvfb\ 15 | xauth\ 16 | curl\ 17 | locales\ 18 | gir1.2-pango-1.0\ 19 | gir1.2-gtk-3.0\ 20 | libglib2.0-dev\ 21 | libgtk-3-dev\ 22 | python-gi\ 23 | python3-gi\ 24 | python-cairo\ 25 | python-gi-cairo 26 | 27 | RUN locale-gen C.UTF-8 && /usr/sbin/update-locale LANG=C.UTF-8 28 | ENV LANG=C.UTF-8 LANGUAGE=C.UTF-8 LC_ALL=C.UTF-8 29 | 30 | RUN mkdir hamster-gtk 31 | WORKDIR hamster-gtk 32 | -------------------------------------------------------------------------------- /ci/run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | err=0 4 | trap 'err=1' ERR 5 | 6 | # Start Xvfb 7 | XVFB_WHD=${XVFB_WHD:-1280x720x16} 8 | 9 | Xvfb :99 -ac -screen 0 $XVFB_WHD -nolisten tcp & 10 | xvfb=$! 11 | 12 | export DISPLAY=:99 13 | 14 | pip install --upgrade pip 15 | pip install -r requirements/test.pip 16 | 17 | python setup.py install 18 | make resources 19 | 20 | make test-all 21 | # See: https://docs.codecov.io/docs/testing-with-docker for details 22 | bash <(curl -s https://codecov.io/bash) 23 | test $err = 0 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hamster-gtk.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hamster-gtk.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/hamster-gtk" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hamster-gtk" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/build_process.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Build Process 3 | ============= 4 | 5 | ``hamster-gtk`` provides an easy to use ``dist`` make target that allows easy 6 | creation of binary (wheel) and source distribution packages. 7 | 8 | One thing to be aware of with regards to this process is that 9 | ``hamster_gtk/resources`` houses non-python data required by the package. This 10 | is mainly auxiliary data (css, menu definitions, etc) required by GTK best 11 | practices. In order to avoid having to manage the distribution of those 12 | individual files we follow GTKs recommendation and create a dedicated 13 | ``GResource`` file that contains all those extra files. This can be done 14 | manually with the ``make resources`` target and will also be triggered just 15 | before the ``dist`` target. It is worth noting that the original “data 16 | source code” itself is not part of the package itself. Only 17 | ``hamster-gtk.gresource`` is actually shipped! On the other hand, it is also 18 | for this reason that ``hamster-gtk.gresource`` is not part of the code 19 | repository as it only ever is needed as part of the package creation process. 20 | 21 | Caveat when testing 22 | ------------------- 23 | 24 | Depending on your particular test setup, that is if you do not run the test 25 | suite against the actual package, but against the local source itself you will 26 | need to create ``hamster-gtk.gresource`` with ``make resources`` in order for 27 | the codebase to work. 28 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. hamster-gtk documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to hamster-gtk's documentation! 7 | ======================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | installation 16 | usage 17 | contributing 18 | packaging 19 | build_process 20 | authors 21 | modules 22 | history 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ easy_install hamster-gtk 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv hamster-gtk 12 | $ pip install hamster-gtk 13 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto endguide/ 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\hamster-gtk.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\hamster-gtk.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | hamster_gtk 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | hamster_gtk 8 | -------------------------------------------------------------------------------- /docs/packaging.rst: -------------------------------------------------------------------------------- 1 | Packaging 2 | ========= 3 | 4 | .. ``hamster-gtk`` follows the `semantic versioning `_ scheme. 5 | .. Each release is packaged and uploaded to `pypi `_. 6 | 7 | We provide a compliant ``setup.py`` which contains all the meta information 8 | relevant to users of ``hamster-gtk``. If you stumble upon any incompatibilities 9 | or dependency issue please let us know. If you are interested in packaging 10 | ``hamster-gtk`` for your preferred distribution or in some other context we 11 | would love to hear from you! 12 | 13 | 14 | About requirements/\*.pip 15 | ------------------------- 16 | We do fully follow Donald Stuffts `argument 17 | `_ that information given 18 | ``setup.py`` is of fundamentally different nature than what may be located 19 | under ``requirements.txt`` (Additional comments can be found in the `packaging 20 | guide 21 | `_ 22 | 23 | and with `Hynek Schlawack 24 | `_). 25 | As far as packaging goes ``setup.py`` is authoritative. We provide a set of 26 | specific environments under ``requirements/*`` that mainly developers and 3rd 27 | parties may find useful. This way we can easily enable contributers to get a 28 | suitable ``virtualenv`` running or specify our test environment in one central 29 | location. If for example you wanted to package ``hamster-gtk`` for 30 | ``debian-stable``, it would be mighty convenient to just provide another 31 | requirements.txt with all the relevant dependencies pinned to what your target 32 | distro would provide. Now you can run the entire test suit against a reliable 33 | representation of said target system. 34 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Usage 3 | ======== 4 | 5 | To use hamster-gtk in a project:: 6 | 7 | import hamster-gtk 8 | -------------------------------------------------------------------------------- /hamster_gtk/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """hamster-gtk is a Gtk based timetracking GUI build om top of ``hamster-lib``.""" 4 | 5 | __author__ = 'Eric Goller' 6 | __email__ = 'eric.goller@ninjaduck.solutions' 7 | __version__ = '0.11.0' 8 | -------------------------------------------------------------------------------- /hamster_gtk/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | 20 | """General purpose helper methods.""" 21 | 22 | from __future__ import absolute_import, unicode_literals 23 | 24 | import datetime 25 | import re 26 | 27 | import six 28 | from six import text_type 29 | 30 | 31 | def _u(string): 32 | """ 33 | Return passed string as a text instance. 34 | 35 | This helper is particularly useful to wrap Gtk 'string return values' as 36 | they depend on the used python version. On python 2 Gtk will return utf-8 37 | encoded bytestrings while on python 3 it will return 'unicode strings'. 38 | 39 | The reason we have to use this and not ``six.u`` is that six does not make 40 | assumptions about the encoding and hence can not be used to safely decode 41 | non ascii bytestrings into unicode. 42 | 43 | Args: 44 | string (str): A string instance. On ``py2`` this will be a bytestring, 45 | on ``py3`` a 'unicode string'. 46 | 47 | Returns: 48 | text_type: Returns a 'unicode text type'. Under ``py2`` that means a 49 | ``unicode`` instance, under ``py3`` this will be a ``str`` instance. 50 | """ 51 | if six.PY2: 52 | return string.decode('utf-8') 53 | else: 54 | return string 55 | 56 | 57 | def show_error(parent, error, message=None): 58 | """ 59 | Display an error dialog. 60 | 61 | Besides the clients own error reporting this is suitable to present backend 62 | errors to the user instead of failing silently. 63 | 64 | This functions runs the dialog a modal and takes care of its destruction afterwards. 65 | 66 | Args: 67 | parent (Gtk.Window): Parrent window. 68 | error (str): Exception message. 69 | message (str, optional): User friendly error message providing some broad context. 70 | 71 | Returns: 72 | None 73 | """ 74 | # We can not import this on a global level due to circular imports. 75 | from .misc import ErrorDialog 76 | 77 | if not message: 78 | message = error 79 | dialog = ErrorDialog(parent, message) 80 | dialog.run() 81 | dialog.destroy() 82 | 83 | 84 | def clear_children(widget): 85 | """ 86 | Remove and destroy all children from a widget. 87 | 88 | It seems GTK really does not have this build in. Iterating over all 89 | seems a bit blunt, but seems to be the way to do this. 90 | """ 91 | for child in widget.get_children(): 92 | child.destroy() 93 | return widget 94 | 95 | 96 | def get_parent_window(widget): 97 | """ 98 | Reliably determine parent window of a widget. 99 | 100 | Just using :meth:`Gtk.Widget.get_toplevel` would return the widget itself 101 | if it had no parent window. 102 | 103 | On the other hand using :meth:`Gtk.Widget.get_ancestor` would return only 104 | the closest :class:`Gtk.Window` in the hierarchy. 105 | 106 | https://developer.gnome.org/gtk3/unstable/GtkWidget.html#gtk-widget-get-toplevel 107 | """ 108 | toplevel = widget.get_toplevel() 109 | if not toplevel.is_toplevel(): 110 | toplevel = None 111 | 112 | return toplevel 113 | 114 | 115 | def calendar_date_to_datetime(date): 116 | """Convert :meth:`Gtk.Calendar.get_date` value to :class:`datetime.date`.""" 117 | year, month, day = date 118 | return datetime.date(int(year), int(month) + 1, int(day)) 119 | 120 | 121 | def decompose_raw_fact_string(text, raw=False): 122 | """ 123 | Try to match a given string with modular regex groups. 124 | 125 | Args: 126 | text (text_type): String to be analysed. 127 | raw (bool): If ``True``, return the raw match instance, if ``False`` return 128 | its corresponding ``groupdict``. 129 | 130 | Returns: 131 | re.MatchObject or dict: ``re.MatchObject`` if ``raw=True``, else ``dict``. 132 | Returning the ``re.MatchObject`` is particularly useful if one is 133 | interested in the groups ``span``s. 134 | 135 | Note: 136 | This is not at all about providing valid facts or even raw facts. This function 137 | is only trying to extract whatever information can be matched to its various 138 | groups (aka 'segments'). 139 | Nevertheless, this can be the basis for future implementations 140 | that replace ``Fact.create_from_raw_string`` with a regex based approach. 141 | """ 142 | time_regex = r'([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]' 143 | # Whilst we do not really want to do sanity checks here being as specific as 144 | # possible will enhance matching accuracy. 145 | relative_time_regex = r'-\d{1,3}' 146 | date_regex = r'20[0-9][0-9]-[0-1][0-9]-[0-3][0-9]' 147 | datetime_regex = r'({date}|{time}|{date} {time})'.format(date=date_regex, time=time_regex) 148 | # Please note the trailing whitespace! 149 | timeinfo_regex = r'({relative} |{datetime} |{datetime} - {datetime} )'.format( 150 | datetime=datetime_regex, 151 | relative=relative_time_regex 152 | ) 153 | # This is the central place where we define which characters are viable for 154 | # our various segments. 155 | # Please note that this is also where we define each segments 'separator'. 156 | activity_regex = r'[^@:#,]+' 157 | category_regex = r'@[^@,#]+' 158 | tag_regex = r' (#[^,]+)' 159 | description_regex = r',.+' 160 | 161 | regex = ( 162 | r'^(?P{timeinfo})?(?P{activity})?(?P{category})?' 163 | '(?P({tag})*)(?P{description})?$'.format( 164 | timeinfo=timeinfo_regex, 165 | activity=activity_regex, 166 | category=category_regex, 167 | tag=tag_regex, 168 | description=description_regex, 169 | ) 170 | ) 171 | 172 | pattern = re.compile(regex, re.UNICODE) 173 | match = pattern.match(text) 174 | result = match 175 | if match and not raw: 176 | result = match.groupdict() 177 | return result 178 | 179 | 180 | def get_delta_string(delta): 181 | """ 182 | Return a human readable representation of ``datetime.timedelta`` instance. 183 | 184 | In most contexts it is not that useful to present the delta in seconds. 185 | Instead we return the delta either in minutes or ``hours:minutes`` depending on the 186 | value. 187 | 188 | Args: 189 | delta (datetime.timedelta): The timedelta instance to render. 190 | 191 | Returns: 192 | text_type: The datetime instance rendered as text. 193 | 194 | Note: 195 | So far, this does not account for large deltas that span days and more. 196 | """ 197 | seconds = delta.total_seconds() 198 | minutes = int(seconds / 60) 199 | if minutes < 60: 200 | result = '{} min'.format(minutes) 201 | else: 202 | result = '{hours:02d}:{minutes:02d}'.format( 203 | hours=int(seconds / 3600), minutes=int((seconds % 3600) / 60)) 204 | return text_type(result) 205 | 206 | 207 | def rgb_to_gtk_rgb(r, g, b): 208 | """Map '255' based RGB values to 0-1 based floats expected by cairo.""" 209 | return float(r) / 255, float(g) / 255, float(b) / 255 210 | -------------------------------------------------------------------------------- /hamster_gtk/misc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides several multi purpose dialoges and widgets.""" 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | from .dialogs import (DateRangeSelectDialog, EditFactDialog, # NOQA 23 | ErrorDialog, HamsterAboutDialog) 24 | -------------------------------------------------------------------------------- /hamster_gtk/misc/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides several multi purpose dialogs.""" 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | from .date_range_select_dialog import DateRangeSelectDialog # NOQA 23 | from .edit_fact_dialog import EditFactDialog # NOQA 24 | from .error_dialog import ErrorDialog # NOQA 25 | from .hamster_about_dialog import HamsterAboutDialog # NOQA 26 | -------------------------------------------------------------------------------- /hamster_gtk/misc/dialogs/date_range_select_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | 19 | """This module contains Dialog for selecting a date range.""" 20 | 21 | from __future__ import absolute_import, unicode_literals 22 | 23 | import calendar 24 | import datetime 25 | from gettext import gettext as _ 26 | 27 | from gi.repository import Gtk 28 | 29 | from hamster_gtk import helpers 30 | 31 | 32 | class DateRangeSelectDialog(Gtk.Dialog): 33 | """ 34 | A Dialog that allows to select two dates that form a 'daterange'. 35 | 36 | The core of the dialog is two :class:`Gtk.Calendar` widgets which allow for 37 | manual setting of start- and enddate. Additionally, three presets are 38 | provided for the users convenience. 39 | """ 40 | 41 | # Gtk.Calendar returns month in a ``0`` based ordering which is why we 42 | # need to add/subtract ``1`` when translating with real live months. 43 | 44 | def __init__(self, parent, *args, **kwargs): 45 | """ 46 | Initialize widget. 47 | 48 | Args: 49 | parent (OverviewScreen): Parent window for this dialog. 50 | """ 51 | super(DateRangeSelectDialog, self).__init__(*args, **kwargs) 52 | self.set_transient_for(parent) 53 | self._mainbox = Gtk.Grid() 54 | self._mainbox.set_hexpand(True) 55 | self._mainbox.set_vexpand(True) 56 | 57 | self._start_calendar = Gtk.Calendar() 58 | self._end_calendar = Gtk.Calendar() 59 | 60 | self._mainbox.attach(self._get_today_widget(), 0, 0, 4, 1) 61 | self._mainbox.attach(self._get_week_widget(), 0, 1, 4, 1) 62 | self._mainbox.attach(self._get_month_widget(), 0, 2, 4, 1) 63 | self._mainbox.attach(self._get_custom_range_label(), 0, 3, 1, 1) 64 | self._mainbox.attach(self._get_custom_range_connection_label(), 2, 3, 1, 1) 65 | self._mainbox.attach(self._start_calendar, 1, 3, 1, 1) 66 | self._mainbox.attach(self._end_calendar, 3, 3, 1, 1) 67 | 68 | self.get_content_area().add(self._mainbox) 69 | self.add_action_widget(self._get_apply_button(), Gtk.ResponseType.APPLY) 70 | self.show_all() 71 | 72 | @property 73 | def daterange(self): 74 | """Return start and end date as per calendar widgets.""" 75 | start = helpers.calendar_date_to_datetime(self._start_calendar.get_date()) 76 | end = helpers.calendar_date_to_datetime(self._end_calendar.get_date()) 77 | return (start, end) 78 | 79 | @daterange.setter 80 | def daterange(self, daterange): 81 | """Set calendar dates according to daterange.""" 82 | start, end = daterange 83 | self._start_calendar.select_month(start.month - 1, start.year) 84 | self._start_calendar.select_day(start.day) 85 | self._end_calendar.select_month(end.month - 1, end.year) 86 | self._end_calendar.select_day(end.day) 87 | 88 | # Widgets 89 | def _get_apply_button(self): 90 | button = Gtk.Button(_('_Apply'), use_underline=True) 91 | return button 92 | 93 | def _get_today_widget(self): 94 | """Return a widget that sets the daterange to today.""" 95 | button = self._get_double_label_button(_("Today"), datetime.date.today()) 96 | button.set_hexpand(True) 97 | button.set_relief(Gtk.ReliefStyle.NONE) 98 | button.connect('clicked', self._on_today_button_clicked) 99 | return button 100 | 101 | def _get_week_widget(self): 102 | """Return a widget that sets the daterange to the current week.""" 103 | start, end = self._get_week_range(datetime.date.today()) 104 | date_text = _("{} to {}".format(start, end)) 105 | button = self._get_double_label_button(_("Current Week"), date_text) 106 | button.set_hexpand(True) 107 | button.set_relief(Gtk.ReliefStyle.NONE) 108 | button.connect('clicked', self._on_week_button_clicked) 109 | return button 110 | 111 | def _get_month_widget(self): 112 | """Return a widget that sets the daterange to the current month.""" 113 | start, end = self._get_month_range(datetime.date.today()) 114 | date_text = _("{} to {}".format(start, end)) 115 | button = self._get_double_label_button(_("Current Month"), date_text) 116 | button.set_hexpand(True) 117 | button.set_relief(Gtk.ReliefStyle.NONE) 118 | button.connect('clicked', self._on_month_button_clicked) 119 | return button 120 | 121 | def _get_start_calendar(self): 122 | """Return ``Gtk.Calendar`` instance for the start date.""" 123 | return Gtk.Calendar() 124 | 125 | def _get_end_calendar(self): 126 | """Return ``Gtk.Calendar`` instance for the end date.""" 127 | return Gtk.Calendar() 128 | 129 | def _get_custom_range_label(self): 130 | """Return a 'heading' label for the widget.""" 131 | return Gtk.Label(_("Custom Range")) 132 | 133 | def _get_custom_range_connection_label(self): 134 | """Return the label to be displayed between the two calendars.""" 135 | return Gtk.Label(_("to")) 136 | 137 | # Helper 138 | def _get_double_label_button(self, left_label, right_label): 139 | """ 140 | Return a special button with two label components. 141 | 142 | The left label will be left aligned the right one right aligned. 143 | """ 144 | button = Gtk.Button() 145 | grid = Gtk.Grid() 146 | button.add(grid) 147 | 148 | left_label = Gtk.Label(left_label) 149 | left_label.set_hexpand(True) 150 | left_label.set_halign(Gtk.Align.START) 151 | 152 | right_label = Gtk.Label(right_label) 153 | right_label.set_hexpand(True) 154 | right_label.set_halign(Gtk.Align.END) 155 | 156 | grid.attach(left_label, 0, 0, 1, 1) 157 | grid.attach(right_label, 1, 0, 1, 1) 158 | return button 159 | 160 | def _get_week_range(self, date): 161 | """Return the start- and enddate of the week a given date is in.""" 162 | def get_offset_to_weekstart(weekday): 163 | """ 164 | Return the distance to the desired start of the week given weekday. 165 | 166 | No extra work is required if we want weeks to start on mondays as 167 | in this case ``weekday=0``. If a different start of the week is 168 | desired, we need to add some adjustments. 169 | """ 170 | offset = weekday 171 | return datetime.timedelta(days=offset) 172 | 173 | start = date - get_offset_to_weekstart(date.weekday()) 174 | end = start + datetime.timedelta(days=6) 175 | return (start, end) 176 | 177 | def _get_month_range(self, date): 178 | """Return the start- and enddate of the month a given date is in.""" 179 | start = date - datetime.timedelta(days=date.day - 1) 180 | days_in_month = calendar.monthrange(date.year, date.month)[1] 181 | end = start + datetime.timedelta(days=days_in_month - 1) 182 | return (start, end) 183 | 184 | # Callbacks 185 | def _on_today_button_clicked(self, button): 186 | today = datetime.date.today() 187 | self.daterange = (today, today) 188 | self.response(Gtk.ResponseType.APPLY) 189 | 190 | def _on_week_button_clicked(self, button): 191 | start, end = self._get_week_range(datetime.date.today()) 192 | self.daterange = (start, end) 193 | self.response(Gtk.ResponseType.APPLY) 194 | 195 | def _on_month_button_clicked(self, button): 196 | start, end = self._get_month_range(datetime.date.today()) 197 | self.daterange = (start, end) 198 | self.response(Gtk.ResponseType.APPLY) 199 | -------------------------------------------------------------------------------- /hamster_gtk/misc/dialogs/edit_fact_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | 19 | """This module contains Dialog for editing facts.""" 20 | 21 | from __future__ import absolute_import, unicode_literals 22 | 23 | from gettext import gettext as _ 24 | 25 | from gi.repository import Gtk 26 | from hamster_lib import Fact 27 | from six import text_type 28 | 29 | from hamster_gtk.helpers import _u 30 | 31 | 32 | class EditFactDialog(Gtk.Dialog): 33 | """ 34 | Dialog that allows editing of a fact instance. 35 | 36 | The user can edit the fact in question by entering a new ``raw fact`` 37 | string and/or specifying particular attribute values in dedicated widgets. 38 | In cases where a ``raw fact`` provided contains information in conflict 39 | with values of those dedicated widgets the latter are authorative. 40 | """ 41 | 42 | def __init__(self, parent, fact): 43 | """ 44 | Initialize dialog. 45 | 46 | Args: 47 | parent (Gtk.Window): Parent window. 48 | fact (hamster_lib.Fact): Fact instance to be edited. 49 | """ 50 | super(EditFactDialog, self).__init__() 51 | self._fact = fact 52 | 53 | self.set_transient_for(parent) 54 | self.set_default_size(600, 200) 55 | # ``self.content_area`` is a ``Gtk.Box``. We strive for an 56 | # ``Gtk.Grid`` only based layout. So we have to add this extra step. 57 | self._mainbox = self._get_main_box() 58 | self.get_content_area().add(self._mainbox) 59 | 60 | # We do not use ``self.add_buttons`` because this only allows to pass 61 | # on strings not actual button instances. We want to pass button 62 | # instances however so we can customize them if we want to. 63 | self.add_action_widget(self._get_delete_button(), Gtk.ResponseType.REJECT) 64 | self.add_action_widget(self._get_apply_button(), Gtk.ResponseType.APPLY) 65 | self.add_action_widget(self._get_cancel_button(), Gtk.ResponseType.CANCEL) 66 | self.show_all() 67 | 68 | @property 69 | def updated_fact(self): 70 | """Fact instance using values at the time of accessing it.""" 71 | def get_raw_fact_value(): 72 | """Get text from raw fact entry field.""" 73 | return _u(self._raw_fact_widget.get_text()) 74 | 75 | def get_description_value(): 76 | """Get unicode value from widget.""" 77 | text_view = self._description_widget.get_child() 78 | text_buffer = text_view.get_buffer() 79 | start, end = text_buffer.get_bounds() 80 | return _u(text_buffer.get_text(start, end, True)) 81 | 82 | # Create a new fact instance from the provided raw string. 83 | fact = Fact.create_from_raw_fact(get_raw_fact_value()) 84 | # Instead of transferring all attributes of the parsed fact to the 85 | # existing ``self._fact`` we just go the other way round and attach the 86 | # old facts PK to the newly created instance. 87 | fact.pk = self._fact.pk 88 | # Explicit description trumps anything that may have been included in 89 | # the `raw_fact``. 90 | fact.description = get_description_value() 91 | return fact 92 | 93 | # Widgets 94 | def _get_main_box(self): 95 | """Return the main layout container storing the content area.""" 96 | grid = Gtk.Grid() 97 | grid.set_hexpand(True) 98 | grid.set_vexpand(True) 99 | grid.set_name('EditDialogMainBox') 100 | grid.set_row_spacing(20) 101 | 102 | self._raw_fact_widget = self._get_raw_fact_widget() 103 | self._description_widget = self._get_description_widget() 104 | grid.attach(self._get_old_fact_widget(), 0, 0, 1, 1) 105 | grid.attach(self._raw_fact_widget, 0, 1, 1, 1) 106 | grid.attach(self._description_widget, 0, 2, 1, 1) 107 | return grid 108 | 109 | def _get_old_fact_widget(self): 110 | """Return a widget representing the fact to be edited.""" 111 | label = Gtk.Label(text_type(self._fact)) 112 | label.set_hexpand(True) 113 | label.set_name('EditDialogOldFactLabel') 114 | return label 115 | 116 | def _get_raw_fact_widget(self): 117 | """Return a widget that accepts user input to provide new ``raw fact`` string.""" 118 | entry = Gtk.Entry() 119 | # [FIXME] 120 | # Maybe it would be sensible to have a serialization helper method as 121 | # part of ``hamster-lib``?! 122 | start_string = self._fact.start.strftime('%Y-%m-%d %H:%M:%S') 123 | end_string = self._fact.end.strftime("%Y-%m-%d %H:%M:%S") 124 | if self._fact.category is None: 125 | label = '{start} - {end} {activity}'.format( 126 | start=start_string, 127 | end=end_string, 128 | activity=text_type(self._fact.activity.name) 129 | ) 130 | else: 131 | label = '{start} - {end} {activity}@{category}'.format( 132 | start=start_string, 133 | end=end_string, 134 | activity=text_type(self._fact.activity.name), 135 | category=text_type(self._fact.category.name) 136 | ) 137 | 138 | entry.set_text(label) 139 | entry.set_name('EditDialogRawFactEntry') 140 | return entry 141 | 142 | def _get_description_widget(self): 143 | """Return a widget that displays and allows editing of ``fact.description``.""" 144 | if self._fact.description: 145 | description = self._fact.description 146 | else: 147 | description = '' 148 | window = Gtk.ScrolledWindow() 149 | text_buffer = Gtk.TextBuffer() 150 | text_buffer.set_text(description) 151 | view = Gtk.TextView.new_with_buffer(text_buffer) 152 | view.set_hexpand(True) 153 | window.add(view) 154 | view.set_name('EditDialogDescriptionWindow') 155 | return window 156 | 157 | def _get_delete_button(self): 158 | """Return a *delete* button.""" 159 | return Gtk.Button(_('_Delete'), use_underline=True) 160 | 161 | def _get_apply_button(self): 162 | """Return a *apply* button.""" 163 | return Gtk.Button(_('_Apply'), use_underline=True) 164 | 165 | def _get_cancel_button(self): 166 | """Return a *cancel* button.""" 167 | return Gtk.Button(_('_Cancel'), use_underline=True) 168 | -------------------------------------------------------------------------------- /hamster_gtk/misc/dialogs/error_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | 19 | """This module contains Dialog for displaying an error message.""" 20 | 21 | 22 | from __future__ import absolute_import, unicode_literals 23 | 24 | from gi.repository import Gtk 25 | 26 | 27 | class ErrorDialog(Gtk.MessageDialog): 28 | """ 29 | Error Dialog used to provide feedback to the user. 30 | 31 | Right now we basicly just pass exception messages to the user. Whilst this is usefull to 32 | provide meaningfull information at this early stage, we probably want to provide more user 33 | friendly messages later on. 34 | """ 35 | 36 | def __init__(self, parent, message, *args, **kwargs): 37 | """Initialize dialog.""" 38 | super(ErrorDialog, self).__init__(*args, buttons=Gtk.ButtonsType.CLOSE, 39 | message_type=Gtk.MessageType.ERROR, text=message, **kwargs) 40 | self.set_transient_for(parent) 41 | -------------------------------------------------------------------------------- /hamster_gtk/misc/dialogs/hamster_about_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | 19 | """This module contains Dialog displaying information about the application.""" 20 | 21 | 22 | from __future__ import absolute_import, unicode_literals 23 | 24 | import sys 25 | from gettext import gettext as _ 26 | 27 | import hamster_lib 28 | from gi.repository import Gtk 29 | 30 | import hamster_gtk 31 | 32 | 33 | class HamsterAboutDialog(Gtk.AboutDialog): 34 | """Basic 'About'-dialog class using Gtk default dialog.""" 35 | 36 | # Whilst we are not perfectly happy with the layout and general 37 | # structure of the dialog it is little effort and works for now. 38 | # Alternatively we could either try to customize is to match our 39 | # expectations or construct our own from scratch. 40 | 41 | def __init__(self, parent, *args, **kwargs): 42 | """Initialize the dialog.""" 43 | super(HamsterAboutDialog, self).__init__(*args, **kwargs) 44 | authors = ['Eric Goller '] 45 | python_version_string = '{}.{}.{}'.format( 46 | sys.version_info.major, sys.version_info.minor, sys.version_info.micro 47 | ) 48 | comments = _( 49 | "Thank you for using 'Hamster-GTK.'" 50 | " Your current runtime uses 'hamster-lib' {lib_version} and is interpretet by" 51 | " Python {python_version}.").format(lib_version=hamster_lib.__version__, 52 | python_version=python_version_string) 53 | 54 | meta = { 55 | 'program-name': "Hamster-GTK", 56 | 'version': hamster_gtk.__version__, 57 | 'copyright': "Copyright © 2015–2016 Eric Goller / ninjaduck.solutions", 58 | 'website': "http://projecthamster.org", 59 | 'website-label': _("Visit Project Hamster Website"), 60 | 'title': _("About Hamster-GTK"), 61 | 'license-type': Gtk.License.GPL_3_0, 62 | 'authors': authors, 63 | 'comments': comments, 64 | } 65 | 66 | for key, value in meta.items(): 67 | self.set_property(key, value) 68 | 69 | self.set_transient_for(parent) 70 | -------------------------------------------------------------------------------- /hamster_gtk/misc/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides several multi purpose widgets.""" 19 | 20 | from .labelled_widgets_grid import LabelledWidgetsGrid # NOQA 21 | from .raw_fact_entry import RawFactEntry # NOQA 22 | -------------------------------------------------------------------------------- /hamster_gtk/misc/widgets/labelled_widgets_grid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """Provides a grid for easy construction of preferences or other displays with similar needs.""" 19 | 20 | from __future__ import absolute_import 21 | 22 | from gi.repository import Gtk 23 | 24 | 25 | class LabelledWidgetsGrid(Gtk.Grid): 26 | """A widget which arranges labelled fields automatically.""" 27 | 28 | # Required else you would need to specify the full module name in ui file 29 | __gtype_name__ = 'LabelledWidgetsGrid' 30 | 31 | def __init__(self, fields=None): 32 | """ 33 | Initialize widget. 34 | 35 | Args: 36 | fields (collections.OrderedDict): Ordered dictionary of {key, (label, widget)}. 37 | """ 38 | super(Gtk.Grid, self).__init__() 39 | 40 | if fields: 41 | self._fields = fields 42 | else: 43 | self._fields = {} 44 | 45 | row = 0 46 | for key, (label, widget) in self._fields.items(): 47 | label_widget = Gtk.Label(label) 48 | label_widget.set_use_underline(True) 49 | label_widget.set_mnemonic_widget(widget) 50 | self.attach(label_widget, 0, row, 1, 1) 51 | self.attach(widget, 1, row, 1, 1) 52 | row += 1 53 | 54 | def get_values(self): 55 | """ 56 | Parse config widgets and construct a {field: value} dict. 57 | 58 | Returns: 59 | dict: Dictionary of config keys/values. 60 | """ 61 | result = {} 62 | for key, (label, widget) in self._fields.items(): 63 | result[key] = widget.get_config_value() 64 | 65 | return result 66 | 67 | def set_values(self, values): 68 | """ 69 | Go through widgets and set their values. 70 | 71 | Args: 72 | values (dict): Dictionary of config keys/values 73 | """ 74 | for key, (label, widget) in self._fields.items(): 75 | widget.set_config_value(values.get(key, '')) 76 | -------------------------------------------------------------------------------- /hamster_gtk/overview/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provide a overview dialog that allows access to all facts.""" 19 | 20 | from .dialogs import OverviewDialog # NOQA 21 | -------------------------------------------------------------------------------- /hamster_gtk/overview/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides an overview dialog allowing access to all facts, and an export dialog.""" 19 | 20 | from .export_dialog import ExportDialog # NOQA 21 | from .overview_dialog import OverviewDialog, Totals # NOQA 22 | -------------------------------------------------------------------------------- /hamster_gtk/overview/dialogs/export_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | 19 | """This Module provides the ``ExportDialog`` class to choose file to be exported.""" 20 | 21 | from __future__ import absolute_import 22 | 23 | import os.path 24 | from gettext import gettext as _ 25 | 26 | from gi.repository import Gtk 27 | 28 | from hamster_gtk.misc.widgets import LabelledWidgetsGrid 29 | 30 | 31 | class ExportDialog(Gtk.FileChooserDialog): 32 | """Dialog used for exporting.""" 33 | 34 | def __init__(self, parent): 35 | """ 36 | Initialize export dialog. 37 | 38 | Args: 39 | parent (Gtk.Window): Parent window for the dialog. 40 | """ 41 | super(ExportDialog, self).__init__(_("Please choose where to export to"), parent, 42 | Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 43 | Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) 44 | 45 | self._export_format_chooser = self._get_export_format_chooser() 46 | self._export_format_chooser.connect('changed', self._on_export_format_changed) 47 | 48 | export_options = LabelledWidgetsGrid( 49 | {'format': (_("Export _format:"), self._export_format_chooser)}) 50 | export_options.show_all() 51 | self.set_extra_widget(export_options) 52 | 53 | self.set_current_name(_("hamster_export")) 54 | self._export_format_chooser.set_active_id('tsv') 55 | 56 | def _get_export_format_chooser(self): 57 | chooser = Gtk.ComboBoxText() 58 | chooser.append('tsv', _("TSV")) 59 | chooser.append('ical', _("iCal")) 60 | chooser.append('xml', _("XML")) 61 | return chooser 62 | 63 | def _on_export_format_changed(self, combobox): 64 | """ 65 | Change file extension of the selected file to the one that was chosen. 66 | 67 | Args: 68 | combobox (Gtk.ComboBoxText): Combo box that was changed. 69 | """ 70 | new_ext = self.get_export_format() 71 | (name, ext) = os.path.splitext(self.get_current_name()) 72 | self.set_current_name('{}.{}'.format(name, new_ext)) 73 | 74 | def get_export_format(self): 75 | """ 76 | Return currently selected export format. 77 | 78 | Returns: 79 | text_type: Currently selected export format. 80 | """ 81 | return self._export_format_chooser.get_active_id() 82 | -------------------------------------------------------------------------------- /hamster_gtk/overview/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides widgets to be used by the overview dialog.""" 19 | 20 | from .charts import Charts # NOQA 21 | from .fact_grid import FactGrid # NOQA 22 | from .misc import HeaderBar, Summary # NOQA 23 | -------------------------------------------------------------------------------- /hamster_gtk/overview/widgets/charts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides widgets related to the rendering of charts.""" 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | import operator 23 | 24 | from gi.repository import GObject, Gtk 25 | 26 | from hamster_gtk import helpers 27 | 28 | 29 | class Charts(Gtk.Grid): 30 | """ 31 | A widget that lists all categories with their commutative ``Fact.delta``. 32 | 33 | Features a bar chart that will use the highest category total-delta as scale. 34 | """ 35 | 36 | # [TODO] Evaluate ordering. 37 | 38 | def __init__(self, totals): 39 | """Initialize widget.""" 40 | super(Charts, self).__init__() 41 | self.set_column_spacing(20) 42 | self.attach(Gtk.Label('Categories'), 0, 0, 1, 1) 43 | self.attach(self._get_barcharts(totals.category), 0, 1, 1, 1) 44 | self.attach(Gtk.Label('Activities'), 1, 0, 1, 1) 45 | self.attach(self._get_barcharts(totals.activity), 1, 1, 1, 1) 46 | self.attach(Gtk.Label('Dates'), 2, 0, 1, 1) 47 | self.attach(self._get_barcharts(totals.date), 2, 1, 1, 1) 48 | 49 | def _get_barcharts(self, totals): 50 | """ 51 | Return a widget to represent all categories in a column. 52 | 53 | Args: 54 | totals (dict): A dict that provides delta values for given keys. {key: delta}. 55 | 56 | Returns: 57 | Gtk.Grid: A Grid which contains one column and as many rows as there 58 | are ``keys`` in ``totals``. Each row contains a barchart with labels 59 | showing the delta relative to the highest delta value in ``totals``. 60 | """ 61 | # The highest amount of time spend. This is the scale for all other totals. 62 | # Python 2.7 does not yet have support for the ``default`` kwarg. 63 | if not totals: 64 | max_total = 0 65 | else: 66 | max_total = max(totals.values()) 67 | # Sorting a dict like this returns a list of tuples. 68 | totals = sorted(totals.items(), key=operator.itemgetter(1), 69 | reverse=True) 70 | 71 | grid = Gtk.Grid() 72 | grid.set_column_spacing(5) 73 | grid.set_row_spacing(5) 74 | 75 | # Build individual 'rows'. 76 | row = 0 77 | for category, delta in totals: 78 | # For reducing font size we opt for explicit markup in accordance 79 | # with (3.2) https://developer.gnome.org/gtk3/3.0/gtk-question-index.html#id530878 80 | # As this solution is relative to the users default font size. 81 | category_label = Gtk.Label() 82 | category_label.set_selectable(True) 83 | category_label.set_halign(Gtk.Align.START) 84 | category_label.set_markup("{}".format(category)) 85 | bar_chart = HorizontalBarChart(delta.total_seconds(), max_total.total_seconds(), 100, 86 | 15) 87 | delta_label = Gtk.Label() 88 | delta_label.set_selectable(True) 89 | delta_label.set_halign(Gtk.Align.START) 90 | delta_label.set_markup("{}".format(GObject.markup_escape_text( 91 | helpers.get_delta_string(delta)))) 92 | grid.attach(category_label, 0, row, 1, 1) 93 | grid.attach(bar_chart, 1, row, 1, 1) 94 | grid.attach(delta_label, 2, row, 1, 1) 95 | row += 1 96 | return grid 97 | 98 | 99 | class HorizontalBarChart(Gtk.DrawingArea): 100 | """ 101 | A simple horizontal bar chart. 102 | 103 | Note: 104 | This solution is not too general. It comes without any coordinate system and labeling. 105 | If you need more, either work towards a dedicated library or incorporate any of the big 106 | charting backends. 107 | """ 108 | 109 | def __init__(self, value, max_value, width=150, height=40): 110 | """Initialize widget.""" 111 | super(HorizontalBarChart, self).__init__() 112 | # [FIXME] Make things more flexible/customizable. 113 | 114 | self._value = float(value) 115 | self._max_value = float(max_value) 116 | # -1 for no hints 117 | self._width_hint = width 118 | self._height_hint = height 119 | 120 | self.set_size_request(self._width_hint, self._height_hint) 121 | 122 | self.connect('draw', self._on_draw) 123 | 124 | def _on_draw(self, widget, context): 125 | """Method called on ``draw`` event. Renders the actual widget.""" 126 | context.set_source_rgb(0.8, 0.8, 0.8) 127 | context.set_line_width(0.5) 128 | 129 | allocation = self.get_allocation() 130 | width_allocated = allocation.width 131 | height_allocated = allocation.height 132 | 133 | bar_length = width_allocated * (self._value / self._max_value) 134 | # [FIXME] Revisit. 135 | # bar_width = self._height_hint 136 | bar_width = height_allocated 137 | 138 | x_start, y_start = 0, 0 139 | 140 | bar_x = int(x_start + bar_length) 141 | bar_y = int(y_start + bar_width) 142 | 143 | context.rectangle(x_start, y_start, bar_x, bar_y) 144 | context.fill() 145 | -------------------------------------------------------------------------------- /hamster_gtk/overview/widgets/fact_grid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides widgets related to the 'listing' of facts.""" 19 | 20 | # [FIXME] 21 | # Adding 'unicode_literals' raises encoding issues. This is a major sign we 22 | # have a unicode issue! 23 | from __future__ import absolute_import 24 | 25 | import operator 26 | 27 | from gi.repository import GObject, Gtk 28 | 29 | from hamster_gtk import helpers 30 | from hamster_gtk.misc.dialogs import EditFactDialog 31 | 32 | 33 | class FactGrid(Gtk.Grid): 34 | """Listing of facts per day.""" 35 | 36 | def __init__(self, controller, initial, *args, **kwargs): 37 | """ 38 | Initialize widget. 39 | 40 | Args: 41 | initial (dict): Dictionary where keys represent individual dates 42 | and values an iterable of facts of that date. 43 | """ 44 | super(FactGrid, self).__init__(*args, **kwargs) 45 | self.set_column_spacing(0) 46 | 47 | initial = sorted(initial.items(), key=operator.itemgetter(0), reverse=True) 48 | 49 | row = 0 50 | for date, facts in initial: 51 | # [FIXME] Order by fact start 52 | self.attach(self._get_date_widget(date), 0, row, 1, 1) 53 | self.attach(self._get_fact_list(controller, facts), 1, row, 1, 1) 54 | row += 1 55 | 56 | def _get_date_widget(self, date): 57 | """ 58 | Return a widget to be used in the 'date column'. 59 | 60 | Args: 61 | date (datetime.date): Date to be displayed. 62 | """ 63 | date_string = date.strftime("%A\n%b %d") 64 | date_box = Gtk.EventBox() 65 | date_box.set_name('DayRowDateBox') 66 | date_label = Gtk.Label() 67 | date_label.set_name('OverviewDateLabel') 68 | date_label.set_markup("{}".format(GObject.markup_escape_text(date_string))) 69 | date_label.set_valign(Gtk.Align.START) 70 | date_label.set_justify(Gtk.Justification.RIGHT) 71 | date_box.add(date_label) 72 | return date_box 73 | 74 | def _get_fact_list(self, controller, facts): 75 | """ 76 | Return a widget representing all of the dates facts. 77 | 78 | We use a ``Gtk.ListBox`` as opposed to just adding widgets representing 79 | the facts right to the ``FactGrid`` in order to make use of 80 | ``Gtk.ListBox`` keyboard and mouse navigation / event handling. 81 | """ 82 | # [FIXME] 83 | # It would be preferable to not have to pass the controller instance 84 | # through all the way, but for now it will do. 85 | return FactListBox(controller, facts) 86 | 87 | 88 | class FactListBox(Gtk.ListBox): 89 | """A List widget that represents each fact in a seperate actionable row.""" 90 | 91 | def __init__(self, controller, facts): 92 | """Initialize widget.""" 93 | super(FactListBox, self).__init__() 94 | 95 | self._controller = controller 96 | 97 | self.set_name('OverviewFactList') 98 | self.set_selection_mode(Gtk.SelectionMode.SINGLE) 99 | self.props.activate_on_single_click = False 100 | self.connect('row-activated', self._on_activate) 101 | 102 | for fact in facts: 103 | row = FactListRow(fact) 104 | self.add(row) 105 | 106 | # Signal callbacks 107 | def _on_activate(self, widget, row): 108 | """Callback trigger if a row is 'activated'.""" 109 | edit_dialog = EditFactDialog(helpers.get_parent_window(self), row.fact) 110 | response = edit_dialog.run() 111 | if response == Gtk.ResponseType.CANCEL: 112 | pass 113 | elif response == Gtk.ResponseType.REJECT: 114 | self._delete_fact(edit_dialog._fact) 115 | elif response == Gtk.ResponseType.APPLY: 116 | self._update_fact(edit_dialog.updated_fact) 117 | edit_dialog.destroy() 118 | 119 | def _update_fact(self, fact): 120 | """Update the a fact with values from edit dialog.""" 121 | try: 122 | self._controller.store.facts.save(fact) 123 | except (ValueError, KeyError) as message: 124 | helpers.show_error(helpers.get_parent_window(self), message) 125 | else: 126 | self._controller.signal_handler.emit('facts-changed') 127 | 128 | def _delete_fact(self, fact): 129 | """Delete fact from the backend. No further confirmation is required.""" 130 | try: 131 | result = self._controller.store.facts.remove(fact) 132 | except (ValueError, KeyError) as error: 133 | helpers.show_error(helpers.get_parent_window(self), error) 134 | else: 135 | self._controller.signal_handler.emit('facts-changed') 136 | return result 137 | 138 | 139 | class FactListRow(Gtk.ListBoxRow): 140 | """A row representing a single fact.""" 141 | 142 | def __init__(self, fact): 143 | """ 144 | Initialize widget. 145 | 146 | Attributes: 147 | fact (hamster_lib.Fact): Fact instance represented by this row. 148 | """ 149 | super(FactListRow, self).__init__() 150 | self.fact = fact 151 | self.set_hexpand(True) 152 | self.set_name('FactListRow') 153 | # [FIXME] 154 | # Switch to grid design. 155 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 156 | time_widget = self._get_time_widget(fact) 157 | fact_box = FactBox(fact) 158 | delta_widget = self._get_delta_widget(fact) 159 | hbox.pack_start(time_widget, False, True, 0) 160 | hbox.pack_start(fact_box, True, True, 0) 161 | hbox.pack_start(delta_widget, False, True, 0) 162 | self.add(hbox) 163 | 164 | def _get_time_widget(self, fact): 165 | """"Return widget to represent ``Fact.start`` and ``Fact.end``.""" 166 | start_time = fact.start.strftime('%H:%M') 167 | end_time = fact.end.strftime('%H:%M') 168 | time_label = Gtk.Label('{start} - {end}'.format(start=start_time, end=end_time)) 169 | time_label.props.valign = Gtk.Align.START 170 | time_label.props.halign = Gtk.Align.START 171 | return time_label 172 | 173 | def _get_delta_widget(self, fact): 174 | """"Return widget to represent ``Fact.delta``.""" 175 | label = Gtk.Label('{} Minutes'.format(fact.get_string_delta())) 176 | label.props.valign = Gtk.Align.START 177 | label.props.halign = Gtk.Align.END 178 | box = Gtk.EventBox() 179 | box.add(label) 180 | return box 181 | 182 | 183 | class FactBox(Gtk.Box): 184 | """ 185 | Widget to render details about a fact. 186 | 187 | Note: 188 | ``Fact.start`` and ``Fact.end`` are not shown by *this* widget. 189 | """ 190 | 191 | def __init__(self, fact): 192 | """Initialize widget.""" 193 | super(FactBox, self).__init__(orientation=Gtk.Orientation.VERTICAL) 194 | self.set_name('OverviewFactBox') 195 | # [FIXME] 196 | # Switch to Grid based design 197 | self.pack_start(self._get_activity_widget(fact), True, True, 0) 198 | self.pack_start(self._get_tags_widget(fact), True, True, 0) 199 | if fact.description: 200 | self.pack_start(self._get_description_widget(fact), False, False, 0) 201 | 202 | def _get_activity_widget(self, fact): 203 | """Return widget to render the activity, including its related category.""" 204 | # [FIXME] 205 | # Once 'preferences/config' is live, we can change this. 206 | # Most likly we do not actually need to jump through extra hoops as 207 | # legacy hamster did but just use a i18n'ed string and be done. 208 | if not fact.category: 209 | category = 'not categorised' 210 | else: 211 | category = str(fact.category) 212 | activity_label = Gtk.Label() 213 | activity_label.set_markup("{activity} - {category}".format( 214 | activity=GObject.markup_escape_text(fact.activity.name), 215 | category=GObject.markup_escape_text(category))) 216 | activity_label.props.halign = Gtk.Align.START 217 | return activity_label 218 | 219 | def _get_tags_widget(self, fact): 220 | """Return widget to represent ``Fact.tags``.""" 221 | def get_tag_widget(name): 222 | tag_label = Gtk.Label() 223 | tag_label.set_markup("{}".format(GObject.markup_escape_text(name))) 224 | tag_label.set_name('OverviewTagLabel') 225 | tag_box = Gtk.EventBox() 226 | tag_box.set_name('OverviewTagBox') 227 | tag_box.add(tag_label) 228 | return tag_box 229 | 230 | tags_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 231 | # [FIXME] 232 | # Switch to Grid based layout. 233 | for tag in fact.tags: 234 | tags_box.pack_start(get_tag_widget(tag.name), False, False, 0) 235 | return tags_box 236 | 237 | def _get_description_widget(self, fact): 238 | """Return a widget to render ``Fact.description``.""" 239 | description_label = Gtk.Label() 240 | description_label.set_name('OverviewDescriptionLabel') 241 | description_label.set_line_wrap(True) 242 | description_label.set_markup("{}".format( 243 | GObject.markup_escape_text(fact.description))) 244 | description_label.props.halign = Gtk.Align.START 245 | return description_label 246 | -------------------------------------------------------------------------------- /hamster_gtk/overview/widgets/misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provide widgets that did not fit in the other modules.""" 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | from gettext import gettext as _ 23 | 24 | from gi.repository import GObject, Gtk 25 | from six import text_type 26 | 27 | from hamster_gtk.helpers import get_parent_window 28 | from hamster_gtk.misc.dialogs import DateRangeSelectDialog 29 | from hamster_gtk.overview.dialogs import ExportDialog 30 | 31 | 32 | class HeaderBar(Gtk.HeaderBar): 33 | """Headerbar used by the overview screen.""" 34 | 35 | def __init__(self, controller, *args, **kwargs): 36 | """Initialize headerbar.""" 37 | super(HeaderBar, self).__init__(*args, **kwargs) 38 | self.set_show_close_button(True) 39 | self.set_title(_("Overview")) 40 | self._daterange_button = self._get_daterange_button() 41 | self.pack_start(self._get_prev_daterange_button()) 42 | self.pack_start(self._get_next_daterange_button()) 43 | self.pack_start(self._daterange_button) 44 | self.pack_end(self._get_export_button()) 45 | 46 | controller.signal_handler.connect('daterange-changed', self._on_daterange_changed) 47 | 48 | # Widgets 49 | def _get_export_button(self): 50 | """Return a button to export facts.""" 51 | button = Gtk.Button(_("Export")) 52 | button.connect('clicked', self._on_export_button_clicked) 53 | return button 54 | 55 | def _get_daterange_button(self): 56 | """Return a button that opens the *select daterange* dialog.""" 57 | # We add a dummy label which will be set properly once a daterange is 58 | # set. 59 | button = Gtk.Button('') 60 | button.connect('clicked', self._on_daterange_button_clicked) 61 | return button 62 | 63 | def _get_prev_daterange_button(self): 64 | """Return a 'previous dateframe' widget.""" 65 | button = Gtk.Button(_("Earlier")) 66 | button.connect('clicked', self._on_previous_daterange_button_clicked) 67 | return button 68 | 69 | def _get_next_daterange_button(self): 70 | """Return a 'next dateframe' widget.""" 71 | button = Gtk.Button(_("Later")) 72 | button.connect('clicked', self._on_next_daterange_button_clicked) 73 | return button 74 | 75 | # Callbacks 76 | def _on_daterange_button_clicked(self, button): 77 | """Callback for when the 'daterange' button is clicked.""" 78 | parent = get_parent_window(self) 79 | dialog = DateRangeSelectDialog(parent) 80 | response = dialog.run() 81 | if response == Gtk.ResponseType.APPLY: 82 | parent._daterange = dialog.daterange 83 | dialog.destroy() 84 | 85 | def _on_daterange_changed(self, sender, daterange): 86 | """Callback to be triggered if the 'daterange' changed.""" 87 | def get_label_text(daterange): 88 | start, end = daterange 89 | if start == end: 90 | text = text_type(start) 91 | else: 92 | text = '{} - {}'.format(start, end) 93 | return text 94 | self._daterange_button.set_label(get_label_text(daterange)) 95 | 96 | def _on_previous_daterange_button_clicked(self, button): 97 | """Callback for when the 'previous' button is clicked.""" 98 | get_parent_window(self).apply_previous_daterange() 99 | 100 | def _on_next_daterange_button_clicked(self, button): 101 | """Callback for when the 'next' button is clicked.""" 102 | get_parent_window(self).apply_next_daterange() 103 | 104 | def _on_export_button_clicked(self, button): 105 | """ 106 | Trigger fact export if button clicked. 107 | 108 | This is the place to run extra logic about where to save/which format. 109 | ``parent._export_facts`` only deals with the actual export. 110 | """ 111 | parent = get_parent_window(self) 112 | dialog = ExportDialog(parent) 113 | response = dialog.run() 114 | if response == Gtk.ResponseType.OK: 115 | parent._export_facts(dialog.get_export_format(), dialog.get_filename()) 116 | else: 117 | pass 118 | dialog.destroy() 119 | 120 | 121 | class Summary(Gtk.Box): 122 | """A widget that shows categories with highest commutative ``Fact.delta``.""" 123 | 124 | def __init__(self, category_totals): 125 | """Initialize widget.""" 126 | super(Summary, self).__init__() 127 | 128 | for category, total in category_totals: 129 | label = Gtk.Label() 130 | label.set_markup("{}: {} minutes".format( 131 | GObject.markup_escape_text(text_type(category)), 132 | int(total.total_seconds() / 60))) 133 | self.pack_start(label, False, False, 10) 134 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides the preferences dialog.""" 19 | 20 | from .preferences_dialog import PreferencesDialog # NOQA 21 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/preferences_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | 20 | """This module provides the preferences dialog.""" 21 | 22 | 23 | from __future__ import absolute_import, unicode_literals 24 | 25 | import collections 26 | from gettext import gettext as _ 27 | 28 | import hamster_lib 29 | from gi.repository import GObject, Gtk 30 | 31 | from hamster_gtk.misc.widgets import LabelledWidgetsGrid 32 | from hamster_gtk.preferences.widgets import (ComboFileChooser, 33 | HamsterSwitch, 34 | HamsterComboBoxText, 35 | HamsterSpinButton, 36 | SimpleAdjustment, TimeEntry) 37 | 38 | 39 | class PreferencesDialog(Gtk.Dialog): 40 | """A dialog that shows and allows editing of config settings.""" 41 | 42 | def __init__(self, parent, app, initial, *args, **kwargs): 43 | """ 44 | Instantiate dialog. 45 | 46 | Args: 47 | parent (Gtk.Window): Dialog parent. 48 | app (HamsterGTK): Main app instance. Needed in order to retrieve 49 | and manipulate config values. 50 | initial (dict): Dictionary of initial config key/values. 51 | """ 52 | super(PreferencesDialog, self).__init__(*args, **kwargs) 53 | 54 | self._parent = parent 55 | self._app = app 56 | 57 | self.set_transient_for(self._parent) 58 | 59 | db_engines = [('sqlite', _("SQLite")), ('postgresql', _("PostgreSQL")), 60 | ('mysql', _("MySQL")), ('oracle', _("Oracle")), ('mssql', _("MSSQL"))] 61 | stores = [(store, hamster_lib.REGISTERED_BACKENDS[store].verbose_name) 62 | for store in hamster_lib.REGISTERED_BACKENDS] 63 | 64 | # We use an ordered dict as the order reflects display order as well. 65 | self._pages = [ 66 | (_('Tracking'), LabelledWidgetsGrid(collections.OrderedDict([ 67 | ('day_start', (_('_Day Start (HH:MM:SS)'), TimeEntry())), 68 | ('fact_min_delta', (_('_Minimal Fact Duration'), 69 | HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))), 70 | ]))), 71 | (_('Storage'), LabelledWidgetsGrid(collections.OrderedDict([ 72 | ('store', (_('_Store'), HamsterComboBoxText(stores))), 73 | ('db_engine', (_('DB _Engine'), HamsterComboBoxText(db_engines))), 74 | ('db_path', (_('DB _Path'), ComboFileChooser())), 75 | ('tmpfile_path', (_('_Temporary file'), ComboFileChooser())), 76 | ]))), 77 | (_('Miscellaneous'), LabelledWidgetsGrid(collections.OrderedDict([ 78 | ('autocomplete_activities_range', (_("Autocomplete Activities Range"), 79 | HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))), 80 | ('autocomplete_split_activity', 81 | (_("Autocomplete activities and categories separately"), 82 | HamsterSwitch())), 83 | ]))), 84 | ] 85 | 86 | notebook = Gtk.Notebook() 87 | notebook.set_name('PreferencesNotebook') 88 | 89 | for title, page in self._pages: 90 | notebook.append_page(page, Gtk.Label(title)) 91 | 92 | if initial: 93 | self._set_config(initial) 94 | 95 | self.get_content_area().add(notebook) 96 | self.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) 97 | self.add_button(_("_Apply"), Gtk.ResponseType.APPLY) 98 | 99 | self.show_all() 100 | 101 | def get_config(self): 102 | """ 103 | Parse config pages and construct a {field: value} dict. 104 | 105 | Returns: 106 | dict: Dictionary of config keys/values. Please be aware that returned 107 | value types are dependent on the associated widget. 108 | """ 109 | result = {} 110 | for title, page in self._pages: 111 | for (key, value) in page.get_values().items(): 112 | result[key] = value 113 | 114 | return result 115 | 116 | def _set_config(self, values): 117 | """ 118 | Go through pages and set their values. 119 | 120 | Args: 121 | values (dict): Dictionary of config keys/values. Please be aware that 122 | values must be of the appropriate type as expected by the associated 123 | widget. 124 | 125 | Raises: 126 | ValueError: If ``bool(values)`` is False. 127 | 128 | """ 129 | if not values: 130 | raise ValueError(_("No values provided!")) 131 | 132 | for title, page in self._pages: 133 | page.set_values(values) 134 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides widgets to be used by the preferences dialog.""" 19 | 20 | from .combo_file_chooser import ComboFileChooser # NOQA 21 | from .config_widget import ConfigWidget # NOQA 22 | from .hamster_switch import HamsterSwitch # NOQA 23 | from .hamster_combo_box_text import HamsterComboBoxText # NOQA 24 | from .hamster_spin_button import HamsterSpinButton, SimpleAdjustment # NOQA 25 | from .time_entry import TimeEntry # NOQA 26 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/combo_file_chooser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides file chooser widget.""" 19 | 20 | # [FIXME] 21 | # Adding 'unicode_literals' raises encoding issues. This is a major sign we 22 | # have a unicode issue! 23 | from __future__ import absolute_import 24 | 25 | from gettext import gettext as _ 26 | 27 | from gi.repository import Gtk 28 | from six import text_type 29 | 30 | # [FIXME] 31 | # Remove once hamster-lib has been patched 32 | from hamster_gtk.helpers import _u, get_parent_window 33 | 34 | from .config_widget import ConfigWidget 35 | 36 | 37 | class ComboFileChooser(Gtk.Grid, ConfigWidget): 38 | """A file chooser that also has an entry for changing the path.""" 39 | 40 | # Required else you would need to specify the full module name in ui file 41 | __gtype_name__ = 'ComboFileChooser' 42 | 43 | def __init__(self): 44 | """Initialize widget.""" 45 | super(Gtk.Grid, self).__init__() 46 | 47 | self._entry = Gtk.Entry() 48 | self._entry.set_hexpand(True) 49 | 50 | self._button = Gtk.Button(_("Choose")) 51 | self._button.connect('clicked', self._on_choose_clicked) 52 | 53 | self.attach(self._entry, 0, 0, 1, 1) 54 | self.attach(self._button, 1, 0, 1, 1) 55 | self.connect('mnemonic-activate', self._on_mnemonic_activate) 56 | 57 | def get_config_value(self): 58 | """ 59 | Return the selected path. 60 | 61 | Returns: 62 | six.text_type: Selected file path. 63 | """ 64 | return _u(self._entry.get_text()) 65 | 66 | def set_config_value(self, path): 67 | """ 68 | Select given file path. 69 | 70 | Args: 71 | path (six.text_type): Path to be selected 72 | """ 73 | self._entry.set_text(text_type(path)) 74 | 75 | def _on_choose_clicked(self, widget): 76 | """Open a dialog to select path and update entry widget with it.""" 77 | toplevel = get_parent_window(self) 78 | 79 | dialog = Gtk.FileChooserDialog(_("Please choose a directory"), toplevel, 80 | Gtk.FileChooserAction.SAVE, (_("_Cancel"), Gtk.ResponseType.CANCEL, 81 | _("_Save"), Gtk.ResponseType.OK)) 82 | dialog.set_filename(self.get_config_value()) 83 | response = dialog.run() 84 | if response == Gtk.ResponseType.OK: 85 | self._entry.set_text(_u(dialog.get_filename())) 86 | 87 | dialog.destroy() 88 | 89 | def _on_mnemonic_activate(self, widget, group_cycling): 90 | """Mnemonic associated with this widget was activated.""" 91 | return self._entry.do_mnemonic_activate(self._entry, group_cycling) 92 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/config_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides “interface” for config widgets.""" 19 | 20 | 21 | class ConfigWidget(object): 22 | """ 23 | Abstract class defining unified setter/getter methods for accessing widget values. 24 | 25 | Depending on a particular widget, the way a value is set/retrieved varies while the semantics 26 | are rather constant. This class provides a consistent interface to be implemented by our custom 27 | widgets, which should help in making the access to their values cleaner and more concise. 28 | """ 29 | 30 | def get_config_value(self): 31 | """Get widget value.""" 32 | raise NotImplementedError 33 | 34 | def set_config_value(self, value): 35 | """Set widget value.""" 36 | raise NotImplementedError 37 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/hamster_combo_box_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides ComboBoxText widget extended to store preferences.""" 19 | 20 | from __future__ import absolute_import 21 | 22 | from gi.repository import Gtk 23 | 24 | # [FIXME] 25 | # Remove once hamster-lib has been patched 26 | from hamster_gtk.helpers import _u 27 | 28 | from .config_widget import ConfigWidget 29 | 30 | 31 | class HamsterComboBoxText(Gtk.ComboBoxText, ConfigWidget): 32 | """A ComboBoxText that implements our unified custom ConfigWidget interface.""" 33 | 34 | # Required else you would need to specify the full module name in ui file 35 | __gtype_name__ = 'HamsterComboBoxText' 36 | 37 | def __init__(self, items=None): 38 | """ 39 | Initialize widget. 40 | 41 | Args: 42 | items (dict): Dict of ids/texts to be selectable. 43 | """ 44 | super(Gtk.ComboBoxText, self).__init__() 45 | 46 | if items is not None: 47 | for id, text in items: 48 | self.append(id, text) 49 | 50 | def get_config_value(self): 51 | """ 52 | Return the id of the selected item. 53 | 54 | Returns: 55 | six.text_type: Identifier of the selected item 56 | """ 57 | return _u(self.get_active_id()) 58 | 59 | def set_config_value(self, id): 60 | """ 61 | Select item with a given id. 62 | 63 | Args: 64 | id (six.text_type): Identifier of an item to be selected 65 | """ 66 | self.set_active_id(id) 67 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/hamster_spin_button.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides SpinButton widget extended to store preferences.""" 19 | 20 | from __future__ import absolute_import 21 | 22 | from collections import namedtuple 23 | 24 | from gi.repository import Gtk 25 | 26 | from .config_widget import ConfigWidget 27 | 28 | SimpleAdjustment = namedtuple('SimpleAdjustment', ('min', 'max', 'step')) 29 | """ 30 | Simpilified version of :class:`Gtk.Adjustment`. 31 | 32 | Args: 33 | min: The minimum value. 34 | max: The maximum value. 35 | step: The amount the value will be increased/decreased 36 | when the corresponding button is clicked. 37 | """ 38 | 39 | 40 | class HamsterSpinButton(Gtk.SpinButton, ConfigWidget): 41 | """A widget for entering a duration.""" 42 | 43 | # Required else you would need to specify the full module name in ui file 44 | __gtype_name__ = 'HamsterSpinButton' 45 | 46 | def __init__(self, adjustment=None, climb_rate=0, digits=0): 47 | """ 48 | Initialize widget. 49 | 50 | Args: 51 | adj (Gtk.Adjustment, SimpleAdjustment, optional): Adjustment for the widget, either 52 | :class:`Gtk.Adjustment` or a :class:`SimpleAdjustment`. 53 | See their respective documentation for more information. Defaults to ``None`` in 54 | which case it can be set later. 55 | climb_rate (float): See Gtk.SpinButton documentation. 56 | digits (int): See Gtk.SpinButton documentation. 57 | """ 58 | super(Gtk.SpinButton, self).__init__() 59 | 60 | self.set_numeric(True) 61 | 62 | if adjustment is not None: 63 | if isinstance(adjustment, SimpleAdjustment): 64 | self._validate_simple_adjustment(adjustment) 65 | adjustment = Gtk.Adjustment(adjustment.min, adjustment.min, adjustment.max, 66 | adjustment.step, 10 * adjustment.step, 0) 67 | elif not isinstance(adjustment, Gtk.Adjustment): 68 | raise ValueError('Instance of SimpleAdjustment or Gtk.Adjustment is expected.') 69 | 70 | self.configure(adjustment, climb_rate, digits) 71 | 72 | def _validate_simple_adjustment(self, adjustment): 73 | if adjustment.min > adjustment.max: 74 | raise ValueError('Minimal value has to be lower than maximal value.') 75 | if adjustment.step == 0: 76 | raise ValueError('Step value has to be non-zero.') 77 | 78 | return True 79 | 80 | def get_config_value(self): 81 | """Return selected value. 82 | 83 | Returns: 84 | int: Number entered into the widget 85 | """ 86 | return self.get_value_as_int() 87 | 88 | def set_config_value(self, value): 89 | """ 90 | Set given value. 91 | 92 | Args: 93 | value (int): Value to be set 94 | """ 95 | self.set_value(int(value)) 96 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/hamster_switch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides a HamsterSwitch widget that implements the ConfigWidget mixin.""" 19 | 20 | from __future__ import absolute_import 21 | 22 | from gi.repository import Gtk 23 | 24 | from .config_widget import ConfigWidget 25 | 26 | 27 | class HamsterSwitch(Gtk.Switch, ConfigWidget): 28 | """A ToggleButton that implements our unified custom ConfigWidget interface.""" 29 | 30 | # Required else you would need to specify the full module name in ui file 31 | __gtype_name__ = 'HamsterSwitch' 32 | 33 | def __init__(self, active=False): 34 | """ 35 | Initialize widget. 36 | 37 | Args: 38 | active (bool, optional): State of the button. Defaults to ``False``. 39 | """ 40 | super(Gtk.Switch, self).__init__() 41 | self.set_active(active) 42 | 43 | def get_config_value(self): 44 | """ 45 | Return the id of the selected item. 46 | 47 | Returns: 48 | bool: ``True`` if state is ``active``, ``False`` else. 49 | """ 50 | return self.get_active() 51 | 52 | def set_config_value(self, active): 53 | """ 54 | Set button state according to passed value. 55 | 56 | Args: 57 | active (bool): If ``True`` button will be set to ``active``. ``inactive`` else. 58 | """ 59 | self.set_active(active) 60 | -------------------------------------------------------------------------------- /hamster_gtk/preferences/widgets/time_entry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This module provides a widget for entering time.""" 19 | 20 | from __future__ import absolute_import 21 | 22 | import datetime 23 | 24 | from gi.repository import Gtk 25 | from six import text_type 26 | 27 | # [FIXME] 28 | # Remove once hamster-lib has been patched 29 | from hamster_gtk.helpers import _u 30 | 31 | from .config_widget import ConfigWidget 32 | 33 | 34 | class TimeEntry(Gtk.Entry, ConfigWidget): 35 | """A widget for entering time.""" 36 | 37 | # Required else you would need to specify the full module name in ui file 38 | __gtype_name__ = 'TimeEntry' 39 | 40 | def get_config_value(self): 41 | """ 42 | Return time entered into the widget. 43 | 44 | The entered time has to match either ``HH:MM:SS`` or ``HH:MM`` form, 45 | otherwise an error is thrown. 46 | 47 | Returns: 48 | datetime.time: Selected time. 49 | 50 | Raises: 51 | ValueError: When the text entered in the field does not constitute a valid time. 52 | """ 53 | result = _u(self.get_text()) 54 | # We are tollerant against malformed time information. 55 | try: 56 | result = datetime.datetime.strptime(result, '%H:%M:%S').time() 57 | except ValueError: 58 | result = datetime.datetime.strptime(result, '%H:%M').time() 59 | 60 | return result 61 | 62 | def set_config_value(self, value): 63 | """ 64 | Set the widgets time string to passed value. 65 | 66 | Args: 67 | value (datetime.time): Time to be selected 68 | """ 69 | self.set_text(text_type(value.strftime('%H:%M:%S'))) 70 | -------------------------------------------------------------------------------- /hamster_gtk/resources/css/hamster-gtk.css: -------------------------------------------------------------------------------- 1 | #DayRowDateBox { 2 | background: #dfdfdf; 3 | color: #2e3436; 4 | } 5 | 6 | #OverviewDateLabel { 7 | padding-left: 10px; 8 | padding-right: 10px; 9 | } 10 | 11 | #OverviewTagLabel { 12 | padding: 2px; 13 | } 14 | 15 | #OverviewDescriptionLabel { 16 | padding-bottom: 20px; 17 | } 18 | 19 | #OverviewTagBox { 20 | background: gray; 21 | border-radius: 5px; 22 | border-color: gray; 23 | border-width: 1px 1px 1px 1px; 24 | border-style: solid; 25 | color: white; 26 | } 27 | 28 | #OverviewFactList { 29 | background: @bg_color; 30 | } 31 | 32 | #OverviewFactBox { 33 | padding-left: 10px; 34 | padding-right: 10px; 35 | } 36 | 37 | /* EditDialog */ 38 | #EditDialogMainBox { 39 | padding: 15px; 40 | } 41 | 42 | /*PreferencesDialog*/ 43 | #PreferencesNotebook { 44 | margin-bottom: 0.5em; 45 | } 46 | -------------------------------------------------------------------------------- /hamster_gtk/resources/gtk/menus.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | app.preferences 6 | Prefere_nces 7 | 8 |
9 |
10 | 11 | app.about 12 | _About 13 | 14 | 15 | app.quit 16 | _Quit 17 | <Primary>q 18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /hamster_gtk/resources/hamster-gtk.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | gtk/menus.ui 4 | css/hamster-gtk.css 5 | 6 | 7 | -------------------------------------------------------------------------------- /hamster_gtk/tracking/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # This file is part of 'hamster-gtk'. 4 | # 5 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # 'hamster-gtk' is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with 'hamster-gtk'. If not, see . 17 | 18 | """This sub module provides class suitable to track the 'ongoing fact'.""" 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | from .screens import TrackingScreen # NOQA 23 | -------------------------------------------------------------------------------- /hamster_gtk/tracking/screens.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | 20 | """Screen to handle tracking of an *ongoing fact*.""" 21 | 22 | 23 | from __future__ import absolute_import, unicode_literals 24 | 25 | import datetime 26 | from gettext import gettext as _ 27 | 28 | from gi.repository import GObject, Gtk 29 | from hamster_lib import Fact 30 | 31 | import hamster_gtk.helpers as helpers 32 | from hamster_gtk.helpers import _u 33 | from hamster_gtk.misc.widgets import RawFactEntry 34 | 35 | 36 | class TrackingScreen(Gtk.Stack): 37 | """Main container for the tracking screen.""" 38 | 39 | def __init__(self, app, *args, **kwargs): 40 | """Setup widget.""" 41 | super(TrackingScreen, self).__init__(*args, **kwargs) 42 | self._app = app 43 | 44 | self.main_window = helpers.get_parent_window(self) 45 | self.set_transition_type(Gtk.StackTransitionType.SLIDE_UP) 46 | self.set_transition_duration(1000) 47 | self.current_fact_view = CurrentFactBox(self._app.controller) 48 | self.current_fact_view.connect('tracking-stopped', self.update) 49 | self.start_tracking_view = StartTrackingBox(self._app) 50 | self.start_tracking_view.connect('tracking-started', self.update) 51 | self.add_titled(self.start_tracking_view, 'start tracking', _("Start Tracking")) 52 | self.add_titled(self.current_fact_view, 'ongoing fact', _("Show Ongoing Fact")) 53 | self.update() 54 | self.show_all() 55 | 56 | def update(self, evt=None): 57 | """ 58 | Determine which widget should be displayed. 59 | 60 | This depends on whether there exists an *ongoing fact* or not. 61 | """ 62 | try: 63 | current_fact = self._app.controller.store.facts.get_tmp_fact() 64 | except KeyError: 65 | self.start_tracking_view.show() 66 | self.set_visible_child(self.start_tracking_view) 67 | else: 68 | self.current_fact_view.update(current_fact) 69 | self.current_fact_view.show() 70 | self.set_visible_child(self.current_fact_view) 71 | self.show_all() 72 | 73 | 74 | class CurrentFactBox(Gtk.Box): 75 | """Box to be used if current fact is present.""" 76 | 77 | __gsignals__ = { 78 | str('tracking-stopped'): (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()), 79 | } 80 | 81 | def __init__(self, controller): 82 | """Setup widget.""" 83 | # We need to wrap this in a vbox to limit its vertical expansion. 84 | # [FIXME] 85 | # Switch to Grid based layout. 86 | super(CurrentFactBox, self).__init__(orientation=Gtk.Orientation.VERTICAL, spacing=10) 87 | self._controller = controller 88 | self.content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 89 | self.pack_start(self.content, False, False, 0) 90 | 91 | def update(self, fact=None): 92 | """Update widget content.""" 93 | for child in self.content.get_children(): 94 | child.destroy() 95 | 96 | if not fact: 97 | try: 98 | fact = self._controller.store.facts.get_tmp_fact() 99 | except KeyError: 100 | # This should never be seen by the user. It would mean that a 101 | # switch to this screen has been triggered without an ongoing 102 | # fact existing. 103 | self.content.pack_start(self._get_invalid_label(), True, True, 0) 104 | self.content.pack_start(self._get_fact_label(fact), True, True, 0) 105 | self.content.pack_start(self._get_cancel_button(), False, False, 0) 106 | self.content.pack_start(self._get_save_button(), False, False, 0) 107 | 108 | def _get_fact_label(self, fact): 109 | text = '{fact}'.format(fact=fact) 110 | return Gtk.Label(text) 111 | 112 | def _get_cancel_button(self): 113 | cancel_button = Gtk.Button(_('Cancel')) 114 | cancel_button.connect('clicked', self._on_cancel_button) 115 | return cancel_button 116 | 117 | def _get_save_button(self): 118 | save_button = Gtk.Button(_('Stop & Save')) 119 | save_button.connect('clicked', self._on_save_button) 120 | return save_button 121 | 122 | def _get_invalid_label(self): 123 | """Return placeholder in case there is no current ongoing fact present.""" 124 | return Gtk.Label(_("There currently is no ongoing fact that could be displayed.")) 125 | 126 | # Callbacks 127 | def _on_cancel_button(self, button): 128 | """ 129 | Triggerd when 'cancel' button clicked. 130 | 131 | Discard current *ongoing fact* without saving. 132 | """ 133 | try: 134 | self._controller.store.facts.cancel_tmp_fact() 135 | except KeyError as err: 136 | helpers.show_error(helpers.get_parent_window(self), err) 137 | else: 138 | self.emit('tracking-stopped') 139 | 140 | def _on_save_button(self, button): 141 | """ 142 | Triggerd when 'save' button clicked. 143 | 144 | Save *ongoing fact* to storage. 145 | """ 146 | try: 147 | self._controller.store.facts.stop_tmp_fact() 148 | except Exception as error: 149 | helpers.show_error(helpers.get_parent_window(self), error) 150 | else: 151 | self.emit('tracking-stopped') 152 | # Inform the controller about the chance. 153 | self._controller.signal_handler.emit('facts-changed') 154 | 155 | 156 | class StartTrackingBox(Gtk.Box): 157 | """Box to be used if no *ongoing fact* is present.""" 158 | 159 | __gsignals__ = { 160 | str('tracking-started'): (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()), 161 | } 162 | 163 | # [FIXME] 164 | # Switch to Grid based layout. 165 | 166 | def __init__(self, app, *args, **kwargs): 167 | """Setup widget.""" 168 | super(StartTrackingBox, self).__init__(orientation=Gtk.Orientation.VERTICAL, 169 | spacing=10, *args, **kwargs) 170 | self._app = app 171 | self.set_homogeneous(False) 172 | 173 | # [FIXME] 174 | # Refactor to call separate 'get_widget' methods instead. 175 | # Introduction text 176 | text = _('Currently no tracked activity. Want to start one?') 177 | self.current_fact_label = Gtk.Label(text) 178 | self.pack_start(self.current_fact_label, False, False, 0) 179 | 180 | # Fact entry field 181 | autocomplete_split_activity = self._app._config['autocomplete_split_activity'] 182 | self.raw_fact_entry = RawFactEntry(self._app, autocomplete_split_activity) 183 | self.raw_fact_entry.connect('activate', self._on_raw_fact_entry_activate) 184 | self.pack_start(self.raw_fact_entry, False, False, 0) 185 | 186 | # Buttons 187 | start_button = Gtk.Button(label=_("Start Tracking")) 188 | start_button.connect('clicked', self._on_start_tracking_button) 189 | self.pack_start(start_button, False, False, 0) 190 | 191 | def _start_ongoing_fact(self): 192 | """ 193 | Start a new *ongoing fact*. 194 | 195 | Note: 196 | Whilst we accept the full ``raw_fact`` syntax, we ignore any ``Fact.end`` 197 | information encoded in the string. Unlike legacy hamster we *only* 198 | deal with *ongoing facts* in this widget. 199 | """ 200 | # [FIXME] 201 | # This should be done in one place only. And the hamster-lib. If at all 202 | # via hamster-lib.helpers. 203 | def complete_tmp_fact(fact): 204 | """Apply fallback logic in case no start time has been encoded.""" 205 | if not fact.start: 206 | fact.start = datetime.datetime.now() 207 | # Make sure we dismiss any extracted end information. 208 | fact.end = None 209 | return fact 210 | 211 | raw_fact = _u(self.raw_fact_entry.props.text) 212 | 213 | try: 214 | fact = Fact.create_from_raw_fact(raw_fact) 215 | except Exception as error: 216 | helpers.show_error(helpers.get_parent_window(self), error) 217 | else: 218 | fact = complete_tmp_fact(fact) 219 | 220 | try: 221 | fact = self._app.controller.store.facts.save(fact) 222 | except Exception as error: 223 | helpers.show_error(self.get_top_level(), error) 224 | else: 225 | self.emit('tracking-started') 226 | self._app.controller.signal_handler.emit('facts-changed') 227 | self.reset() 228 | 229 | def reset(self): 230 | """Clear all data entry fields.""" 231 | self.raw_fact_entry.props.text = '' 232 | 233 | # Callbacks 234 | def _on_start_tracking_button(self, button): 235 | """Callback for the 'start tracking' button.""" 236 | self._start_ongoing_fact() 237 | 238 | def _on_raw_fact_entry_activate(self, evt): 239 | """Callback for when ``enter`` is pressed within the entry.""" 240 | self._start_ongoing_fact() 241 | -------------------------------------------------------------------------------- /misc/org.projecthamster.hamster-gtk.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=hamster-gtk 3 | Exec=hamster-gtk 4 | Type=Application 5 | Terminal=false 6 | Encoding=UTF-8 7 | Categories=GTK;GNOME;Utility; 8 | Version=1.0 9 | -------------------------------------------------------------------------------- /requirements/dev.pip: -------------------------------------------------------------------------------- 1 | # This file specifies all packages required for local development 2 | # This file is not part of our packaging specification. It is used to generate 3 | # a reproduceable development environment. For check out ``docs/packaging``. 4 | # For package-requirements refer to ``setup.py``. 5 | 6 | -r ./docs.pip 7 | -r ./test.pip 8 | 9 | bumpversion==0.5.3 10 | ipython==5.2.0 11 | wheel==0.29.0 12 | watchdog==0.8.3 13 | -------------------------------------------------------------------------------- /requirements/docs.pip: -------------------------------------------------------------------------------- 1 | # This file specifies all packages required to build the documentation. 2 | 3 | Sphinx==1.5.2 4 | sphinx-rtd-theme==0.1.9 5 | -------------------------------------------------------------------------------- /requirements/test.pip: -------------------------------------------------------------------------------- 1 | # This file is used for local testing with and without tox as well as for 2 | # testing on the CI servers. 3 | # This file is not part of our packaging specification. It is used to generate 4 | # a reproducible test environment. For check out ``docs/packaging``. 5 | # For package-requirements refer to ``setup.py``. 6 | # 7 | # Some of the requirements are repeated in ``tox.ini`` to explicity check their version. 8 | # Including them here as well has the advantage of keeping them up to date via requires.io 9 | 10 | 11 | check-manifest==0.35 12 | coverage==4.3.4 13 | doc8==0.7.0 14 | # This is needed to prevent ``pytest-factoryboy`` to pull a more recent (faulty) 15 | # version. See #214 for details. 16 | factory-boy==2.8.1 17 | fauxfactory==2.0.9 18 | flake8==3.2.1 19 | flake8-debugger==1.4.0 20 | flake8-print==2.0.2 21 | freezegun==0.3.8 22 | future==0.16.0 23 | isort==4.2.5 24 | pep8-naming==0.4.1 25 | pep257==0.7.0 26 | pytest==3.0.6 27 | pytest-faker==2.0.0 28 | pytest-factoryboy==1.3.0 29 | pytest-mock==1.5.0 30 | pytest-xvfb==1.0.0 31 | tox==2.5.0 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.11.0 3 | commit = True 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:hamster_gtk/__init__.py] 9 | 10 | [coverage:run] 11 | branch = True 12 | source = hamster_gtk 13 | 14 | [isort] 15 | not_skip = __init__.py 16 | known_third_party = faker, factory, fauxfactory, freezegun, future, gi, hamster_lib, 17 | past, pytest, pytest_factoryboy, six 18 | 19 | [tool:pytest] 20 | addopt = 21 | --tb=short 22 | --strict 23 | --rsx 24 | 25 | [flake8] 26 | exclude = build/*.py,docs/*.py,*/.ropeproject/* 27 | ignore = E128 28 | max-line-length = 99 29 | 30 | [check-manifest] 31 | ignore = hamster_gtk/resources/hamster-gtk.gresource 32 | 33 | [doc8] 34 | ignore-path = *.egg-info/,.tox/,docs/_build/ 35 | 36 | [wheel] 37 | universal = 1 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Packing metadata for setuptools.""" 4 | 5 | from io import open 6 | 7 | try: 8 | from setuptools import setup, find_packages 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | 13 | with open('README.rst', encoding='utf-8') as readme_file: 14 | readme = readme_file.read() 15 | 16 | with open('HISTORY.rst', encoding='utf-8') as history_file: 17 | history = history_file.read().replace('.. :changelog:', '') 18 | 19 | requirements = [ 20 | 'orderedset', 21 | 'hamster-lib >= 0.13.0', 22 | ] 23 | 24 | setup( 25 | name='hamster-gtk', 26 | version='0.11.0', 27 | description="A GTK interface to the hamster time tracker.", 28 | long_description=readme + '\n\n' + history, 29 | author="Eric Goller", 30 | author_email='eric.goller@projecthamster.org', 31 | url='https://github.com/projecthamster/hamster-gtk', 32 | packages=find_packages(exclude=['tests*']), 33 | install_requires=requirements, 34 | license="GPL3", 35 | zip_safe=False, 36 | keywords='hamster-gtk', 37 | classifiers=[ 38 | 'Development Status :: 2 - Pre-Alpha', 39 | 'Intended Audience :: End Users/Desktop', 40 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 41 | 'Natural Language :: English', 42 | "Programming Language :: Python :: 2", 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.5', 46 | ], 47 | entry_points=''' 48 | [gui_scripts] 49 | hamster-gtk=hamster_gtk.hamster_gtk:_main 50 | ''', 51 | 52 | package_data={ 53 | 'hamster_gtk': ['resources/hamster-gtk.gresource'], 54 | }, 55 | ) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test for hamster-gtk.""" 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unittest fixtures.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import datetime 8 | 9 | import fauxfactory 10 | import pytest 11 | from gi.repository import Gtk 12 | from hamster_lib.helpers.config_helpers import HamsterAppDirs 13 | from pytest_factoryboy import register 14 | 15 | from hamster_gtk import hamster_gtk 16 | 17 | from . import factories 18 | 19 | register(factories.CategoryFactory) 20 | register(factories.ActivityFactory) 21 | register(factories.FactFactory) 22 | 23 | 24 | @pytest.fixture 25 | def file_path(request, faker): 26 | """Return a file path.""" 27 | return faker.uri_path() 28 | 29 | 30 | @pytest.fixture 31 | def appdirs(request): 32 | """Return HamsterAppDirs instance.""" 33 | return HamsterAppDirs('hamster-gtk') 34 | 35 | 36 | # Instances 37 | @pytest.fixture 38 | def app(request): 39 | """ 40 | Return an ``Application`` fixture. 41 | 42 | Please note: the app has just been started but not activated. 43 | """ 44 | app = hamster_gtk.HamsterGTK() 45 | app._startup(app) 46 | return app 47 | 48 | 49 | @pytest.fixture 50 | def main_window(request, app): 51 | """Return a ``ApplicationWindow`` fixture.""" 52 | return hamster_gtk.MainWindow(app) 53 | 54 | 55 | @pytest.fixture 56 | def header_bar(request, app): 57 | """ 58 | Return a HeaderBar instance. 59 | 60 | Note: 61 | This instance has not been added to any parent window yet! 62 | """ 63 | return hamster_gtk.HeaderBar(app) 64 | 65 | 66 | @pytest.fixture 67 | def dummy_window(request): 68 | """ 69 | Return a generic :class:`Gtk.Window` instance. 70 | 71 | This is useful for tests that do not actually rely on external 72 | functionality. 73 | """ 74 | return Gtk.Window() 75 | 76 | 77 | @pytest.fixture(params=( 78 | fauxfactory.gen_string('utf8'), 79 | fauxfactory.gen_string('cjk'), 80 | fauxfactory.gen_string('latin1'), 81 | fauxfactory.gen_string('cyrillic'), 82 | )) 83 | def word_parametrized(request): 84 | """Return a string paramized with various different charakter constelations.""" 85 | return request.param 86 | 87 | 88 | @pytest.fixture 89 | def facts_grouped_by_date(request, fact_factory): 90 | """Return a dict with facts ordered by date.""" 91 | return {} 92 | 93 | 94 | @pytest.fixture 95 | def set_of_facts(request, fact_factory): 96 | """Provide a set of randomized fact instances.""" 97 | return fact_factory.build_batch(5) 98 | 99 | 100 | @pytest.fixture 101 | def config(request, tmpdir): 102 | """Return a dict of config keys and values.""" 103 | config = { 104 | 'store': 'sqlalchemy', 105 | 'day_start': datetime.time(5, 30, 0), 106 | 'fact_min_delta': 1, 107 | 'tmpfile_path': tmpdir.join('tmpfile.hamster'), 108 | 'db_engine': 'sqlite', 109 | 'db_path': ':memory:', 110 | 'autocomplete_activities_range': 30, 111 | 'autocomplete_split_activity': False, 112 | } 113 | return config 114 | -------------------------------------------------------------------------------- /tests/factories.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | """Factories providing randomized object instances.""" 4 | 5 | from __future__ import unicode_literals 6 | 7 | import datetime 8 | 9 | import factory 10 | import fauxfactory 11 | from future.utils import python_2_unicode_compatible 12 | from hamster_lib import objects 13 | 14 | 15 | @python_2_unicode_compatible 16 | class CategoryFactory(factory.Factory): 17 | """Factory providing randomized ``hamster_lib.Category`` instances.""" 18 | 19 | pk = None 20 | # Although we do not need to reference to the object beeing created and 21 | # ``LazyFunction`` seems sufficient it is not as we could not pass on the 22 | # string encoding. ``LazyAttribute`` allows us to specify a lambda that 23 | # circumvents this problem. 24 | name = factory.LazyAttribute(lambda x: fauxfactory.gen_string('utf8')) 25 | 26 | class Meta: 27 | model = objects.Category 28 | 29 | 30 | @python_2_unicode_compatible 31 | class ActivityFactory(factory.Factory): 32 | """Factory providing randomized ``hamster_lib.Activity`` instances.""" 33 | 34 | pk = None 35 | name = factory.Faker('word') 36 | category = factory.SubFactory(CategoryFactory) 37 | deleted = False 38 | 39 | class Meta: 40 | model = objects.Activity 41 | 42 | 43 | @python_2_unicode_compatible 44 | class FactFactory(factory.Factory): 45 | """ 46 | Factory providing randomized ``hamster_lib.Category`` instances. 47 | 48 | Instances have a duration of 3 hours. 49 | """ 50 | 51 | pk = None 52 | activity = factory.SubFactory(ActivityFactory) 53 | start = factory.Faker('date_time') 54 | end = factory.LazyAttribute(lambda o: o.start + datetime.timedelta(hours=3)) 55 | description = factory.Faker('paragraph') 56 | 57 | class Meta: 58 | model = objects.Fact 59 | -------------------------------------------------------------------------------- /tests/misc/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the misc submodule.""" 2 | -------------------------------------------------------------------------------- /tests/misc/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the misc submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import datetime 8 | 9 | import fauxfactory 10 | import pytest 11 | from gi.repository import GObject, Gtk 12 | from six import text_type 13 | 14 | from hamster_gtk import helpers, overview 15 | from hamster_gtk.misc import dialogs, widgets 16 | 17 | 18 | @pytest.fixture 19 | def daterange_select_dialog(request, main_window, app): 20 | """Return a functional DateRangeSelectDialog instance.""" 21 | overview_dialog = overview.OverviewDialog(main_window, app) 22 | return dialogs.DateRangeSelectDialog(overview_dialog) 23 | 24 | 25 | @pytest.fixture(params=(0, 7, 15, 30, 55)) 26 | def daterange_offset_parametrized(request): 27 | """Return various daterange offset variants as easy to use timedeltas.""" 28 | return datetime.timedelta(days=request.param) 29 | 30 | 31 | @pytest.fixture 32 | def daterange(request): 33 | """Return a randomized daterange tuple.""" 34 | offset = datetime.timedelta(days=7) 35 | start = fauxfactory.gen_date() 36 | return (start, start + offset) 37 | 38 | 39 | @pytest.fixture 40 | def daterange_parametrized(request, daterange_offset_parametrized): 41 | """Return daterange parametrized with various lengths.""" 42 | start = fauxfactory.gen_date() 43 | return (start, start + daterange_offset_parametrized) 44 | 45 | 46 | @pytest.fixture(params=( 47 | (datetime.date(2016, 7, 10), (datetime.date(2016, 7, 4), datetime.date(2016, 7, 10))), 48 | (datetime.date(2016, 7, 1), (datetime.date(2016, 6, 27), datetime.date(2016, 7, 3))), 49 | (datetime.date(2016, 7, 18), (datetime.date(2016, 7, 18), datetime.date(2016, 7, 24))), 50 | )) 51 | def weekrange_parametrized(request): 52 | """Return parametrized ``date``/``weekrange`` pairs.""" 53 | return request.param 54 | 55 | 56 | @pytest.fixture(params=( 57 | (datetime.date(2016, 7, 10), (datetime.date(2016, 7, 1), datetime.date(2016, 7, 31))), 58 | (datetime.date(2016, 2, 2), (datetime.date(2016, 2, 1), datetime.date(2016, 2, 29))), 59 | )) 60 | def monthrange_parametrized(request): 61 | """Return parametrized ``date``/``monthrange`` pairs.""" 62 | return request.param 63 | 64 | 65 | @pytest.fixture 66 | def edit_fact_dialog(request, fact, dummy_window): 67 | """Return a edit fact dialog for a generic fact.""" 68 | return dialogs.EditFactDialog(dummy_window, fact) 69 | 70 | 71 | @pytest.fixture 72 | def raw_fact_entry(request, app): 73 | """Return a ``RawFactEntry`` instance.""" 74 | return widgets.RawFactEntry(app) 75 | 76 | 77 | @pytest.fixture 78 | def raw_fact_completion(request, app): 79 | """Return a RawFactCompletion`` instance.""" 80 | return widgets.raw_fact_entry.RawFactCompletion(app) 81 | 82 | 83 | @pytest.fixture 84 | def activity_model_static(request): 85 | """ 86 | Return a ``ListStore`` instance with the 'foo@bar' activity as it's only row. 87 | 88 | While this fixture is not too generic and has its activity hard coded it allows 89 | for more specific testing as we can know for sure which (sub) string apear and 90 | which do not. 91 | """ 92 | model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) 93 | model.append(['foo@bar', 'foo', 'bar']) 94 | return model 95 | 96 | 97 | @pytest.fixture 98 | def activity_model(request, activity): 99 | """Return a ``ListStore`` instance with a generic ``Activity`` as its only row.""" 100 | model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) 101 | model.append([ 102 | helpers.serialise_activity(activity), 103 | text_type(activity.name), 104 | text_type(activity.category.name) 105 | ]) 106 | return model 107 | -------------------------------------------------------------------------------- /tests/misc/test_dialogs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Most of the widget related methods are tested very naivly. Hardly any 5 | properties are checked right now. It is mostly about checking if they can be 6 | instantiated at all. 7 | Once the actuall design/layout becomes more solidified it may be worth while to 8 | elaborate on those. It should be fairly simple now that we provide the 9 | infrastructure. 10 | """ 11 | 12 | import datetime 13 | 14 | from freezegun import freeze_time 15 | from gi.repository import Gtk 16 | 17 | from hamster_gtk import helpers 18 | from hamster_gtk.misc import dialogs 19 | 20 | 21 | class TestDateRangeSelectDialog(object): 22 | """Unittests for the daterange select dialog.""" 23 | 24 | def test_init(self, dummy_window): 25 | assert dialogs.DateRangeSelectDialog(dummy_window) 26 | 27 | def test_daterange_getter(self, daterange_select_dialog, daterange_parametrized): 28 | """Make sure a tuple of start- end enddate is returned.""" 29 | start, end = daterange_parametrized 30 | daterange_select_dialog._start_calendar.select_month(start.month - 1, start.year) 31 | daterange_select_dialog._start_calendar.select_day(start.day) 32 | daterange_select_dialog._end_calendar.select_month(end.month - 1, end.year) 33 | daterange_select_dialog._end_calendar.select_day(end.day) 34 | assert daterange_select_dialog.daterange == daterange_parametrized 35 | 36 | def test_daterange_setter(self, daterange_select_dialog, daterange_parametrized): 37 | """Make sure the correct start- and endtime is set on the calendars.""" 38 | start, end = daterange_parametrized 39 | dialog = daterange_select_dialog 40 | daterange_select_dialog.daterange = daterange_parametrized 41 | assert helpers.calendar_date_to_datetime(dialog._start_calendar.get_date()) == start 42 | assert helpers.calendar_date_to_datetime(dialog._end_calendar.get_date()) == end 43 | 44 | def test__get_apply_button(self, daterange_select_dialog): 45 | """Make sure widget matches expectation.""" 46 | result = daterange_select_dialog._get_apply_button() 47 | assert isinstance(result, Gtk.Button) 48 | 49 | def test__get_today_widget(self, daterange_select_dialog, mocker): 50 | """Make sure widget matches expectation.""" 51 | daterange_select_dialog._on_today_button_clicked = mocker.MagicMock() 52 | result = daterange_select_dialog._get_today_widget() 53 | result.emit('clicked') 54 | assert daterange_select_dialog._on_today_button_clicked.called 55 | 56 | def test__get_week_widget(self, daterange_select_dialog, mocker): 57 | """Make sure widget matches expectation.""" 58 | daterange_select_dialog._on_week_button_clicked = mocker.MagicMock() 59 | result = daterange_select_dialog._get_week_widget() 60 | result.emit('clicked') 61 | assert daterange_select_dialog._on_week_button_clicked.called 62 | 63 | def test__get_month_widget(self, daterange_select_dialog, mocker): 64 | """Make sure widget matches expectation.""" 65 | daterange_select_dialog._on_month_button_clicked = mocker.MagicMock() 66 | result = daterange_select_dialog._get_month_widget() 67 | result.emit('clicked') 68 | assert daterange_select_dialog._on_month_button_clicked.called 69 | 70 | def test__get_start_calendar(self, daterange_select_dialog): 71 | """Make sure widget matches expectation.""" 72 | result = daterange_select_dialog._get_start_calendar() 73 | assert isinstance(result, Gtk.Calendar) 74 | 75 | def test__get_end_calendar(self, daterange_select_dialog): 76 | """Make sure widget matches expectation.""" 77 | result = daterange_select_dialog._get_end_calendar() 78 | assert isinstance(result, Gtk.Calendar) 79 | 80 | def test__get_custom_range_label(self, daterange_select_dialog): 81 | """Make sure widget matches expectation.""" 82 | result = daterange_select_dialog._get_custom_range_label() 83 | assert isinstance(result, Gtk.Label) 84 | 85 | def test__get_custom_range_connection_label(self, daterange_select_dialog): 86 | """Make sure widget matches expectation.""" 87 | result = daterange_select_dialog._get_custom_range_connection_label() 88 | assert isinstance(result, Gtk.Label) 89 | 90 | def test__get_double_label_button(self, daterange_select_dialog, word_parametrized): 91 | """Make sure widget matches expectation.""" 92 | l_label = word_parametrized 93 | r_label = word_parametrized 94 | result = daterange_select_dialog._get_double_label_button(l_label, r_label) 95 | assert isinstance(result, Gtk.Button) 96 | 97 | def test__get_week_range(self, daterange_select_dialog, weekrange_parametrized): 98 | """Make the right daterange is returned.""" 99 | date, expectation = weekrange_parametrized 100 | result = daterange_select_dialog._get_week_range(date) 101 | assert result == expectation 102 | 103 | def test__get_month_range(self, daterange_select_dialog, monthrange_parametrized): 104 | """Make the right daterange is returned.""" 105 | date, expectation = monthrange_parametrized 106 | result = daterange_select_dialog._get_month_range(date) 107 | assert result == expectation 108 | 109 | @freeze_time('2016-04-01') 110 | def test__on_today_button_clicked(self, daterange_select_dialog, mocker): 111 | """Test that 'datetime' is set to today and right response is triggered.""" 112 | daterange_select_dialog.response = mocker.MagicMock() 113 | daterange_select_dialog._on_today_button_clicked(None) 114 | assert daterange_select_dialog.daterange == (datetime.date(2016, 4, 1), 115 | datetime.date(2016, 4, 1)) 116 | assert daterange_select_dialog.response.called_with(Gtk.ResponseType.APPLY) 117 | 118 | @freeze_time('2016-04-01') 119 | def test__on_week_button_clicked(self, daterange_select_dialog, mocker): 120 | """Test that 'datetime' is set to 'this week' and right response is triggered.""" 121 | daterange_select_dialog.response = mocker.MagicMock() 122 | daterange_select_dialog._on_week_button_clicked(None) 123 | assert daterange_select_dialog.daterange == (datetime.date(2016, 3, 28), 124 | datetime.date(2016, 4, 3)) 125 | assert daterange_select_dialog.response.called_with(Gtk.ResponseType.APPLY) 126 | 127 | @freeze_time('2016-04-01') 128 | def test__on_month_button_clicked(self, daterange_select_dialog, mocker): 129 | """Test that 'datetime' is set to 'this month' and right response is triggered.""" 130 | daterange_select_dialog.response = mocker.MagicMock() 131 | daterange_select_dialog._on_month_button_clicked(None) 132 | assert daterange_select_dialog.daterange == (datetime.date(2016, 4, 1), 133 | datetime.date(2016, 4, 30)) 134 | assert daterange_select_dialog.response.called_with(Gtk.ResponseType.APPLY) 135 | 136 | 137 | class TestEditFactDialog(object): 138 | """Unittests for the edit dialog.""" 139 | 140 | def test_init(self, fact, dummy_window): 141 | result = dialogs.EditFactDialog(dummy_window, fact) 142 | assert result 143 | 144 | def test__get_main_box(self, edit_fact_dialog): 145 | """Make sure the returned container matches expectation.""" 146 | result = edit_fact_dialog._get_main_box() 147 | assert len(result.get_children()) == 3 148 | assert isinstance(result, Gtk.Grid) 149 | 150 | def test__get_old_fact_widget(self, edit_fact_dialog): 151 | """Test the widget representing the original fact.""" 152 | result = edit_fact_dialog._get_old_fact_widget() 153 | assert isinstance(result, Gtk.Label) 154 | 155 | def test__get_raw_fact_widget(self, edit_fact_dialog): 156 | """Test the widget representing the new fact.""" 157 | result = edit_fact_dialog._get_raw_fact_widget() 158 | assert isinstance(result, Gtk.Entry) 159 | 160 | def test__get_desciption_widget(self, edit_fact_dialog): 161 | """Test the description widget matches expectation.""" 162 | result = edit_fact_dialog._get_description_widget() 163 | assert isinstance(result, Gtk.ScrolledWindow) 164 | 165 | def test__get_delete_button(self, edit_fact_dialog): 166 | """Make sure the delete button matches expectations.""" 167 | result = edit_fact_dialog._get_delete_button() 168 | assert isinstance(result, Gtk.Button) 169 | 170 | def test__get_apply_button(self, edit_fact_dialog): 171 | """Make sure the apply button matches expectations.""" 172 | result = edit_fact_dialog._get_apply_button() 173 | assert isinstance(result, Gtk.Button) 174 | 175 | def test__get_cancel_button(self, edit_fact_dialog): 176 | """Make sure the cancel button matches expectations.""" 177 | result = edit_fact_dialog._get_cancel_button() 178 | assert isinstance(result, Gtk.Button) 179 | 180 | # [FIXME] 181 | # Add tests for changed values. 182 | def test_updated_fact_same(self, dummy_window, fact): 183 | """Make sure the property returns Fact with matching field values.""" 184 | dialog = dialogs.EditFactDialog(dummy_window, fact) 185 | result = dialog.updated_fact 186 | assert result.as_tuple() == fact.as_tuple() 187 | 188 | 189 | class TestErrorDialog(object): 190 | """Unittests for ErrorDialog.""" 191 | 192 | def test_init_with_parent_window(self, dummy_window): 193 | """Test instances where toplevel is a window instance.""" 194 | result = dialogs.ErrorDialog(dummy_window, '') 195 | assert result 196 | -------------------------------------------------------------------------------- /tests/misc/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the miscellaneous widgets.""" 2 | -------------------------------------------------------------------------------- /tests/misc/widgets/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the misc widgets submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import collections 8 | 9 | import pytest 10 | 11 | from hamster_gtk.misc import widgets 12 | from hamster_gtk.preferences.widgets import (ComboFileChooser, 13 | HamsterComboBoxText) 14 | 15 | 16 | @pytest.fixture 17 | def preference_page_fields(request): 18 | """Return a static dict of valid fields suitable to be consumed by ``LabelledWidgetsGrid``.""" 19 | return collections.OrderedDict(( 20 | ('store', ('_Store', HamsterComboBoxText([]))), 21 | ('db_engine', ('DB _Engine', HamsterComboBoxText([]))), 22 | ('db_path', ('DB _Path', ComboFileChooser())), 23 | ('tmpfile_path', ('_Temporary file', ComboFileChooser())), 24 | )) 25 | 26 | 27 | @pytest.fixture 28 | def labelled_widgets_grid(request, preference_page_fields): 29 | """Return a ``LabelledWidgetsGrid`` instance.""" 30 | return widgets.LabelledWidgetsGrid(preference_page_fields) 31 | -------------------------------------------------------------------------------- /tests/misc/widgets/test_labelled_widgets_grid.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | """Unittests for LabelledWidgetsGrid.""" 20 | 21 | from __future__ import unicode_literals 22 | 23 | import pytest 24 | 25 | from hamster_gtk.misc.widgets import LabelledWidgetsGrid 26 | 27 | 28 | @pytest.mark.parametrize('no_fields', (True, False)) 29 | def test_init(preference_page_fields, no_fields): 30 | """Make sure instantiation works with and without fields provided.""" 31 | if no_fields: 32 | grid = LabelledWidgetsGrid() 33 | assert grid._fields == {} 34 | else: 35 | grid = LabelledWidgetsGrid(preference_page_fields) 36 | assert grid._fields == preference_page_fields 37 | rows = len(grid.get_children()) / 2 38 | assert rows == len(grid._fields) 39 | 40 | 41 | def test_get_values(labelled_widgets_grid, preference_page_fields, mocker): 42 | """Make sure widget fetches values for all its sub-widgets.""" 43 | for key, (label, widget) in preference_page_fields.items(): 44 | widget.get_config_value = mocker.MagicMock() 45 | labelled_widgets_grid.get_values() 46 | for key, (label, widget) in preference_page_fields.items(): 47 | assert widget.get_config_value.called 48 | 49 | 50 | def test_set_values(labelled_widgets_grid, preference_page_fields, mocker): 51 | """Make sure widget sets values for all its sub-widgets.""" 52 | for key, (label, widget) in preference_page_fields.items(): 53 | widget.set_config_value = mocker.MagicMock() 54 | labelled_widgets_grid.set_values(preference_page_fields) 55 | for key, (label, widget) in preference_page_fields.items(): 56 | assert widget.set_config_value.called 57 | -------------------------------------------------------------------------------- /tests/misc/widgets/test_raw_fact_completion.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | """Unittests for RawFactCompletion.""" 20 | 21 | from __future__ import absolute_import, unicode_literals 22 | 23 | from orderedset import OrderedSet 24 | 25 | from gi.repository import Gtk 26 | 27 | from hamster_gtk.misc.widgets.raw_fact_entry import RawFactCompletion 28 | 29 | 30 | def test_init(app, mocker): 31 | """Test instantiation.""" 32 | result = RawFactCompletion(app) 33 | assert result.get_model() 34 | assert result.get_text_column() == 0 35 | 36 | 37 | def test__get_stores(raw_fact_completion, activity_factory, mocker): 38 | """Make sure the ``ListStore`` is populated to our expectations.""" 39 | raw_fact_completion._get_activities = mocker.MagicMock( 40 | return_value=activity_factory.build_batch(5)) 41 | raw_fact_completion._populate_stores(None) 42 | assert raw_fact_completion._get_activities.called 43 | for key, store in raw_fact_completion.segment_models.items(): 44 | assert isinstance(store, Gtk.ListStore) 45 | assert len(store) == 5 46 | 47 | 48 | def test__get_activities(app, raw_fact_completion, fact_factory, mocker): 49 | """Make sure that we fetch the right activities and remove duplicates.""" 50 | # In reality we can not get duplicate facts but separate facts with the 51 | # same ``Activity`` but this will do just fine. 52 | fact_1, fact_2 = fact_factory.build_batch(2) 53 | app.controller.facts.get_all = mocker.MagicMock( 54 | return_value=[fact_1, fact_2, fact_1]) 55 | result = raw_fact_completion._get_activities() 56 | assert result == OrderedSet([fact_1.activity, fact_2.activity]) 57 | -------------------------------------------------------------------------------- /tests/misc/widgets/test_raw_fact_entry.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | # This file is part of 'hamster-gtk'. 5 | # 6 | # 'hamster-gtk' is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 'hamster-gtk' is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with 'hamster-gtk'. If not, see . 18 | 19 | """Unittests for RawFactEntry.""" 20 | 21 | from __future__ import absolute_import, unicode_literals 22 | 23 | from hamster_gtk.misc.widgets import RawFactEntry 24 | 25 | 26 | def test_init(app): 27 | assert RawFactEntry(app) 28 | 29 | 30 | def test__on_facts_changed(raw_fact_entry): 31 | old_completion = raw_fact_entry.get_completion() 32 | raw_fact_entry._on_facts_changed(None) 33 | assert raw_fact_entry.get_completion() is not old_completion 34 | -------------------------------------------------------------------------------- /tests/overview/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the overview submodule.""" 2 | -------------------------------------------------------------------------------- /tests/overview/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the overview submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import datetime 8 | import random 9 | 10 | import pytest 11 | 12 | from hamster_gtk.overview import dialogs, widgets 13 | 14 | 15 | # Instances 16 | 17 | @pytest.fixture 18 | def charts(request, totals): 19 | """Return a Charts instance.""" 20 | return widgets.Charts(totals) 21 | 22 | 23 | @pytest.fixture 24 | def fact_grid(request, app): 25 | """ 26 | Retrurn a generic FactGrid instance. 27 | 28 | Note: 29 | This instance does not have a parent associated. 30 | """ 31 | return widgets.fact_grid.FactGrid(app.controller, {}) 32 | 33 | 34 | @pytest.fixture 35 | def fact_list_box(request, app, set_of_facts): 36 | """Return a FactListBox with random facts.""" 37 | return widgets.fact_grid.FactListBox(app.controller, set_of_facts) 38 | 39 | 40 | @pytest.fixture 41 | def factlist_row(request, fact): 42 | """Return a plain FactListRow instance.""" 43 | return widgets.fact_grid.FactListRow(fact) 44 | 45 | 46 | @pytest.fixture 47 | def factbox(request, fact): 48 | """Return a plain FactBox instance.""" 49 | return widgets.fact_grid.FactBox(fact) 50 | 51 | 52 | @pytest.fixture 53 | def overview_dialog(main_window, app): 54 | """Return a generic :class:`OverViewDialog` instance.""" 55 | return dialogs.OverviewDialog(main_window, app) 56 | 57 | # Data 58 | 59 | 60 | @pytest.fixture 61 | def bar_chart_data(request): 62 | """ 63 | Return a (value, max_value) tuple suitable to instantiate a BarChart. 64 | 65 | The value is randomized. BarChart widgets also expect a max_value that 66 | establishes the baseline (100%) for the chart. This fixtures provides such 67 | a value that makes sure that in effect the value is is in between 5% - 100% 68 | of that max value. 69 | """ 70 | value = random.randrange(1, 100) 71 | # In case value is max value for the total set 72 | minimal_max_value = value 73 | # Value is at least 5% of the max value for total set 74 | maximal_max_value = 20 * value 75 | max_value = random.randrange(minimal_max_value, maximal_max_value) 76 | return (value, max_value) 77 | 78 | 79 | @pytest.fixture 80 | def category_highest_totals(request, faker): 81 | """Provide a list of timedeltas representing highest category totals.""" 82 | amount = 3 83 | return [(faker.name(), faker.time_delta()) for i in range(amount)] 84 | 85 | 86 | @pytest.fixture 87 | def totals(request, faker): 88 | """Return a randomized 'Totals'-tuple.""" 89 | amount = 5 90 | category_totals = {faker.name(): faker.time_delta() for i in range(amount)} 91 | return dialogs.Totals(activity=[], category=category_totals, date=[]) 92 | 93 | 94 | @pytest.fixture(params=(0, 7, 15, 30, 55)) 95 | def daterange_offset_parametrized(request): 96 | """Return various daterange offset variants as easy to use timedeltas.""" 97 | return datetime.timedelta(days=request.param) 98 | -------------------------------------------------------------------------------- /tests/overview/test_dialogs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import pytest 5 | 6 | 7 | class TestOverviewDialog(object): 8 | """Unittests for the overview dialog.""" 9 | 10 | def test__get_facts(self, overview_dialog, mocker): 11 | """Make sure that daterange is considered when fetching facts.""" 12 | overview_dialog._app.store.facts.get_all = mocker.MagicMock() 13 | overview_dialog._get_facts() 14 | assert overview_dialog._app.store.facts.get_all.called_with(*overview_dialog._daterange) 15 | 16 | @pytest.mark.parametrize('exception', (TypeError, ValueError)) 17 | def test__get_facts_handled_exception(self, overview_dialog, exception, mocker): 18 | """Make sure that we show error dialog if we encounter an expected exception.""" 19 | overview_dialog._app.store.facts.get_all = mocker.MagicMock(side_effect=exception) 20 | show_error = mocker.patch( 21 | 'hamster_gtk.overview.dialogs.overview_dialog.helpers.show_error') 22 | result = overview_dialog._get_facts() 23 | assert result is None 24 | assert show_error.called 25 | 26 | def test__get_facts_unhandled_exception(self, overview_dialog, mocker): 27 | """Make sure that we do not intercept unexpected exceptions.""" 28 | overview_dialog._app.store.facts.get_all = mocker.MagicMock(side_effect=Exception) 29 | with pytest.raises(Exception): 30 | overview_dialog._get_facts() 31 | 32 | # [FIXME] 33 | # It is probably good to also have a more comprehensive test that actually 34 | # checks if a file with particular content is written. 35 | @pytest.mark.parametrize('format_writer', ( 36 | ('tsv', 'hamster_gtk.overview.dialogs.overview_dialog.reports.TSVWriter'), 37 | ('ical', 'hamster_gtk.overview.dialogs.overview_dialog.reports.ICALWriter'), 38 | ('xml', 'hamster_gtk.overview.dialogs.overview_dialog.reports.XMLWriter') 39 | )) 40 | def test__export_facts(self, overview_dialog, tmpdir, mocker, format_writer): 41 | """ 42 | Make sure the proper report class is instantiated and writter. 43 | 44 | With all its mocks this test is not the best one imageinable, but as 45 | the method will change rapidly soon this does for now. 46 | """ 47 | target_format, writer = format_writer 48 | writer = mocker.patch(writer) 49 | overview_dialog._get_facts = mocker.MagicMock(return_value={}) 50 | result = overview_dialog._export_facts(target_format, tmpdir.strpath) 51 | assert result is None 52 | assert writer.called 53 | assert overview_dialog._get_facts.called 54 | -------------------------------------------------------------------------------- /tests/overview/test_widgets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import datetime 6 | 7 | import pytest 8 | from gi.repository import Gtk 9 | 10 | from hamster_gtk.overview import widgets 11 | 12 | 13 | class TestFactGrid(object): 14 | """Unittests for FactGrid.""" 15 | 16 | def test_init(self, app): 17 | """Make sure minimal initialisation works.""" 18 | fact_grid = widgets.FactGrid(app.controller, {}) 19 | assert fact_grid 20 | 21 | def test__get_date_widget(self, fact_grid): 22 | """Make sure expected label is returned.""" 23 | result = fact_grid._get_date_widget(datetime.date.today()) 24 | assert isinstance(result, Gtk.EventBox) 25 | 26 | def test__get_fact_list(self, app, fact_grid): 27 | """Make sure a FactListBox is retuned.""" 28 | result = fact_grid._get_fact_list(app.controller, []) 29 | assert isinstance(result, widgets.fact_grid.FactListBox) 30 | 31 | 32 | class TestFactListBox(object): 33 | """Unittest for FactListBox.""" 34 | 35 | def test_init(self, app, set_of_facts): 36 | """Test that instantiation works as expected.""" 37 | result = widgets.fact_grid.FactListBox(app.controller, set_of_facts) 38 | assert isinstance(result, widgets.fact_grid.FactListBox) 39 | assert len(result.get_children()) == len(set_of_facts) 40 | 41 | def test__on_activate_reject(self, fact_list_box, fact, mocker): 42 | """Make sure an edit dialog is created, processed and then destroyed.""" 43 | fact_list_box.get_toplevel = Gtk.Window 44 | mocker.patch('hamster_gtk.overview.widgets.fact_grid.EditFactDialog.run', 45 | return_value=Gtk.ResponseType.REJECT) 46 | fact_list_box._delete_fact = mocker.MagicMock() 47 | row = mocker.MagicMock() 48 | row.fact = fact 49 | fact_list_box._on_activate(None, row) 50 | assert fact_list_box._delete_fact.called 51 | 52 | def test__on_activate_apply(self, fact_list_box, fact, mocker): 53 | """Make sure an edit dialog is created, processed and then destroyed.""" 54 | fact_list_box.get_toplevel = Gtk.Window 55 | mocker.patch('hamster_gtk.overview.widgets.fact_grid.EditFactDialog.run', 56 | return_value=Gtk.ResponseType.APPLY) 57 | fact_list_box._update_fact = mocker.MagicMock() 58 | row = mocker.MagicMock() 59 | row.fact = fact 60 | fact_list_box._on_activate(None, row) 61 | assert fact_list_box._update_fact.called 62 | 63 | def test__delete_fact(self, request, fact_list_box, fact, mocker): 64 | """Make sure that ``facts-changed`` signal is emitted.""" 65 | fact_list_box._controller.store.facts.remove = mocker.MagicMock() 66 | fact_list_box.emit = mocker.MagicMock() 67 | result = fact_list_box._delete_fact(fact) 68 | assert fact_list_box._controller.store.facts.remove.called 69 | assert result is result 70 | assert fact_list_box.emit.called_with('facts-changed') 71 | 72 | @pytest.mark.parametrize('exception', (KeyError, ValueError)) 73 | def test__delete_fact_expected_exception(self, request, fact_list_box, exception, fact, 74 | mocker): 75 | """Make sure that we show error dialog if we encounter an expected exception.""" 76 | fact_list_box._controller.store.facts.remove = mocker.MagicMock(side_effect=exception) 77 | show_error = mocker.patch('hamster_gtk.overview.widgets.fact_grid.helpers.show_error') 78 | fact_list_box.emit = mocker.MagicMock() 79 | result = fact_list_box._delete_fact(fact) 80 | assert result is None 81 | assert show_error.called 82 | assert fact_list_box.emit.called is False 83 | 84 | def test__delete_fact_unexpected_exception(self, request, fact_list_box, fact, mocker): 85 | """Make sure that we do not intercept unexpected exceptions.""" 86 | fact_list_box._controller.store.facts.remove = mocker.MagicMock(side_effect=Exception) 87 | with pytest.raises(Exception): 88 | fact_list_box._on_cancel_button(fact) 89 | 90 | 91 | class TestFactListRow(object): 92 | """Unittests for FactListRow.""" 93 | 94 | def test_init(self, fact): 95 | """Make sure instantiated object matches expectations.""" 96 | result = widgets.fact_grid.FactListRow(fact) 97 | assert isinstance(result, widgets.fact_grid.FactListRow) 98 | hbox = result.get_children()[0] 99 | children = hbox.get_children() 100 | assert isinstance(children[0], Gtk.Label) 101 | assert isinstance(children[1], widgets.fact_grid.FactBox) 102 | assert isinstance(children[2], Gtk.EventBox) 103 | 104 | def test_get_time_widget(self, factlist_row, fact): 105 | """Make sure widget matches expectations.""" 106 | result = factlist_row._get_time_widget(fact) 107 | assert isinstance(result, Gtk.Label) 108 | 109 | def test_get_delta_widget(self, factlist_row, fact): 110 | """Make sure widget matches expectations.""" 111 | result = factlist_row._get_delta_widget(fact) 112 | assert isinstance(result, Gtk.EventBox) 113 | 114 | 115 | class TestFactBox(object): 116 | """Unittests for FactBox.""" 117 | 118 | def test_init(self, fact): 119 | """Make sure instantiated object matches expectations.""" 120 | result = widgets.fact_grid.FactBox(fact) 121 | assert isinstance(result, widgets.fact_grid.FactBox) 122 | assert len(result.get_children()) == 3 123 | 124 | def test_init_without_description(self, fact): 125 | """Make sure instantiated object matches expectations.""" 126 | fact.description = '' 127 | result = widgets.fact_grid.FactBox(fact) 128 | assert isinstance(result, widgets.fact_grid.FactBox) 129 | assert len(result.get_children()) == 2 130 | 131 | def test__get_activity_widget(self, factbox, fact): 132 | """Make sure instantiated object matches expectations.""" 133 | result = factbox._get_activity_widget(fact) 134 | assert isinstance(result, Gtk.Label) 135 | 136 | def test__get_tags_widget(self, factbox, fact): 137 | """Make sure instantiated object matches expectations.""" 138 | # [FIXME] 139 | # Once the method is not just using a dummy tag, this needs to be 140 | # refactored. 141 | result = factbox._get_tags_widget(fact) 142 | assert isinstance(result, Gtk.Box) 143 | assert len(result.get_children()) == len(fact.tags) 144 | 145 | def test__get_desciption_widget(self, factbox, fact): 146 | """Make sure instantiated object matches expectations.""" 147 | result = factbox._get_description_widget(fact) 148 | assert isinstance(result, Gtk.Label) 149 | result = factbox._get_description_widget(fact) 150 | assert isinstance(result, Gtk.Label) 151 | 152 | 153 | class TestCharts(object): 154 | """Unittests for Charts.""" 155 | 156 | def test_init(self, totals): 157 | """Make sure instance matches expectation.""" 158 | result = widgets.Charts(totals) 159 | assert isinstance(result, widgets.Charts) 160 | assert len(result.get_children()) == 6 161 | 162 | def test__get_barcharts(self, charts, totals): 163 | """Make sure widget matches expectations.""" 164 | result = charts._get_barcharts(totals.category) 165 | assert isinstance(result, Gtk.Grid) 166 | # Each category will trigger adding 3 children. 167 | assert len(result.get_children()) == 3 * len(totals.category) 168 | 169 | 170 | class TestHorizontalBarChart(object): 171 | """Unittests for HorizontalBarChart.""" 172 | 173 | # [FIXME] 174 | # Figure out a way to test the draw function properly. 175 | 176 | def test_init(self, bar_chart_data): 177 | """Make sure instance matches expectations.""" 178 | value, max_value = bar_chart_data 179 | result = widgets.charts.HorizontalBarChart(value, max_value) 180 | assert isinstance(result, widgets.charts.HorizontalBarChart) 181 | 182 | 183 | class TestSummary(object): 184 | """Unittests for Summery.""" 185 | 186 | def test_init(self, category_highest_totals): 187 | """Test that instance meets expectation.""" 188 | result = widgets.Summary(category_highest_totals) 189 | assert isinstance(result, widgets.Summary) 190 | assert len(result.get_children()) == len(category_highest_totals) 191 | -------------------------------------------------------------------------------- /tests/preferences/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the preferences submodule.""" 2 | -------------------------------------------------------------------------------- /tests/preferences/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the preferences submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import datetime 8 | 9 | import fauxfactory 10 | import pytest 11 | 12 | from hamster_gtk.preferences.preferences_dialog import PreferencesDialog 13 | 14 | 15 | # Data 16 | @pytest.fixture(params=('sqlalchemy',)) 17 | def store_parametrized(request): 18 | """Return a parametrized store value.""" 19 | return request.param 20 | 21 | 22 | @pytest.fixture(params=( 23 | datetime.time(0, 0, 0), 24 | datetime.time(5, 30, 0), 25 | datetime.time(17, 22, 0), 26 | )) 27 | def day_start_parametrized(request): 28 | """Return a parametrized day_start value.""" 29 | return request.param 30 | 31 | 32 | @pytest.fixture(params=(0, 1, 30, 60)) 33 | def fact_min_delta_parametrized(request): 34 | """Return a parametrized fact_min_delta value.""" 35 | return request.param 36 | 37 | 38 | @pytest.fixture(params=( 39 | # fauxfactory.gen_utf8(), 40 | # fauxfactory.gen_latin1(), 41 | fauxfactory.gen_alphanumeric(), 42 | )) 43 | def tmpfile_path_parametrized(request, tmpdir): 44 | """Return a parametrized tmpfile_path value.""" 45 | return tmpdir.mkdir(request.param).join('tmpfile.hamster') 46 | 47 | 48 | @pytest.fixture(params=( 49 | 'sqlite', 50 | )) 51 | def db_engine_parametrized(request): 52 | """Return a parametrized db_engine value.""" 53 | return request.param 54 | 55 | 56 | @pytest.fixture(params=( 57 | # fauxfactory.gen_utf8(), 58 | # fauxfactory.gen_latin1(), 59 | fauxfactory.gen_alphanumeric(), 60 | ':memory:', 61 | )) 62 | def db_path_parametrized(request, tmpdir): 63 | """Return a parametrized db_path value.""" 64 | if not request.param == ':memory:': 65 | path = tmpdir.mkdir(request.param).join('hamster.file') 66 | else: 67 | path = request.param 68 | return path 69 | 70 | 71 | @pytest.fixture(params=(0, 1, 30, 60)) 72 | def autocomplete_activities_range_parametrized(request): 73 | """Return a parametrized autocomplete_activities_range value.""" 74 | return request.param 75 | 76 | 77 | @pytest.fixture(params=(True, False)) 78 | def autocomplete_split_activity_parametrized(request): 79 | """Return a parametrized autocomplete_split_activity value.""" 80 | return request.param 81 | 82 | 83 | @pytest.fixture 84 | def config_parametrized(request, store_parametrized, day_start_parametrized, 85 | fact_min_delta_parametrized, tmpfile_path_parametrized, db_engine_parametrized, 86 | db_path_parametrized, autocomplete_activities_range_parametrized, 87 | autocomplete_split_activity_parametrized): 88 | """Return a config fixture with heavily parametrized config values.""" 89 | return { 90 | 'store': store_parametrized, 91 | 'day_start': day_start_parametrized, 92 | 'fact_min_delta': fact_min_delta_parametrized, 93 | 'tmpfile_path': tmpfile_path_parametrized, 94 | 'db_engine': db_engine_parametrized, 95 | 'db_path': db_path_parametrized, 96 | 'autocomplete_activities_range': autocomplete_activities_range_parametrized, 97 | 'autocomplete_split_activity': autocomplete_split_activity_parametrized, 98 | } 99 | 100 | 101 | # Instances 102 | @pytest.fixture 103 | def preferences_dialog(request, dummy_window, app, config): 104 | """Return a ``PreferenceDialog`` instance.""" 105 | return PreferencesDialog(dummy_window, app, config) 106 | -------------------------------------------------------------------------------- /tests/preferences/test_preferences_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pytest 6 | 7 | from hamster_gtk.preferences import PreferencesDialog 8 | 9 | 10 | class TestPreferencesDialog(object): 11 | """Unittests for PreferencesDialog.""" 12 | 13 | @pytest.mark.parametrize('empty_initial', (True, False)) 14 | def test_init(self, dummy_window, app, config, empty_initial): 15 | """Make instantiation works as expected.""" 16 | if empty_initial: 17 | config = {} 18 | result = PreferencesDialog(dummy_window, app, config) 19 | grids = result.get_content_area().get_children()[0].get_children() 20 | # This assumes 2 children per config entry (label and widget). 21 | grid_entry_counts = [len(g.get_children()) / 2 for g in grids] 22 | assert sum(grid_entry_counts) == 8 23 | 24 | def test_get_config(self, preferences_dialog, config_parametrized): 25 | """ 26 | Make sure retrieval of field values works as expected. 27 | 28 | In particular we need to make sure that unicode/utf-8 handling works as 29 | expected. 30 | """ 31 | preferences_dialog._set_config(config_parametrized) 32 | result = preferences_dialog.get_config() 33 | assert result == config_parametrized 34 | 35 | def test_set_config(self, preferences_dialog, config_parametrized): 36 | """Make sure setting the field values works as expected.""" 37 | preferences_dialog._set_config(config_parametrized) 38 | for title, page in preferences_dialog._pages: 39 | for key, (label, widget) in page._fields.items(): 40 | assert widget.get_config_value() == config_parametrized[key] 41 | 42 | def test_set_config_empty_value(self, preferences_dialog): 43 | with pytest.raises(ValueError): 44 | preferences_dialog._set_config({}) 45 | -------------------------------------------------------------------------------- /tests/preferences/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the preferences widgets.""" 2 | -------------------------------------------------------------------------------- /tests/preferences/widgets/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the preferences submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import datetime 8 | import os.path 9 | 10 | import pytest 11 | from gi.repository import Gtk 12 | 13 | from hamster_gtk.preferences import widgets 14 | 15 | 16 | # Instances 17 | 18 | @pytest.fixture 19 | def hamster_combo_box_text(request, combo_box_items): 20 | """Return a HamsterComboBoxText instance.""" 21 | widget = widgets.HamsterComboBoxText() 22 | for id, item in combo_box_items: 23 | widget.append(id, item) 24 | return widget 25 | 26 | 27 | @pytest.fixture 28 | def combo_file_chooser(request): 29 | """Return a ComboFileChooser instance.""" 30 | return widgets.ComboFileChooser() 31 | 32 | 33 | @pytest.fixture 34 | def hamster_spin_button(request, adjustment): 35 | """Return a HamsterSpinButton instance.""" 36 | return widgets.HamsterSpinButton(adjustment) 37 | 38 | 39 | @pytest.fixture 40 | def time_entry(request): 41 | """Return a TimeEntry instance.""" 42 | return widgets.TimeEntry() 43 | 44 | 45 | # Data 46 | 47 | @pytest.fixture 48 | def combo_box_items(request, faker): 49 | """Return a collection of items to be placed into ComboBox.""" 50 | amount = 5 51 | return [(faker.user_name(), faker.name()) for i in range(amount)] 52 | 53 | 54 | @pytest.fixture 55 | def paths(request, faker): 56 | """Return a list of file paths.""" 57 | amount = 3 58 | return [os.path.join(faker.uri(), faker.uri_path()) for i in range(amount)] 59 | 60 | 61 | @pytest.fixture 62 | def adjustment(request, numbers): 63 | """Return a list of random numbers.""" 64 | value = sorted(numbers)[len(numbers) // 2] 65 | lower = min(numbers) 66 | upper = max(numbers) 67 | step_increment = 1 68 | page_increment = 5 69 | page_size = 0 70 | return Gtk.Adjustment(value, lower, upper, step_increment, page_increment, page_size) 71 | 72 | 73 | @pytest.fixture 74 | def simple_adjustment(request, faker): 75 | """Return a ``SimpleAdjustment``.""" 76 | a = faker.random_number() 77 | b = faker.random_number() 78 | if a == b: 79 | b += 1 80 | step = faker.random_number(digits=2) 81 | return widgets.SimpleAdjustment(min=min(a, b), max=max(a, b), step=step) 82 | 83 | 84 | @pytest.fixture 85 | def numbers(request, faker): 86 | """Return a list of random numbers.""" 87 | amount = 8 88 | return [faker.random_number(digits=5) for i in range(amount)] 89 | 90 | 91 | @pytest.fixture 92 | def times(request, faker): 93 | """Return a list of random times.""" 94 | amount = 3 95 | return [datetime.datetime.strptime(faker.time(), '%H:%M:%S').time() for i in range(amount)] 96 | 97 | 98 | @pytest.fixture 99 | def times_without_seconds(request, times): 100 | """Return a list of random times rounded down to whole minutes.""" 101 | return [time.replace(second=0) for time in times] 102 | -------------------------------------------------------------------------------- /tests/preferences/widgets/test_combo_file_chooser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | from gi.repository import Gtk 6 | 7 | from hamster_gtk.preferences import widgets 8 | 9 | 10 | class TestComboFileChooser(object): 11 | """Unittests for ComboFileChooser.""" 12 | 13 | def test_init(self): 14 | """Make sure minimal initialisation works.""" 15 | file_chooser = widgets.ComboFileChooser() 16 | assert file_chooser 17 | 18 | def test_instance(self): 19 | """Make sure the widget is still a Grid.""" 20 | file_chooser = widgets.ComboFileChooser() 21 | assert isinstance(file_chooser, Gtk.Grid) 22 | 23 | def test_mnemonic(self, combo_file_chooser): 24 | """Make sure the widget can be accessed using mnemonic.""" 25 | assert combo_file_chooser._on_mnemonic_activate(combo_file_chooser, False) 26 | 27 | def test_get_config_value(self, combo_file_chooser, paths): 28 | """Make sure the widget value is retrieved correctly.""" 29 | for path in paths: 30 | combo_file_chooser._entry.set_text(path) 31 | assert combo_file_chooser.get_config_value() == path 32 | 33 | def test_set_config_value(self, combo_file_chooser, paths): 34 | """Make sure the widget value is set correctly.""" 35 | for path in paths: 36 | combo_file_chooser.set_config_value(path) 37 | assert combo_file_chooser._entry.get_text() == path 38 | -------------------------------------------------------------------------------- /tests/preferences/widgets/test_config_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import pytest 6 | 7 | from hamster_gtk.preferences import widgets 8 | 9 | 10 | class TestConfigWidget(object): 11 | """Unittests for ConfigWidget.""" 12 | 13 | def test_get_config_value(self): 14 | """Calling get_config_value should raise NotImplementedError.""" 15 | widget = widgets.ConfigWidget() 16 | with pytest.raises(NotImplementedError): 17 | widget.get_config_value() 18 | 19 | def test_set_config_value(self): 20 | """Calling set_config_value should raise NotImplementedError.""" 21 | widget = widgets.ConfigWidget() 22 | with pytest.raises(NotImplementedError): 23 | widget.set_config_value(None) 24 | -------------------------------------------------------------------------------- /tests/preferences/widgets/test_hamster_combo_box_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import random 6 | 7 | from gi.repository import Gtk 8 | 9 | from hamster_gtk.preferences import widgets 10 | 11 | 12 | class TestHamsterComboBoxText(object): 13 | """Unittests for HamsterComboBoxText.""" 14 | 15 | def test_init(self): 16 | """Make sure minimal initialisation works.""" 17 | combo_box = widgets.HamsterComboBoxText() 18 | assert combo_box 19 | 20 | def test_instance(self): 21 | """Make sure the widget is still a ComboBoxText.""" 22 | combo_box = widgets.HamsterComboBoxText() 23 | assert isinstance(combo_box, Gtk.ComboBoxText) 24 | 25 | def test_values_constructor(self, combo_box_items): 26 | """Make sure the ComboBoxText can be populated via constructor.""" 27 | combo_box = widgets.HamsterComboBoxText(combo_box_items) 28 | assert len(combo_box.get_model()) == len(combo_box_items) 29 | 30 | def test_get_config_value(self, hamster_combo_box_text, combo_box_items): 31 | """Make sure the widget value is retrieved correctly.""" 32 | random.shuffle(combo_box_items) 33 | for id, text in combo_box_items: 34 | hamster_combo_box_text.set_active_id(id) 35 | assert hamster_combo_box_text.get_config_value() == id 36 | 37 | def test_set_config_value(self, hamster_combo_box_text, combo_box_items): 38 | """Make sure the widget value is set correctly.""" 39 | random.shuffle(combo_box_items) 40 | for id, text in combo_box_items: 41 | hamster_combo_box_text.set_config_value(id) 42 | assert hamster_combo_box_text.get_active_id() == id 43 | -------------------------------------------------------------------------------- /tests/preferences/widgets/test_hamster_spin_button.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import pytest 6 | from gi.repository import Gtk 7 | 8 | from hamster_gtk.preferences.widgets import HamsterSpinButton, SimpleAdjustment 9 | 10 | 11 | class TestHamsterSpinButton(object): 12 | """Unittests for HamsterSpinButton.""" 13 | 14 | def test_init(self): 15 | """Make sure minimal initialisation works.""" 16 | spin = HamsterSpinButton() 17 | assert spin 18 | 19 | def test_instance(self): 20 | """Make sure the widget is still a SpinButton.""" 21 | spin = HamsterSpinButton() 22 | assert isinstance(spin, Gtk.SpinButton) 23 | 24 | def test_params_constructor(self, simple_adjustment): 25 | """Make sure the widget can be set up via constructor.""" 26 | spin = HamsterSpinButton(simple_adjustment) 27 | adjustment = spin.get_adjustment() 28 | assert adjustment.get_lower() == simple_adjustment.min 29 | assert adjustment.get_upper() == simple_adjustment.max 30 | assert adjustment.get_step_increment() == simple_adjustment.step 31 | 32 | def test_params_constructor_adjustment(self, adjustment): 33 | """Make sure the widget can be set up by passing :class:`Gtk.Adjustment` to constructor.""" 34 | spin = HamsterSpinButton(adjustment) 35 | assert spin.get_adjustment() == adjustment 36 | 37 | def test_params_constructor_overlapping_bounds(self, simple_adjustment): 38 | """Passing lower bound greater than upper bound to the constructor should fail.""" 39 | new_adj = SimpleAdjustment(min=simple_adjustment.max, max=simple_adjustment.min, 40 | step=simple_adjustment.step) 41 | with pytest.raises(ValueError): 42 | HamsterSpinButton(new_adj) 43 | 44 | def test_params_constructor_zero_step(self, simple_adjustment): 45 | """Passing zero as a step to the constructor should fail.""" 46 | new_adj = SimpleAdjustment(min=simple_adjustment.min, max=simple_adjustment.max, step=0) 47 | with pytest.raises(ValueError): 48 | HamsterSpinButton(new_adj) 49 | 50 | def test_params_constructor_invalid(self): 51 | """ 52 | Passing a value that is neither a :class:`Gtk.Adjustment`, nor a :class:`SimpleAdjustment` 53 | to the constructor should fail. 54 | """ 55 | with pytest.raises(ValueError): 56 | HamsterSpinButton(42) 57 | 58 | def test_get_config_value(self, hamster_spin_button, numbers): 59 | """Make sure the widget value is retrieved correctly.""" 60 | for number in numbers: 61 | hamster_spin_button.set_value(number) 62 | assert hamster_spin_button.get_config_value() == number 63 | 64 | def test_set_config_value(self, hamster_spin_button, numbers): 65 | """Make sure the widget value is set correctly.""" 66 | for number in numbers: 67 | hamster_spin_button.set_config_value(number) 68 | assert hamster_spin_button.get_value_as_int() == number 69 | -------------------------------------------------------------------------------- /tests/preferences/widgets/test_time_entry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import pytest 6 | from gi.repository import Gtk 7 | 8 | from hamster_gtk.preferences import widgets 9 | 10 | 11 | class TestTimeEntry(object): 12 | """Unittests for TimeEntry.""" 13 | 14 | def test_init(self): 15 | """Make sure minimal initialisation works.""" 16 | entry = widgets.TimeEntry() 17 | assert entry 18 | 19 | def test_instance(self): 20 | """Make sure the widget is still a Entry.""" 21 | entry = widgets.TimeEntry() 22 | assert isinstance(entry, Gtk.Entry) 23 | 24 | def test_get_invalid(self, time_entry): 25 | """Make sure an error is raised when the entered value is invalid.""" 26 | time_entry.set_text('moo') 27 | with pytest.raises(ValueError): 28 | time_entry.get_config_value() 29 | 30 | def test_get_config_value(self, time_entry, times): 31 | """Make sure the widget value is retrieved correctly.""" 32 | for time in times: 33 | time_entry.set_text(time.strftime('%H:%M:%S')) 34 | assert time_entry.get_config_value() == time 35 | 36 | def test_get_config_value_short(self, time_entry, times_without_seconds): 37 | """Make sure the widget value is retrieved correctly when using the short time form.""" 38 | for time in times_without_seconds: 39 | time_entry.set_text(time.strftime('%H:%M')) 40 | assert time_entry.get_config_value() == time 41 | 42 | def test_set_config_value(self, time_entry, times): 43 | """Make sure the widget value is set correctly.""" 44 | for time in times: 45 | time_entry.set_config_value(time) 46 | assert time_entry.get_text() == time.strftime('%H:%M:%S') 47 | 48 | def test_set_config_value_short(self, time_entry, times_without_seconds): 49 | """Make sure the widget value is set correctly evem if our value ommits seconds.""" 50 | for time in times_without_seconds: 51 | time_entry.set_config_value(time) 52 | assert time_entry.get_text() == time.strftime('%H:%M:%S') 53 | -------------------------------------------------------------------------------- /tests/test_hamster-gtk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | import os.path 7 | 8 | from gi.repository import Gtk 9 | 10 | import hamster_gtk.hamster_gtk as hamster_gtk 11 | from hamster_gtk.tracking import TrackingScreen 12 | 13 | 14 | class TestHamsterGTK(object): 15 | """Unittests for the main app class.""" 16 | 17 | def test_instantiation(self): 18 | """Make sure class instatiation works as intended.""" 19 | app = hamster_gtk.HamsterGTK() 20 | assert app 21 | 22 | def test__reload_config(self, app, config, mocker): 23 | """Make sure a config is retrieved and stored as instance attribute.""" 24 | app._get_config_from_file = mocker.MagicMock(return_value=config) 25 | result = app._reload_config() 26 | assert result == config 27 | assert app._config == config 28 | 29 | def test__get_default_config(self, app, appdirs): 30 | """Make sure the defaults use appdirs for relevant paths.""" 31 | result = app._get_default_config() 32 | assert len(result) == 8 33 | assert os.path.dirname(result['tmpfile_path']) == appdirs.user_data_dir 34 | assert os.path.dirname(result['db_path']) == appdirs.user_data_dir 35 | 36 | def test__config_to_configparser(self, app, config): 37 | """Make sure conversion of a config dictionary matches expectations.""" 38 | result = app._config_to_configparser(config) 39 | assert result.get('Backend', 'store') == config['store'] 40 | assert datetime.datetime.strptime( 41 | result.get('Backend', 'day_start'), '%H:%M:%S' 42 | ).time() == config['day_start'] 43 | assert int(result.get('Backend', 'fact_min_delta')) == config['fact_min_delta'] 44 | assert result.get('Backend', 'tmpfile_path') == config['tmpfile_path'] 45 | assert result.get('Backend', 'db_engine') == config['db_engine'] 46 | assert result.get('Backend', 'db_path') == config['db_path'] 47 | 48 | def test__get_configparser_to_config(self, app, config): 49 | """Make sure conversion works as expected.""" 50 | # [FIXME] 51 | # Maybe we find a better way to do this? 52 | cp_instance = app._config_to_configparser(config) 53 | result = app._configparser_to_config(cp_instance) 54 | assert result['store'] == cp_instance.get('Backend', 'store') 55 | assert result['day_start'] == datetime.datetime.strptime( 56 | cp_instance.get('Backend', 'day_start'), '%H:%M:%S').time() 57 | assert result['fact_min_delta'] == int(cp_instance.get('Backend', 'fact_min_delta')) 58 | assert result['tmpfile_path'] == cp_instance.get('Backend', 'tmpfile_path') 59 | assert result['db_engine'] == cp_instance.get('Backend', 'db_engine') 60 | assert result['db_path'] == cp_instance.get('Backend', 'db_path') 61 | 62 | def test__config_changed(self, app, config, mocker): 63 | """Make sure the controller *and* client config is updated.""" 64 | app._reload_config = mocker.MagicMock(return_value=config) 65 | app.controller.update_config = mocker.MagicMock() 66 | app._config_changed(None) 67 | assert app._reload_config.called 68 | assert app.controller.update_config.called_with(config) 69 | 70 | def test__create_actions(self, app, mocker): 71 | """Test that that actions are created.""" 72 | app.add_action = mocker.MagicMock() 73 | app._create_actions() 74 | assert app.add_action.call_count == 4 75 | 76 | def test__on_about_action(self, app, mocker): 77 | """Make sure an about dialog is created.""" 78 | about_class = mocker.patch('hamster_gtk.hamster_gtk.AboutDialog') 79 | app._on_about_action(None, None) 80 | assert about_class.called 81 | assert about_class.return_value.run.called 82 | 83 | def test__on_overview_action(self, app, mocker): 84 | """Make sure an overview dialog is created.""" 85 | overview_class = mocker.patch('hamster_gtk.hamster_gtk.OverviewDialog') 86 | app._on_overview_action(None, None) 87 | assert overview_class.called 88 | assert overview_class.return_value.run.called 89 | 90 | def test__on_preferences_action(self, app, mocker): 91 | """Make sure a preference dialog is created.""" 92 | preferences_class = mocker.patch('hamster_gtk.hamster_gtk.PreferencesDialog') 93 | app._on_preferences_action(None, None) 94 | assert preferences_class.called 95 | assert preferences_class.return_value.run.called 96 | 97 | def test__on_preferences_action_apply(self, app, mocker): 98 | """Make sure config is saved when apply is pressed in preference dialog.""" 99 | mocker.patch('hamster_gtk.hamster_gtk.PreferencesDialog.run', 100 | return_value=Gtk.ResponseType.APPLY) 101 | app.save_config = mocker.MagicMock() 102 | app._on_preferences_action(None, None) 103 | assert app.save_config.called 104 | 105 | def test__on_preferences_action_cancel(self, app, mocker): 106 | """Make sure config is not saved when cancel is pressed in preference dialog.""" 107 | mocker.patch('hamster_gtk.hamster_gtk.PreferencesDialog.run', 108 | return_value=Gtk.ResponseType.CANCEL) 109 | app.save_config = mocker.MagicMock() 110 | app._on_preferences_action(None, None) 111 | assert not app.save_config.called 112 | 113 | 114 | class TestMainWindow(object): 115 | """Unittests for the main application window.""" 116 | 117 | def test_init(self, app): 118 | """Make sure class setup works up as intended.""" 119 | window = hamster_gtk.MainWindow(app) 120 | assert isinstance(window.get_titlebar(), hamster_gtk.HeaderBar) 121 | assert isinstance(window.app, hamster_gtk.HamsterGTK) 122 | assert isinstance(window.get_children()[0], TrackingScreen) 123 | 124 | 125 | class TestHeaderBar(object): 126 | """Unittests for main window titlebar.""" 127 | 128 | def test_initial_anatomy(self, header_bar): 129 | """Test that the bars initial setup is as expected.""" 130 | assert header_bar.props.title == 'Hamster-GTK' 131 | assert header_bar.props.subtitle == 'Your friendly time tracker.' 132 | assert header_bar.props.show_close_button 133 | assert len(header_bar.get_children()) == 1 134 | 135 | def test__get_overview_button(self, header_bar, mocker): 136 | """Test that that button returned matches expectation.""" 137 | header_bar._on_overview_button = mocker.MagicMock() 138 | result = header_bar._get_overview_button() 139 | assert isinstance(result, Gtk.Button) 140 | result.emit('clicked') 141 | assert header_bar._on_overview_button.called 142 | 143 | def test__on_overview_button(self, main_window, mocker): 144 | """Make sure a new overview is created if none exist.""" 145 | bar = main_window.get_titlebar() 146 | overview_class = mocker.patch('hamster_gtk.hamster_gtk.OverviewDialog') 147 | bar._on_overview_button(None) 148 | assert overview_class.called 149 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | 5 | from gi.repository import Gtk 6 | 7 | import pytest 8 | 9 | import hamster_gtk.helpers as helpers 10 | 11 | 12 | def test_get_parent_window_standalone(request): 13 | """Make sure the parent window of a windowless widget is None.""" 14 | label = Gtk.Label('foo') 15 | assert helpers.get_parent_window(label) is None 16 | 17 | 18 | def test_get_parent_window(request): 19 | """Make sure the parent window of a widget placed in the window is determined correctly.""" 20 | window = Gtk.Window() 21 | label = Gtk.Label('foo') 22 | window.add(label) 23 | assert helpers.get_parent_window(label) == window 24 | 25 | 26 | @pytest.mark.parametrize(('text', 'expectation'), [ 27 | # Date, time and datetime 28 | ('2016-02-01 12:00 ', 29 | {'timeinfo': '2016-02-01 12:00 ', 30 | }), 31 | ('2016-02-01 ', 32 | {'timeinfo': '2016-02-01 ', 33 | }), 34 | ('12:00 ', 35 | {'timeinfo': '12:00 ', 36 | }), 37 | # Timeranges 38 | ('2016-02-01 12:00 - 2016-02-03 15:00 ', 39 | {'timeinfo': '2016-02-01 12:00 - 2016-02-03 15:00 ', 40 | }), 41 | ('12:00 - 2016-02-03 15:00 ', 42 | {'timeinfo': '12:00 - 2016-02-03 15:00 ', 43 | }), 44 | ('12:00 - 15:00 ', 45 | {'timeinfo': '12:00 - 15:00 ', 46 | }), 47 | ('2016-01-01 12:00 ,lorum_ipsum', 48 | {'timeinfo': '2016-01-01 12:00 ', 49 | 'description': ',lorum_ipsum', 50 | }), 51 | ('2016-01-01 12:00 foo@bar #t1 #t2,lorum_ipsum', 52 | {'timeinfo': '2016-01-01 12:00 ', 53 | 'activity': 'foo', 54 | 'category': '@bar', 55 | 'tags': ' #t1 #t2', 56 | 'description': ',lorum_ipsum', 57 | }), 58 | ('12:00 - 15:00 foo@bar #t1 #t2,lorum_ipsum', 59 | {'timeinfo': '12:00 - 15:00 ', 60 | 'activity': 'foo', 61 | 'category': '@bar', 62 | 'tags': ' #t1 #t2', 63 | 'description': ',lorum_ipsum', 64 | }), 65 | ('2016-02-20 12:00 - 2016-02-20 15:00 foo@bar #t1 #t2,lorum_ipsum', 66 | {'timeinfo': '2016-02-20 12:00 - 2016-02-20 15:00 ', 67 | 'activity': 'foo', 68 | 'category': '@bar', 69 | 'tags': ' #t1 #t2', 70 | 'description': ',lorum_ipsum', 71 | }), 72 | ('2016-02-20 12:00 - 2016-02-20 15:00 foo,bar, lorum_ipsum', 73 | {'timeinfo': '2016-02-20 12:00 - 2016-02-20 15:00 ', 74 | 'activity': 'foo', 75 | 'description': ',bar, lorum_ipsum', 76 | }), 77 | # Others 78 | # Using a ``#`` in the activity name will cause the entire regex to fail. 79 | ('2016-02-20 12:00 - 2016-02-20 15:00 foo#bar@bar #t1 #t2,lorum_ipsum', {}), 80 | # Using a `` #`` will cause the regex to understand it as a tag. 81 | ('2016-02-20 12:00 - 2016-02-20 15:00 foo #bar@bar #t1 #t2,lorum_ipsum', 82 | {'timeinfo': '2016-02-20 12:00 - 2016-02-20 15:00 ', 83 | 'activity': 'foo', 84 | 'tags': ' #bar@bar #t1 #t2', 85 | 'description': ',lorum_ipsum', 86 | }), 87 | ('a #b', 88 | {'tags': ' #b'} 89 | ), 90 | ('a #b@c', 91 | {'activity': 'a', 92 | 'tags': ' #b@c', 93 | }), 94 | ('foo', {'activity': 'foo'}), 95 | ('foo@bar', 96 | {'activity': 'foo', 97 | 'category': '@bar' 98 | }), 99 | ('@bar', 100 | {'category': '@bar' 101 | }), 102 | (' #t1', 103 | {'tags': ' #t1', 104 | }), 105 | (' #t1 #t2', 106 | {'tags': ' #t1 #t2', 107 | }), 108 | (' ##t1 #t#2', 109 | {'tags': ' ##t1 #t#2', 110 | }), 111 | (',lorum_ipsum', 112 | {'description': ',lorum_ipsum', 113 | }), 114 | # 'Malformed' raw fact strings 115 | ('2016-02-20 12:00 - foo@bar #t1 #t2,lorum_ipsum', 116 | {'timeinfo': '2016-02-20 12:00 ', 117 | 'activity': '- foo', 118 | 'category': '@bar', 119 | 'tags': ' #t1 #t2', 120 | 'description': ',lorum_ipsum', 121 | }), 122 | # Invalid 123 | ('2016-02-20 12:00-2016-02-20 15:00 foo@bar #t1 #t2,lorum_ipsum', {}), 124 | ('2016-02-20 12:00-2016-02-20 15:00 foo#t1@bar #t1 #t2,lorum_ipsum', {}), 125 | ('2016-02-20 12:00-2016-02-20 15:00 foo,blub@bar #t1 #t2,lorum_ipsum', {}), 126 | ('2016-02-20 12:00-2016-02-20 15:00 foo:blub@bar #t1 #t2,lorum_ipsum', {}), 127 | ]) 128 | def test_decompose_raw_fact_string(request, text, expectation): 129 | result = helpers.decompose_raw_fact_string(text) 130 | if expectation: 131 | for key, value in expectation.items(): 132 | assert result[key] == value 133 | else: 134 | assert result is None 135 | 136 | 137 | @pytest.mark.parametrize(('minutes', 'expectation'), ( 138 | (1, '1 min'), 139 | (30, '30 min'), 140 | (59, '59 min'), 141 | (60, '01:00'), 142 | (300, '05:00'), 143 | )) 144 | def test__get_delta_string(minutes, expectation): 145 | delta = datetime.timedelta(minutes=minutes) 146 | result = helpers.get_delta_string(delta) 147 | assert result == expectation 148 | -------------------------------------------------------------------------------- /tests/test_minimal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import gi 3 | from gi.repository import Gtk 4 | 5 | gi.require_version('Gdk', '3.0') # NOQA 6 | 7 | 8 | def test_minimal(request): 9 | """Minimal test to showcase strange segfault behaviour when run with tox.""" 10 | box = Gtk.Box() 11 | assert box 12 | -------------------------------------------------------------------------------- /tests/tracking/__init__.py: -------------------------------------------------------------------------------- 1 | """Unittests for the tracking submodule.""" 2 | -------------------------------------------------------------------------------- /tests/tracking/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Fixtures for unittesting the tracking submodule.""" 4 | 5 | from __future__ import absolute_import, unicode_literals 6 | 7 | import pytest 8 | 9 | from hamster_gtk.tracking import screens 10 | 11 | 12 | @pytest.fixture 13 | def tracking_screen(request, app): 14 | """Return a plain TrackingScreen instance.""" 15 | return screens.TrackingScreen(app) 16 | 17 | 18 | @pytest.fixture 19 | def start_tracking_box(request, app): 20 | """Provide a plain StartTrackingBox instance.""" 21 | return screens.StartTrackingBox(app) 22 | 23 | 24 | @pytest.fixture 25 | def current_fact_box(request, app): 26 | """Provide a plain CurrentFactBox instance.""" 27 | return screens.CurrentFactBox(app.controller) 28 | -------------------------------------------------------------------------------- /tests/tracking/test_screens.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import pytest 6 | from gi.repository import Gtk 7 | from six import text_type 8 | 9 | from hamster_gtk.helpers import _u 10 | from hamster_gtk.tracking import screens 11 | 12 | 13 | class TestTrackingScreen(object): 14 | """Unittests for tracking screen.""" 15 | 16 | def test_init(self, app): 17 | """Make sure instance matches expectation.""" 18 | result = screens.TrackingScreen(app) 19 | assert isinstance(result, screens.TrackingScreen) 20 | assert len(result.get_children()) == 2 21 | 22 | def test_update_with_ongoing_fact(self, tracking_screen, fact, mocker): 23 | """Make sure current fact view is shown.""" 24 | fact.end is None 25 | tracking_screen._app.controller.store.facts.get_tmp_fact = mocker.MagicMock( 26 | return_value=fact) 27 | tracking_screen.update() 28 | result = tracking_screen.get_visible_child() 29 | assert result == tracking_screen.current_fact_view 30 | assert isinstance(result, screens.CurrentFactBox) 31 | 32 | def test_update_with_no_ongoing_fact(self, tracking_screen, mocker): 33 | """Make sure start tracking view is shown.""" 34 | tracking_screen._app.controller.store.facts.get_tmp_fact = mocker.MagicMock( 35 | side_effect=KeyError) 36 | tracking_screen.update() 37 | result = tracking_screen.get_visible_child() 38 | assert result == tracking_screen.start_tracking_view 39 | assert isinstance(result, screens.StartTrackingBox) 40 | 41 | 42 | class TestStartTrackingBox(object): 43 | """Unittests for TrackingBox.""" 44 | 45 | def test_init(self, app): 46 | """Make sure instances matches expectation.""" 47 | result = screens.StartTrackingBox(app) 48 | assert isinstance(result, screens.StartTrackingBox) 49 | assert len(result.get_children()) == 3 50 | 51 | def test__on_start_tracking_button(self, start_tracking_box, fact, mocker): 52 | """Make sure a new 'ongoing fact' is created.""" 53 | # [FIXME] 54 | # We need to find a viable way to check if signals are emitted! 55 | start_tracking_box._app.controller.store.facts.save = mocker.MagicMock() 56 | raw_fact = '{fact.activity.name}@{fact.category.name}'.format(fact=fact) 57 | start_tracking_box.raw_fact_entry.props.text = raw_fact 58 | start_tracking_box._on_start_tracking_button(None) 59 | assert start_tracking_box._app.controller.store.facts.save.called 60 | 61 | def test__reset(self, start_tracking_box): 62 | """Make sure all relevant widgets are reset.""" 63 | start_tracking_box.raw_fact_entry.props.text = 'foobar' 64 | start_tracking_box.reset() 65 | assert start_tracking_box.raw_fact_entry.props.text == '' 66 | 67 | 68 | class TestCurrentFactBox(object): 69 | """Unittests for CurrentFactBox.""" 70 | 71 | def test_init(self, app): 72 | result = screens.CurrentFactBox(app.controller) 73 | assert isinstance(result, screens.CurrentFactBox) 74 | 75 | def test_update_initial_fact(self, current_fact_box, fact): 76 | """Make sure update re-creates as widgets as expected.""" 77 | assert not current_fact_box.content.get_children() 78 | current_fact_box.update(fact) 79 | assert len(current_fact_box.content.get_children()) == 3 80 | label = current_fact_box.content.get_children()[0] 81 | expectation = '{activity.name}@{activity.category}'.format(activity=fact.activity) 82 | assert expectation in _u(label.get_text()) 83 | 84 | def test__get_fact_label(self, current_fact_box, fact): 85 | """Make sure that the label matches expectations.""" 86 | result = current_fact_box._get_fact_label(fact) 87 | assert isinstance(result, Gtk.Label) 88 | assert _u(result.get_text()) == text_type(fact) 89 | 90 | def test__get_cancel_button(self, current_fact_box): 91 | """Make sure widget matches expectation.""" 92 | result = current_fact_box._get_cancel_button() 93 | assert isinstance(result, Gtk.Button) 94 | 95 | def test__get_save_button(self, current_fact_box): 96 | """Make sure widget matches expectation.""" 97 | result = current_fact_box._get_save_button() 98 | assert isinstance(result, Gtk.Button) 99 | 100 | def test__get_invalid_label(self, current_fact_box): 101 | """Make sure widget matches expectation.""" 102 | result = current_fact_box._get_invalid_label() 103 | assert isinstance(result, Gtk.Label) 104 | 105 | def test_on_cancel_buton(self, request, current_fact_box, mocker): 106 | """Make sure that 'tracking-stopped' signal is emitted.""" 107 | current_fact_box._controller.store.facts.cancel_tmp_fact = mocker.MagicMock() 108 | current_fact_box.emit = mocker.MagicMock() 109 | result = current_fact_box._on_cancel_button(None) 110 | assert current_fact_box._controller.store.facts.cancel_tmp_fact.called 111 | assert result is None 112 | assert current_fact_box.emit.called_with('tracking-stopped') 113 | 114 | def test_on_cancel_buton_expected_exception(self, request, current_fact_box, mocker): 115 | """Make sure that we show error dialog if we encounter an expected exception.""" 116 | current_fact_box._controller.store.facts.cancel_tmp_fact = mocker.MagicMock( 117 | side_effect=KeyError) 118 | show_error = mocker.patch('hamster_gtk.tracking.screens.helpers.show_error') 119 | current_fact_box.emit = mocker.MagicMock() 120 | result = current_fact_box._on_cancel_button(None) 121 | assert result is None 122 | assert show_error.called 123 | assert current_fact_box.emit.called is False 124 | 125 | def test_on_cancel_buton_unexpected_exception(self, request, current_fact_box, mocker): 126 | """Make sure that we do not intercept unexpected exceptions.""" 127 | current_fact_box._controller.store.facts.cancel_tmp_fact = mocker.MagicMock( 128 | side_effect=Exception) 129 | with pytest.raises(Exception): 130 | current_fact_box._on_cancel_button(None) 131 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # We skip isort because it conflicts with setting GTK version in inports 3 | # https://github.com/timothycrosley/isort/issues/295 4 | envlist = py27, py35, docs, flake8, manifest, pep257 5 | 6 | [testenv] 7 | setenv = 8 | PYTHONPATH = {toxinidir}:{toxinidir}/hamster_gtk 9 | sitepackages=True 10 | whitelist_externals = 11 | make 12 | passenv = 13 | SPHINXOPTS_BUILD 14 | SPHINXOPTS_LINKCHECK 15 | XVFB_WHD 16 | DISPLAY 17 | commands = 18 | pip install -r requirements/test.pip 19 | make coverage 20 | 21 | [testenv:docs] 22 | basepython = python2 23 | # We need access to GTK for autodocs 24 | sitepackages = True 25 | deps = doc8==0.7.0 26 | commands = 27 | pip install -r requirements/docs.pip 28 | make docs BUILDDIR={envtmpdir} SPHINXOPTS={env:SPHINXOPTS_BUILD:''} 29 | make --directory=docs linkcheck BUILDDIR={envtmpdir} SPHINXOPTS={env:SPHINXOPTS_LINKCHECK:} 30 | doc8 31 | 32 | [testenv:flake8] 33 | basepython = python3 34 | deps = 35 | flake8==3.2.1 36 | flake8-debugger==1.4.0 37 | flake8-print==2.0.2 38 | pep8-naming==0.4.1 39 | skip_install = True 40 | commands = flake8 setup.py hamster_gtk/ tests/ 41 | 42 | [testenv:manifest] 43 | basepython = python3 44 | deps = check-manifest==0.35 45 | skip_install = True 46 | commands = 47 | check-manifest -v 48 | 49 | [testenv:pep257] 50 | basepython = python3 51 | skip_install = True 52 | deps = 53 | pep257==0.7.0 54 | commands = 55 | pep257 setup.py hamster_gtk/ tests/ 56 | --------------------------------------------------------------------------------