debian-python-redmine/redmine/resources.py

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)