1086 lines
36 KiB
Python
1086 lines
36 KiB
Python
from datetime import datetime
|
|
|
|
from distutils.version import LooseVersion
|
|
|
|
from .utilities import to_string
|
|
from .managers import ResourceManager
|
|
from .exceptions import (
|
|
ValidationError,
|
|
ForbiddenError,
|
|
ResourceAttrError,
|
|
ReadonlyAttrError,
|
|
CustomFieldValueError,
|
|
ResourceVersionMismatchError,
|
|
ResourceNotFoundError
|
|
)
|
|
|
|
# Resources which when accessed from some other
|
|
# resource should become a ResourceSet object
|
|
_RESOURCE_SET_MAP = {
|
|
'trackers': 'Tracker',
|
|
'issue_categories': 'IssueCategory',
|
|
'custom_fields': 'CustomField',
|
|
'groups': 'Group',
|
|
'users': 'User',
|
|
'memberships': 'ProjectMembership',
|
|
'relations': 'IssueRelation',
|
|
'attachments': 'Attachment',
|
|
'watchers': 'User',
|
|
'journals': 'IssueJournal',
|
|
'children': 'Issue',
|
|
'roles': 'Role',
|
|
'issues': 'Issue',
|
|
'projects': 'Project',
|
|
'notes': 'Note',
|
|
'deals': 'Deal',
|
|
'contacts': 'Contact',
|
|
'related_contacts': 'Contact',
|
|
}
|
|
|
|
# Resources which when accessed from some other
|
|
# resource should become a Resource object
|
|
_RESOURCE_MAP = {
|
|
'author': 'User',
|
|
'assigned_to': 'User',
|
|
'project': 'Project',
|
|
'tracker': 'Tracker',
|
|
'status': 'IssueStatus',
|
|
'user': 'User',
|
|
'issue': 'Issue',
|
|
'priority': 'Enumeration',
|
|
'activity': 'Enumeration',
|
|
'category': 'IssueCategory',
|
|
'fixed_version': 'Version',
|
|
'contact': 'Contact',
|
|
}
|
|
|
|
# Resources which when accessed from some other
|
|
# resource should be requested from Redmine
|
|
_RESOURCE_RELATIONS_MAP = {
|
|
'wiki_pages': 'WikiPage',
|
|
'memberships': 'ProjectMembership',
|
|
'issue_categories': 'IssueCategory',
|
|
'versions': 'Version',
|
|
'news': 'News',
|
|
'relations': 'IssueRelation',
|
|
'time_entries': 'TimeEntry',
|
|
'issues': 'Issue',
|
|
'contacts': 'Contact',
|
|
'deals': 'Deal',
|
|
'deal_categories': 'DealCategory',
|
|
}
|
|
|
|
# Resource attributes which when set should
|
|
# also set another resource id to its value
|
|
_RESOURCE_SINGLE_ATTR_ID_MAP = {
|
|
'parent_id': 'parent',
|
|
'project_id': 'project',
|
|
'tracker_id': 'tracker',
|
|
'priority_id': 'priority',
|
|
'assigned_to_id': 'assigned_to',
|
|
'category_id': 'category',
|
|
'fixed_version_id': 'fixed_version',
|
|
'parent_issue_id': 'parent',
|
|
'issue_id': 'issue',
|
|
'activity_id': 'activity',
|
|
'status_id': 'status',
|
|
'contact_id': 'contact',
|
|
}
|
|
|
|
# Resource attributes which when set should
|
|
# also set another resource ids to their value
|
|
_RESOURCE_MULTIPLE_ATTR_ID_MAP = {
|
|
'user_ids': 'users',
|
|
'role_ids': 'roles',
|
|
}
|
|
|
|
|
|
class _Resource(object):
|
|
"""Implementation of Redmine resource"""
|
|
redmine_version = None
|
|
requirements = ()
|
|
container_all = None
|
|
container_one = None
|
|
container_filter = None
|
|
container_create = None
|
|
container_update = None
|
|
query_all = None
|
|
query_one = None
|
|
query_filter = None
|
|
query_create = None
|
|
query_update = None
|
|
query_delete = None
|
|
|
|
_includes = ()
|
|
_relations = ()
|
|
_relations_name = None
|
|
_unconvertible = ('name', 'description')
|
|
_members = ('manager',)
|
|
_create_readonly = ('id', 'created_on', 'updated_on', 'author', 'user', 'project', 'issue')
|
|
_update_readonly = _create_readonly
|
|
__length_hint__ = None # fixes Python 2.6 list() call on resource object
|
|
|
|
def __init__(self, manager, attributes):
|
|
"""Accepts manager instance object and resource attributes dict"""
|
|
self.manager = manager
|
|
self._attributes = dict((include, None) for include in self._includes)
|
|
self._attributes.update(dict((relation, None) for relation in self._relations))
|
|
self._attributes.update(attributes)
|
|
self._create_readonly += self._relations + self._includes
|
|
self._update_readonly += self._relations + self._includes
|
|
self._changes = {}
|
|
|
|
if self._relations_name is None:
|
|
self._relations_name = self.__class__.__name__.lower()
|
|
|
|
def __getitem__(self, item):
|
|
"""Provides a dictionary-like access to resource attributes"""
|
|
return getattr(self, item)
|
|
|
|
def __setitem__(self, item, value):
|
|
"""Provides a dictionary-like setter for resource attributes"""
|
|
return setattr(self, item, value)
|
|
|
|
def __getattr__(self, item):
|
|
"""Returns the requested attribute and makes a conversion if needed"""
|
|
if item.startswith('_'):
|
|
raise AttributeError
|
|
|
|
if item in self._attributes:
|
|
# If item shouldn't be converted let's return it as it is
|
|
if item in self._unconvertible:
|
|
return self._attributes[item]
|
|
|
|
# If item should be a Resource object, let's convert it
|
|
elif item in _RESOURCE_MAP:
|
|
manager = ResourceManager(self.manager.redmine, _RESOURCE_MAP[item])
|
|
return manager.to_resource(self._attributes[item])
|
|
|
|
# If item should be a ResourceSet object, let's convert it
|
|
elif item in _RESOURCE_SET_MAP and self._attributes[item] is not None:
|
|
manager = ResourceManager(self.manager.redmine, _RESOURCE_SET_MAP[item])
|
|
return manager.to_resource_set(self._attributes[item])
|
|
|
|
# If item is a relation and should be requested from Redmine, let's do it
|
|
elif item in self._relations and self._attributes[item] is None:
|
|
filters = {'{0}_id'.format(self._relations_name): self.internal_id}
|
|
manager = ResourceManager(self.manager.redmine, _RESOURCE_RELATIONS_MAP[item])
|
|
self._attributes[item] = manager.filter(**filters)
|
|
return self._attributes[item]
|
|
|
|
# If item is an include and should be requested from Redmine, let's do it
|
|
elif item in self._includes and self._attributes[item] is None:
|
|
self._attributes[item] = self.refresh(include=item)._attributes[item] or []
|
|
return getattr(self, item)
|
|
|
|
try:
|
|
# If the requested item is a date/datetime string
|
|
# we need to convert it to the appropriate object
|
|
possible_dt = str(self._attributes[item])
|
|
|
|
try:
|
|
return datetime.strptime(possible_dt, self.manager.redmine.datetime_format)
|
|
except ValueError:
|
|
return datetime.strptime(possible_dt, self.manager.redmine.date_format).date()
|
|
except ValueError:
|
|
return self._attributes[item]
|
|
except KeyError:
|
|
if self.is_new():
|
|
if item in ('id', 'version'):
|
|
return 0
|
|
return ''
|
|
|
|
return self._action_if_attribute_absent()
|
|
|
|
def __setattr__(self, item, value):
|
|
"""Sets the requested attribute"""
|
|
if item in self._members or item.startswith('_'):
|
|
super(_Resource, self).__setattr__(item, value)
|
|
elif item in self._create_readonly and self.is_new():
|
|
raise ReadonlyAttrError
|
|
elif item in self._update_readonly and not self.is_new():
|
|
raise ReadonlyAttrError
|
|
elif item == 'custom_fields':
|
|
for org_index, org_field in enumerate(self._attributes.setdefault('custom_fields', [])):
|
|
if 'value' not in org_field:
|
|
self._attributes['custom_fields'][org_index]['value'] = '0'
|
|
|
|
try:
|
|
for new_index, new_field in enumerate(value):
|
|
if org_field['id'] == new_field['id']:
|
|
self._attributes['custom_fields'][org_index]['value'] = self.manager.prepare_params(
|
|
value.pop(new_index))['value']
|
|
except (TypeError, KeyError):
|
|
raise CustomFieldValueError
|
|
|
|
self._attributes['custom_fields'].extend(value)
|
|
self._changes[item] = self._attributes['custom_fields']
|
|
else:
|
|
prep_item, prep_value = self.manager.prepare_params({item: value}).popitem()
|
|
self._changes[prep_item] = prep_value
|
|
self._attributes[item] = value
|
|
|
|
if item in _RESOURCE_SINGLE_ATTR_ID_MAP:
|
|
self._attributes[_RESOURCE_SINGLE_ATTR_ID_MAP[item]] = {'id': value}
|
|
elif item in _RESOURCE_MULTIPLE_ATTR_ID_MAP:
|
|
self._attributes[_RESOURCE_MULTIPLE_ATTR_ID_MAP[item]] = [{'id': member_id} for member_id in value]
|
|
|
|
def refresh(self, **params):
|
|
"""Reloads resource data from Redmine"""
|
|
return self.manager.get(self.internal_id, **params)
|
|
|
|
def pre_create(self):
|
|
"""Tasks that should be done before creating the resource"""
|
|
pass
|
|
|
|
def post_create(self):
|
|
"""Tasks that should be done after creating the resource"""
|
|
pass
|
|
|
|
def pre_update(self):
|
|
"""Tasks that should be done before updating the resource"""
|
|
pass
|
|
|
|
def post_update(self):
|
|
"""Tasks that should be done after updating the resource"""
|
|
pass
|
|
|
|
def save(self):
|
|
"""Creates or updates a resource"""
|
|
if not self.is_new():
|
|
self.pre_update()
|
|
self.manager.update(self.internal_id, **self._changes)
|
|
self._attributes['updated_on'] = datetime.utcnow().strftime(self.manager.redmine.datetime_format)
|
|
self.post_update()
|
|
else:
|
|
self.pre_create()
|
|
for item, value in self.manager.create(**self._changes):
|
|
self._attributes[item] = value
|
|
self.post_create()
|
|
|
|
self._changes = {}
|
|
return True
|
|
|
|
@classmethod
|
|
def translate_params(cls, params):
|
|
"""Translates internal param names to the real Redmine param names if needed"""
|
|
return params
|
|
|
|
@property
|
|
def url(self):
|
|
"""Returns full url to the resource for humans if there is one"""
|
|
if self.query_one is not None:
|
|
return '{0}{1}'.format(
|
|
self.manager.redmine.url,
|
|
self.query_one.format(self.internal_id).replace('.json', '')
|
|
)
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def internal_id(self):
|
|
"""Returns identifier of the resource for usage in internals of the library"""
|
|
return self.id
|
|
|
|
def is_new(self):
|
|
"""Checks if resource was just created and not yet saved to Redmine or it is an existing resource"""
|
|
return False if 'id' in self._attributes or 'created_on' in self._attributes else True
|
|
|
|
def _action_if_attribute_absent(self):
|
|
"""Whether we should raise an exception in case of attribute absence or just return None"""
|
|
raise_attr_exception = self.manager.redmine.raise_attr_exception
|
|
|
|
if isinstance(raise_attr_exception, bool) and raise_attr_exception:
|
|
raise ResourceAttrError
|
|
elif isinstance(raise_attr_exception, (list, tuple)) and self.__class__.__name__ in raise_attr_exception:
|
|
raise ResourceAttrError
|
|
|
|
return None
|
|
|
|
def __dir__(self):
|
|
"""We need to show only real Redmine resource attributes on dir() call"""
|
|
return list(self._attributes.keys())
|
|
|
|
def __iter__(self):
|
|
"""Provides a way to iterate through resource attributes and its values"""
|
|
return iter(self._attributes.items())
|
|
|
|
def __int__(self):
|
|
"""Integer representation of the Redmine resource object"""
|
|
return self.id
|
|
|
|
def __str__(self):
|
|
"""Informal representation of the Redmine resource object"""
|
|
return to_string(self.name)
|
|
|
|
def __repr__(self):
|
|
"""Official representation of the Redmine resource object"""
|
|
return '<{0}.{1} #{2} "{3}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.name)
|
|
)
|
|
|
|
|
|
class Project(_Resource):
|
|
redmine_version = '1.0'
|
|
container_all = 'projects'
|
|
container_one = 'project'
|
|
container_create = 'project'
|
|
container_update = 'project'
|
|
query_all = '/projects.json'
|
|
query_one = '/projects/{0}.json'
|
|
query_create = '/projects.json'
|
|
query_update = '/projects/{0}.json'
|
|
query_delete = '/projects/{0}.json'
|
|
|
|
_includes = ('trackers', 'issue_categories', 'enabled_modules')
|
|
_relations = (
|
|
'wiki_pages',
|
|
'memberships',
|
|
'issue_categories',
|
|
'time_entries',
|
|
'versions',
|
|
'news',
|
|
'issues',
|
|
'contacts',
|
|
'deals',
|
|
'deal_categories',
|
|
)
|
|
_unconvertible = _Resource._unconvertible + ('identifier', 'status')
|
|
_update_readonly = _Resource._update_readonly + ('identifier',)
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'parent' and item in self._attributes:
|
|
return ResourceManager(self.manager.redmine, 'Project').to_resource(self._attributes[item])
|
|
|
|
value = super(Project, self).__getattr__(item)
|
|
|
|
if item == 'enabled_modules':
|
|
value = [module.get('name') if isinstance(module, dict) else module for module in value]
|
|
|
|
return value
|
|
|
|
|
|
class Issue(_Resource):
|
|
redmine_version = '1.0'
|
|
container_all = 'issues'
|
|
container_one = 'issue'
|
|
container_filter = 'issues'
|
|
container_create = 'issue'
|
|
container_update = 'issue'
|
|
query_all = '/issues.json'
|
|
query_one = '/issues/{0}.json'
|
|
query_filter = '/issues.json'
|
|
query_create = '/projects/{project_id}/issues.json'
|
|
query_update = '/issues/{0}.json'
|
|
query_delete = '/issues/{0}.json'
|
|
|
|
_includes = ('children', 'attachments', 'relations', 'changesets', 'journals', 'watchers')
|
|
_relations = ('relations', 'time_entries')
|
|
_unconvertible = _Resource._unconvertible + ('subject', 'notes')
|
|
_create_readonly = _Resource._create_readonly + ('spent_hours',)
|
|
_update_readonly = _create_readonly
|
|
|
|
class Watcher:
|
|
"""An issue watcher implementation"""
|
|
def __init__(self, issue):
|
|
self._redmine = issue.manager.redmine
|
|
self._issue_id = issue.internal_id
|
|
|
|
if self._redmine.ver is not None and LooseVersion(str(self._redmine.ver)) < LooseVersion('2.3'):
|
|
raise ResourceVersionMismatchError
|
|
|
|
def add(self, user_id):
|
|
"""Adds user to issue watchers list"""
|
|
url = '{0}/issues/{1}/watchers.json'.format(self._redmine.url, self._issue_id)
|
|
return self._redmine.request('post', url, data={'user_id': user_id})
|
|
|
|
def remove(self, user_id):
|
|
"""Removes user from issue watchers list"""
|
|
url = '{0}/issues/{1}/watchers/{2}.json'.format(self._redmine.url, self._issue_id, user_id)
|
|
return self._redmine.request('delete', url)
|
|
|
|
@classmethod
|
|
def translate_params(cls, params):
|
|
if 'version_id' in params:
|
|
params['fixed_version_id'] = params.pop('version_id')
|
|
|
|
return super(Issue, cls).translate_params(params)
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'version':
|
|
return super(Issue, self).__getattr__('fixed_version')
|
|
elif item == 'watcher':
|
|
return Issue.Watcher(self)
|
|
elif item == 'parent' and item in self._attributes:
|
|
return ResourceManager(self.manager.redmine, 'Issue').to_resource(self._attributes[item])
|
|
|
|
return super(Issue, self).__getattr__(item)
|
|
|
|
def __setattr__(self, item, value):
|
|
if item == 'version_id':
|
|
super(Issue, self).__setattr__('fixed_version_id', value)
|
|
else:
|
|
super(Issue, self).__setattr__(item, value)
|
|
|
|
def __str__(self):
|
|
try:
|
|
return to_string(self.subject)
|
|
except ResourceAttrError:
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
try:
|
|
return '<{0}.{1} #{2} "{3}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.subject)
|
|
)
|
|
except ResourceAttrError:
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class TimeEntry(_Resource):
|
|
redmine_version = '1.1'
|
|
container_all = 'time_entries'
|
|
container_one = 'time_entry'
|
|
container_filter = 'time_entries'
|
|
container_create = 'time_entry'
|
|
container_update = 'time_entry'
|
|
query_all = '/time_entries.json'
|
|
query_one = '/time_entries/{0}.json'
|
|
query_filter = '/time_entries.json'
|
|
query_create = '/time_entries.json'
|
|
query_update = '/time_entries/{0}.json'
|
|
query_delete = '/time_entries/{0}.json'
|
|
|
|
@classmethod
|
|
def translate_params(cls, params):
|
|
if 'from_date' in params:
|
|
params['from'] = params.pop('from_date')
|
|
|
|
if 'to_date' in params:
|
|
params['to'] = params.pop('to_date')
|
|
|
|
return super(TimeEntry, cls).translate_params(params)
|
|
|
|
def __str__(self):
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class Enumeration(_Resource):
|
|
redmine_version = '2.2'
|
|
container_filter = '{resource}'
|
|
query_filter = '/enumerations/{resource}.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/enumerations/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
|
|
class Attachment(_Resource):
|
|
redmine_version = '1.3'
|
|
container_one = 'attachment'
|
|
query_one = '/attachments/{0}.json'
|
|
|
|
def download(self, savepath=None, filename=None):
|
|
return self.manager.redmine.download(self.content_url, savepath, filename)
|
|
|
|
def __str__(self):
|
|
try:
|
|
return to_string(self.filename)
|
|
except ResourceAttrError:
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
try:
|
|
return '<{0}.{1} #{2} "{3}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.filename)
|
|
)
|
|
except ResourceAttrError:
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class IssueJournal(_Resource):
|
|
redmine_version = '1.0'
|
|
_unconvertible = ('notes',)
|
|
|
|
def __str__(self):
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class WikiPage(_Resource):
|
|
redmine_version = '2.2'
|
|
container_filter = 'wiki_pages'
|
|
container_one = 'wiki_page'
|
|
container_create = 'wiki_page'
|
|
container_update = 'wiki_page'
|
|
query_filter = '/projects/{project_id}/wiki/index.json'
|
|
query_one = '/projects/{project_id}/wiki/{0}.json'
|
|
query_create = '/projects/{project_id}/wiki/{title}.json'
|
|
query_update = '/projects/{project_id}/wiki/{0}.json'
|
|
query_delete = '/projects/{project_id}/wiki/{0}.json'
|
|
|
|
_includes = ('attachments',)
|
|
_unconvertible = _Resource._unconvertible + ('title', 'text')
|
|
_create_readonly = _Resource._create_readonly + ('version',)
|
|
_update_readonly = _create_readonly
|
|
|
|
def refresh(self, **params):
|
|
return super(WikiPage, self).refresh(**dict(params, project_id=self.manager.params.get('project_id', 0)))
|
|
|
|
def post_update(self):
|
|
self._attributes['version'] = self._attributes.get('version', 0) + 1
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}{1}'.format(
|
|
self.manager.redmine.url,
|
|
self.query_one.format(
|
|
self.internal_id,
|
|
project_id=self.manager.params.get('project_id', 0)
|
|
).replace('.json', '')
|
|
)
|
|
|
|
@property
|
|
def internal_id(self):
|
|
return to_string(self.title)
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'parent' and item in self._attributes:
|
|
manager = ResourceManager(self.manager.redmine, 'WikiPage')
|
|
manager.params['project_id'] = self.manager.params.get('project_id', 0)
|
|
return manager.to_resource(self._attributes[item])
|
|
|
|
# If a text attribute of a resource is missing, we should
|
|
# refresh a resource automatically for user's convenience
|
|
try:
|
|
return super(WikiPage, self).__getattr__(item)
|
|
except ResourceAttrError:
|
|
if 'text' not in self._attributes:
|
|
self._attributes = self.refresh()._attributes
|
|
|
|
return super(WikiPage, self).__getattr__(item)
|
|
|
|
def __int__(self):
|
|
return self.version
|
|
|
|
def __str__(self):
|
|
return self.internal_id
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} "{2}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.internal_id
|
|
)
|
|
|
|
|
|
class ProjectMembership(_Resource):
|
|
redmine_version = '1.4'
|
|
container_filter = 'memberships'
|
|
container_one = 'membership'
|
|
container_update = 'membership'
|
|
container_create = 'membership'
|
|
query_filter = '/projects/{project_id}/memberships.json'
|
|
query_one = '/memberships/{0}.json'
|
|
query_create = '/projects/{project_id}/memberships.json'
|
|
query_update = '/memberships/{0}.json'
|
|
query_delete = '/memberships/{0}.json'
|
|
|
|
_create_readonly = _Resource._create_readonly + ('user', 'roles')
|
|
_update_readonly = _create_readonly
|
|
|
|
def __str__(self):
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class IssueCategory(_Resource):
|
|
redmine_version = '1.3'
|
|
container_filter = 'issue_categories'
|
|
container_one = 'issue_category'
|
|
container_update = 'issue_category'
|
|
container_create = 'issue_category'
|
|
query_filter = '/projects/{project_id}/issue_categories.json'
|
|
query_one = '/issue_categories/{0}.json'
|
|
query_create = '/projects/{project_id}/issue_categories.json'
|
|
query_update = '/issue_categories/{0}.json'
|
|
query_delete = '/issue_categories/{0}.json'
|
|
|
|
|
|
class IssueRelation(_Resource):
|
|
redmine_version = '1.3'
|
|
container_filter = 'relations'
|
|
container_one = 'relation'
|
|
container_create = 'relation'
|
|
query_filter = '/issues/{issue_id}/relations.json'
|
|
query_one = '/relations/{0}.json'
|
|
query_create = '/issues/{issue_id}/relations.json'
|
|
query_delete = '/relations/{0}.json'
|
|
|
|
def __str__(self):
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class Version(_Resource):
|
|
redmine_version = '1.3'
|
|
container_filter = 'versions'
|
|
container_one = 'version'
|
|
container_create = 'version'
|
|
container_update = 'version'
|
|
query_filter = '/projects/{project_id}/versions.json'
|
|
query_one = '/versions/{0}.json'
|
|
query_create = '/projects/{project_id}/versions.json'
|
|
query_update = '/versions/{0}.json'
|
|
query_delete = '/versions/{0}.json'
|
|
|
|
_unconvertible = ('status',)
|
|
|
|
|
|
class User(_Resource):
|
|
redmine_version = '1.1'
|
|
container_all = 'users'
|
|
container_one = 'user'
|
|
container_filter = 'users'
|
|
container_create = 'user'
|
|
container_update = 'user'
|
|
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'
|
|
|
|
_includes = ('memberships', 'groups')
|
|
_relations = ('issues', 'time_entries', 'contacts', 'deals')
|
|
_relations_name = 'assigned_to'
|
|
_unconvertible = ('status',)
|
|
_create_readonly = _Resource._create_readonly + ('api_key', 'last_login_on')
|
|
_update_readonly = _create_readonly
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'time_entries':
|
|
self._relations_name = 'user'
|
|
value = super(User, self).__getattr__(item)
|
|
self._relations_name = 'assigned_to'
|
|
return value
|
|
|
|
return super(User, self).__getattr__(item)
|
|
|
|
def __str__(self):
|
|
try:
|
|
return super(User, self).__str__()
|
|
except ResourceAttrError:
|
|
return '{0} {1}'.format(to_string(self.firstname), to_string(self.lastname))
|
|
|
|
def __repr__(self):
|
|
try:
|
|
return super(User, self).__repr__()
|
|
except ResourceAttrError:
|
|
return '<{0}.{1} #{2} "{3} {4}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.firstname),
|
|
to_string(self.lastname)
|
|
)
|
|
|
|
|
|
class Group(_Resource):
|
|
redmine_version = '2.1'
|
|
container_all = 'groups'
|
|
container_one = 'group'
|
|
container_create = 'group'
|
|
container_update = 'group'
|
|
query_all = '/groups.json'
|
|
query_one = '/groups/{0}.json'
|
|
query_create = '/groups.json'
|
|
query_update = '/groups/{0}.json'
|
|
query_delete = '/groups/{0}.json'
|
|
|
|
_includes = ('memberships', 'users')
|
|
|
|
class User:
|
|
"""A group user implementation"""
|
|
def __init__(self, group):
|
|
self._redmine = group.manager.redmine
|
|
self._group_id = group.internal_id
|
|
|
|
def add(self, user_id):
|
|
"""Adds user to a group"""
|
|
url = '{0}/groups/{1}/users.json'.format(self._redmine.url, self._group_id)
|
|
return self._redmine.request('post', url, data={'user_id': user_id})
|
|
|
|
def remove(self, user_id):
|
|
"""Removes user from a group"""
|
|
url = '{0}/groups/{1}/users/{2}.json'.format(self._redmine.url, self._group_id, user_id)
|
|
return self._redmine.request('delete', url)
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'user':
|
|
return Group.User(self)
|
|
|
|
return super(Group, self).__getattr__(item)
|
|
|
|
|
|
class Role(_Resource):
|
|
redmine_version = '1.4'
|
|
container_all = 'roles'
|
|
container_one = 'role'
|
|
query_all = '/roles.json'
|
|
query_one = '/roles/{0}.json'
|
|
|
|
|
|
class News(_Resource):
|
|
redmine_version = '1.1'
|
|
container_all = 'news'
|
|
container_filter = 'news'
|
|
query_all = '/news.json'
|
|
query_filter = '/news.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/news/{1}'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2} "{3}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.title)
|
|
)
|
|
|
|
|
|
class IssueStatus(_Resource):
|
|
redmine_version = '1.3'
|
|
container_all = 'issue_statuses'
|
|
query_all = '/issue_statuses.json'
|
|
|
|
_relations = ('issues',)
|
|
_relations_name = 'status'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/issue_statuses/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
|
|
class Tracker(_Resource):
|
|
redmine_version = '1.3'
|
|
container_all = 'trackers'
|
|
query_all = '/trackers.json'
|
|
|
|
_relations = ('issues',)
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/trackers/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
|
|
class Query(_Resource):
|
|
redmine_version = '1.3'
|
|
container_all = 'queries'
|
|
query_all = '/queries.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/projects/{1}/issues?query_id={2}'.format(
|
|
self.manager.redmine.url,
|
|
self._attributes.get('project_id', 0),
|
|
self.internal_id
|
|
)
|
|
|
|
|
|
class CustomField(_Resource):
|
|
redmine_version = '2.4'
|
|
container_all = 'custom_fields'
|
|
query_all = '/custom_fields.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/custom_fields/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
def __getattr__(self, item):
|
|
# If custom field was created after the creation of the resource,
|
|
# i.e. project, and it's not used in the resource, there will be
|
|
# no value attribute defined, that is why we need to return 0 or
|
|
# we'll get an exception
|
|
if item == 'value' and item not in self._attributes:
|
|
return 0
|
|
|
|
# Redmine <2.5.2 returns only single tracker instead of a list of
|
|
# all available trackers, see http://www.redmine.org/issues/16739
|
|
# for details
|
|
elif item == 'trackers' and 'tracker' in self._attributes[item]:
|
|
self._attributes[item] = [self._attributes[item]['tracker']]
|
|
|
|
return super(CustomField, self).__getattr__(item)
|
|
|
|
|
|
class Note(_Resource):
|
|
redmine_version = '2.1'
|
|
requirements = (('CRM plugin', '3.2.4'),)
|
|
container_one = 'note'
|
|
query_one = '/notes/{0}.json'
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'source' and item in self._attributes and self._attributes[item].get('type') in ('Deal', 'Contact'):
|
|
manager = ResourceManager(self.manager.redmine, self._attributes[item]['type'])
|
|
return manager.to_resource(self._attributes[item])
|
|
|
|
return super(Note, self).__getattr__(item)
|
|
|
|
def __str__(self):
|
|
return self.content
|
|
|
|
def __repr__(self):
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class Contact(_Resource):
|
|
redmine_version = '1.2.1'
|
|
requirements = ('CRM plugin',)
|
|
container_all = 'contacts'
|
|
container_one = 'contact'
|
|
container_filter = 'contacts'
|
|
container_create = 'contact'
|
|
container_update = 'contact'
|
|
query_all = '/contacts.json'
|
|
query_one = '/contacts/{0}.json'
|
|
query_filter = '/contacts.json'
|
|
query_create = '/projects/{project_id}/contacts.json'
|
|
query_update = '/contacts/{0}.json'
|
|
query_delete = '/contacts/{0}.json'
|
|
|
|
_includes = ('notes', 'contacts', 'deals', 'issues')
|
|
_unconvertible = _Resource._unconvertible + ('company', 'skype_name')
|
|
|
|
class Project:
|
|
"""A contact project implementation"""
|
|
def __init__(self, contact):
|
|
self._redmine = contact.manager.redmine
|
|
self._contact_id = contact.internal_id
|
|
|
|
if self._redmine.ver is not None and LooseVersion(str(self._redmine.ver)) < LooseVersion('2.3'):
|
|
raise ResourceVersionMismatchError
|
|
|
|
def add(self, project_id):
|
|
"""Adds project to contact's project list"""
|
|
url = '{0}/contacts/{1}/projects.json'.format(self._redmine.url, self._contact_id)
|
|
|
|
try:
|
|
return self._redmine.request('post', url, data={'project': {'id': project_id}})
|
|
except ResourceNotFoundError:
|
|
raise ValidationError("Attempt to add contact to a project that doesn't exist")
|
|
except ForbiddenError:
|
|
raise ValidationError(
|
|
'Attempt to add contact to a project that either has contacts module disabled or is read-only')
|
|
|
|
def remove(self, project_id):
|
|
"""Removes project from contact's project list"""
|
|
url = '{0}/contacts/{1}/projects/{2}.json'.format(self._redmine.url, self._contact_id, project_id)
|
|
|
|
try:
|
|
return self._redmine.request('delete', url)
|
|
except ResourceNotFoundError:
|
|
raise ValidationError("Attempt to remove contact from a project that doesn't exist")
|
|
except ForbiddenError:
|
|
raise ValidationError(
|
|
'Attempt to remove contact from a project that either has contacts module disabled or is read-only')
|
|
|
|
@classmethod
|
|
def translate_params(cls, params):
|
|
if isinstance(params.get('tag_list'), (list, tuple)):
|
|
params['tag_list'] = ','.join(params['tag_list'])
|
|
|
|
if 'phones' in params:
|
|
params['phone'] = ','.join(params.pop('phones'))
|
|
|
|
if 'emails' in params:
|
|
params['email'] = ','.join(params.pop('emails'))
|
|
|
|
return super(Contact, cls).translate_params(params)
|
|
|
|
def __getattr__(self, item):
|
|
if item == 'project':
|
|
return Contact.Project(self)
|
|
elif item == 'phones':
|
|
return [p.get('number') if isinstance(p, dict) else p for p in self._attributes.get('phones', [])]
|
|
elif item == 'emails':
|
|
return [e.get('address') if isinstance(e, dict) else e for e in self._attributes.get('emails', [])]
|
|
elif item == 'avatar' and item in self._attributes:
|
|
manager = ResourceManager(self.manager.redmine, 'Attachment')
|
|
return manager.to_resource({'id': self._attributes[item].get('attachment_id', 0)})
|
|
|
|
return super(Contact, self).__getattr__(item)
|
|
|
|
def __str__(self):
|
|
try:
|
|
return super(Contact, self).__str__()
|
|
except ResourceAttrError:
|
|
if not getattr(self, 'last_name', False):
|
|
return '{0}'.format(to_string(self.first_name))
|
|
else:
|
|
return '{0} {1}'.format(to_string(self.first_name), to_string(self.last_name))
|
|
|
|
def __repr__(self):
|
|
try:
|
|
return super(Contact, self).__repr__()
|
|
except ResourceAttrError:
|
|
if not getattr(self, 'last_name', False):
|
|
return '<{0}.{1} #{2} "{3}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.first_name),
|
|
)
|
|
else:
|
|
return '<{0}.{1} #{2} "{3} {4}">'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id,
|
|
to_string(self.first_name),
|
|
to_string(self.last_name)
|
|
)
|
|
|
|
|
|
class ContactTag(_Resource):
|
|
redmine_version = '2.3'
|
|
requirements = (('CRM plugin', '3.4.0'),)
|
|
container_all = 'tags'
|
|
query_all = '/contacts_tags.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/contacts_tags/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
|
|
class CrmQuery(_Resource):
|
|
redmine_version = '2.3'
|
|
requirements = (('CRM plugin', '3.3.0'),)
|
|
container_filter = 'queries'
|
|
query_filter = '/crm_queries.json?object_type={resource}'
|
|
|
|
_relations = ('deals',)
|
|
_relations_name = 'query'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/projects/{1}/{2}s?query_id={3}'.format(
|
|
self.manager.redmine.url,
|
|
self._attributes.get('project_id', 0),
|
|
self.manager.params.get('resource', ''),
|
|
self.internal_id
|
|
)
|
|
|
|
|
|
class Deal(_Resource):
|
|
redmine_version = '1.2.1'
|
|
requirements = ('CRM plugin',)
|
|
container_all = 'deals'
|
|
container_one = 'deal'
|
|
container_filter = 'deals'
|
|
container_create = 'deal'
|
|
container_update = 'deal'
|
|
query_all = '/deals.json'
|
|
query_one = '/deals/{0}.json'
|
|
query_filter = '/deals.json'
|
|
query_create = '/projects/{project_id}/deals.json'
|
|
query_update = '/deals/{0}.json'
|
|
query_delete = '/deals/{0}.json'
|
|
|
|
_includes = ('notes',)
|
|
|
|
def __getattr__(self, item):
|
|
if item in ('category', 'status') and item in self._attributes:
|
|
manager = ResourceManager(self.manager.redmine, 'Deal{0}'.format(item.capitalize()))
|
|
return manager.to_resource(self._attributes[item])
|
|
|
|
return super(Deal, self).__getattr__(item)
|
|
|
|
def __str__(self):
|
|
try:
|
|
return super(Deal, self).__str__()
|
|
except ResourceAttrError:
|
|
return str(self.id)
|
|
|
|
def __repr__(self):
|
|
try:
|
|
return super(Deal, self).__repr__()
|
|
except ResourceAttrError:
|
|
return '<{0}.{1} #{2}>'.format(
|
|
self.__class__.__module__,
|
|
self.__class__.__name__,
|
|
self.id
|
|
)
|
|
|
|
|
|
class DealStatus(_Resource):
|
|
redmine_version = '2.3'
|
|
requirements = (('CRM plugin', '3.3.0'),)
|
|
container_all = 'deal_statuses'
|
|
query_all = '/deal_statuses.json'
|
|
|
|
_relations = ('deals',)
|
|
_relations_name = 'status'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/deal_statuses/{1}/edit'.format(self.manager.redmine.url, self.internal_id)
|
|
|
|
|
|
class DealCategory(_Resource):
|
|
redmine_version = '2.3'
|
|
requirements = (('CRM plugin', '3.3.0'),)
|
|
container_filter = 'deal_categories'
|
|
query_filter = '/projects/{project_id}/deal_categories.json'
|
|
|
|
@property
|
|
def url(self):
|
|
return '{0}/deal_categories/edit?id={1}'.format(self.manager.redmine.url, self.internal_id)
|