Skip to content

Commit

Permalink
Add ValidationError handling
Browse files Browse the repository at this point in the history
Add basic decorators for realm connection
Add realm_connection decorator to ResponseProcessor and SessionManager

Add initial documentation structure

Remove django-tenant-schemas dependency from project (make it as optional)
  • Loading branch information
bedilbek committed Feb 12, 2020
1 parent 169c838 commit 5e6a05a
Show file tree
Hide file tree
Showing 23 changed files with 375 additions and 101 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.6.0] - 2020-02-12

### Added

- Add ValidationError handling
- Add basic decorators for realm connection
- Add realm_connection decorator to ResponseProcessor and SessionManager
- Add initial documentation structure

### Removed

- Remove django-tenant-schemas dependency from project (make it as optional)


[0.6.0]: https://github.com/weltlink/django-quickbooks/compare/0.5...0.6
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This is an ongoing project to integrate any Django project with Quickbooks Deskt
integration support for <b>Python 3.6+</b> and <b>Django 2.0+</b>


| Version: | 0.5.1 |
| Version: | 0.6.0 |
|-----------|--------------------------------------------------------------------------------------------------------------------|
| Download: | https://pypi.org/project/django-quickbooks/ |
| Source: | https://github.com/weltlink/django-quickbooks/ |
Expand Down Expand Up @@ -56,6 +56,10 @@ integration support for <b>Python 3.6+</b> and <b>Django 2.0+</b>
Soap server for Quickbooks Web Connector is built on top of Spyne and Lxml.


## Setup & Documentation
**This is just a short guide for quickstart**, however


## Links
* [Django Web Framework](https://www.djangoproject.com/)
* [Spyne Official Repository](https://github.com/arskom/spyne)
Expand Down
23 changes: 23 additions & 0 deletions django_quickbooks/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import connection

from django_quickbooks.settings import qbwc_settings


def realm_connection():
return qbwc_settings.REALM_CONNECTION_DECORATOR


def base_realm_connection(func):
def connect(realm, *args, **kwargs):
return func(realm, *args, **kwargs)

return connect


def base_realm_tenant_connection(func):
def connect(realm, *args, **kwargs):
if hasattr(realm, 'schema_name') and hasattr(connection, 'set_schema'):
connection.set_schema(realm.schema_name)
return func(realm, *args, **kwargs)

return connect
48 changes: 44 additions & 4 deletions django_quickbooks/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
from django.utils.translation import ugettext_lazy as _


class ValidationCode:
REQUIRED = 'required'
MIN_LENGTH = 'min_length'
MAX_LENGTH = 'max_length'
INVALID_TYPE = 'invalid_type'


VALIDATION_MESSAGES = {
ValidationCode.REQUIRED: _('This field is required'),
ValidationCode.MIN_LENGTH: _('The minimum length for the field is %s'),
ValidationCode.MAX_LENGTH: _('The maximum length for the field is %s'),
ValidationCode.INVALID_TYPE: _('Invalid type %s for the field type %s'),
}


def _get_error_details(detail, code):
return {code: detail}


class QbException(Exception):
def __init__(self, error):
self.error = error


class ValidationError(Exception):
pass
default_detail = _('Invalid attribute.')
default_code = 'invalid'

def __init__(self, detail=None, code=None):
if detail is None:
detail = self.default_detail
if code is None:
code = self.default_code

# Several errors may be collected together, thus
if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]

self.detail = _get_error_details(detail, code)


class ValidationOptionNotFound(Exception):
class ValidationOptionNotFound(QbException):
pass


class QBXMLParseError(Exception):
class QBXMLParseError(QbException):
pass


class QBXMLStatusError(Exception):
class QBXMLStatusError(QbException):
pass
38 changes: 23 additions & 15 deletions django_quickbooks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,6 @@ class Meta:
abstract = True


class Realm(RealmMixin):
schema_name = models.CharField(max_length=100, unique=True)
is_active = models.BooleanField(default=True)

class Meta:
abstract = False


class RealmSession(RealmSessionMixin):
realm = models.ForeignKey(Realm, on_delete=models.CASCADE, related_name='sessions')

class Meta:
abstract = False


class QBDTaskMixin(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
qb_operation = models.CharField(max_length=25)
Expand Down Expand Up @@ -102,6 +87,29 @@ def get_request(self):
return None


# Below models are concrete implementations of above classes
# As initially I was working with django-tenant-schemas package I had to convert to that architecture
# Thus, below models are extended with schema_name concept that is the core of the django-tenant-schemas package:
# For more information: https://github.com/bernardopires/django-tenant-schemas

# NOTICE: you also find decorators in the package that only work with django-tenant-schemas


class Realm(RealmMixin):
schema_name = models.CharField(max_length=100, unique=True)
is_active = models.BooleanField(default=True)

class Meta:
abstract = False


class RealmSession(RealmSessionMixin):
realm = models.ForeignKey(Realm, on_delete=models.CASCADE, related_name='sessions')

class Meta:
abstract = False


class QBDTask(QBDTaskMixin):
realm = models.ForeignKey(Realm, on_delete=models.CASCADE, related_name='qb_tasks')

Expand Down
22 changes: 15 additions & 7 deletions django_quickbooks/objects/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from abc import ABC

from django_quickbooks import QUICKBOOKS_ENUMS
Expand All @@ -19,15 +20,22 @@ class BaseObject(ABC):

def __setattr__(self, key, value):
if key in self.fields:
if self.validator.validate(value, **self.fields[key]):
pass
else:
raise ValidationError
super().__setattr__(key, value)
super().__setattr__(key, value)

def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
errors = []
# FIXME: ValidationError handling still seems clumsy, need to reviewed
for field_name, value in kwargs.items():
try:
self.validator.validate(field_name, value, **self.fields[field_name])
setattr(self, field_name, value)
except ValidationError as exc:
errors.append(exc.detail)

if errors:
logger = logging.getLogger('django.request')
logging.error(errors)
raise ValidationError(errors)

for field_name, options in self.fields.items():
if not hasattr(self, field_name):
Expand Down
18 changes: 6 additions & 12 deletions django_quickbooks/processors/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.utils import timezone
from django.utils.decorators import method_decorator
from lxml import etree

from django_quickbooks import QBXML_RESPONSE_STATUS_CODES
from django_quickbooks.decorators import realm_connection
from django_quickbooks.exceptions import QBXMLParseError, QBXMLStatusError


Expand All @@ -12,8 +13,7 @@ class ResponseProcessor:
op_type = None
obj_class = None

def __init__(self, realm, response, hresult, message):
self.realm = realm
def __init__(self, response, hresult, message):
self._actual_response_type = None
self._response_body = None
self._response = response
Expand All @@ -23,7 +23,7 @@ def __init__(self, realm, response, hresult, message):

def _process(self):
if self.hresult:
raise QBXMLStatusError
raise QBXMLStatusError(self.message)
qbxml_root = etree.fromstring(self._response)
if qbxml_root.tag != 'QBXML':
raise QBXMLParseError('QBXML tag not found')
Expand All @@ -45,7 +45,8 @@ def _process(self):
def is_valid(self) -> bool:
return '%s%sRs' % (self.resource, self.op_type) == self._actual_response_type

def process(self):
@method_decorator(realm_connection())
def process(self, realm):
assert self.resource, 'resource attribute is not defined during class definition ' \
'of %s' % self.__class__.__name__
assert self.op_type, 'op_type attribute is not defined during class definition ' \
Expand All @@ -71,24 +72,17 @@ def update(self, local_obj, obj):
local_obj.save()

def find_by_list_id(self, list_id):
# FIXME: connection should not be initiated for changing schemas (django-tenant-schemas should be exctracted
# from the project
connection.set_schema(self.realm.schema_name)
try:
return self.local_model_class.objects.get(qbd_object_id=list_id)
except ObjectDoesNotExist:
return None

def find_by_name(self, name, field_name='name'):
# FIXME: connection should not be initiated for changing schemas (django-tenant-schemas should be exctracted
# from the project
connection.set_schema(self.realm.schema_name)
try:
return self.local_model_class.objects.get(**{field_name: name})
except ObjectDoesNotExist:
return None

def create(self, obj):
connection.set_schema(self.realm.schema_name)
customer = self.local_model_class.from_qbd_obj(obj)
customer.save()
12 changes: 6 additions & 6 deletions django_quickbooks/processors/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class CustomerQueryResponseProcessor(ResponseProcessor, ResponseProcessorMixin):
local_model_class = LocalCustomer
obj_class = Customer

def process(self):
cont = super().process()
def process(self, realm):
cont = super().process(realm)
if not cont:
return False
for customer_ret in list(self._response_body):
Expand All @@ -36,8 +36,8 @@ class CustomerAddResponseProcessor(ResponseProcessor, ResponseProcessorMixin):
local_model_class = LocalCustomer
obj_class = Customer

def process(self):
cont = super().process()
def process(self, relam):
cont = super().process(realm)
if not cont:
return False
for customer_ret in list(self._response_body):
Expand All @@ -57,8 +57,8 @@ class CustomerModResponseProcessor(ResponseProcessor, ResponseProcessorMixin):
local_model_class = LocalCustomer
obj_class = Customer

def process(self):
cont = super().process()
def process(self, realm):
cont = super().process(realm)
if not cont:
return False
for customer_ret in list(self._response_body):
Expand Down
8 changes: 2 additions & 6 deletions django_quickbooks/processors/invoice.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.utils import timezone

from django_quickbooks import QUICKBOOKS_ENUMS, qbwc_settings
Expand All @@ -16,8 +15,8 @@ class InvoiceAddResponseProcessor(ResponseProcessor, ResponseProcessorMixin):
resource = QUICKBOOKS_ENUMS.RESOURCE_INVOICE
op_type = QUICKBOOKS_ENUMS.OPP_ADD

def process(self):
cont = super().process()
def process(self, realm):
cont = super().process(realm)
if not cont:
return False
for invoice_ret in list(self._response_body):
Expand All @@ -33,9 +32,6 @@ def process(self):
return True

def find_by_id(self, id):
# FIXME: connection should not be initiated for changing schemas (django-tenant-schemas should be exctracted
# from the project
connection.set_schema(self.realm.schema_name)
try:
return self.local_model_class.objects.get(id=id)
except ObjectDoesNotExist:
Expand Down
4 changes: 2 additions & 2 deletions django_quickbooks/processors/item_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class ItemServiceQueryResponseProcessor(ResponseProcessor):
op_type = QUICKBOOKS_ENUMS.OPP_QR
obj_class = ItemService

def process(self):
cont = super().process()
def process(self, realm):
cont = super().process(realm)
if not cont:
return False

Expand Down
21 changes: 13 additions & 8 deletions django_quickbooks/session_manager.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.db import connection
import logging

from django.utils.decorators import method_decorator
from lxml import etree

from django_quickbooks import get_realm_session_model, get_realm_model, get_qbd_task_model
from django_quickbooks.core.session_manager import BaseSessionManager
from django_quickbooks.decorators import realm_connection
from django_quickbooks.exceptions import QBXMLParseError, QBXMLStatusError
from django_quickbooks.queue_manager import RabbitMQManager

Expand All @@ -26,11 +29,9 @@ def set_ticket(self, realm):
def in_session(self, realm):
return RealmSession.objects.is_active(realm)

def add_new_jobs(self, realm):
@method_decorator(realm_connection())
def add_new_jobs(self, realm=None):
queryset = QBDTask.objects.filter(realm=realm).order_by('created_at')
# FIXME: connection should not be initiated for changing schemas (django-tenant-schemas should be exctracted
# from the project
connection.set_schema(realm.schema_name)
for qb_task in queryset:
self.publish_message(qb_task.get_request(), str(realm.id))
queryset.delete()
Expand All @@ -44,13 +45,17 @@ def process_response(self, ticket, response, hresult, message):
processors = get_processors()
for processor in processors:
try:
processed = processor(realm, response, hresult, message).process()
processed = processor(response, hresult, message).process(realm)
if processed:
break

except QBXMLParseError:
except QBXMLParseError as exc:
logger = logging.getLogger('django.request')
logger.error(exc.error)
return -1
except QBXMLStatusError:
except QBXMLStatusError as exc:
logger = logging.getLogger('django.request')
logger.error(exc.error)
return -1

self._continue_iterative_response(ticket, response)
Expand Down
Loading

0 comments on commit 5e6a05a

Please sign in to comment.