Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom fields. #13

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f5134af
Add support for custom fields.
pedrorodriguesgomes Sep 26, 2017
3781991
Add support to update custom fields.
pedrorodriguesgomes Jul 31, 2018
3f3da6e
Add support to update custom fields.
pedrorodriguesgomes Jul 31, 2018
9216afe
Update set custom field for data type Date.
pedrorodriguesgomes Aug 1, 2018
2bcc015
Fix email issue.
pedrorodriguesgomes Aug 2, 2018
ec9f0e1
Fix email issue in Creatable mixin.
pedrorodriguesgomes Sep 5, 2018
15b2cfa
Add cache to custom fields.
pedrorodriguesgomes Sep 12, 2018
8c23c0c
Fix update mixin to support lead without email.
pedrorodriguesgomes Oct 3, 2018
07aef54
Add support to search for users.
pedrorodriguesgomes Oct 19, 2018
52fb956
Fix update for Person.
pedrorodriguesgomes Nov 13, 2018
4872162
Update mixins.py
pedrorodriguesgomes Nov 13, 2018
6c1c236
Update Opportunity fields.
pedrorodriguesgomes Nov 15, 2018
55b7102
Update for create with emails params
Jan 30, 2019
bfe915f
Merge pull request #1 from Seedstars/creatable_emails_update
pedrorodriguesgomes Jan 30, 2019
af16b24
Fix Input Invalid error for category field
asayed18 May 29, 2019
1a16c86
Merge pull request #2 from ahmed-abdelsalam/patch-1
Henv Jun 4, 2019
2513b00
fix update opportunity problem
asayed18 Jun 18, 2019
ad60d96
Merge pull request #3 from ahmed-abdelsalam/patch-2
Henv Jun 18, 2019
658c74e
fix update custom_field type DATE
asayed18 Jul 22, 2019
36e5f53
Merge pull request #4 from ahmed-abdelsalam/patch-4
Henv Jul 22, 2019
4acd557
fix win_probability none bug
Jul 24, 2019
b719ede
Merge pull request #6 from ahmed-abdelsalam/patch-4
Henv Jul 31, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions prospyr/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
TYPE_DATE = 'Date'
TYPE_DROPDOWN = 'Dropdown'
TYPE_MULTISELECT = 'MultiSelect'
TYPE_FLOAT = 'Float'
TYPE_DATE = 'Date'
TYPE_STRING = 'String'
TYPE_TEXT = 'Text'
TYPE_URL = 'URL'
TYPE_PERCENTAGE = 'Percentage'
TYPE_CURRENCY = 'Currency'
41 changes: 38 additions & 3 deletions prospyr/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import absolute_import, print_function, unicode_literals

from functools import wraps

import datetime
import arrow
from arrow.parser import ParserError
from marshmallow import ValidationError, fields
Expand All @@ -15,10 +15,33 @@
from prospyr.validate import WhitespaceEmail


class FieldsCache:
def __init__(self):
"""Constructor"""
self.cache = {}

def contains(self, key):
""" Returns True or False depending on whether or not the key is in the cache."""
return key in self.cache

def get(self, key):
"""Return value for the key."""
return self.cache[key]['value']

def set(self, key, value):
"""Set key value in cache."""
self.cache[key] = {'date_created': datetime.datetime.now(),
'value': value}


CACHE = FieldsCache()


class Unix(fields.Field):
"""
datetime.datetime <-> unix timestamp
"""

def _serialize(self, value, attr, obj):
try:
return arrow.get(value).timestamp
Expand All @@ -36,6 +59,7 @@ class Email(fields.Email):
"""
ProsperWorks emails can have leading and trailing spaces.
"""

def __init__(self, *args, **kwargs):
super(Email, self).__init__(self, *args, **kwargs)

Expand All @@ -61,6 +85,7 @@ def normalise_many(fn, default=False):
wrapped they can assume `value` is a collection. From there, (self, values,
attr, data) makes more sense as the signature.
"""

@wraps(fn)
def wrapper(self, value, attr, data):
many = getattr(self, 'many', default)
Expand All @@ -71,6 +96,7 @@ def wrapper(self, value, attr, data):
return res[0]
else:
return res

return wrapper


Expand All @@ -84,11 +110,12 @@ class NestedResource(fields.Field):
"""

def __init__(self, resource_cls, default=missing_, many=False,
id_only=False, **kwargs):
id_only=False, custom_field=False, **kwargs):
self.resource_cls = resource_cls
self.schema = type(resource_cls.Meta.schema)
self.many = many
self.id_only = id_only
self.custom_field = custom_field
super(NestedResource, self).__init__(default=default, many=many,
**kwargs)

Expand All @@ -98,6 +125,14 @@ def _deserialize(self, values, attr, data):
for value in values:
if self.id_only:
resources.append(self.resource_cls.objects.get(id=value['id']))
elif self.custom_field:
if CACHE.contains(value['custom_field_definition_id']):
resource = CACHE.get(value['custom_field_definition_id'])
else:
resource = self.resource_cls.objects.get(id=value['custom_field_definition_id'])
CACHE.set(value['custom_field_definition_id'], resource)
resource.value = value['value']
resources.append(resource)
else:
resources.append(self.resource_cls.from_api_data(value))
return resources
Expand Down Expand Up @@ -147,7 +182,7 @@ def _deserialize(self, values, attr, data):
# the resource isn't modelled yet
from prospyr.resources import Placeholder
name = encode_typename(self.placeholder_types[idtype])
resource_cls = type(name, (Placeholder, ), {})
resource_cls = type(name, (Placeholder,), {})
resource = resource_cls(id=value['id'])
else:
# modelled resource; fetch
Expand Down
132 changes: 127 additions & 5 deletions prospyr/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from __future__ import absolute_import, print_function, unicode_literals

import re
from logging import getLogger

from datetime import datetime
from requests import codes

from prospyr.exceptions import ApiError
from prospyr.constants import *

logger = getLogger(__name__)

Expand All @@ -19,7 +21,7 @@ class Creatable(object):
# pworks uses 200 OK for creates. 201 CREATED is here through optimism.
_create_success_codes = {codes.created, codes.ok}

def create(self, using='default'):
def create(self, using='default', email=None, emails=None):
"""
Create a new instance of this Resource. True on success.
"""
Expand All @@ -29,7 +31,14 @@ def create(self, using='default'):
)
conn = self._get_conn(using)
path = self.Meta.create_path
resp = conn.post(conn.build_absolute_url(path), json=self._raw_data)

data = self._raw_data
if email:
data['email'] = {'category': 'work', 'email': email}

if emails:
data['emails'] = [{'email': emails, 'category': 'work'}]
resp = conn.post(conn.build_absolute_url(path), json=data)

if resp.status_code in self._create_success_codes:
data = self._load_raw(resp.json())
Expand Down Expand Up @@ -59,7 +68,6 @@ def read(self, using='default'):
resp = conn.get(conn.build_absolute_url(path))
if resp.status_code not in self._read_success_codes:
raise ApiError(resp.status_code, resp.text)

data = self._load_raw(resp.json())
self._set_fields(data)
return True
Expand Down Expand Up @@ -87,7 +95,7 @@ class Updateable(object):

_update_success_codes = {codes.ok}

def update(self, using='default'):
def update(self, using='default', email=None, emails=None):
"""
Update this Resource. True on success.
"""
Expand All @@ -96,8 +104,62 @@ def update(self, using='default'):

# can't update IDs
data = self._raw_data
data['custom_fields'] = []
data.pop('id')

# convert to PW style
for cf in self._raw_data['custom_fields']:
if 'value' in cf:
if cf['data_type'] == TYPE_DROPDOWN:
value = int(cf['value']) if cf['value'] else None
elif cf['data_type'] == TYPE_MULTISELECT:
value = [int(v) for v in eval(cf['value'])]
elif cf['data_type'] == TYPE_FLOAT:
value = float(cf['value']) if cf['value'] else None
elif cf['data_type'] == TYPE_DATE:
date_match = re.search(r'\d{2}/\d{2}/\d{4}', cf['value'])
if date_match:
value = date_match.group()
else:
value = None
else:
value = cf['value']
else:
value = ''
data['custom_fields'].append(
{'custom_field_definition_id': cf['id'], 'value': value}
)
if email:
try:
data['email']['email'] = email
except KeyError:
# this may happen if the lead doesn't have an email, by default we add as work email
data['email'] = {'email': email, 'category': 'work'}
if emails:
try:
data['emails'][0]['email'] = emails
except KeyError:
# this may happen if the lead doesn't have an email, by default we add as work email
data['emails'] = [{'email': emails, 'category': 'work'}]
try:
if 'phone_numbers' in data:
for number in data['phone_numbers']:
if number['category'] is None :
number['category'] = 'work' # set default category value to work
except KeyError:
data['phone_numbers'] = [{'number': '', 'category': 'work'}]
try:
if 'websites' in data:
for website in data['websites']:
if website['category'] is None :
website['category'] = 'work' # set default category value to work
except KeyError:
data['websites'] = [{'url': '', 'category': 'work'}]
if type(self).__name__ == 'Opportunity':
try:
del data['close_date']
except KeyError:
pass
conn = self._get_conn(using)
path = self.Meta.detail_path.format(id=self.id)
resp = conn.put(conn.build_absolute_url(path), json=data)
Expand Down Expand Up @@ -134,3 +196,63 @@ def delete(self, using='default'):

class ReadWritable(Creatable, Readable, Updateable, Deletable):
pass


class CustomFieldMixin(object):
class Meta:
abstract = True

def get_custom_field_value(cls, field_name):
"""
Return custom field value depending on data type.
"""
value = ''
for field in cls.custom_fields:
if field.name == field_name:
if field.value:
if field.data_type in [TYPE_STRING, TYPE_TEXT, TYPE_FLOAT, TYPE_URL, TYPE_PERCENTAGE,
TYPE_CURRENCY]:
value = field.value
elif field.data_type == TYPE_DROPDOWN:
for option in field.options:
if option['id'] == field.value:
value = option['name']
elif field.data_type == TYPE_MULTISELECT:
values = []
for val in field.value:
for option in field.options:
if option['id'] == val:
values.append(option['name'])
value = ','.join(values)
elif field.data_type == TYPE_DATE:
value = datetime.fromtimestamp(field.value).date()
return value

def set_custom_field_value(cls, field_name, value):
"""
Set custom field value.
"""
custom_fields = cls.custom_fields
index = 0
for field in cls.custom_fields:
if field.name == field_name:
if value is None:
custom_fields[index].value = None
elif field.data_type in [TYPE_STRING, TYPE_TEXT, TYPE_FLOAT, TYPE_URL, TYPE_PERCENTAGE,
TYPE_CURRENCY]:
custom_fields[index].value = value
elif field.data_type == TYPE_DROPDOWN:
for option in field.options:
if option['name'].lower().strip() == value.lower().strip():
custom_fields[index].value = option['id']
elif field.data_type == TYPE_MULTISELECT:
values = []
for val in value:
for option in field.options:
if option['name'] == val:
values.append(option['id'])
custom_fields[index].value = values
elif field.data_type == TYPE_DATE:
custom_fields[index].value = value
index += 1
cls.custom_fields = custom_fields
Loading