From 5e6a05a98564fe4a96ba2f0941727a186a578a94 Mon Sep 17 00:00:00 2001 From: bedilbek Date: Wed, 12 Feb 2020 18:10:20 +0500 Subject: [PATCH] Add ValidationError handling 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) --- CHANGELOG.md | 19 +++++ README.md | 6 +- django_quickbooks/decorators.py | 23 ++++++ django_quickbooks/exceptions.py | 48 ++++++++++- django_quickbooks/models.py | 38 +++++---- django_quickbooks/objects/base.py | 22 +++-- django_quickbooks/processors/base.py | 18 ++-- django_quickbooks/processors/customer.py | 12 +-- django_quickbooks/processors/invoice.py | 8 +- django_quickbooks/processors/item_service.py | 4 +- django_quickbooks/session_manager.py | 21 +++-- django_quickbooks/settings.py | 3 + django_quickbooks/signals/__init__.py | 12 +-- django_quickbooks/signals/customer.py | 8 +- django_quickbooks/signals/invoice.py | 8 +- django_quickbooks/signals/qbd_task.py | 4 +- django_quickbooks/validators.py | 87 ++++++++++++++------ docs/Makefile | 20 +++++ docs/make.bat | 35 ++++++++ docs/source/conf.py | 55 +++++++++++++ docs/source/index.rst | 20 +++++ requirements_docs.txt | 2 + setup.cfg | 3 +- 23 files changed, 375 insertions(+), 101 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 django_quickbooks/decorators.py create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 requirements_docs.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fb1cf18 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 3ea618f..1a6be90 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is an ongoing project to integrate any Django project with Quickbooks Deskt integration support for Python 3.6+ and Django 2.0+ -| Version: | 0.5.1 | +| Version: | 0.6.0 | |-----------|--------------------------------------------------------------------------------------------------------------------| | Download: | https://pypi.org/project/django-quickbooks/ | | Source: | https://github.com/weltlink/django-quickbooks/ | @@ -56,6 +56,10 @@ integration support for Python 3.6+ and Django 2.0+ 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) diff --git a/django_quickbooks/decorators.py b/django_quickbooks/decorators.py new file mode 100644 index 0000000..2d23a37 --- /dev/null +++ b/django_quickbooks/decorators.py @@ -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 diff --git a/django_quickbooks/exceptions.py b/django_quickbooks/exceptions.py index fa9bdd3..1e2090d 100644 --- a/django_quickbooks/exceptions.py +++ b/django_quickbooks/exceptions.py @@ -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 diff --git a/django_quickbooks/models.py b/django_quickbooks/models.py index 48ea779..aef3619 100644 --- a/django_quickbooks/models.py +++ b/django_quickbooks/models.py @@ -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) @@ -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') diff --git a/django_quickbooks/objects/base.py b/django_quickbooks/objects/base.py index 86ff484..76a6951 100644 --- a/django_quickbooks/objects/base.py +++ b/django_quickbooks/objects/base.py @@ -1,3 +1,4 @@ +import logging from abc import ABC from django_quickbooks import QUICKBOOKS_ENUMS @@ -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): diff --git a/django_quickbooks/processors/base.py b/django_quickbooks/processors/base.py index dc6e95f..586b80d 100644 --- a/django_quickbooks/processors/base.py +++ b/django_quickbooks/processors/base.py @@ -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 @@ -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 @@ -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') @@ -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 ' \ @@ -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() diff --git a/django_quickbooks/processors/customer.py b/django_quickbooks/processors/customer.py index 6eccdab..9f08677 100644 --- a/django_quickbooks/processors/customer.py +++ b/django_quickbooks/processors/customer.py @@ -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): @@ -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): @@ -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): diff --git a/django_quickbooks/processors/invoice.py b/django_quickbooks/processors/invoice.py index 30ab2e8..e4f386b 100644 --- a/django_quickbooks/processors/invoice.py +++ b/django_quickbooks/processors/invoice.py @@ -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 @@ -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): @@ -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: diff --git a/django_quickbooks/processors/item_service.py b/django_quickbooks/processors/item_service.py index 6495c5d..2b51819 100644 --- a/django_quickbooks/processors/item_service.py +++ b/django_quickbooks/processors/item_service.py @@ -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 diff --git a/django_quickbooks/session_manager.py b/django_quickbooks/session_manager.py index ebe946f..108566b 100644 --- a/django_quickbooks/session_manager.py +++ b/django_quickbooks/session_manager.py @@ -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 @@ -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() @@ -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) diff --git a/django_quickbooks/settings.py b/django_quickbooks/settings.py index 179afb9..eaea123 100644 --- a/django_quickbooks/settings.py +++ b/django_quickbooks/settings.py @@ -13,6 +13,8 @@ 'REALM_MODEL_CLASS': 'django_quickbooks.models.Realm', 'REALM_SESSION_MODEL_CLASS': 'django_quickbooks.models.RealmSession', 'QBD_TASK_MODEL_CLASS': 'django_quickbooks.models.QBDTask', + + 'REALM_CONNECTION_DECORATOR': 'django_quickbooks.decorators.base_realm_tenant_connection', 'RESPONSE_PROCESSORS': ( 'django_quickbooks.processors.CustomerQueryResponseProcessor', @@ -49,6 +51,7 @@ 'REALM_MODEL_CLASS', 'REALM_SESSION_MODEL_CLASS', 'QBD_TASK_MODEL_CLASS', + 'REALM_CONNECTION_DECORATOR', ) diff --git a/django_quickbooks/signals/__init__.py b/django_quickbooks/signals/__init__.py index ce60309..c58ef10 100644 --- a/django_quickbooks/signals/__init__.py +++ b/django_quickbooks/signals/__init__.py @@ -5,15 +5,15 @@ "qb_resource", "object_id", "content_type", - "schema_name", + "realm_id", "instance", ]) -customer_created = Signal(providing_args=["qbd_model_mixin_obj", "schema_name"]) -customer_updated = Signal(providing_args=["qbd_model_mixin_obj", "schema_name"]) -invoice_created = Signal(providing_args=["qbd_model_mixin_obj", "schema_name"]) -invoice_updated = Signal(providing_args=["qbd_model_mixin_obj", "schema_name"]) -qbd_first_time_connected = Signal(providing_args=["schema_name"]) +customer_created = Signal(providing_args=["qbd_model_mixin_obj", "realm_id"]) +customer_updated = Signal(providing_args=["qbd_model_mixin_obj", "realm_id"]) +invoice_created = Signal(providing_args=["qbd_model_mixin_obj", "realm_id"]) +invoice_updated = Signal(providing_args=["qbd_model_mixin_obj", "realm_id"]) +qbd_first_time_connected = Signal(providing_args=["realm_id"]) from django_quickbooks.signals.customer import * from django_quickbooks.signals.invoice import * diff --git a/django_quickbooks/signals/customer.py b/django_quickbooks/signals/customer.py index c5b2ebe..16bc149 100644 --- a/django_quickbooks/signals/customer.py +++ b/django_quickbooks/signals/customer.py @@ -8,24 +8,24 @@ @receiver(customer_created) -def create_qbd_customer(sender, qbd_model_mixin_obj, schema_name, *args, **kwargs): +def create_qbd_customer(sender, qbd_model_mixin_obj, realm_id, *args, **kwargs): qbd_task_create.send( sender=qbd_model_mixin_obj.__class__, qb_operation=QUICKBOOKS_ENUMS.OPP_ADD, qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER, object_id=qbd_model_mixin_obj.id, content_type=ContentType.objects.get_for_model(qbd_model_mixin_obj), - schema_name=schema_name, + realm_id=realm_id, ) @receiver(customer_updated) -def update_qbd_customer(sender, qbd_model_mixin_obj, schema_name, *args, **kwargs): +def update_qbd_customer(sender, qbd_model_mixin_obj, realm_id, *args, **kwargs): qbd_task_create.send( sender=qbd_model_mixin_obj.__class__, qb_operation=QUICKBOOKS_ENUMS.OPP_MOD, qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER, object_id=qbd_model_mixin_obj.id, content_type=ContentType.objects.get_for_model(qbd_model_mixin_obj), - schema_name=schema_name, + realm_id=realm_id, ) diff --git a/django_quickbooks/signals/invoice.py b/django_quickbooks/signals/invoice.py index 7dc0cce..3771060 100644 --- a/django_quickbooks/signals/invoice.py +++ b/django_quickbooks/signals/invoice.py @@ -8,24 +8,24 @@ @receiver(invoice_created) -def create_qbd_invoice(sender, qbd_model_mixin_obj, schema_name, *args, **kwargs): +def create_qbd_invoice(sender, qbd_model_mixin_obj, realm_id, *args, **kwargs): qbd_task_create.send( sender=qbd_model_mixin_obj.__class__, qb_operation=QUICKBOOKS_ENUMS.OPP_ADD, qb_resource=QUICKBOOKS_ENUMS.RESOURCE_INVOICE, object_id=qbd_model_mixin_obj.id, content_type=ContentType.objects.get_for_model(qbd_model_mixin_obj), - schema_name=schema_name, + realm_id=realm_id, ) @receiver(invoice_updated) -def update_qbd_invoice(sender, qbd_model_mixin_obj, schema_name, *args, **kwargs): +def update_qbd_invoice(sender, qbd_model_mixin_obj, realm_id, *args, **kwargs): qbd_task_create.send( sender=qbd_model_mixin_obj.__class__, qb_operation=QUICKBOOKS_ENUMS.OPP_MOD, qb_resource=QUICKBOOKS_ENUMS.RESOURCE_INVOICE, object_id=qbd_model_mixin_obj.id, content_type=ContentType.objects.get_for_model(qbd_model_mixin_obj), - schema_name=schema_name, + realm_id=realm_id, ) diff --git a/django_quickbooks/signals/qbd_task.py b/django_quickbooks/signals/qbd_task.py index 3601833..d8dcd06 100644 --- a/django_quickbooks/signals/qbd_task.py +++ b/django_quickbooks/signals/qbd_task.py @@ -9,9 +9,9 @@ @receiver(qbd_task_create) -def create_qbd_task(sender, qb_operation, qb_resource, object_id, content_type, schema_name, instance=None, *args, **kwargs): +def create_qbd_task(sender, qb_operation, qb_resource, object_id, content_type, realm_id, instance=None, *args, **kwargs): try: - realm = RealmModel.objects.get(schema_name=schema_name) + realm = RealmModel.objects.get(id=realm_id) data = dict( qb_resource=qb_resource, object_id=object_id, diff --git a/django_quickbooks/validators.py b/django_quickbooks/validators.py index 522289d..160ccbd 100644 --- a/django_quickbooks/validators.py +++ b/django_quickbooks/validators.py @@ -1,7 +1,6 @@ -from itertools import starmap - from django_quickbooks import QUICKBOOKS_ENUMS -from django_quickbooks.exceptions import ValidationOptionNotFound +from django_quickbooks.exceptions import VALIDATION_MESSAGES, ValidationCode +from django_quickbooks.exceptions import ValidationOptionNotFound, ValidationError def obj_type_validator(value): @@ -30,35 +29,60 @@ def operation_type(value): def is_list(value): - return isinstance(value, list) + if not isinstance(value, list): + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), str), + ValidationCode.INVALID_TYPE) def str_type_validator(value): - return isinstance(value, str) + if not isinstance(value, str): + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), str), + ValidationCode.INVALID_TYPE) def es_type_validator(value): - return isinstance(value, str) and value.isnumeric() + if not isinstance(value, str) or not value.isnumeric(): + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), str), + ValidationCode.INVALID_TYPE) def id_type_validator(value): - return str_type_validator(value) + if not isinstance(value, str): + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), str), + ValidationCode.INVALID_TYPE) def bool_type_validator(value): - return value in [1, 0, 'true', 'false', '1', '0'] + if value not in [1, 0, 'true', 'false', '1', '0']: + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), bool), + ValidationCode.INVALID_TYPE) def min_length_validator(value, length): - return len(value) >= length + if len(value) < length: + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.MIN_LENGTH] % length, ValidationCode.MIN_LENGTH) def max_length_validator(value, length): - return len(value) <= length + if len(value) > length: + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.MAX_LENGTH] % length, ValidationCode.MAX_LENGTH) def float_type_validator(value): - return isinstance(value, float) + if not isinstance(value, float): + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), float), + ValidationCode.INVALID_TYPE) + + +def required_validator(value, required=False): + if not value and required: + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.REQUIRED], ValidationCode.REQUIRED) + + +def many_validator(value, many=False): + if not isinstance(value, list) and many: + raise ValidationError(VALIDATION_MESSAGES[ValidationCode.INVALID_TYPE] % (type(value), list), + ValidationCode.INVALID_TYPE) class SchemeValidator: @@ -82,32 +106,49 @@ class SchemeValidator: max_length=max_length_validator, ) - def validate(self, value, **options): - required = options.pop('required', False) + def validate(self, field_name, value, **options): + errors = [] - if not value and not required: + required = options.pop('required', False) - return True + try: + required_validator(value, required) + except ValidationError as exc: + errors.append(exc.detail) many = options.pop('many', False) - if many and not isinstance(value, list): + try: # should be given list type but given something else - return False + many_validator(value, many) + except ValidationError as exc: + errors.append(exc.detail) if many: - return all([self.validate(single_value, **options) for single_value in value]) + for single_value in value: + try: + self.validate(single_value, **options) + except ValidationError as exc: + errors.append(exc.detail) + + if errors: + raise ValidationError(errors, field_name) validator = options.pop('validator') typ = validator['type'] - if not self.type_validators[typ](value): - return False + try: + self.type_validators[typ](value) + except ValidationError as exc: + errors.append(exc.detail) for value, key in options.items(): if value not in self.option_validators: raise ValidationOptionNotFound - if not self.option_validators[value](key): - return False + try: + self.option_validators[value](key) + except ValidationError as exc: + errors.append(exc.detail) - return True + if errors: + raise ValidationError(errors, field_name) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..9534b01 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..851bfda --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,55 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'django-quickbooks' +copyright = '2020, Bedilbek Khamidov' +author = 'Bedilbek Khamidov' + +# The full version, including alpha/beta/rc tags +release = '0.6.0' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..69744e3 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. django-quickbooks documentation master file, created by + sphinx-quickstart on Tue Feb 11 15:07:01 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +django-quickbooks documentation +============================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..a246d48 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,2 @@ +Sphinx==2.4.0 + diff --git a/setup.cfg b/setup.cfg index 0ec0300..8e2a49d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = django-quickbooks -version = 0.5.1 +version = 0.6.0 description = A Django app to integrate with quickbooks. url = https://github.com/weltlink/django-quickbooks author = Bedilbek Khamidov @@ -34,3 +34,4 @@ install_requires = [options.extras_require] rabbit = pika>=1.1.0 celery = celery==4.3.0 +tenant = django-tenant-schemas==1.10.0