├── .editorconfig ├── .flake8 ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .isort.cfg ├── .travis.yml ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── VERSION ├── django_snow ├── __init__.py ├── apps.py ├── helpers │ ├── __init__.py │ ├── exceptions.py │ └── snow_request_handler.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_changemgmt_add_createtime_closetime.py │ ├── 0003_auto_20190607_1500.py │ └── __init__.py └── models.py ├── runtests.py ├── setup.cfg ├── setup.py ├── testapp ├── __init__.py ├── settings.py └── tests.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | tab_width = 4 13 | # Width of Github's online editor/diff display 14 | max_line_length = 119 15 | 16 | # Use 2 spaces for the HTML files 17 | [*.html] 18 | indent_size = 2 19 | 20 | # The JSON files contain newlines inconsistently 21 | [*.json] 22 | indent_size = 2 23 | insert_final_newline = ignore 24 | 25 | # Minified JavaScript files shouldn't be changed 26 | [**.min.js] 27 | indent_style = ignore 28 | insert_final_newline = ignore 29 | 30 | # Makefiles always use tabs for indentation 31 | [Makefile] 32 | indent_style = tab 33 | 34 | # Batch files use tabs for indentation 35 | [*.bat] 36 | indent_style = tab 37 | 38 | [*.md] 39 | trim_trailing_whitespace = false 40 | 41 | [*.yml] 42 | indent_size = 2 43 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=119 3 | exclude=.eggs,.tox,migrations,.venv,.coverage 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | skip=.tox,migrations,docs 3 | atomic=true 4 | multi_line_output=5 5 | known_standard_library= 6 | known_third_party= 7 | known_first_party=django_snow 8 | line_length=119 9 | lines_after_imports=2 10 | default_section=THIRDPARTY 11 | not_skip=__init__.py 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | dist: xenial 4 | cache: pip 5 | python: 6 | - 2.7 7 | - 3.4 8 | - 3.5 9 | - 3.6 10 | - 3.7 11 | env: 12 | - DJANGO=1.8 13 | - DJANGO=1.9 14 | - DJANGO=1.10 15 | - DJANGO=1.11 16 | - DJANGO=2.0 17 | - DJANGO=2.1 18 | matrix: 19 | exclude: 20 | - python: 2.7 21 | env: DJANGO=2.0 22 | - python: 3.6 23 | env: DJANGO=1.8 24 | - python: 3.6 25 | env: DJANGO=1.9 26 | - python: 3.6 27 | env: DJANGO=1.10 28 | - python: 3.7 29 | env: DJANGO=1.8 30 | - python: 3.7 31 | env: DJANGO=1.9 32 | - python: 3.7 33 | env: DJANGO=1.10 34 | fast_finish: true 35 | include: 36 | - python: 3.6 37 | env: TOXENV=flake8 38 | install: pip install tox-travis codecov 39 | script: tox 40 | after_success: coverage combine && codecov 41 | branches: 42 | only: 43 | - master 44 | - /^v?\d+\.\d+(\.\d+)?(-\S*)?$/ 45 | deploy: 46 | provider: pypi 47 | user: jwilhelm 48 | password: 49 | secure: LzZk5DWafy6kZR//EH6iFsozmXVyAu86uuLKg0A82rLJBEV/ajLsbcLIk/G2FQdELs+s//6+v34r9PxeUbYE6xcb7hL+JF/dNotDnWG/AoCDD+xfHQqsIXUUnDbkTzm2a/d/QnC+3di6rnHzJlRSPX2wT2w6pHcQGbSE2bwOjHUe+apNWy7mImG9dF43nBEy16ihD2enEF+GVBe4SHl6eI9neqJWlHIUbMPG2LR/Nuo19AQCNfs9cFVwCNXWEhK+DY0PvMW/xnnYb/QxQcmgd9HtjC+I4ahj2Ggx0xkFaOjVmcIjFkxQj/ZQUxhYm0vJjrP43vSNUmzgfBdITnRvvJfS7bz+NZX9gx058XaazzyX7BWA/kSdJUq3DpoeTCaYzQrzi/DKgf0ox7jfQx9HvwMCSkel6Xf0K5YM3kylW/EhANbXDI4irtHUoU0BVQbXGKITgRtGbNL4zNUq60LwM7jpgFTVlzl+Fz5EAGqpVTuZtKiaPRNfYJorrQgtGG7ekFMDd1LP9UZ5hKTUAO7m5IPAcnUcLTT6BEXKbQSCz+kRj+F1nwZH6E4K90FKMaW/2opt2v5kyXsgli5ysrKmwfcXyhntxzr2eLTg75tMoJW1w5b7EJ7UY8b9JUAGiCVQCheFAIGaoL7UwBb0C1DHUm3l4nOaTlYwmaDOivT5jS8= 50 | on: 51 | tags: true 52 | python: 3.6 53 | condition: $TOXENV = flake8 54 | distributions: sdist bdist_wheel 55 | repo: godaddy/django-snow 56 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 1.2.0 - Mon Jan 29, 2018 2 | - Fixed #1: Store the open and close time for a CO 3 | - Enhancements to the way the SNow API is utilized 4 | 5 | Version 1.1.0 - Thu Nov 16, 2017 6 | - Updated docs 7 | - Add a method to close a CO with errors 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss@godaddy.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | include VERSION 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ================= 3 | 4 | .. image:: http://unmaintained.tech/badge.svg 5 | :target: http://unmaintained.tech 6 | :alt: No Maintenance Intended 7 | 8 | .. image:: https://img.shields.io/pypi/v/django-snow.svg 9 | :target: https://pypi.python.org/pypi/django-snow 10 | :alt: Latest Version 11 | 12 | .. image:: https://travis-ci.org/godaddy/django-snow.svg?branch=master 13 | :target: https://travis-ci.org/godaddy/django-snow 14 | :alt: Test/build status 15 | 16 | .. image:: https://codecov.io/gh/godaddy/django-snow/branch/master/graph/badge.svg 17 | :target: https://codecov.io/gh/godaddy/django-snow 18 | :alt: Code coverage 19 | 20 | **django-snow** is a django app to manage ServiceNow tickets from within a django project. 21 | 22 | This project is no longer maintained, and is welcoming new maintainers. If you would like to take over development of this project, please contact oss@godaddy.com. 23 | 24 | 25 | Installation 26 | ============ 27 | 28 | :: 29 | 30 | pip install django-snow 31 | 32 | Configuration 33 | ============= 34 | **django-snow** requires the following settings to be set in your Django settings: 35 | 36 | * ``SNOW_INSTANCE`` - The ServiceNow instance where the tickets should be created 37 | * ``SNOW_API_USER`` - The ServiceNow API User 38 | * ``SNOW_API_PASS`` - The ServiceNow API User's Password 39 | * ``SNOW_ASSIGNMENT_GROUP`` (Optional) - The group to which the tickets should be assigned. 40 | If this is not provided, each call to create the tickets should be provided with an `assignment_group` argument. 41 | See the API documentation for more details 42 | * ``SNOW_DEFAULT_CHANGE_TYPE`` (Optional) - Default Change Request Type. If not provided, 43 | `standard` will considered as the default type. 44 | 45 | Usage 46 | ===== 47 | 48 | Creation 49 | -------- 50 | ``ChangeRequestHandler.create_change_request`` has the following parameters and return value: 51 | 52 | **Parameters** 53 | 54 | * ``title`` - The title of the change request 55 | * ``description`` - The description of the change request 56 | * ``assignment_group`` - The group to which the change request is to be assigned. 57 | This is **optional** if ``SNOW_ASSIGNMENT_GROUP`` django settings is available, else, it is **mandatory** 58 | * ``payload`` (Optional) - The payload for creating the Change Request. 59 | 60 | **Returns** 61 | 62 | ``ChangeRequest`` model - The model created from the created Change Order. 63 | 64 | **Example** 65 | 66 | .. code-block:: python 67 | 68 | from django_snow.helpers import ChangeRequestHandler 69 | 70 | def change_data(self): 71 | co_handler = ChangeRequestHandler() 72 | change_request = co_handler.create_change_request('Title', 'Description', 'assignment_group') 73 | 74 | 75 | Updating 76 | -------- 77 | ``ChangeRequestHandler.update_change_request`` method signature: 78 | 79 | **Parameters** 80 | 81 | * ``change_request`` - The ``ChangeRequest`` Model 82 | * ``payload`` - The payload to pass to the ServiceNow REST API. 83 | 84 | **Example** 85 | 86 | .. code-block:: python 87 | 88 | from django_snow.models import ChangeRequest 89 | from django_snow.helpers import ChangeRequestHandler 90 | 91 | def change_data(self): 92 | change_request = ChangeRequest.objects.filter(...) 93 | co_handler = ChangeRequestHandler() 94 | 95 | payload = { 96 | 'description': 'updated description', 97 | 'state': ChangeRequest.TICKET_STATE_IN_PROGRESS 98 | } 99 | 100 | co_handler.update_change_request(change_request, payload) 101 | 102 | 103 | Closing 104 | ------- 105 | ``ChangeRequestHandler.close_change_request`` has the following signature: 106 | 107 | **Parameters** 108 | 109 | * ``change_request`` - The ``ChangeRequest`` Model representing the Change Order to be closed. 110 | 111 | **Example** 112 | 113 | .. code-block:: python 114 | 115 | from django_snow.models import ChangeRequest 116 | from django_snow.helpers import ChangeRequestHandler 117 | 118 | def change_data(self): 119 | change_request = ChangeRequest.objects.filter(...) 120 | co_handler = ChangeRequestHandler() 121 | 122 | co_handler.close_change_request(change_request) 123 | 124 | Closing with error 125 | ------------------ 126 | ``ChangeRequestHandler.close_change_request_with_error`` method signature: 127 | 128 | **Parameters** 129 | 130 | * ``change_request`` - The ``ChangeRequest`` Model representing the Change Order to be closed with error 131 | * ``payload`` - The payload to pass to the ServiceNow REST API. 132 | 133 | **Example** 134 | 135 | .. code-block:: python 136 | 137 | from django_snow.models import ChangeRequest 138 | from django_snow.helpers import ChangeRequestHandler 139 | 140 | def change_data(self): 141 | change_request = ChangeRequest.objects.filter(...) 142 | co_handler = ChangeRequestHandler() 143 | 144 | payload = { 145 | 'description': 'updated description', 146 | 'title': 'foo' 147 | } 148 | 149 | co_handler.close_change_request_with_error(change_request, payload) 150 | 151 | Models 152 | ====== 153 | 154 | ChangeRequest 155 | ------------- 156 | The ``ChangeRequest`` model has the following attributes: 157 | 158 | * ``sys_id`` - The sys_id of the Change Request. 159 | * ``number`` - Change Request Number. 160 | * ``title`` - The title of the Change Request a.k.a short_description. 161 | * ``description`` - Description for the change request 162 | * ``assignment_group_guid`` - The GUID of the group to which the Change Request is assigned to 163 | * ``state`` - The State of the Change Request. Can be any one of the following ``ChangeRequest``'s constants: 164 | 165 | * ``TICKET_STATE_OPEN`` - '1' 166 | * ``TICKET_STATE_IN_PROGRESS`` - '2' 167 | * ``TICKET_STATE_COMPLETE`` - '3' 168 | * ``TICKET_STATE_COMPLETE_WITH_ERRORS`` - '4' 169 | 170 | 171 | Supported Ticket Types 172 | ====================== 173 | * Change Requests 174 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /django_snow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/django-snow/f5a0e238d9289ac56e4df1c49601b11157b0b725/django_snow/__init__.py -------------------------------------------------------------------------------- /django_snow/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ServiceNow(AppConfig): 5 | name = 'service-now' 6 | verbose_name = 'ServiceNow' 7 | 8 | def ready(self): 9 | pass 10 | -------------------------------------------------------------------------------- /django_snow/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .snow_request_handler import ChangeRequestHandler 2 | 3 | 4 | __all__ = [ 5 | 'ChangeRequestHandler' 6 | ] 7 | -------------------------------------------------------------------------------- /django_snow/helpers/exceptions.py: -------------------------------------------------------------------------------- 1 | class ChangeRequestException(Exception): 2 | """ 3 | Errors occurred during Change Request CRUD operations 4 | """ 5 | pass 6 | -------------------------------------------------------------------------------- /django_snow/helpers/snow_request_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pysnow 4 | from django.conf import settings 5 | from django.utils import timezone 6 | from requests.exceptions import HTTPError 7 | 8 | from ..models import ChangeRequest 9 | from .exceptions import ChangeRequestException 10 | 11 | 12 | logger = logging.getLogger('django_snow') 13 | 14 | 15 | class ChangeRequestHandler: 16 | """ 17 | SNow Change Request Handler. 18 | """ 19 | 20 | group_guid_dict = {} 21 | 22 | # Service Now table REST endpoints 23 | CHANGE_REQUEST_TABLE_PATH = '/table/change_request' 24 | USER_GROUP_TABLE_PATH = '/table/sys_user_group' 25 | 26 | def __init__(self): 27 | self._client = None 28 | self.snow_instance = settings.SNOW_INSTANCE 29 | self.snow_api_user = settings.SNOW_API_USER 30 | self.snow_api_pass = settings.SNOW_API_PASS 31 | self.snow_assignment_group = getattr(settings, 'SNOW_ASSIGNMENT_GROUP', None) 32 | self.snow_default_cr_type = getattr(settings, 'SNOW_DEFAULT_CHANGE_TYPE', 'standard') 33 | 34 | def create_change_request(self, title, description, assignment_group=None, payload=None): 35 | """ 36 | Create a change request with the given payload. 37 | """ 38 | client = self._get_client() 39 | change_requests = client.resource(api_path=self.CHANGE_REQUEST_TABLE_PATH) 40 | payload = payload or {} 41 | payload['short_description'] = title 42 | payload['description'] = description 43 | 44 | if 'type' not in payload: 45 | payload['type'] = self.snow_default_cr_type 46 | if 'assignment_group' not in payload: 47 | payload['assignment_group'] = self.get_snow_group_guid(assignment_group or self.snow_assignment_group) 48 | 49 | try: 50 | result = change_requests.create(payload=payload) 51 | except HTTPError as e: 52 | logger.error('Could not create change request due to %s', e.response.text) 53 | raise ChangeRequestException('Could not create change request due to %s.' % e.response.text) 54 | 55 | # This piece of code is for legacy SNow instances. (probably Geneva and before it) 56 | if 'error' in result: 57 | logger.error('Could not create change request due to %s', result['error']) 58 | raise ChangeRequestException('Could not create change request due to %s' % result['error']) 59 | 60 | change_request = ChangeRequest.objects.create( 61 | sys_id=result['sys_id'], 62 | number=result['number'], 63 | title=result['short_description'], 64 | description=result['description'], 65 | assignment_group_guid=result['assignment_group']['value'], 66 | state=result['state'] 67 | ) 68 | 69 | return change_request 70 | 71 | def close_change_request(self, change_request): 72 | """Mark the change request as completed.""" 73 | 74 | payload = {'state': ChangeRequest.TICKET_STATE_COMPLETE} 75 | change_request.closed_time = timezone.now() 76 | self.update_change_request(change_request, payload) 77 | 78 | def close_change_request_with_error(self, change_request, payload): 79 | """Mark the change request as completed with error. 80 | 81 | The possible keys for the payload are: 82 | * `title` 83 | * `description` 84 | 85 | :param change_request: The change request to be closed 86 | :type change_request: :class:`django_snow.models.ChangeRequest` 87 | :param payload: A dict of data to be updated while closing change request 88 | :type payload: dict 89 | """ 90 | payload['state'] = ChangeRequest.TICKET_STATE_COMPLETE_WITH_ERRORS 91 | change_request.closed_time = timezone.now() 92 | self.update_change_request(change_request, payload) 93 | 94 | def update_change_request(self, change_request, payload): 95 | """Update the change request with the data from the kwargs. 96 | 97 | The possible keys for the payload are: 98 | * `title` 99 | * `description` 100 | * `state` 101 | 102 | :param change_request: The change request to be updated 103 | :type change_request: :class:`django_snow.models.ChangeRequest` 104 | :param payload: A dict of data to be updated while updating the change request 105 | :type payload: dict 106 | """ 107 | client = self._get_client() 108 | 109 | # Get the record and update it 110 | change_requests = client.resource(api_path=self.CHANGE_REQUEST_TABLE_PATH) 111 | 112 | try: 113 | result = change_requests.update(query={'sys_id': change_request.sys_id.hex}, payload=payload) 114 | except HTTPError as e: 115 | logger.error('Could not update change request due to %s', e.response.text) 116 | raise ChangeRequestException('Could not update change request due to %s' % e.response.text) 117 | 118 | # This piece of code is for legacy SNow instances. (probably Geneva and before it) 119 | if 'error' in result: 120 | logger.error('Could not update change request due to %s', result['error']) 121 | raise ChangeRequestException('Could not update change request due to %s' % result['error']) 122 | 123 | change_request.state = result['state'] 124 | change_request.title = result['short_description'] 125 | change_request.description = result['description'] 126 | change_request.assignment_group_guid = result['assignment_group']['value'] 127 | change_request.save() 128 | 129 | return result 130 | 131 | def _get_client(self): 132 | if self._client is None: 133 | self._client = pysnow.Client( 134 | instance=self.snow_instance, user=self.snow_api_user, password=self.snow_api_pass 135 | ) 136 | return self._client 137 | 138 | def get_snow_group_guid(self, group_name): 139 | """ 140 | Get the SNow Group's GUID from the Group Name 141 | """ 142 | 143 | if group_name not in self.group_guid_dict: 144 | client = self._get_client() 145 | user_groups = client.resource(api_path=self.USER_GROUP_TABLE_PATH) 146 | response = user_groups.get(query={'name': group_name}) 147 | result = response.one() 148 | self.group_guid_dict[group_name] = result['sys_id'] 149 | 150 | return self.group_guid_dict[group_name] 151 | 152 | def clear_group_guid_cache(self): 153 | """ 154 | Clear the SNow Group Name - GUID cache. 155 | """ 156 | self.group_guid_dict.clear() 157 | -------------------------------------------------------------------------------- /django_snow/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-08-08 00:06 3 | from __future__ import unicode_literals 4 | 5 | import django.core.validators 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ChangeRequest', 19 | fields=[ 20 | ('sys_id', models.UUIDField(primary_key=True, serialize=False)), 21 | ('number', models.CharField(help_text='The Change Order number', max_length=32)), 22 | ('title', models.CharField(help_text='Title of the ServiceNow Change Request', max_length=160)), 23 | ('description', models.TextField(help_text='Description of the ServiceNow Change Request', validators=[django.core.validators.MaxLengthValidator(4000)])), 24 | ('assignment_group_guid', models.UUIDField()), 25 | ('state', models.CharField(choices=[('1', 'Open'), ('2', 'In Progress'), ('3', 'Complete'), ('4', 'Complete With Errors')], help_text='The current state the the change order is in.', max_length=1)), 26 | ], 27 | options={ 28 | 'verbose_name': 'service-now change request', 29 | 'verbose_name_plural': 'service-now change requests', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /django_snow/migrations/0002_changemgmt_add_createtime_closetime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-12-21 22:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('django_snow', '0001_initial'), 13 | ] 14 | 15 | # TODO state field must be altered to include the latest changes (once finalized). 16 | operations = [ 17 | migrations.AddField( 18 | model_name='changerequest', 19 | name='closed_time', 20 | field=models.DateTimeField(help_text='Timestamp when the Change Request was closed', null=True), 21 | ), 22 | migrations.AddField( 23 | model_name='changerequest', 24 | name='created_time', 25 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, help_text='Timestamp when the Change Request was created'), 26 | preserve_default=False, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /django_snow/migrations/0003_auto_20190607_1500.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.13 on 2019-06-07 22:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_snow', '0002_changemgmt_add_createtime_closetime'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='changerequest', 15 | name='state', 16 | field=models.CharField(choices=[('1', 'Open'), ('2', 'In Progress'), ('3', 'Complete'), ('4', 'Complete With Errors')], help_text='The current state the change order is in.', max_length=3), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_snow/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/django-snow/f5a0e238d9289ac56e4df1c49601b11157b0b725/django_snow/migrations/__init__.py -------------------------------------------------------------------------------- /django_snow/models.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import MaxLengthValidator 2 | from django.db import models 3 | 4 | 5 | class ChangeRequest(models.Model): 6 | """ 7 | SNow Change Request Model Class. 8 | """ 9 | 10 | # TODO: Review the states to be included by default. Some are used only in legacy instances (Geneva and before), 11 | # and some are used only in later instances. 12 | # https://docs.servicenow.com/bundle/kingston-it-service-management/page/product/change-management/task/state-model-activate-tasks.html 13 | 14 | # The state of the Change Request 15 | TICKET_STATE_OPEN = '1' 16 | TICKET_STATE_IN_PROGRESS = '2' 17 | TICKET_STATE_COMPLETE = '3' 18 | TICKET_STATE_COMPLETE_WITH_ERRORS = '4' 19 | TICKET_STATE_CHOICES = ( 20 | (TICKET_STATE_OPEN, 'Open'), 21 | (TICKET_STATE_IN_PROGRESS, 'In Progress'), 22 | (TICKET_STATE_COMPLETE, 'Complete'), 23 | (TICKET_STATE_COMPLETE_WITH_ERRORS, 'Complete With Errors'), 24 | ) 25 | 26 | # The 32 character GUID for a SNow record 27 | sys_id = models.UUIDField( 28 | max_length=32, 29 | primary_key=True 30 | ) 31 | 32 | number = models.CharField( 33 | max_length=32, 34 | help_text="The Change Order number" 35 | ) 36 | 37 | title = models.CharField( 38 | max_length=160, # From the Change Request Title field's maxlength 39 | help_text="Title of the ServiceNow Change Request" 40 | ) 41 | 42 | description = models.TextField( 43 | # From the Change Request Description's data-length attribute 44 | validators=[MaxLengthValidator(4000)], 45 | help_text="Description of the ServiceNow Change Request" 46 | ) 47 | 48 | # The GUID of the Group to which the Ticket was assigned to 49 | assignment_group_guid = models.UUIDField( 50 | max_length=32 51 | ) 52 | 53 | state = models.CharField( 54 | max_length=3, # TODO: Review this decision. 55 | choices=TICKET_STATE_CHOICES, 56 | help_text='The current state the change order is in.' 57 | ) 58 | 59 | # The time at which the Change Request was created. 60 | created_time = models.DateTimeField( 61 | auto_now_add=True, 62 | help_text='Timestamp when the Change Request was created' 63 | ) 64 | 65 | # The time at which the Change Request was closed. 66 | closed_time = models.DateTimeField( 67 | null=True, 68 | help_text='Timestamp when the Change Request was closed' 69 | ) 70 | 71 | def __str__(self): 72 | return self.number 73 | 74 | class Meta: 75 | verbose_name = 'service-now change request' 76 | verbose_name_plural = 'service-now change requests' 77 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import os 4 | import sys 5 | 6 | import django 7 | from django.conf import settings 8 | from django.test.utils import get_runner 9 | 10 | 11 | def runtests(): 12 | logging.disable(logging.CRITICAL) 13 | os.environ['DJANGO_SETTINGS_MODULE'] = 'testapp.settings' 14 | django.setup() 15 | TestRunner = get_runner(settings) # NOQA: N806 16 | test_runner = TestRunner() 17 | failures = test_runner.run_tests(["testapp"]) 18 | sys.exit(bool(failures)) 19 | 20 | 21 | if __name__ == "__main__": 22 | runtests() 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | with open('VERSION', 'r') as vfile: 10 | VERSION = vfile.read().strip() 11 | 12 | with open('README.rst', 'r') as rfile: 13 | README = rfile.read() 14 | 15 | setup( 16 | name='django-snow', 17 | version=VERSION, 18 | author='Pradeep Kumar Rajasekaran', 19 | author_email='prajasekaran@godaddy.com', 20 | license='MIT', 21 | description='Django package for creation of ServiceNow Tickets', 22 | long_description=README, 23 | packages=find_packages(), 24 | include_package_data=True, 25 | url='https://github.com/godaddy/django-snow', 26 | download_url='https://github.com/godaddy/django-snow/archive/master.tar.gz', 27 | install_requires=[ 28 | 'Django>=1.8', 29 | 'pysnow>=0.6.4', 30 | ], 31 | tests_require=[ 32 | 'six', 33 | ], 34 | 35 | classifiers=[ 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Environment :: Web Environment', 38 | 'Intended Audience :: Developers', 39 | 'License :: OSI Approved :: MIT License', 40 | 'Natural Language :: English', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Programming Language :: Python :: 3.5', 48 | 'Programming Language :: Python :: 3.6', 49 | 'Framework :: Django', 50 | 'Framework :: Django :: 1.8', 51 | 'Framework :: Django :: 1.9', 52 | 'Framework :: Django :: 1.10', 53 | 'Framework :: Django :: 1.11', 54 | 'Topic :: Software Development :: Libraries :: Python Modules', 55 | ], 56 | zip_safe=False, 57 | test_suite='runtests.runtests' 58 | ) 59 | -------------------------------------------------------------------------------- /testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/django-snow/f5a0e238d9289ac56e4df1c49601b11157b0b725/testapp/__init__.py -------------------------------------------------------------------------------- /testapp/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'Blah Blah Blah' 2 | DATABASES = { 3 | 'default': { 4 | 'ENGINE': 'django.db.backends.sqlite3', 5 | 'NAME': ':memory:' 6 | } 7 | } 8 | 9 | INSTALLED_APPS = ['django_snow'] 10 | -------------------------------------------------------------------------------- /testapp/tests.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import six 4 | from django.test import TestCase, override_settings 5 | from requests.exceptions import HTTPError 6 | 7 | from django_snow.helpers import ChangeRequestHandler 8 | from django_snow.helpers.exceptions import ChangeRequestException 9 | from django_snow.models import ChangeRequest 10 | 11 | 12 | try: 13 | from unittest import mock 14 | except ImportError: 15 | import mock 16 | 17 | 18 | @override_settings( 19 | SNOW_INSTANCE='devgodaddy', 20 | SNOW_API_USER='snow_user', 21 | SNOW_API_PASS='snow_pass', 22 | SNOW_ASSIGNMENT_GROUP='assignment_group' 23 | ) 24 | @mock.patch('django_snow.helpers.snow_request_handler.pysnow') 25 | class TestChangeRequestHandler(TestCase): 26 | 27 | def setUp(self): 28 | self.mock_pysnow_client = mock.MagicMock() 29 | self.change_request_handler = ChangeRequestHandler() 30 | 31 | def tearDown(self): 32 | self.change_request_handler.clear_group_guid_cache() 33 | 34 | def test__get_client(self, mock_pysnow): 35 | mock_pysnow.Client.return_value = self.mock_pysnow_client 36 | self.assertIs( 37 | self.mock_pysnow_client, 38 | self.change_request_handler._get_client() 39 | ) 40 | 41 | def test__get_client_once_initialized_returns_same_instance(self, mock_pysnow): 42 | mock_pysnow.Client.return_value = self.mock_pysnow_client 43 | self.change_request_handler._get_client() 44 | self.assertIs( 45 | self.mock_pysnow_client, 46 | self.change_request_handler._get_client() 47 | ) 48 | 49 | def test_settings_and_table_name(self, mock_pysnow): 50 | self.assertEqual(self.change_request_handler._client, None) 51 | self.assertEqual(self.change_request_handler.snow_instance, 'devgodaddy') 52 | self.assertEqual(self.change_request_handler.snow_api_user, 'snow_user') 53 | self.assertEqual(self.change_request_handler.snow_api_pass, 'snow_pass') 54 | self.assertEqual(self.change_request_handler.snow_assignment_group, 'assignment_group') 55 | self.assertEqual(self.change_request_handler.snow_default_cr_type, 'standard') 56 | self.assertEqual(self.change_request_handler.CHANGE_REQUEST_TABLE_PATH, '/table/change_request') 57 | self.assertEqual(self.change_request_handler.USER_GROUP_TABLE_PATH, '/table/sys_user_group') 58 | 59 | def test_create_change_request(self, mock_pysnow): 60 | fake_insert_retval = { 61 | 'sys_id': uuid.uuid4(), 62 | 'number': 'CHG0000001', 63 | 'short_description': 'bar', 64 | 'description': 'herp', 65 | 'assignment_group': {'value': uuid.uuid4()}, 66 | 'state': '2' 67 | } 68 | 69 | fake_resource = mock.MagicMock() 70 | fake_resource.create.return_value = fake_insert_retval 71 | 72 | self.mock_pysnow_client.resource.return_value = fake_resource 73 | mock_pysnow.Client.return_value = self.mock_pysnow_client 74 | 75 | co = self.change_request_handler.create_change_request('Title', 'Description', payload={}) 76 | last_co = ChangeRequest.objects.last() 77 | 78 | self.assertEqual(co.pk, last_co.pk) 79 | self.assertEqual(co.sys_id, fake_insert_retval['sys_id']) 80 | self.assertEqual(co.number, fake_insert_retval['number']) 81 | self.assertEqual(co.title, fake_insert_retval['short_description']) 82 | self.assertEqual(co.description, fake_insert_retval['description']) 83 | self.assertEqual(co.assignment_group_guid, fake_insert_retval['assignment_group']['value']) 84 | 85 | def test_create_change_request_parameters(self, mock_pysnow): 86 | expected_payload = { 87 | 'type': 'normal', 88 | 'assignment_group': 'bar', 89 | 'short_description': 'Title', 90 | 'description': 'Description' 91 | } 92 | 93 | fake_insert_retval = { 94 | 'sys_id': uuid.uuid4(), 95 | 'number': 'CHG0000001', 96 | 'short_description': 'Title', 97 | 'description': 'Description', 98 | 'assignment_group': {'value': uuid.uuid4()}, 99 | 'state': '2' 100 | } 101 | 102 | fake_resource = mock.MagicMock() 103 | fake_resource.create.return_value = fake_insert_retval 104 | self.mock_pysnow_client.resource.return_value = fake_resource 105 | mock_pysnow.Client.return_value = self.mock_pysnow_client 106 | 107 | payload = { 108 | 'type': 'normal', 109 | 'assignment_group': 'bar' 110 | } 111 | self.change_request_handler.create_change_request('Title', 'Description', None, payload=payload) 112 | fake_resource.create.assert_called_with(payload=expected_payload) 113 | 114 | def test_create_change_request_default_parameters(self, mock_pysnow): 115 | expected_payload = { 116 | 'short_description': 'Title', 117 | 'description': 'Description', 118 | 'type': 'standard', 119 | 'assignment_group': 'bar' 120 | } 121 | 122 | fake_insert_retval = { 123 | 'sys_id': uuid.uuid4(), 124 | 'number': 'CHG0000001', 125 | 'short_description': 'Title', 126 | 'description': 'Description', 127 | 'assignment_group': {'value': uuid.uuid4()}, 128 | 'state': '2' 129 | } 130 | 131 | fake_resource = mock.MagicMock() 132 | 133 | # For Assignment Group GUID 134 | fake_asgn_group_guid_response = mock.MagicMock() 135 | fake_asgn_group_guid_response.one.return_value = {'sys_id': 'bar'} 136 | fake_resource.get.return_value = fake_asgn_group_guid_response 137 | 138 | fake_resource.create.return_value = fake_insert_retval 139 | self.mock_pysnow_client.resource.return_value = fake_resource 140 | mock_pysnow.Client.return_value = self.mock_pysnow_client 141 | 142 | self.change_request_handler.create_change_request('Title', 'Description', None, payload={}) 143 | fake_resource.create.assert_called_with(payload=expected_payload) 144 | 145 | def test_create_change_request_raises_exception_for_http_error(self, mock_pysnow): 146 | fake_resource = mock.MagicMock() 147 | 148 | fake_exception = HTTPError() 149 | fake_exception.response = mock.MagicMock() 150 | fake_exception.response.text.return_value = 'Foobar' 151 | 152 | fake_resource.create.side_effect = fake_exception 153 | 154 | self.mock_pysnow_client.resource.return_value = fake_resource 155 | mock_pysnow.Client.return_value = self.mock_pysnow_client 156 | 157 | with six.assertRaisesRegex(self, ChangeRequestException, 'Could not create change request due to.*'): 158 | self.change_request_handler.create_change_request('Title', 'Description', None, payload={}) 159 | 160 | def test_create_change_request_raises_exception_when_error_in_result(self, mock_pysnow): 161 | fake_insert_retval = { 162 | 'error': 'some error message' 163 | } 164 | 165 | fake_resource = mock.MagicMock() 166 | fake_resource.create.return_value = fake_insert_retval 167 | 168 | self.mock_pysnow_client.resource.return_value = fake_resource 169 | mock_pysnow.Client.return_value = self.mock_pysnow_client 170 | 171 | with six.assertRaisesRegex(self, ChangeRequestException, 'Could not create change request due to.*'): 172 | self.change_request_handler.create_change_request('Title', 'Description', None, payload={}) 173 | 174 | @mock.patch('django_snow.helpers.snow_request_handler.ChangeRequestHandler.update_change_request') 175 | def test_close_change_request(self, mock_update_request, mock_pysnow): 176 | fake_change_order = mock.MagicMock() 177 | 178 | mock_update_request.return_value = 'foo' 179 | change_request_handler = ChangeRequestHandler() 180 | 181 | change_request_handler.close_change_request(fake_change_order) 182 | 183 | mock_update_request.assert_called_with(fake_change_order, {'state': ChangeRequest.TICKET_STATE_COMPLETE}) 184 | 185 | @mock.patch('django_snow.helpers.snow_request_handler.ChangeRequestHandler.update_change_request') 186 | def test_close_change_request_with_error(self, mock_update_request, mock_pysnow): 187 | fake_change_order = mock.MagicMock() 188 | mock_update_request.return_value = 'foo' 189 | change_request_handler = ChangeRequestHandler() 190 | payload = {'description': 'foo'} 191 | change_request_handler.close_change_request_with_error(fake_change_order, payload) 192 | 193 | mock_update_request.assert_called_with( 194 | fake_change_order, {'state': ChangeRequest.TICKET_STATE_COMPLETE_WITH_ERRORS, 'description': 'foo'} 195 | ) 196 | 197 | def test_update_change_request(self, mock_pysnow): 198 | fake_resource = mock.MagicMock() 199 | fake_change_order = mock.MagicMock() 200 | 201 | retval = { 202 | 'state': ChangeRequest.TICKET_STATE_COMPLETE, 203 | 'short_description': 'Short Description', 204 | 'description': 'Long Description', 205 | 'assignment_group': {'value': uuid.uuid4()} 206 | } 207 | 208 | fake_resource.update.return_value = retval 209 | self.mock_pysnow_client.resource.return_value = fake_resource 210 | mock_pysnow.Client.return_value = self.mock_pysnow_client 211 | 212 | ret_val = self.change_request_handler.update_change_request(fake_change_order, payload='{"foo": "bar"}') 213 | self.assertEqual(fake_change_order.state, ChangeRequest.TICKET_STATE_COMPLETE) 214 | self.assertEqual(fake_change_order.title, retval['short_description']) 215 | self.assertEqual(fake_change_order.description, retval['description']) 216 | self.assertEqual(fake_change_order.assignment_group_guid, retval['assignment_group']['value']) 217 | self.assertEqual(ret_val, retval) 218 | 219 | def test_update_change_request_raises_exception_for_http_error(self, mock_pysnow): 220 | fake_resource = mock.MagicMock() 221 | fake_change_order = mock.MagicMock() 222 | 223 | fake_exception = HTTPError() 224 | fake_exception.response = mock.MagicMock() 225 | fake_exception.response.text.return_value = 'Foobar' 226 | 227 | fake_resource.update.side_effect = fake_exception 228 | 229 | self.mock_pysnow_client.resource.return_value = fake_resource 230 | mock_pysnow.Client.return_value = self.mock_pysnow_client 231 | 232 | with six.assertRaisesRegex(self, ChangeRequestException, 'Could not update change request due to '): 233 | self.change_request_handler.update_change_request(fake_change_order, payload='{"foo": "bar"}') 234 | 235 | def test_update_change_request_raises_exception_for_error_in_result(self, mock_pysnow): 236 | fake_resource = mock.MagicMock() 237 | fake_change_order = mock.MagicMock() 238 | 239 | fake_resource.update.return_value = {'error': '3'} 240 | self.mock_pysnow_client.resource.return_value = fake_resource 241 | mock_pysnow.Client.return_value = self.mock_pysnow_client 242 | 243 | with six.assertRaisesRegex(self, ChangeRequestException, 'Could not update change request due to '): 244 | self.change_request_handler.update_change_request(fake_change_order, payload='{"foo": "bar"}') 245 | 246 | def test_get_snow_group_guid_cached_result(self, mock_pysnow): 247 | fake_resource = mock.MagicMock() 248 | fake_response = mock.MagicMock() 249 | fake_response.one.return_value = {'sys_id': 'bar'} 250 | fake_resource.get.return_value = fake_response 251 | self.mock_pysnow_client.resource.return_value = fake_resource 252 | mock_pysnow.Client.return_value = self.mock_pysnow_client 253 | 254 | self.change_request_handler.get_snow_group_guid('foo') 255 | cached_guid = self.change_request_handler.get_snow_group_guid('foo') 256 | 257 | # resource.get() should be called only once, since the value from previous call should have been cached. 258 | fake_resource.get.assert_called_once_with(query={'name': 'foo'}) 259 | self.assertEqual(cached_guid, 'bar') 260 | 261 | def test_get_snow_group_guid(self, mock_pysnow): 262 | fake_resource = mock.MagicMock() 263 | fake_response = mock.MagicMock() 264 | fake_response.one.return_value = {'sys_id': 'yo'} 265 | fake_resource.get.return_value = fake_response 266 | self.mock_pysnow_client.resource.return_value = fake_resource 267 | mock_pysnow.Client.return_value = self.mock_pysnow_client 268 | 269 | self.assertEqual(self.change_request_handler.get_snow_group_guid('hello'), 'yo') 270 | 271 | def test_clear_snow_group_guid_cache(self, mock_pysnow): 272 | fake_resource = mock.MagicMock() 273 | fake_response = mock.MagicMock() 274 | fake_response.one.return_value = {'sys_id': 'some_id'} 275 | fake_resource.get.return_value = fake_response 276 | self.mock_pysnow_client.resource.return_value = fake_resource 277 | mock_pysnow.Client.return_value = self.mock_pysnow_client 278 | 279 | self.change_request_handler.get_snow_group_guid('hello') 280 | self.change_request_handler.get_snow_group_guid('hello') 281 | self.assertEqual(fake_resource.get.call_count, 1) 282 | 283 | self.change_request_handler.clear_group_guid_cache() 284 | self.change_request_handler.get_snow_group_guid('hello') 285 | self.change_request_handler.get_snow_group_guid('hello') 286 | self.assertEqual(fake_resource.get.call_count, 2) 287 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,34,35}-django18 4 | py{27,34,35}-django{19,110} 5 | py{27,34,35,36,37}-django111 6 | py{34,35,36,37}-django20 7 | py{35,36,37}-django21, 8 | flake8 9 | skip_missing_interpreters = true 10 | 11 | [testenv] 12 | setenv = 13 | PYTHONDONTWRITEBYTECODE=1 14 | PYTHONWARNINGS=once 15 | DJANGO_SETTINGS_MODULE=testapp.settings 16 | COVERAGE_PROCESS_START=.coveragerc 17 | basepython = 18 | py27: python2.7 19 | py34: python3.4 20 | py35: python3.5 21 | py36: python3.6 22 | py37: python3.7 23 | deps = 24 | coverage_pth 25 | pysnow 26 | py27: mock 27 | django18: Django>=1.8,<1.9 28 | django19: Django>=1.9,<1.10 29 | django110: Django>=1.10,<1.11 30 | django111: Django>=1.11,<2.0 31 | django20: Django>=2.0,<2.1 32 | django21: Django>=2.1,<2.2 33 | commands = python setup.py test 34 | 35 | [testenv:flake8] 36 | deps = 37 | flake8 38 | flake8-isort 39 | flake8-polyfill 40 | isort 41 | mccabe 42 | pep8 43 | pep8-naming 44 | pycodestyle 45 | pyflakes 46 | testfixtures 47 | basepython = python3.6 48 | commands = flake8 49 | 50 | [travis] 51 | python = 52 | 2.7: py27 53 | 3.4: py34 54 | 3.5: py35 55 | 3.6: py36, flake8 56 | 3.7: py37 57 | 58 | [travis:env] 59 | DJANGO = 60 | 1.8: django18 61 | 1.9: django19 62 | 1.10: django110 63 | 1.11: django111 64 | 2.0: django20 65 | 2.1: django21 66 | 67 | [coverage:run] 68 | branch = True 69 | source = django_snow 70 | parallel = True 71 | 72 | [coverage:paths] 73 | source = 74 | django_snow 75 | */site-packages 76 | --------------------------------------------------------------------------------