diff -Nru python-redmine-2.2.1/CHANGELOG.rst python-redmine-2.3.0/CHANGELOG.rst --- python-redmine-2.2.1/CHANGELOG.rst 2019-02-28 11:40:05.000000000 +0000 +++ python-redmine-2.3.0/CHANGELOG.rst 2020-05-21 11:45:19.000000000 +0000 @@ -1,6 +1,45 @@ Changelog --------- +2.3.0 (2020-05-21) +++++++++++++++++++ + +**Improvements**: + +- Support custom filename in ``redmine.upload()`` +- Support for ``get()`` and ``update()`` operations for ``/my/account`` endpoint which doesn't require admin + privileges by using ``me`` as an id, i.e. ``redmine.user.get('me')`` or ``redmine.user.update('me',firstname='John')`` + (requires Redmine >= 4.1.0) +- News ``create()``, ``update()``, ``delete()`` operations support (requires Redmine >= 4.1.0) +- ResourceSet's ``export()`` method now supports ``columns`` keyword argument which can be either an iterable + of column names, an "all" string which tells Python-Redmine to export all available columns, "all_gui" string + for GUI like behaviour or iterable of elements with "all_gui" string and additional columns to export +- Added support for special characters in WikiPage titles (`Issue #222 `__) (thanks to `Radek Czajka `__) +- Added ``return_response`` and ``ignore_response`` parameters to engine which allow to skip response processing + and speed up the create/update/delete operation in case response body isn't needed (see + `docs `__ for details) + +**Changes**: + +- *Backwards Incompatible:* Requests version required >= 2.23.0 +- *Backwards Incompatible:* Removed Python 3.4 support as it's not supported by Requests anymore + +**Bugfixes**: + +- User's ``send_information`` field wasn't sent correctly to Redmine so account information emails were + never sent (`Issue #227 `__) (thanks to + `wodny `__) +- Project resource ``default_version`` attribute was returned as a dict instead of being converted to + Resource object +- Resource object was leaking memory during initialization (`Issue #257 `__) (thanks to `yihli `__) + +**Documentation**: + +- Introduced detailed parameter list for ``redmine.session`` +- Mentioned support for ``admin`` in User's resource create/update + 2.2.1 (2019-02-28) ++++++++++++++++++ diff -Nru python-redmine-2.2.1/debian/changelog python-redmine-2.3.0/debian/changelog --- python-redmine-2.2.1/debian/changelog 2022-02-14 11:29:51.000000000 +0000 +++ python-redmine-2.3.0/debian/changelog 2022-11-02 22:15:36.000000000 +0000 @@ -1,3 +1,32 @@ +python-redmine (2.3.0-3) unstable; urgency=medium + + * QA upload. + + [ Debian Janitor ] + * Set upstream metadata fields: Repository. + + -- Jelmer Vernooij Wed, 02 Nov 2022 22:15:36 +0000 + +python-redmine (2.3.0-2) unstable; urgency=medium + + * QA upload. + + [ Debian Janitor ] + * Set field Upstream-Name in debian/copyright. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository-Browse. + + -- Jelmer Vernooij Wed, 26 Oct 2022 13:43:07 +0100 + +python-redmine (2.3.0-1) unstable; urgency=medium + + * QA upload. + * New upstream release. + * debian/: Apply "wrap-and-sort -abst". + * debian/control: Bump Standards-Version to 4.6.1. + * debian/control: Set package maintainer to Debian QA Group. + + -- Boyuan Yang Wed, 12 Oct 2022 13:38:46 -0400 + python-redmine (2.2.1-2) unstable; urgency=medium * Update my email address to @debian.org diff -Nru python-redmine-2.2.1/debian/control python-redmine-2.3.0/debian/control --- python-redmine-2.2.1/debian/control 2022-02-14 11:25:28.000000000 +0000 +++ python-redmine-2.3.0/debian/control 2022-11-02 22:15:36.000000000 +0000 @@ -1,15 +1,16 @@ Source: python-redmine -Maintainer: Benjamin Drung +Maintainer: Debian QA Group Section: python Priority: optional -Build-Depends: debhelper-compat (= 13), - dh-python, - python3-all, - python3-coverage, - python3-nose, - python3-requests, - python3-setuptools -Standards-Version: 4.6.0 +Build-Depends: + debhelper-compat (= 13), + dh-python, + python3-all, + python3-coverage, + python3-nose, + python3-requests, + python3-setuptools, +Standards-Version: 4.6.1 Rules-Requires-Root: no Homepage: https://github.com/maxtepkeev/python-redmine Vcs-Browser: https://salsa.debian.org/debian/python-redmine @@ -17,7 +18,9 @@ Package: python3-redminelib Architecture: all -Depends: ${misc:Depends}, ${python3:Depends} +Depends: + ${misc:Depends}, + ${python3:Depends}, Description: Python library for the Redmine RESTful API (Python 3) Python Redmine is a library for communicating with a Redmine project management application. Redmine exposes some of it's data via RESTful diff -Nru python-redmine-2.2.1/debian/copyright python-redmine-2.3.0/debian/copyright --- python-redmine-2.2.1/debian/copyright 2022-02-14 11:28:45.000000000 +0000 +++ python-redmine-2.3.0/debian/copyright 2022-11-02 22:15:36.000000000 +0000 @@ -1,5 +1,6 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Source: https://pypi.python.org/pypi/python-redmine +Upstream-Name: python-redmine Files: * Copyright: 2015-2018 Max Tepkeev diff -Nru python-redmine-2.2.1/debian/tests/control python-redmine-2.3.0/debian/tests/control --- python-redmine-2.2.1/debian/tests/control 2019-07-23 12:46:33.000000000 +0000 +++ python-redmine-2.3.0/debian/tests/control 2022-11-02 22:15:36.000000000 +0000 @@ -1,4 +1,6 @@ Test-Command: cp -r tests "$AUTOPKGTEST_TMP"; for py in $(py3versions -r 2>/dev/null); do cd "$AUTOPKGTEST_TMP"; echo "Testing with $py:"; $py -m unittest discover -v; done -Depends: python3-all, python3-redminelib +Depends: + python3-all, + python3-redminelib, Restrictions: allow-stderr Features: test-name=python3-unittest diff -Nru python-redmine-2.2.1/debian/upstream/metadata python-redmine-2.3.0/debian/upstream/metadata --- python-redmine-2.2.1/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-redmine-2.3.0/debian/upstream/metadata 2022-11-02 22:15:36.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/maxtepkeev/python-redmine/issues +Bug-Submit: https://github.com/maxtepkeev/python-redmine/issues/new +Repository: https://github.com/maxtepkeev/python-redmine.git +Repository-Browse: https://github.com/maxtepkeev/python-redmine diff -Nru python-redmine-2.2.1/LICENSE python-redmine-2.3.0/LICENSE --- python-redmine-2.2.1/LICENSE 2019-01-13 10:14:36.000000000 +0000 +++ python-redmine-2.3.0/LICENSE 2020-05-20 12:31:32.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright 2019 Maxim Tepkeev +Copyright 2020 Maxim Tepkeev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff -Nru python-redmine-2.2.1/NOTICE python-redmine-2.3.0/NOTICE --- python-redmine-2.2.1/NOTICE 2019-01-12 10:45:54.000000000 +0000 +++ python-redmine-2.3.0/NOTICE 2020-05-07 09:14:49.000000000 +0000 @@ -3,7 +3,7 @@ Six License =========== -Copyright (c) 2010-2019 Benjamin Peterson +Copyright (c) 2010-2020 Benjamin Peterson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -Nru python-redmine-2.2.1/PKG-INFO python-redmine-2.3.0/PKG-INFO --- python-redmine-2.2.1/PKG-INFO 2019-02-28 11:42:23.000000000 +0000 +++ python-redmine-2.3.0/PKG-INFO 2020-05-21 11:48:11.953657900 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: python-redmine -Version: 2.2.1 +Version: 2.3.0 Summary: Library for communicating with a Redmine project management application Home-page: https://github.com/maxtepkeev/python-redmine Author: Maxim Tepkeev @@ -63,7 +63,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API - * Supports Python 2.7, 3.4 - 3.7, PyPy and PyPy3 + * Supports Python 2.7, 3.5 - 3.8, PyPy and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented @@ -112,6 +112,45 @@ Changelog --------- + 2.3.0 (2020-05-21) + ++++++++++++++++++ + + **Improvements**: + + - Support custom filename in ``redmine.upload()`` + - Support for ``get()`` and ``update()`` operations for ``/my/account`` endpoint which doesn't require admin + privileges by using ``me`` as an id, i.e. ``redmine.user.get('me')`` or ``redmine.user.update('me',firstname='John')`` + (requires Redmine >= 4.1.0) + - News ``create()``, ``update()``, ``delete()`` operations support (requires Redmine >= 4.1.0) + - ResourceSet's ``export()`` method now supports ``columns`` keyword argument which can be either an iterable + of column names, an "all" string which tells Python-Redmine to export all available columns, "all_gui" string + for GUI like behaviour or iterable of elements with "all_gui" string and additional columns to export + - Added support for special characters in WikiPage titles (`Issue #222 `__) (thanks to `Radek Czajka `__) + - Added ``return_response`` and ``ignore_response`` parameters to engine which allow to skip response processing + and speed up the create/update/delete operation in case response body isn't needed (see + `docs `__ for details) + + **Changes**: + + - *Backwards Incompatible:* Requests version required >= 2.23.0 + - *Backwards Incompatible:* Removed Python 3.4 support as it's not supported by Requests anymore + + **Bugfixes**: + + - User's ``send_information`` field wasn't sent correctly to Redmine so account information emails were + never sent (`Issue #227 `__) (thanks to + `wodny `__) + - Project resource ``default_version`` attribute was returned as a dict instead of being converted to + Resource object + - Resource object was leaking memory during initialization (`Issue #257 `__) (thanks to `yihli `__) + + **Documentation**: + + - Introduced detailed parameter list for ``redmine.session`` + - Mentioned support for ``admin`` in User's resource create/update + 2.2.1 (2019-02-28) ++++++++++++++++++ @@ -772,10 +811,10 @@ Classifier: Environment :: Web Environment Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* diff -Nru python-redmine-2.2.1/python_redmine.egg-info/PKG-INFO python-redmine-2.3.0/python_redmine.egg-info/PKG-INFO --- python-redmine-2.2.1/python_redmine.egg-info/PKG-INFO 2019-02-28 11:42:23.000000000 +0000 +++ python-redmine-2.3.0/python_redmine.egg-info/PKG-INFO 2020-05-21 11:48:11.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: python-redmine -Version: 2.2.1 +Version: 2.3.0 Summary: Library for communicating with a Redmine project management application Home-page: https://github.com/maxtepkeev/python-redmine Author: Maxim Tepkeev @@ -63,7 +63,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API - * Supports Python 2.7, 3.4 - 3.7, PyPy and PyPy3 + * Supports Python 2.7, 3.5 - 3.8, PyPy and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented @@ -112,6 +112,45 @@ Changelog --------- + 2.3.0 (2020-05-21) + ++++++++++++++++++ + + **Improvements**: + + - Support custom filename in ``redmine.upload()`` + - Support for ``get()`` and ``update()`` operations for ``/my/account`` endpoint which doesn't require admin + privileges by using ``me`` as an id, i.e. ``redmine.user.get('me')`` or ``redmine.user.update('me',firstname='John')`` + (requires Redmine >= 4.1.0) + - News ``create()``, ``update()``, ``delete()`` operations support (requires Redmine >= 4.1.0) + - ResourceSet's ``export()`` method now supports ``columns`` keyword argument which can be either an iterable + of column names, an "all" string which tells Python-Redmine to export all available columns, "all_gui" string + for GUI like behaviour or iterable of elements with "all_gui" string and additional columns to export + - Added support for special characters in WikiPage titles (`Issue #222 `__) (thanks to `Radek Czajka `__) + - Added ``return_response`` and ``ignore_response`` parameters to engine which allow to skip response processing + and speed up the create/update/delete operation in case response body isn't needed (see + `docs `__ for details) + + **Changes**: + + - *Backwards Incompatible:* Requests version required >= 2.23.0 + - *Backwards Incompatible:* Removed Python 3.4 support as it's not supported by Requests anymore + + **Bugfixes**: + + - User's ``send_information`` field wasn't sent correctly to Redmine so account information emails were + never sent (`Issue #227 `__) (thanks to + `wodny `__) + - Project resource ``default_version`` attribute was returned as a dict instead of being converted to + Resource object + - Resource object was leaking memory during initialization (`Issue #257 `__) (thanks to `yihli `__) + + **Documentation**: + + - Introduced detailed parameter list for ``redmine.session`` + - Mentioned support for ``admin`` in User's resource create/update + 2.2.1 (2019-02-28) ++++++++++++++++++ @@ -772,10 +811,10 @@ Classifier: Environment :: Web Environment Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* diff -Nru python-redmine-2.2.1/python_redmine.egg-info/requires.txt python-redmine-2.3.0/python_redmine.egg-info/requires.txt --- python-redmine-2.2.1/python_redmine.egg-info/requires.txt 2019-02-28 11:42:23.000000000 +0000 +++ python-redmine-2.3.0/python_redmine.egg-info/requires.txt 2020-05-21 11:48:11.000000000 +0000 @@ -1 +1 @@ -requests>=2.20.0 +requests>=2.23.0 diff -Nru python-redmine-2.2.1/README.rst python-redmine-2.3.0/README.rst --- python-redmine-2.2.1/README.rst 2019-01-04 10:44:32.000000000 +0000 +++ python-redmine-2.3.0/README.rst 2020-05-07 10:26:23.000000000 +0000 @@ -54,7 +54,7 @@ * Supports 100% of Redmine API * Supports external Redmine plugins API -* Supports Python 2.7, 3.4 - 3.7, PyPy and PyPy3 +* Supports Python 2.7, 3.5 - 3.8, PyPy and PyPy3 * Supports different request engines * Extendable via custom resources and custom request engines * Extensively documented diff -Nru python-redmine-2.2.1/redminelib/engines/base.py python-redmine-2.3.0/redminelib/engines/base.py --- python-redmine-2.2.1/redminelib/engines/base.py 2019-01-01 15:01:53.000000000 +0000 +++ python-redmine-2.3.0/redminelib/engines/base.py 2020-05-19 14:12:07.000000000 +0000 @@ -17,11 +17,18 @@ :param string password: (optional). Password used for authentication. :param dict requests: (optional). Connection options. :param string impersonate: (optional). Username to impersonate. + :param bool ignore_response (optional). If True no response processing will be done at all. + :param bool return_response (optional). Whether to return response or None. :param bool return_raw_response (optional). Whether to return raw or json encoded responses. """ + self.ignore_response = options.pop('ignore_response', False) + self.return_response = options.pop('return_response', True) self.return_raw_response = options.pop('return_raw_response', False) self.requests = dict(dict(headers={}, params={}, data={}), **options.get('requests', {})) + if self.ignore_response: + self.requests['stream'] = True + if options.get('impersonate') is not None: self.requests['headers']['X-Redmine-Switch-User'] = options['impersonate'] @@ -132,6 +139,9 @@ :param obj response: (required). Response object with response details. """ + if self.ignore_response: + return None + if response.history: r = response.history[0] if r.is_redirect and r.request.url.startswith('http://') and response.request.url.startswith('https://'): @@ -140,7 +150,9 @@ status_code = response.status_code if status_code in (200, 201, 204): - if self.return_raw_response: + if not self.return_response: + return None + elif self.return_raw_response: return response elif not response.content.strip(): return True diff -Nru python-redmine-2.2.1/redminelib/__init__.py python-redmine-2.3.0/redminelib/__init__.py --- python-redmine-2.2.1/redminelib/__init__.py 2019-01-03 07:27:49.000000000 +0000 +++ python-redmine-2.3.0/redminelib/__init__.py 2020-05-21 11:25:05.000000000 +0000 @@ -33,15 +33,14 @@ :param raise_attr_exception: (optional). Control over resource attribute access exception raising. :type raise_attr_exception: bool or tuple :param cls engine: (optional). Engine that will be used to make requests to Redmine. - :param bool return_raw_response (optional). Whether engine should return raw or json encoded responses. """ self.url = url.rstrip('/') - self.ver = kwargs.get('version', None) - self.date_format = kwargs.get('date_format', '%Y-%m-%d') - self.datetime_format = kwargs.get('datetime_format', '%Y-%m-%dT%H:%M:%SZ') - self.raise_attr_exception = kwargs.get('raise_attr_exception', True) + self.ver = kwargs.pop('version', None) + self.date_format = kwargs.pop('date_format', '%Y-%m-%d') + self.datetime_format = kwargs.pop('datetime_format', '%Y-%m-%dT%H:%M:%SZ') + self.raise_attr_exception = kwargs.pop('raise_attr_exception', True) - engine = kwargs.get('engine', engines.DefaultEngine) + engine = kwargs.pop('engine', engines.DefaultEngine) if not inspect.isclass(engine) or not issubclass(engine, engines.BaseEngine): raise exceptions.EngineClassError @@ -87,18 +86,20 @@ finally: self.engine = engine - def upload(self, f): + def upload(self, f, filename=None): """ Uploads file from file path / file stream to Redmine and returns an assigned token. :param f: (required). File path / stream that will be uploaded. :type f: string or file-like object + :param filename: (optional). Filename for the file that will be uploaded. """ if self.ver is not None and LooseVersion(str(self.ver)) < LooseVersion('1.4.0'): raise exceptions.VersionMismatchError('File uploading') url = '{0}/uploads.json'.format(self.url) headers = {'Content-Type': 'application/octet-stream'} + params = {'filename': filename or ''} # There're myriads of file-like object implementations here and there and some of them don't have # a "read" method, which is wrong, but that's what we have, on the other hand it looks like all of @@ -128,7 +129,7 @@ stream = open(f, 'rb') close = True - response = self.engine.request('post', url, data=stream, headers=headers) + response = self.engine.request('post', url, params=params, data=stream, headers=headers) if close: stream.close() diff -Nru python-redmine-2.2.1/redminelib/managers/base.py python-redmine-2.3.0/redminelib/managers/base.py --- python-redmine-2.2.1/redminelib/managers/base.py 2019-01-04 09:08:54.000000000 +0000 +++ python-redmine-2.3.0/redminelib/managers/base.py 2020-05-21 10:35:24.000000000 +0000 @@ -2,7 +2,7 @@ Defines base Redmine resource manager class and it's infrastructure. """ -from .. import utilities, resultsets, exceptions +from .. import resultsets, exceptions class ResourceManager(object): @@ -54,6 +54,22 @@ manager.params = params return manager + def _construct_get_url(self, path): + """ + Constructs URL for get method. + + :param string path: absolute URL path. + """ + return self.redmine.url + path + + def _prepare_get_request(self, request): + """ + Makes the necessary preparations for get request data. + + :param dict request: Request data. + """ + return self.resource_class.bulk_decode(request, self) + def get(self, resource_id, **params): """ Returns a Resource object from Redmine by resource id. @@ -72,20 +88,31 @@ return resource try: - self.url = self.redmine.url + self.resource_class.query_one.format(resource_id, **params) - except KeyError as exception: - raise exceptions.ValidationError('{0} argument is required'.format(exception)) + self.url = self._construct_get_url(self.resource_class.query_one.format(resource_id, **params)) + except KeyError as e: + raise exceptions.ValidationError('{0} argument is required'.format(e)) - self.params = self.resource_class.bulk_decode(params, self) + self.params = self._prepare_get_request(params) self.container = self.resource_class.container_one try: - return self.to_resource(self.redmine.engine.request('get', self.url, params=self.params)[self.container]) + response = self.redmine.engine.request('get', self.url, params=self.params) except exceptions.ResourceNotFoundError as e: if self.resource_class.requirements: raise exceptions.ResourceRequirementsError(self.resource_class.requirements) raise e + return self._process_get_response(self.params, response) + + def _process_get_response(self, request, response): + """ + Processes get response and constructs resource object. + + :param dict request: Original request data. + :param any response: Response received from Redmine for this request data. + """ + return self.to_resource(response[self.container]) + def all(self, **params): """ Returns a ResourceSet object with all Resource objects. @@ -149,17 +176,19 @@ if not fields: raise exceptions.ResourceNoFieldsProvidedError - formatter = utilities.MemorizeFormatter() - try: - url = self._construct_create_url(formatter.format(self.resource_class.query_create, **fields)) + url = self._construct_create_url(self.resource_class.query_create.format(**fields)) except KeyError as e: raise exceptions.ValidationError('{0} field is required'.format(e)) - self.params = formatter.used_kwargs + self.params = self.resource_class.query_create.formatter.used_kwargs self.container = self.resource_class.container_create - request = self._prepare_create_request(formatter.unused_kwargs) + request = self._prepare_create_request(self.resource_class.query_create.formatter.unused_kwargs) response = self.redmine.engine.request(self.resource_class.http_method_create, url, data=request) + + if response is None: + return None + resource = self._process_create_response(request, response) self.url = self.redmine.url + self.resource_class.query_one.format(resource.internal_id, **fields) return resource @@ -203,22 +232,24 @@ if not fields: raise exceptions.ResourceNoFieldsProvidedError - formatter = utilities.MemorizeFormatter() - try: - query_update = formatter.format(self.resource_class.query_update, resource_id, **fields) + query_update = self.resource_class.query_update.format(resource_id, **fields) except KeyError as e: param = e.args[0] if param in self.params: fields[param] = self.params[param] - query_update = formatter.format(self.resource_class.query_update, resource_id, **fields) + query_update = self.resource_class.query_update.format(resource_id, **fields) else: raise exceptions.ValidationError('{0} argument is required'.format(e)) url = self._construct_update_url(query_update) - request = self._prepare_update_request(formatter.unused_kwargs) + request = self._prepare_update_request(self.resource_class.query_update.formatter.unused_kwargs) response = self.redmine.engine.request(self.resource_class.http_method_update, url, data=request) + + if response is None: + return None + return self._process_update_response(request, response) def _process_update_response(self, request, response): @@ -264,6 +295,10 @@ request = self._prepare_delete_request(params) response = self.redmine.engine.request(self.resource_class.http_method_delete, url, params=request) + + if response is None: + return None + return self._process_delete_response(request, response) def _process_delete_response(self, request, response): diff -Nru python-redmine-2.2.1/redminelib/managers/__init__.py python-redmine-2.3.0/redminelib/managers/__init__.py --- python-redmine-2.2.1/redminelib/managers/__init__.py 2018-05-02 11:33:02.000000000 +0000 +++ python-redmine-2.3.0/redminelib/managers/__init__.py 2020-05-20 13:05:32.000000000 +0000 @@ -3,4 +3,4 @@ """ from .base import ResourceManager -from .standard import WikiPageManager, FileManager +from .standard import FileManager, WikiPageManager, UserManager, NewsManager diff -Nru python-redmine-2.2.1/redminelib/managers/standard.py python-redmine-2.3.0/redminelib/managers/standard.py --- python-redmine-2.2.1/redminelib/managers/standard.py 2018-05-01 17:29:11.000000000 +0000 +++ python-redmine-2.3.0/redminelib/managers/standard.py 2020-05-21 10:35:18.000000000 +0000 @@ -6,6 +6,14 @@ from .. import exceptions +class FileManager(ResourceManager): + def _process_create_response(self, request, response): + if response is True: + response = {self.container: {'id': int(request[self.container]['token'].split('.')[0])}} + + return super(FileManager, self)._process_create_response(request, response) + + class WikiPageManager(ResourceManager): def _process_create_response(self, request, response): if response is True: @@ -14,9 +22,34 @@ return super(WikiPageManager, self)._process_create_response(request, response) -class FileManager(ResourceManager): +class UserManager(ResourceManager): + @staticmethod + def _check_custom_url(path): + if path.endswith('/me.json'): + path = '/my/account.json' + + return path + + def _construct_get_url(self, path): + return super(UserManager, self)._construct_get_url(self._check_custom_url(path)) + + def _prepare_create_request(self, request): + request = super(UserManager, self)._prepare_create_request(request) + request['send_information'] = request[self.container].pop('send_information', False) + return request + + def _construct_update_url(self, path): + return super(UserManager, self)._construct_update_url(self._check_custom_url(path)) + + def _prepare_update_request(self, request): + request = super(UserManager, self)._prepare_update_request(request) + request['send_information'] = request[self.resource_class.container_update].pop('send_information', False) + return request + + +class NewsManager(ResourceManager): def _process_create_response(self, request, response): if response is True: - response = {self.container: {'id': int(request[self.container]['token'].split('.')[0])}} + response = {self.container: self.redmine.news.filter(**self.params)[0].raw()} - return super(FileManager, self)._process_create_response(request, response) + return super(NewsManager, self)._process_create_response(request, response) diff -Nru python-redmine-2.2.1/redminelib/resources/base.py python-redmine-2.3.0/redminelib/resources/base.py --- python-redmine-2.2.1/redminelib/resources/base.py 2019-01-04 09:57:02.000000000 +0000 +++ python-redmine-2.3.0/redminelib/resources/base.py 2020-05-21 11:27:59.000000000 +0000 @@ -19,6 +19,8 @@ which name starts with Base are considered base classes and not added to the registry. """ def __new__(mcs, name, bases, attrs): + mcs.update_query_strings(attrs) + cls = super(Registrar, mcs).__new__(mcs, name, bases, attrs) if name.startswith('Base'): # base classes shouldn't be added to the registry @@ -54,6 +56,17 @@ return registry[name].setdefault('class', cls) @staticmethod + def update_query_strings(attrs): + """ + Updates all `query_*` string attributes to use ResourceQueryFormatter by default. + """ + for k, v in attrs.items(): + if k.startswith('query_') and v is not None: + attrs[k] = utilities.ResourceQueryStr(v) + + return attrs + + @staticmethod def update_cls_attr(cls, name, value): """ Updates class attribute's value by first copying the current value and then updating it with @@ -97,6 +110,7 @@ query_update = None query_delete = None search_hints = None + extra_export_columns = [] http_method_create = 'post' http_method_update = 'put' http_method_delete = 'delete' @@ -125,8 +139,8 @@ relations_includes = self._relations + self._includes self.manager = manager - self._create_readonly += relations_includes - self._update_readonly += relations_includes + self._create_readonly = self._create_readonly[:] + relations_includes + self._update_readonly = self._update_readonly[:] + relations_includes self._decoded_attrs = dict(dict.fromkeys(relations_includes), **attributes) self._encoded_attrs = {} self._changes = {} @@ -243,7 +257,8 @@ if attr == 'uploads': for index, attachment in enumerate(value): if 'token' not in attachment: - value[index]['token'] = manager.redmine.upload(attachment.pop('path', ''))['token'] + value[index]['token'] = manager.redmine.upload( + attachment.pop('path', ''), filename=attachment.get('filename'))['token'] return attr, value elif attr == 'include' and isinstance(value, (list, tuple)): @@ -370,7 +385,11 @@ self.post_update() else: self.pre_create() - self._decoded_attrs = self.manager.create(**self._changes).raw() + resource = self.manager.create(**self._changes) + + if resource is not None: + self._decoded_attrs = resource.raw() + self.post_create() self._changes = {} diff -Nru python-redmine-2.2.1/redminelib/resources/standard.py python-redmine-2.3.0/redminelib/resources/standard.py --- python-redmine-2.2.1/redminelib/resources/standard.py 2019-02-22 12:00:08.000000000 +0000 +++ python-redmine-2.3.0/redminelib/resources/standard.py 2020-05-21 10:39:02.000000000 +0000 @@ -30,6 +30,7 @@ 'news', 'issues', 'files'] _unconvertible = BaseResource._unconvertible + ['identifier', 'status'] _update_readonly = BaseResource._update_readonly + ['identifier'] + _resource_map = {'default_version': 'Version'} _resource_set_map = { 'custom_fields': 'CustomField', 'trackers': 'Tracker', @@ -73,6 +74,7 @@ query_update = '/issues/{0}.json' query_delete = '/issues/{0}.json' search_hints = ['issue', 'issue closed'] + extra_export_columns = ['description', 'last_notes'] _repr = [['id', 'subject'], ['title'], ['id']] _includes = ['children', 'attachments', 'relations', 'changesets', 'journals', 'watchers'] @@ -201,12 +203,13 @@ redmine_version = '2.2' container_filter = '{resource}' query_filter = '/enumerations/{resource}.json' + query_url = '/enumerations/{0}/edit' _resource_set_map = {'custom_fields': 'CustomField'} @property def url(self): - return '{0}/enumerations/{1}/edit'.format(self.manager.redmine.url, self.internal_id) + return self.manager.redmine.url + self.query_url.format(self.internal_id) class Attachment(BaseResource): @@ -393,12 +396,14 @@ container_filter = 'users' container_create = 'user' container_update = 'user' + query_all_export = '/users.{format}' query_all = '/users.json' query_one = '/users/{0}.json' query_filter = '/users.json' query_create = '/users.json' query_update = '/users/{0}.json' query_delete = '/users/{0}.json' + manager_class = managers.UserManager _repr = [['id', 'firstname', 'lastname'], ['id', 'name']] _includes = ['memberships', 'groups'] @@ -485,24 +490,34 @@ class News(BaseResource): redmine_version = '1.1' container_all = 'news' + container_one = 'news' container_filter = 'news' + container_create = 'news' + container_update = 'news' query_all_export = '/news.{format}' query_all = '/news.json' + query_one = '/news/{0}.json' query_filter = '/news.json' + query_create = '/projects/{project_id}/news.json' + query_update = '/news/{0}.json' + query_delete = '/news/{0}.json' + query_url = '/news/{0}' search_hints = ['news'] + manager_class = managers.NewsManager _repr = [['id', 'title']] _resource_map = {'project': 'Project', 'author': 'User'} @property def url(self): - return '{0}/news/{1}'.format(self.manager.redmine.url, self.internal_id) + return self.manager.redmine.url + self.query_url.format(self.internal_id) class IssueStatus(BaseResource): redmine_version = '1.3' container_all = 'issue_statuses' query_all = '/issue_statuses.json' + query_url = '/issue_statuses/{0}/edit' _relations = ['issues'] _relations_name = 'status' @@ -510,37 +525,40 @@ @property def url(self): - return '{0}/issue_statuses/{1}/edit'.format(self.manager.redmine.url, self.internal_id) + return self.manager.redmine.url + self.query_url.format(self.internal_id) class Tracker(BaseResource): redmine_version = '1.3' container_all = 'trackers' query_all = '/trackers.json' + query_url = '/trackers/{0}/edit' _relations = ['issues'] _resource_set_map = {'issues': 'Issue'} @property def url(self): - return '{0}/trackers/{1}/edit'.format(self.manager.redmine.url, self.internal_id) + return self.manager.redmine.url + self.query_url.format(self.internal_id) class Query(BaseResource): redmine_version = '1.3' container_all = 'queries' query_all = '/queries.json' + query_url = '/projects/{0}/issues?query_id={1}' @property def url(self): - return '{0}/projects/{1}/issues?query_id={2}'.format( - self.manager.redmine.url, self._decoded_attrs.get('project_id', 0), self.internal_id) + return self.manager.redmine.url + self.query_url.format( + self._decoded_attrs.get('project_id', 0), self.internal_id) class CustomField(BaseResource): redmine_version = '2.4' container_all = 'custom_fields' query_all = '/custom_fields.json' + query_url = '/custom_fields/{0}/edit' _resource_set_map = {'trackers': 'Tracker', 'roles': 'Role'} @@ -566,4 +584,4 @@ @property def url(self): - return '{0}/custom_fields/{1}/edit'.format(self.manager.redmine.url, self.internal_id) + return self.manager.redmine.url + self.query_url.format(self.internal_id) diff -Nru python-redmine-2.2.1/redminelib/resultsets.py python-redmine-2.3.0/redminelib/resultsets.py --- python-redmine-2.2.1/redminelib/resultsets.py 2019-01-04 09:42:57.000000000 +0000 +++ python-redmine-2.3.0/redminelib/resultsets.py 2020-05-06 17:11:49.000000000 +0000 @@ -6,7 +6,9 @@ import functools import itertools -from . import lookups, utilities, exceptions +from distutils.version import LooseVersion + +from . import lookups, exceptions class BaseResourceSet(object): @@ -42,24 +44,49 @@ return self._total_count - def export(self, fmt, savepath=None, filename=None): + def export(self, fmt, savepath=None, filename=None, columns=None, encoding='UTF-8'): """ Exports all resources from resource set to requested format if Resource supports that. :param string fmt: (required). Format to use for export, e.g. atom, csv, txt, pdf, html etc. :param string savepath: (optional). Path where to save the file. :param string filename: (optional). Name that will be used for the file. + :param columns: (optional). Iterable of column names, "all_gui" for GUI behaviour or "all" for all columns. + :param encoding: (optional). Encoding that will be used by Redmine for the result file. + :type columns: iterable or string """ if self.manager.resource_class.query_all_export is None: raise exceptions.ExportNotSupported - formatter = utilities.MemorizeFormatter() + url = self.manager.redmine.url + self.manager.resource_class.query_all_export.format( + format=fmt, **self.manager.params) + + params = dict(self.manager.resource_class.query_all_export.formatter.unused_kwargs, encoding=encoding) - url = self.manager.redmine.url + formatter.format( - self.manager.resource_class.query_all_export, format=fmt, **self.manager.params) + if columns is not None: + if columns == 'all': + columns = ['all', 'all_inline'] + self.manager.resource_class.extra_export_columns + + if self.manager.redmine.ver is not None and LooseVersion(str(self.manager.redmine.ver)) < '3.4.0': + params.update(dict.fromkeys(self.manager.resource_class.extra_export_columns, 1), columns='all') + elif 'all_gui' in columns: + if columns == 'all_gui': + columns = ['all', 'all_inline'] + + if self.manager.redmine.ver is not None and LooseVersion(str(self.manager.redmine.ver)) < '3.4.0': + params['columns'] = 'all' + else: + if self.manager.redmine.ver is not None and LooseVersion(str(self.manager.redmine.ver)) < '3.4.0': + params.update(dict.fromkeys(columns, 1), columns='all') + + columns = list(columns) + ['all', 'all_inline'] + + # Redmine >= 3.4.0 happily accepts c[] array with column names + # for older versions the above hack with params is being used + params['c[]'] = columns try: - return self.manager.redmine.download(url, savepath, filename, params=formatter.unused_kwargs) + return self.manager.redmine.download(url, savepath, filename, params=params) except exceptions.UnknownError as e: if e.status_code == 406: raise exceptions.ExportFormatNotSupportedError diff -Nru python-redmine-2.2.1/redminelib/utilities.py python-redmine-2.3.0/redminelib/utilities.py --- python-redmine-2.2.1/redminelib/utilities.py 2019-01-01 18:47:22.000000000 +0000 +++ python-redmine-2.3.0/redminelib/utilities.py 2019-08-18 13:53:53.000000000 +0000 @@ -7,6 +7,11 @@ import string import functools +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + def fix_unicode(cls): """ @@ -65,9 +70,9 @@ return result -class MemorizeFormatter(string.Formatter): +class ResourceQueryFormatter(string.Formatter): """ - Memorizes all arguments, used during string formatting. + Quotes query and memorizes all arguments, used during string formatting. """ def __init__(self): self.used_kwargs = {} @@ -79,3 +84,16 @@ self.used_kwargs[item] = kwargs.pop(item) self.unused_kwargs = kwargs + + def format_field(self, value, format_spec): + return quote(super(ResourceQueryFormatter, self).format_field(value, format_spec).encode('utf-8')) + + +class ResourceQueryStr(str): + """ + Extends default string with additional formatting capabilities. + """ + formatter = ResourceQueryFormatter() + + def format(self, *args, **kwargs): + return self.formatter.format(self, *args, **kwargs) diff -Nru python-redmine-2.2.1/redminelib/version.py python-redmine-2.3.0/redminelib/version.py --- python-redmine-2.2.1/redminelib/version.py 2019-02-28 11:40:05.000000000 +0000 +++ python-redmine-2.3.0/redminelib/version.py 2020-05-21 11:45:19.000000000 +0000 @@ -1 +1 @@ -__version__ = '2.2.1' +__version__ = '2.3.0' diff -Nru python-redmine-2.2.1/setup.py python-redmine-2.3.0/setup.py --- python-redmine-2.2.1/setup.py 2019-01-12 11:44:05.000000000 +0000 +++ python-redmine-2.3.0/setup.py 2020-05-07 10:35:55.000000000 +0000 @@ -22,7 +22,7 @@ tests_require = ['nose', 'coverage'] -if sys.version_info[:2] < (3, 3): +if sys.version_info[:2] == (2, 7): tests_require.append('mock') exec(open('redminelib/version.py').read()) @@ -41,8 +41,8 @@ description='Library for communicating with a Redmine project management application', long_description=open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read(), keywords='redmine redmineup redminecrm redminelib easyredmine', - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', - install_requires=['requests>=2.20.0'], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + install_requires=['requests>=2.23.0'], tests_require=tests_require, cmdclass={'test': NoseTests}, zip_safe=False, @@ -57,10 +57,10 @@ 'Environment :: Web Environment', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy' ], diff -Nru python-redmine-2.2.1/tests/responses/standard.py python-redmine-2.3.0/tests/responses/standard.py --- python-redmine-2.2.1/tests/responses/standard.py 2018-05-01 14:26:13.000000000 +0000 +++ python-redmine-2.3.0/tests/responses/standard.py 2020-05-08 13:41:47.000000000 +0000 @@ -24,6 +24,7 @@ }, 'wiki_page': { 'get': {'wiki_page': {'title': 'Foo', 'version': 1}}, + 'get_special': {'wiki_page': {'title': 'Foo%Bar', 'version': 1}}, 'filter': {'wiki_pages': [{'title': 'Foo', 'version': 1}, {'title': 'Bar', 'version': 2}]}, }, 'project_membership': { @@ -56,8 +57,9 @@ 'all': {'roles': [{'name': 'Foo', 'id': 1}, {'name': 'Bar', 'id': 2}]}, }, 'news': { - 'all': {'news': [{'title': 'Foo', 'id': 1}, {'title': 'Bar', 'id': 2}]}, - 'filter': {'news': [{'title': 'Foo', 'id': 1}, {'title': 'Bar', 'id': 2}]}, + 'get': {'news': {'title': 'Foo', 'id': 1}}, + 'all': {'news': [{'title': 'Foo', 'id': 2}, {'title': 'Bar', 'id': 1}]}, + 'filter': {'news': [{'title': 'Foo', 'id': 2}, {'title': 'Bar', 'id': 1}]}, }, 'issue_status': { 'all': {'issue_statuses': [{'name': 'Foo', 'id': 1}, {'name': 'Bar', 'id': 2}]}, diff -Nru python-redmine-2.2.1/tests/test_engines.py python-redmine-2.3.0/tests/test_engines.py --- python-redmine-2.2.1/tests/test_engines.py 2017-04-10 18:53:44.000000000 +0000 +++ python-redmine-2.3.0/tests/test_engines.py 2020-05-19 14:12:07.000000000 +0000 @@ -29,6 +29,17 @@ self.response.content = '' self.assertEqual(self.redmine.engine.request('put', self.url), True) + def test_returns_none_with_ignore_response_true(self): + with self.redmine.session(ignore_response=True): + self.assertEqual(self.redmine.engine.ignore_response, True) + self.assertEqual(self.redmine.engine.requests['stream'], True) + self.assertEqual(self.redmine.engine.request('post', self.url), None) + + def test_returns_none_with_return_response_false(self): + with self.redmine.session(return_response=False): + self.assertEqual(self.redmine.engine.return_response, False) + self.assertEqual(self.redmine.engine.request('post', self.url), None) + def test_session_not_implemented_exception(self): self.assertRaises(NotImplementedError, lambda: engines.BaseEngine()) diff -Nru python-redmine-2.2.1/tests/test_managers.py python-redmine-2.3.0/tests/test_managers.py --- python-redmine-2.2.1/tests/test_managers.py 2019-01-04 10:18:43.000000000 +0000 +++ python-redmine-2.3.0/tests/test_managers.py 2020-05-20 14:20:12.000000000 +0000 @@ -12,7 +12,7 @@ class ResourceManagerTestCase(BaseRedmineTestCase): def test_has_custom_repr(self): - self.assertEqual(repr(self.redmine.user), '') + self.assertEqual(repr(self.redmine.issue), '') def test_supports_additional_resources(self): self.assertIsInstance(self.redmine.foo_resource, managers.ResourceManager) @@ -136,6 +136,10 @@ self.assertEqual(project._decoded_attrs, defaults) self.assertEqual(repr(project), '') + def test_create_resource_returns_none(self): + with self.redmine.session(return_response=False): + self.assertEqual(self.redmine.user.create(firstname='John', lastname='Smith'), None) + def test_update_resource(self): self.response.content = '' manager = self.redmine.wiki_page @@ -165,6 +169,10 @@ self.assertEquals(len(w), 1) self.assertIs(w[0].category, exceptions.PerformanceWarning) + def test_update_resource_returns_none(self): + with self.redmine.session(return_response=False): + self.assertEqual(self.redmine.issue.update(1, subject='Bar'), None) + def test_delete_resource(self): self.response.content = '' self.assertEqual(self.redmine.wiki_page.delete(b'\xcf\x86oo'.decode('utf-8'), project_id=1), True) @@ -174,6 +182,10 @@ self.response.content = '' self.assertEqual(self.redmine.wiki_page.delete(b'\xcf\x86oo'.decode('utf-8'), project_id=1), True) + def test_delete_resource_returns_none(self): + with self.redmine.session(return_response=False): + self.assertEqual(self.redmine.user.delete(1), None) + def test_resource_get_method_unsupported_exception(self): self.assertRaises(exceptions.ResourceBadMethodError, lambda: self.redmine.issue_journal.get(1)) diff -Nru python-redmine-2.2.1/tests/test_redmine.py python-redmine-2.3.0/tests/test_redmine.py --- python-redmine-2.2.1/tests/test_redmine.py 2019-01-04 10:18:43.000000000 +0000 +++ python-redmine-2.3.0/tests/test_redmine.py 2020-05-21 11:33:28.000000000 +0000 @@ -62,7 +62,7 @@ def test_successful_file_upload(self): self.response.status_code = 201 self.response.json.return_value = {'upload': {'id': 1, 'token': '123456'}} - self.assertEqual(self.redmine.upload('foo')['token'], '123456') + self.assertEqual(self.redmine.upload('foo', filename='foo.jpg')['token'], '123456') def test_successful_filestream_upload(self): from io import StringIO diff -Nru python-redmine-2.2.1/tests/test_resources_standard.py python-redmine-2.3.0/tests/test_resources_standard.py --- python-redmine-2.2.1/tests/test_resources_standard.py 2018-05-01 17:29:11.000000000 +0000 +++ python-redmine-2.3.0/tests/test_resources_standard.py 2020-05-20 17:33:58.000000000 +0000 @@ -702,6 +702,17 @@ wiki_page = self.redmine.wiki_page.get('Foo', project_id=1) self.assertEqual(wiki_page.title, 'Foo') + def test_wiki_page_get_special(self): + """Test getting a wiki page with special char in title.""" + self.response.json.return_value = responses['wiki_page']['get_special'] + wiki_page = self.redmine.wiki_page.get('Foo%Bar', project_id=1) + self.assertEqual( + self.patch_requests.call_args[0][1], + '{0}/projects/1/wiki/Foo%25Bar.json'.format(self.url) + ) + self.assertEqual(wiki_page.title, 'Foo%Bar') + self.assertEqual(wiki_page.url, 'http://foo.bar/projects/1/wiki/Foo%25Bar') + def test_wiki_page_filter(self): self.response.json.return_value = responses['wiki_page']['filter'] wiki_pages = self.redmine.wiki_page.filter(project_id=1) @@ -714,6 +725,17 @@ wiki_page = self.redmine.wiki_page.create(project_id='foo', title='Foo') self.assertEqual(wiki_page.title, 'Foo') + def test_wiki_page_create_special(self): + """Test creating a wiki page with special char in title.""" + self.response.status_code = 201 + self.response.json.return_value = responses['wiki_page']['get_special'] + wiki_page = self.redmine.wiki_page.create(project_id='foo', title='Foo%Bar') + self.assertEqual( + self.patch_requests.call_args[0][1], + '{0}/projects/foo/wiki/Foo%25Bar.json'.format(self.url) + ) + self.assertEqual(wiki_page.title, 'Foo%Bar') + def test_wiki_page_delete(self): self.response.json.return_value = responses['wiki_page']['get'] wiki_page = self.redmine.wiki_page.get('Foo', project_id=1) @@ -996,6 +1018,12 @@ self.assertEqual(user.id, 1) self.assertEqual(user.firstname, 'John') + def test_user_get_account(self): + self.response.json.return_value = responses['user']['get'] + user = self.redmine.user.get('me') + self.assertEqual(user.firstname, 'John') + self.assertTrue(self.patch_requests.call_args[0][1].endswith('/my/account.json')) + def test_user_all(self): self.response.json.return_value = responses['user']['all'] users = self.redmine.user.all() @@ -1019,6 +1047,13 @@ self.assertEqual(user.firstname, 'John') self.assertEqual(user.lastname, 'Smith') + def test_user_create_with_send_information(self): + import json + self.response.status_code = 201 + self.response.json.return_value = responses['user']['get'] + self.redmine.user.create(firstname='John', lastname='Smith', send_information=True) + self.assertEqual(json.loads(self.patch_requests.call_args[1]['data'])['send_information'], True) + def test_user_delete(self): self.response.json.return_value = responses['user']['get'] user = self.redmine.user.get(1) @@ -1033,6 +1068,16 @@ user.firstname = 'Bar' self.assertIsInstance(user.save(), resources.User) + def test_user_update_account(self): + self.redmine.user.update('me', lastname='Foo', firstname='Bar') + self.assertTrue(self.patch_requests.call_args[0][1].endswith('/my/account.json')) + + def test_user_update_with_send_information(self): + import json + self.response.json.return_value = responses['user']['get'] + self.redmine.user.update(1, firstname='John', lastname='Smith', send_information=True) + self.assertEqual(json.loads(self.patch_requests.call_args[1]['data'])['send_information'], True) + def test_user_custom_str(self): self.response.json.return_value = responses['user']['get'] self.assertEqual(str(self.redmine.user.get(1)), 'John Smith') @@ -1167,7 +1212,7 @@ self.assertEqual(self.redmine.news.resource_class.redmine_version, '1.1') def test_news_get(self): - self.response.json.return_value = responses['news']['all'] + self.response.json.return_value = responses['news']['get'] news = self.redmine.news.get(1) self.assertEqual(news.id, 1) self.assertEqual(news.title, 'Foo') @@ -1175,22 +1220,49 @@ def test_news_all(self): self.response.json.return_value = responses['news']['all'] news = self.redmine.news.all() - self.assertEqual(news[0].id, 1) + self.assertEqual(news[0].id, 2) self.assertEqual(news[0].title, 'Foo') - self.assertEqual(news[1].id, 2) + self.assertEqual(news[1].id, 1) self.assertEqual(news[1].title, 'Bar') def test_news_filter(self): self.response.json.return_value = responses['news']['filter'] news = self.redmine.news.filter(project_id=1) - self.assertEqual(news[0].id, 1) + self.assertEqual(news[0].id, 2) self.assertEqual(news[0].title, 'Foo') - self.assertEqual(news[1].id, 2) + self.assertEqual(news[1].id, 1) self.assertEqual(news[1].title, 'Bar') + def test_news_create(self): + self.response.status_code = 201 + self.response.json.return_value = responses['news']['get'] + news = self.redmine.news.create(project_id=1, title='Foo') + self.assertEqual(news.title, 'Foo') + + def test_news_create_empty_response(self): + self.set_patch_side_effect([ + mock.Mock(status_code=204, history=[], content=''), + mock.Mock(status_code=201, history=[], **{'json.return_value': responses['news']['filter']}) + ]) + news = self.redmine.news.create(project_id=1, title='Foo') + self.assertEqual(news.title, 'Foo') + + def test_news_delete(self): + self.response.json.return_value = responses['news']['get'] + news = self.redmine.news.get(1) + self.response.content = '' + self.assertEqual(news.delete(), True) + self.assertEqual(self.redmine.news.delete(1), True) + + def test_news_update(self): + self.response.json.return_value = responses['news']['get'] + news = self.redmine.news.get(1) + news.title = 'Bar' + self.assertIsInstance(news.save(), resources.News) + def test_news_url(self): self.response.json.return_value = responses['news']['filter'] - self.assertEqual(self.redmine.news.filter(project_id=1)[0].url, '{0}/news/1'.format(self.url)) + self.assertEqual(self.redmine.news.filter(project_id=1)[0].url, '{0}/news/2'.format(self.url)) @mock.patch('redminelib.open', mock.mock_open(), create=True) def test_news_export(self): @@ -1204,7 +1276,7 @@ def test_news_repr(self): self.response.json.return_value = responses['news']['filter'] - self.assertEqual(repr(self.redmine.news.filter(project_id=1)[0]), '') + self.assertEqual(repr(self.redmine.news.filter(project_id=1)[0]), '') def test_issue_status_version(self): self.assertEqual(self.redmine.issue_status.resource_class.redmine_version, '1.3') diff -Nru python-redmine-2.2.1/tests/test_resultsets.py python-redmine-2.3.0/tests/test_resultsets.py --- python-redmine-2.2.1/tests/test_resultsets.py 2018-04-29 17:17:19.000000000 +0000 +++ python-redmine-2.3.0/tests/test_resultsets.py 2020-05-06 17:25:41.000000000 +0000 @@ -178,6 +178,32 @@ self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar'), '/foo/bar/issues.txt') + @mock.patch('redminelib.open', mock.mock_open(), create=True) + def test_export_with_all_columns(self): + self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all'), '/foo/bar/issues.txt') + self.redmine.ver = '3.3.0' + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all'), '/foo/bar/issues.txt') + + @mock.patch('redminelib.open', mock.mock_open(), create=True) + def test_export_with_all_gui_columns(self): + self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all_gui'), '/foo/bar/issues.txt') + self.redmine.ver = '3.3.0' + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns='all_gui'), '/foo/bar/issues.txt') + + @mock.patch('redminelib.open', mock.mock_open(), create=True) + def test_export_with_all_gui_extra_columns(self): + self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['all_gui']), '/foo/bar/issues.txt') + self.redmine.ver = '3.3.0' + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['all_gui']), '/foo/bar/issues.txt') + + @mock.patch('redminelib.open', mock.mock_open(), create=True) + def test_export_with_custom_columns(self): + self.response.iter_content = lambda chunk_size: (str(num) for num in range(0, 5)) + self.assertEqual(self.redmine.issue.all().export('txt', '/foo/bar', columns=['status']), '/foo/bar/issues.txt') + def test_export_not_supported_exception(self): self.assertRaises(exceptions.ExportNotSupported, lambda: self.redmine.custom_field.all().export('pdf'))