This is an automated email from the ASF dual-hosted git repository.

gjm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bloodhound-core.git


The following commit(s) were added to refs/heads/main by this push:
     new dc29fee  Add .editorconfig and license reporting
dc29fee is described below

commit dc29feeb1ad53261ac5b5e69e862d3635f8a0c49
Author: Gary Martin <g...@apache.org>
AuthorDate: Sat Apr 8 02:08:31 2023 +0100

    Add .editorconfig and license reporting
    
    * Fix license & headers with help from Apache Rat
    * Used black to fix some format inconsistencies
---
 trackers/views.py => .editorconfig        |  14 ++-
 .gitignore                                |   1 +
 bh_core/__init__.py => .rat-ignore        |   3 +
 LICENSE                                   | 202 ++++++++++++++++++++++++++++++
 Makefile                                  |  20 +++
 NOTICE                                    |   5 +
 bh_core/__init__.py                       |   1 -
 bh_core/settings.py                       |  81 ++++++------
 bh_core/urls.py                           |  22 +---
 docker/db/Dockerfile                      |  17 +++
 docker/docker-compose.yaml                |  17 +++
 functional_tests.py                       |  15 ++-
 pom.xml                                   |  40 ++++++
 pyproject.toml                            |  17 +++
 pytest.ini                                |  17 +++
 trackers/api/serializers.py               | 161 +++++++++++++-----------
 trackers/api/tests/test_product_api.py    |  50 ++++----
 trackers/api/tests/test_ticket_api.py     | 120 +++++++-----------
 trackers/api/tests/test_ticket_api_hyp.py |  25 ++--
 trackers/api/urls.py                      |  56 +++++----
 trackers/api/views.py                     |  32 ++---
 trackers/apps.py                          |   4 +-
 trackers/fixtures/empty.yml               |  18 +++
 trackers/models.py                        |  52 ++++----
 trackers/tests/__init__.py                |  16 +++
 trackers/tests/test_models.py             |  24 ++--
 trackers/tests/tests.py                   |   8 +-
 trackers/urls.py                          |   4 +-
 trackers/views.py                         |   2 +-
 29 files changed, 702 insertions(+), 342 deletions(-)

diff --git a/trackers/views.py b/.editorconfig
similarity index 83%
copy from trackers/views.py
copy to .editorconfig
index 0861c17..bc6be01 100644
--- a/trackers/views.py
+++ b/.editorconfig
@@ -15,8 +15,16 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-from django.http import HttpResponse
+root = true
 
+[*]
+end_of_line = lf
+insert_final_newline = true
 
-def home(request):
-    return HttpResponse('<html><title>Bloodhound Trackers</title></html>')
+[*.{html,js,sh}]
+indent_style = space
+indent_size = 2
+
+[*.py]
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
index fd75ab4..17a2fea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ db.sqlite3
 tags
 *.log
 .hypothesis/
+.pytest_cache/
diff --git a/bh_core/__init__.py b/.rat-ignore
similarity index 96%
copy from bh_core/__init__.py
copy to .rat-ignore
index 534df97..aaad8b7 100644
--- a/bh_core/__init__.py
+++ b/.rat-ignore
@@ -15,3 +15,6 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
+**/*.pyc
+README.md
+poetry.lock
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
index c446a69..fe10912 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 DOCKER_BIN := $(shell command -v docker || command -v podman)
 SELENIUM_CONTAINER ?= selenium-server
 
@@ -89,3 +106,6 @@ full-clean: clean-deplock clean-venv clean-db selenium-clean
 
 show-urls: manage-show_urls
 .PHONY: show-urls
+
+rat-report:
+       mvn apache-rat:check
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..9e70de9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache Bloodhound
+Copyright 2012-2023 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/)
diff --git a/bh_core/__init__.py b/bh_core/__init__.py
index 534df97..084b296 100644
--- a/bh_core/__init__.py
+++ b/bh_core/__init__.py
@@ -14,4 +14,3 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-
diff --git a/bh_core/settings.py b/bh_core/settings.py
index a6de119..472583c 100644
--- a/bh_core/settings.py
+++ b/bh_core/settings.py
@@ -41,7 +41,7 @@ BASE_DIR = 
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'zcsm4+ng(*1ct-5ufjreki3d6emagywyn(&$hj8i$2lun*pm&r'
+SECRET_KEY = "zcsm4+ng(*1ct-5ufjreki3d6emagywyn(&$hj8i$2lun*pm&r"
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
@@ -52,56 +52,56 @@ ALLOWED_HOSTS = []
 # Application definition
 
 INSTALLED_APPS = [
-    'trackers.apps.TrackersConfig',
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'rest_framework',
-    'django_extensions',
-    'drf_yasg',
+    "trackers.apps.TrackersConfig",
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "rest_framework",
+    "django_extensions",
+    "drf_yasg",
 ]
 
 MIDDLEWARE = [
-    'django.middleware.security.SecurityMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 ]
 
-ROOT_URLCONF = 'bh_core.urls'
+ROOT_URLCONF = "bh_core.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [],
-        'APP_DIRS': True,
-        'OPTIONS': {
-            'context_processors': [
-                'django.template.context_processors.debug',
-                'django.template.context_processors.request',
-                'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = 'bh_core.wsgi.application'
+WSGI_APPLICATION = "bh_core.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
     }
 }
 
@@ -111,16 +111,16 @@ DATABASES = {
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": 
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 
'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": 
"django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 
'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": 
"django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 
'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": 
"django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -128,9 +128,9 @@ AUTH_PASSWORD_VALIDATORS = [
 # Internationalization
 # https://docs.djangoproject.com/en/2.0/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
 
 USE_I18N = True
 
@@ -142,9 +142,8 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/2.0/howto/static-files/
 
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
 
 REST_FRAMEWORK = {
-    'DEFAULT_PERMISSION_CLASSES': [
-    ],
+    "DEFAULT_PERMISSION_CLASSES": [],
 }
diff --git a/bh_core/urls.py b/bh_core/urls.py
index a285126..59d9419 100644
--- a/bh_core/urls.py
+++ b/bh_core/urls.py
@@ -15,27 +15,13 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-"""bh_core URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
-    https://docs.djangoproject.com/en/2.0/topics/http/urls/
-Examples:
-Function views
-    1. Add an import:  from my_app import views
-    2. Add a URL to urlpatterns:  path('', views.home, name='home')
-Class-based views
-    1. Add an import:  from other_app.views import Home
-    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
-Including another URLconf
-    1. Import the include() function: from django.urls import include, path
-    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
-"""
+"""URL Configuration for bh_core."""
 
 from django.contrib import admin
 from django.urls import include, path
 
 urlpatterns = [
-    path('', include('trackers.urls')),
-    path('api-auth/', include('rest_framework.urls')),
-    path('admin/', admin.site.urls),
+    path("", include("trackers.urls")),
+    path("api-auth/", include("rest_framework.urls")),
+    path("admin/", admin.site.urls),
 ]
diff --git a/docker/db/Dockerfile b/docker/db/Dockerfile
index a0fd219..33976f6 100644
--- a/docker/db/Dockerfile
+++ b/docker/db/Dockerfile
@@ -1,3 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 FROM docker.io/postgres:latest
 ENV LANG en_US.UTF-8
 
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index e00d866..17045aa 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -1,3 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 version: "3.0"
 services:
   db:
diff --git a/functional_tests.py b/functional_tests.py
index 5ba3a99..cfd0872 100644
--- a/functional_tests.py
+++ b/functional_tests.py
@@ -26,8 +26,7 @@ class SeleniumTestCase(unittest.TestCase):
     def setUp(self):
         server = "http://127.0.0.1:4444/wd/hub";
         self.browser = webdriver.Remote(
-            command_executor=server,
-            desired_capabilities=DesiredCapabilities.FIREFOX
+            command_executor=server, 
desired_capabilities=DesiredCapabilities.FIREFOX
         )
         self.browser.implicitly_wait(3)
 
@@ -37,17 +36,17 @@ class SeleniumTestCase(unittest.TestCase):
 
 class HomePageViewTest(SeleniumTestCase):
     def test_user_can_see_homepage(self):
-        self.browser.get('http://localhost:8000')
+        self.browser.get("http://localhost:8000";)
 
-        self.assertIn('Bloodhound', self.browser.title)
+        self.assertIn("Bloodhound", self.browser.title)
 
 
 class ApiHomePageViewTest(SeleniumTestCase):
     def test_user_can_see_api_homepage(self):
-        self.browser.get('http://localhost:8000/api')
+        self.browser.get("http://localhost:8000/api";)
 
-        self.assertIn('Api Root', self.browser.title)
+        self.assertIn("Api Root", self.browser.title)
 
 
-if __name__ == '__main__':
-    unittest.main(warnings='ignore')
+if __name__ == "__main__":
+    unittest.main(warnings="ignore")
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..13ac4ea
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,40 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<project>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.bloodhound-core</groupId>
+  <artifactId>bloodhound-core-license-report</artifactId>
+  <version>1</version>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <version>0.15</version>
+      </plugin>
+    </plugins>
+  </build>
+
+  <properties>
+    <rat.excludesFile>.rat-ignore</rat.excludesFile>
+  </properties>
+</project>
diff --git a/pyproject.toml b/pyproject.toml
index 83135ca..62d6c66 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 [tool.poetry]
 name = "bloodhound-core"
 version = "0.1.0"
diff --git a/pytest.ini b/pytest.ini
index 7cf71fa..bd90fa3 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +1,19 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 [pytest]
 DJANGO_SETTINGS_MODULE = bh_core.settings
diff --git a/trackers/api/serializers.py b/trackers/api/serializers.py
index 703c7b0..8007def 100644
--- a/trackers/api/serializers.py
+++ b/trackers/api/serializers.py
@@ -1,3 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 from django.contrib.auth.models import User, Group
 from django.shortcuts import get_object_or_404
 from rest_framework import serializers
@@ -15,19 +32,19 @@ from functools import partial
 
 def get_self_url(obj, context, obj_type):
     keywords = {
-        'product_prefix': obj.product.prefix,
+        "product_prefix": obj.product.prefix,
     }
-    if obj_type == 'ticket':
-        keywords['product_ticket_id'] = obj.product_ticket_id
-    elif obj_type == 'ticketchange':
-        keywords['time'] = obj.time
+    if obj_type == "ticket":
+        keywords["product_ticket_id"] = obj.product_ticket_id
+    elif obj_type == "ticketchange":
+        keywords["time"] = obj.time
     else:
-        keywords['name'] = obj.name
+        keywords["name"] = obj.name
 
     return reverse(
-        f'product-{obj_type}s-detail',
+        f"product-{obj_type}s-detail",
         kwargs=keywords,
-        request=context['request'],
+        request=context["request"],
     )
 
 
@@ -36,32 +53,30 @@ class 
ProductChildSerializer(serializers.HyperlinkedModelSerializer):
 
     def get_product_url(self, obj):
         keywords = {
-            'prefix': obj.product.prefix,
+            "prefix": obj.product.prefix,
         }
         return reverse(
-            'product-detail',
-            kwargs=keywords,
-            request=self.context['request']
+            "product-detail", kwargs=keywords, request=self.context["request"]
         )
 
     def create(self, validated_data):
-        if 'prefix' not in self.context['view'].kwargs.keys():
-            prefix = self.context['view'].kwargs['product_prefix']
+        if "prefix" not in self.context["view"].kwargs.keys():
+            prefix = self.context["view"].kwargs["product_prefix"]
             product = get_object_or_404(Product.objects.all(), prefix=prefix)
-            validated_data['product'] = product
+            validated_data["product"] = product
         return super().create(validated_data)
 
 
 class UserSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = User
-        fields = ('url', 'username', 'email', 'is_staff')
+        fields = ("url", "username", "email", "is_staff")
 
 
 class GroupSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = Group
-        fields = ('url', 'name')
+        fields = ("url", "name")
 
 
 class TicketChangeSerializer(ProductChildSerializer):
@@ -69,10 +84,10 @@ class TicketChangeSerializer(ProductChildSerializer):
 
     class Meta:
         model = TicketChange
-        fields = ('url', 'time', 'author', 'field', 'oldvalue', 'newvalue')
+        fields = ("url", "time", "author", "field", "oldvalue", "newvalue")
 
     def get_url(self, obj):
-        return get_self_url(obj, self.context, 'ticketchange')
+        return get_self_url(obj, self.context, "ticketchange")
 
 
 class TicketSerializer(ProductChildSerializer):
@@ -81,27 +96,27 @@ class TicketSerializer(ProductChildSerializer):
     class Meta:
         model = Ticket
         fields = (
-            'product_url',
-            'url',
-            'product_ticket_id',
-            'summary',
-            'description',
-            'time',
-            'changetime',
-            'reporter',
-            'owner',
-            'cc',
-            'status',
-            'severity',
-            'priority',
-            'keywords',
+            "product_url",
+            "url",
+            "product_ticket_id",
+            "summary",
+            "description",
+            "time",
+            "changetime",
+            "reporter",
+            "owner",
+            "cc",
+            "status",
+            "severity",
+            "priority",
+            "keywords",
         )
         extra_kwargs = {
-            'product_ticket_id': {'required': False},
+            "product_ticket_id": {"required": False},
         }
 
     def get_url(self, obj):
-        return get_self_url(obj, self.context, 'ticket')
+        return get_self_url(obj, self.context, "ticket")
 
 
 class ComponentSerializer(ProductChildSerializer):
@@ -110,15 +125,15 @@ class ComponentSerializer(ProductChildSerializer):
     class Meta:
         model = Component
         fields = (
-            'product_url',
-            'url',
-            'name',
-            'description',
-            'owner',
+            "product_url",
+            "url",
+            "name",
+            "description",
+            "owner",
         )
 
     def get_url(self, obj):
-        return get_self_url(obj, self.context, 'component')
+        return get_self_url(obj, self.context, "component")
 
 
 class MilestoneSerializer(ProductChildSerializer):
@@ -127,16 +142,16 @@ class MilestoneSerializer(ProductChildSerializer):
     class Meta:
         model = Milestone
         fields = (
-            'product_url',
-            'url',
-            'name',
-            'description',
-            'due',
-            'completed',
+            "product_url",
+            "url",
+            "name",
+            "description",
+            "due",
+            "completed",
         )
 
     def get_url(self, obj):
-        return get_self_url(obj, self.context, 'milestone')
+        return get_self_url(obj, self.context, "milestone")
 
 
 class VersionSerializer(ProductChildSerializer):
@@ -145,57 +160,57 @@ class VersionSerializer(ProductChildSerializer):
     class Meta:
         model = Version
         fields = (
-            'product_url',
-            'url',
-            'name',
-            'description',
-            'time',
+            "product_url",
+            "url",
+            "name",
+            "description",
+            "time",
         )
 
     def get_url(self, obj):
-        return get_self_url(obj, self.context, 'version')
+        return get_self_url(obj, self.context, "version")
 
 
 ProductHyperlinkedModelSerializer = partial(
     serializers.HyperlinkedIdentityField,
-    lookup_field='prefix',
-    lookup_url_kwarg='product_prefix',
+    lookup_field="prefix",
+    lookup_url_kwarg="product_prefix",
 )
 
 
 class ProductSerializer(serializers.HyperlinkedModelSerializer):
     url = serializers.HyperlinkedIdentityField(
-        view_name='product-detail',
-        lookup_field='prefix',
+        view_name="product-detail",
+        lookup_field="prefix",
     )
     tickets_url = ProductHyperlinkedModelSerializer(
-        view_name='product-tickets-list',
+        view_name="product-tickets-list",
     )
     components_url = ProductHyperlinkedModelSerializer(
-        view_name='product-components-list',
+        view_name="product-components-list",
     )
     milestones_url = ProductHyperlinkedModelSerializer(
-        view_name='product-milestones-list',
+        view_name="product-milestones-list",
     )
     versions_url = ProductHyperlinkedModelSerializer(
-        view_name='product-versions-list',
+        view_name="product-versions-list",
     )
 
     ticketchanges_url = ProductHyperlinkedModelSerializer(
-        view_name='product-ticketchanges-list',
+        view_name="product-ticketchanges-list",
     )
 
     class Meta:
         model = Product
         fields = (
-            'url',
-            'prefix',
-            'name',
-            'description',
-            'owner',
-            'tickets_url',
-            'components_url',
-            'milestones_url',
-            'versions_url',
-            'ticketchanges_url',
+            "url",
+            "prefix",
+            "name",
+            "description",
+            "owner",
+            "tickets_url",
+            "components_url",
+            "milestones_url",
+            "versions_url",
+            "ticketchanges_url",
         )
diff --git a/trackers/api/tests/test_product_api.py 
b/trackers/api/tests/test_product_api.py
index 0a43226..f3e3dd5 100644
--- a/trackers/api/tests/test_product_api.py
+++ b/trackers/api/tests/test_product_api.py
@@ -26,26 +26,26 @@ class ProductsApiTest(APITestCase):
     """Test for GET all products API"""
 
     def setUp(self):
-        self.ally = Product.objects.create(prefix='ALY', name='Project Alice')
-        self.bob = Product.objects.create(prefix='BOB', name='Project Robert')
+        self.ally = Product.objects.create(prefix="ALY", name="Project Alice")
+        self.bob = Product.objects.create(prefix="BOB", name="Project Robert")
 
         self.new_product_data = {
-            'prefix': 'CAR',
-            'name': 'Project Caroline',
+            "prefix": "CAR",
+            "name": "Project Caroline",
         }
 
         self.product_data = {
-            'prefix': self.ally.prefix,
-            'name': 'Project Alan',
+            "prefix": self.ally.prefix,
+            "name": "Project Alan",
         }
 
         self.bad_product_data = {
-            'prefix': self.bob.prefix,
-            'name': '',
+            "prefix": self.bob.prefix,
+            "name": "",
         }
 
     def test_get_all_products(self):
-        response = self.client.get(reverse('product-list'))
+        response = self.client.get(reverse("product-list"))
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
@@ -55,35 +55,33 @@ class ProductsApiTest(APITestCase):
 
     def test_get_product(self):
         response = self.client.get(
-            reverse('product-detail', args=[self.ally.prefix]),
+            reverse("product-detail", args=[self.ally.prefix]),
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['prefix'], self.ally.prefix)
-        self.assertEqual(response.data['name'], self.ally.name)
+        self.assertEqual(response.data["prefix"], self.ally.prefix)
+        self.assertEqual(response.data["name"], self.ally.name)
 
     def test_get_invalid_product(self):
-        response = self.client.get(
-            reverse('product-detail', args=['randomnonsense'])
-        )
+        response = self.client.get(reverse("product-detail", 
args=["randomnonsense"]))
 
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_create_product(self):
         response = self.client.post(
-            reverse('product-list'),
+            reverse("product-list"),
             self.new_product_data,
         )
 
-        product = Product.objects.get(prefix=self.new_product_data['prefix'])
+        product = Product.objects.get(prefix=self.new_product_data["prefix"])
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(product.prefix, self.new_product_data['prefix'])
-        self.assertEqual(product.name, self.new_product_data['name'])
+        self.assertEqual(product.prefix, self.new_product_data["prefix"])
+        self.assertEqual(product.name, self.new_product_data["name"])
 
     def test_create_bad_product(self):
         response = self.client.post(
-            reverse('product-list'),
+            reverse("product-list"),
             self.bad_product_data,
         )
 
@@ -91,20 +89,20 @@ class ProductsApiTest(APITestCase):
 
     def test_update_product(self):
         response = self.client.put(
-            reverse('product-detail', args=[self.ally.prefix]),
+            reverse("product-detail", args=[self.ally.prefix]),
             self.product_data,
         )
 
-        product = Product.objects.get(prefix=self.product_data['prefix'])
+        product = Product.objects.get(prefix=self.product_data["prefix"])
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertNotEqual(self.ally.name, product.name)
-        self.assertEqual(self.product_data['prefix'], product.prefix)
-        self.assertEqual(self.product_data['name'], product.name)
+        self.assertEqual(self.product_data["prefix"], product.prefix)
+        self.assertEqual(self.product_data["name"], product.name)
 
     def test_update_product_bad_data(self):
         response = self.client.put(
-            reverse('product-detail', args=[self.bob.prefix]),
+            reverse("product-detail", args=[self.bob.prefix]),
             self.bad_product_data,
         )
 
@@ -112,7 +110,7 @@ class ProductsApiTest(APITestCase):
 
     def test_delete_product(self):
         response = self.client.delete(
-            reverse('product-detail', args=[self.ally.prefix]),
+            reverse("product-detail", args=[self.ally.prefix]),
         )
 
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
diff --git a/trackers/api/tests/test_ticket_api.py 
b/trackers/api/tests/test_ticket_api.py
index b4e30c0..79ce2c5 100644
--- a/trackers/api/tests/test_ticket_api.py
+++ b/trackers/api/tests/test_ticket_api.py
@@ -67,7 +67,7 @@ class TicketApiTest(APITestCase):
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['summary'], self.record1.summary)
+        self.assertEqual(response.data["summary"], self.record1.summary)
 
     def test_get_invalid_ticket(self):
         response = self.client.get(
@@ -90,18 +90,14 @@ class TicketApiTest(APITestCase):
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
         record = Ticket.objects.get(
-            product=self.product,
-            product_ticket_id=response.data['product_ticket_id']
+            product=self.product, 
product_ticket_id=response.data["product_ticket_id"]
         )
 
-        self.assertEqual(response.data['summary'], record.summary)
+        self.assertEqual(response.data["summary"], record.summary)
 
     def test_create_invalid_product(self):
         response = self.client.post(
-            reverse(
-                'product-tickets-list',
-                kwargs={"product_prefix": "INVALID"}
-            ),
+            reverse("product-tickets-list", kwargs={"product_prefix": 
"INVALID"}),
             self.request_data,
         )
 
@@ -109,7 +105,7 @@ class TicketApiTest(APITestCase):
 
     def test_create_missing_summary(self):
         response = self.client.post(
-            reverse('product-tickets-list', kwargs={"product_prefix": "BH"}),
+            reverse("product-tickets-list", kwargs={"product_prefix": "BH"}),
             self.bad_request_data,
         )
 
@@ -130,12 +126,11 @@ class TicketApiTest(APITestCase):
         old_summary = self.record1.summary
 
         record = Ticket.objects.get(
-            product=self.product,
-            product_ticket_id=response.data['product_ticket_id']
+            product=self.product, 
product_ticket_id=response.data["product_ticket_id"]
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['summary'], record.summary)
+        self.assertEqual(response.data["summary"], record.summary)
         self.assertNotEqual(old_summary, record.summary)
 
     def test_update_ticket_bad_data(self):
@@ -155,11 +150,11 @@ class TicketApiTest(APITestCase):
     def test_delete_ticket(self):
         response = self.client.delete(
             reverse(
-                'product-tickets-detail',
+                "product-tickets-detail",
                 kwargs={
                     "product_prefix": self.record1.product.prefix,
                     "product_ticket_id": self.record1.product_ticket_id,
-                }
+                },
             ),
         )
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -175,8 +170,12 @@ class ComponentApiTest(APITestCase):
 
     def setUp(self):
         self.product = Product.objects.create(prefix="BH", name="Bloodhound")
-        self.record1 = Component.objects.create(product=self.product, 
name="Component 1")
-        self.record2 = Component.objects.create(product=self.product, 
name="Component 2")
+        self.record1 = Component.objects.create(
+            product=self.product, name="Component 1"
+        )
+        self.record2 = Component.objects.create(
+            product=self.product, name="Component 2"
+        )
 
         self.request_data = {
             "name": "Example Name",
@@ -209,7 +208,7 @@ class ComponentApiTest(APITestCase):
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['name'], self.record1.name)
+        self.assertEqual(response.data["name"], self.record1.name)
 
     def test_get_missing_component(self):
         response = self.client.get(
@@ -225,28 +224,19 @@ class ComponentApiTest(APITestCase):
 
     def test_create_component(self):
         response = self.client.post(
-            reverse(
-                "product-components-list",
-                kwargs={"product_prefix": "BH"}
-            ),
+            reverse("product-components-list", kwargs={"product_prefix": 
"BH"}),
             self.request_data,
         )
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-        record = Component.objects.get(
-            product=self.product,
-            name=response.data['name']
-        )
+        record = Component.objects.get(product=self.product, 
name=response.data["name"])
 
-        self.assertEqual(response.data['name'], record.name)
+        self.assertEqual(response.data["name"], record.name)
 
     def test_create_component_with_invalid_product(self):
         response = self.client.post(
-            reverse(
-                'product-components-list',
-                kwargs={"product_prefix": "INVALID"}
-            ),
+            reverse("product-components-list", kwargs={"product_prefix": 
"INVALID"}),
             self.request_data,
         )
 
@@ -291,11 +281,11 @@ class ComponentApiTest(APITestCase):
     def test_delete_component(self):
         response = self.client.delete(
             reverse(
-                'product-components-detail',
+                "product-components-detail",
                 kwargs={
                     "product_prefix": self.record1.product.prefix,
                     "name": self.record1.name,
-                }
+                },
             ),
         )
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -311,8 +301,12 @@ class MilestoneApiTest(APITestCase):
 
     def setUp(self):
         self.product = Product.objects.create(prefix="BH", name="Bloodhound")
-        self.record1 = Milestone.objects.create(product=self.product, 
name="Milestone 1")
-        self.record2 = Milestone.objects.create(product=self.product, 
name="Milestone 2")
+        self.record1 = Milestone.objects.create(
+            product=self.product, name="Milestone 1"
+        )
+        self.record2 = Milestone.objects.create(
+            product=self.product, name="Milestone 2"
+        )
 
         self.request_data = {
             "name": "Example Name",
@@ -346,7 +340,7 @@ class MilestoneApiTest(APITestCase):
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['name'], self.record1.name)
+        self.assertEqual(response.data["name"], self.record1.name)
 
     def test_get_missing_milestone(self):
         response = self.client.get(
@@ -362,28 +356,19 @@ class MilestoneApiTest(APITestCase):
 
     def test_create_milestone(self):
         response = self.client.post(
-            reverse(
-                "product-milestones-list",
-                kwargs={"product_prefix": "BH"}
-            ),
+            reverse("product-milestones-list", kwargs={"product_prefix": 
"BH"}),
             self.request_data,
         )
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-        record = Milestone.objects.get(
-            product=self.product,
-            name=response.data['name']
-        )
+        record = Milestone.objects.get(product=self.product, 
name=response.data["name"])
 
-        self.assertEqual(response.data['description'], record.description)
+        self.assertEqual(response.data["description"], record.description)
 
     def test_create_milestone_with_invalid_product(self):
         response = self.client.post(
-            reverse(
-                'product-milestones-list',
-                kwargs={"product_prefix": "INVALID"}
-            ),
+            reverse("product-milestones-list", kwargs={"product_prefix": 
"INVALID"}),
             self.request_data,
         )
 
@@ -403,13 +388,10 @@ class MilestoneApiTest(APITestCase):
 
         old_name = self.record1.name
 
-        record = Milestone.objects.get(
-            product=self.product,
-            name=response.data['name']
-        )
+        record = Milestone.objects.get(product=self.product, 
name=response.data["name"])
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['description'], record.description)
+        self.assertEqual(response.data["description"], record.description)
         self.assertNotEqual(old_name, record.name)
 
     def test_update_milestone_bad_data(self):
@@ -429,11 +411,11 @@ class MilestoneApiTest(APITestCase):
     def test_delete_milestone(self):
         response = self.client.delete(
             reverse(
-                'product-milestones-detail',
+                "product-milestones-detail",
                 kwargs={
                     "product_prefix": self.record1.product.prefix,
                     "name": self.record1.name,
-                }
+                },
             ),
         )
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -484,7 +466,7 @@ class VersionApiTest(APITestCase):
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data['name'], self.record1.name)
+        self.assertEqual(response.data["name"], self.record1.name)
 
     def test_get_missing_version(self):
         response = self.client.get(
@@ -500,28 +482,19 @@ class VersionApiTest(APITestCase):
 
     def test_create_version(self):
         response = self.client.post(
-            reverse(
-                "product-versions-list",
-                kwargs={"product_prefix": "BH"}
-            ),
+            reverse("product-versions-list", kwargs={"product_prefix": "BH"}),
             self.request_data,
         )
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-        record = Version.objects.get(
-            product=self.product,
-            name=response.data['name']
-        )
+        record = Version.objects.get(product=self.product, 
name=response.data["name"])
 
-        self.assertEqual(response.data['description'], record.description)
+        self.assertEqual(response.data["description"], record.description)
 
     def test_create_version_with_invalid_product(self):
         response = self.client.post(
-            reverse(
-                'product-versions-list',
-                kwargs={"product_prefix": "INVALID"}
-            ),
+            reverse("product-versions-list", kwargs={"product_prefix": 
"INVALID"}),
             self.request_data,
         )
 
@@ -542,10 +515,7 @@ class VersionApiTest(APITestCase):
 
         old_name = self.record1.name
 
-        record = Version.objects.get(
-            product=self.product,
-            name=response.data['name']
-        )
+        record = Version.objects.get(product=self.product, 
name=response.data["name"])
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertNotEqual(new_name, old_name)
@@ -568,11 +538,11 @@ class VersionApiTest(APITestCase):
     def test_delete_version(self):
         response = self.client.delete(
             reverse(
-                'product-versions-detail',
+                "product-versions-detail",
                 kwargs={
                     "product_prefix": self.record1.product.prefix,
                     "name": self.record1.name,
-                }
+                },
             ),
         )
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
diff --git a/trackers/api/tests/test_ticket_api_hyp.py 
b/trackers/api/tests/test_ticket_api_hyp.py
index f54fc50..e7b3c81 100644
--- a/trackers/api/tests/test_ticket_api_hyp.py
+++ b/trackers/api/tests/test_ticket_api_hyp.py
@@ -23,14 +23,18 @@ from rest_framework import status
 
 from ...models import Product
 
-name_st = st.text(
-    st.characters(
-        blacklist_characters="/",
-        max_codepoint=1000,
-        blacklist_categories=("Cc", "Cs")
-    ),
-    min_size=1
-).map(lambda x: x.strip()).filter(lambda s: len(s) > 0)
+name_st = (
+    st.text(
+        st.characters(
+            blacklist_characters="/",
+            max_codepoint=1000,
+            blacklist_categories=("Cc", "Cs"),
+        ),
+        min_size=1,
+    )
+    .map(lambda x: x.strip())
+    .filter(lambda s: len(s) > 0)
+)
 
 
 class CommonAPIPropertiesTestCase(TestCase):
@@ -40,10 +44,7 @@ class CommonAPIPropertiesTestCase(TestCase):
         self.client = APIClient()
         self.factory = APIRequestFactory()
         self.product = Product.objects.create(prefix="BH", name="Bloodhound")
-        self.list_uri = reverse(
-            self.list_view_name,
-            kwargs={"product_prefix": "BH"}
-        )
+        self.list_uri = reverse(self.list_view_name, kwargs={"product_prefix": 
"BH"})
 
 
 class NameTestsMixin:
diff --git a/trackers/api/urls.py b/trackers/api/urls.py
index 6b5eaa4..db4f0d4 100644
--- a/trackers/api/urls.py
+++ b/trackers/api/urls.py
@@ -22,37 +22,47 @@ from rest_framework_nested import routers
 from . import views
 
 router = routers.DefaultRouter()
-router.register('users', views.UserViewSet)
-router.register('groups', views.GroupViewSet)
-router.register('products', views.ProductViewSet)
+router.register("users", views.UserViewSet)
+router.register("groups", views.GroupViewSet)
+router.register("products", views.ProductViewSet)
 
-products_router = routers.NestedDefaultRouter(router, 'products', 
lookup='product')
-products_router.register('tickets', views.TicketViewSet, 
basename='product-tickets')
-products_router.register('components', views.ComponentViewSet, 
basename='product-components')
-products_router.register('milestones', views.MilestoneViewSet, 
basename='product-milestones')
-products_router.register('versions', views.VersionViewSet, 
basename='product-versions')
-products_router.register('ticketchanges', views.TicketChangeViewSet, 
basename='product-ticketchanges')
+products_router = routers.NestedDefaultRouter(router, "products", 
lookup="product")
+products_router.register("tickets", views.TicketViewSet, 
basename="product-tickets")
+products_router.register(
+    "components", views.ComponentViewSet, basename="product-components"
+)
+products_router.register(
+    "milestones", views.MilestoneViewSet, basename="product-milestones"
+)
+products_router.register("versions", views.VersionViewSet, 
basename="product-versions")
+products_router.register(
+    "ticketchanges", views.TicketChangeViewSet, 
basename="product-ticketchanges"
+)
 
 urlpatterns = [
-    path('', include(router.urls)),
-    path('', include(products_router.urls)),
-    path('openapi', get_schema_view(
-        title="Apache Bloodhound",
-        version="0.1.0",
-    ), name='openapi-schema'),
+    path("", include(router.urls)),
+    path("", include(products_router.urls)),
     path(
-        'swagger<str:format>',
+        "openapi",
+        get_schema_view(
+            title="Apache Bloodhound",
+            version="0.1.0",
+        ),
+        name="openapi-schema",
+    ),
+    path(
+        "swagger<str:format>",
         views.schema_view.without_ui(cache_timeout=0),
-        name='schema-json',
+        name="schema-json",
     ),
     path(
-        'swagger/',
-        views.schema_view.with_ui('swagger', cache_timeout=0),
-        name='schema-swagger-ui',
+        "swagger/",
+        views.schema_view.with_ui("swagger", cache_timeout=0),
+        name="schema-swagger-ui",
     ),
     path(
-        'redoc/',
-        views.schema_view.with_ui('redoc', cache_timeout=0),
-        name='schema-redoc',
+        "redoc/",
+        views.schema_view.with_ui("redoc", cache_timeout=0),
+        name="schema-redoc",
     ),
 ]
diff --git a/trackers/api/views.py b/trackers/api/views.py
index d7f31c9..d62d8a3 100644
--- a/trackers/api/views.py
+++ b/trackers/api/views.py
@@ -25,8 +25,8 @@ from .. import models
 
 schema_view = get_schema_view(
     openapi.Info(
-        title='Bloodhound Core API',
-        default_version='v1',
+        title="Bloodhound Core API",
+        default_version="v1",
     ),
     public=True,
     permission_classes=(permissions.AllowAny,),
@@ -46,57 +46,57 @@ class GroupViewSet(viewsets.ModelViewSet):
 class ProductViewSet(viewsets.ModelViewSet):
     queryset = models.Product.objects.all()
     serializer_class = serializers.ProductSerializer
-    lookup_field = 'prefix'
+    lookup_field = "prefix"
 
 
 class TicketChangeViewSet(viewsets.ModelViewSet):
     queryset = models.TicketChange.objects.all()
     serializer_class = serializers.TicketChangeSerializer
-    lookup_field = 'time'
+    lookup_field = "time"
 
     def get_queryset(self, *args, **kwargs):
-        prefix = self.kwargs['product_prefix']
+        prefix = self.kwargs["product_prefix"]
         return models.TicketChange.objects.filter(product=prefix)
 
 
 class TicketViewSet(viewsets.ModelViewSet):
     queryset = models.Ticket.objects.all()
     serializer_class = serializers.TicketSerializer
-    lookup_field = 'product_ticket_id'
+    lookup_field = "product_ticket_id"
 
     def get_queryset(self, *args, **kwargs):
-        prefix = self.kwargs['product_prefix']
+        prefix = self.kwargs["product_prefix"]
         return models.Ticket.objects.filter(product=prefix)
 
 
 class ComponentViewSet(viewsets.ModelViewSet):
     queryset = models.Component.objects.all()
     serializer_class = serializers.ComponentSerializer
-    lookup_field = 'name'
-    lookup_value_regex = '[^/]+'
+    lookup_field = "name"
+    lookup_value_regex = "[^/]+"
 
     def get_queryset(self, *args, **kwargs):
-        prefix = self.kwargs['product_prefix']
+        prefix = self.kwargs["product_prefix"]
         return models.Component.objects.filter(product=prefix)
 
 
 class MilestoneViewSet(viewsets.ModelViewSet):
     queryset = models.Milestone.objects.all()
     serializer_class = serializers.MilestoneSerializer
-    lookup_field = 'name'
-    lookup_value_regex = '[^/]+'
+    lookup_field = "name"
+    lookup_value_regex = "[^/]+"
 
     def get_queryset(self, *args, **kwargs):
-        prefix = self.kwargs['product_prefix']
+        prefix = self.kwargs["product_prefix"]
         return models.Milestone.objects.filter(product=prefix)
 
 
 class VersionViewSet(viewsets.ModelViewSet):
     queryset = models.Version.objects.all()
     serializer_class = serializers.VersionSerializer
-    lookup_field = 'name'
-    lookup_value_regex = '[^/]+'
+    lookup_field = "name"
+    lookup_value_regex = "[^/]+"
 
     def get_queryset(self, *args, **kwargs):
-        prefix = self.kwargs['product_prefix']
+        prefix = self.kwargs["product_prefix"]
         return models.Version.objects.filter(product=prefix)
diff --git a/trackers/apps.py b/trackers/apps.py
index 9615c59..243c1d2 100644
--- a/trackers/apps.py
+++ b/trackers/apps.py
@@ -19,5 +19,5 @@ from django.apps import AppConfig
 
 
 class TrackersConfig(AppConfig):
-    default_auto_field = 'django.db.models.BigAutoField'
-    name = 'trackers'
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "trackers"
diff --git a/trackers/fixtures/empty.yml b/trackers/fixtures/empty.yml
index fe51488..fba50b6 100644
--- a/trackers/fixtures/empty.yml
+++ b/trackers/fixtures/empty.yml
@@ -1 +1,19 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+---
 []
diff --git a/trackers/models.py b/trackers/models.py
index 443e92c..0444976 100644
--- a/trackers/models.py
+++ b/trackers/models.py
@@ -33,7 +33,7 @@ class Product(models.Model):
     owner = models.TextField(blank=True, null=True)
 
     class Meta:
-        db_table = 'bloodhound_product'
+        db_table = "bloodhound_product"
 
 
 class ProductConfig(models.Model):
@@ -45,8 +45,8 @@ class ProductConfig(models.Model):
     value = models.TextField(blank=True, null=True)
 
     class Meta:
-        db_table = 'bloodhound_productconfig'
-        unique_together = (('product', 'section', 'option'),)
+        db_table = "bloodhound_productconfig"
+        unique_together = (("product", "section", "option"),)
 
 
 class ProductResourceMap(models.Model):
@@ -57,7 +57,7 @@ class ProductResourceMap(models.Model):
     resource_id = models.TextField(blank=True, null=True)
 
     class Meta:
-        db_table = 'bloodhound_productresourcemap'
+        db_table = "bloodhound_productresourcemap"
 
 
 class Component(models.Model):
@@ -73,8 +73,8 @@ class Component(models.Model):
     )
 
     class Meta:
-        db_table = 'component'
-        unique_together = (('name', 'product'),)
+        db_table = "component"
+        unique_together = (("name", "product"),)
 
 
 class Enum(models.Model):
@@ -86,8 +86,8 @@ class Enum(models.Model):
     product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
     class Meta:
-        db_table = 'enum'
-        unique_together = (('type', 'name', 'product'),)
+        db_table = "enum"
+        unique_together = (("type", "name", "product"),)
 
 
 class Milestone(models.Model):
@@ -104,8 +104,8 @@ class Milestone(models.Model):
     )
 
     class Meta:
-        db_table = 'milestone'
-        unique_together = (('name', 'product'),)
+        db_table = "milestone"
+        unique_together = (("name", "product"),)
 
 
 class Version(models.Model):
@@ -121,8 +121,8 @@ class Version(models.Model):
     )
 
     class Meta:
-        db_table = 'version'
-        unique_together = (('name', 'product'),)
+        db_table = "version"
+        unique_together = (("name", "product"),)
 
 
 class Ticket(models.Model):
@@ -133,7 +133,7 @@ class Ticket(models.Model):
         Enum,
         on_delete=models.PROTECT,
         db_column="type",
-        related_name='%(app_label)s_%(class)s_type_related',
+        related_name="%(app_label)s_%(class)s_type_related",
         blank=True,
         null=True,
     )
@@ -170,7 +170,7 @@ class Ticket(models.Model):
         Enum,
         on_delete=models.PROTECT,
         db_column="resolution",
-        related_name='%(app_label)s_%(class)s_resolution_related',
+        related_name="%(app_label)s_%(class)s_resolution_related",
         blank=True,
         null=True,
     )
@@ -178,11 +178,11 @@ class Ticket(models.Model):
     description = models.TextField(blank=True, null=True)
     keywords = models.TextField(blank=True, null=True)
     product = models.ForeignKey(Product, on_delete=models.PROTECT, 
db_column="product")
-    product_ticket_id = models.IntegerField(db_column='id', editable=False)
+    product_ticket_id = models.IntegerField(db_column="id", editable=False)
 
     class Meta:
-        db_table = 'ticket'
-        unique_together = (('product', 'product_ticket_id'),)
+        db_table = "ticket"
+        unique_together = (("product", "product_ticket_id"),)
 
     def save(self, *args, **kwargs):
         if self._state.adding:
@@ -193,7 +193,7 @@ class Ticket(models.Model):
             #     recording last used on product model
             product_tickets = Ticket.objects.filter(product=self.product)
             if product_tickets.exists():
-                newest = product_tickets.latest('product_ticket_id')
+                newest = product_tickets.latest("product_ticket_id")
                 new_id = 1 + newest.product_ticket_id
             else:
                 new_id = 1
@@ -207,8 +207,8 @@ class TicketChange(models.Model):
     ticket = models.ForeignKey(
         Ticket,
         on_delete=models.PROTECT,
-        db_column='ticket',
-        related_name='%(app_label)s_%(class)s_ticket_related',
+        db_column="ticket",
+        related_name="%(app_label)s_%(class)s_ticket_related",
     )
     time = models.BigIntegerField(blank=True, null=True)
     author = models.TextField(blank=True, null=True)
@@ -222,8 +222,8 @@ class TicketChange(models.Model):
     )
 
     class Meta:
-        db_table = 'ticket_change'
-        unique_together = (('ticket', 'time', 'field', 'product'),)
+        db_table = "ticket_change"
+        unique_together = (("ticket", "time", "field", "product"),)
 
 
 class TicketCustom(models.Model):
@@ -235,8 +235,8 @@ class TicketCustom(models.Model):
     product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
     class Meta:
-        db_table = 'ticket_custom'
-        unique_together = (('ticket', 'name', 'product'),)
+        db_table = "ticket_custom"
+        unique_together = (("ticket", "name", "product"),)
 
 
 class Report(models.Model):
@@ -249,5 +249,5 @@ class Report(models.Model):
     product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
     class Meta:
-        db_table = 'report'
-        unique_together = (('id', 'product'),)
+        db_table = "report"
+        unique_together = (("id", "product"),)
diff --git a/trackers/tests/__init__.py b/trackers/tests/__init__.py
index e69de29..084b296 100644
--- a/trackers/tests/__init__.py
+++ b/trackers/tests/__init__.py
@@ -0,0 +1,16 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
diff --git a/trackers/tests/test_models.py b/trackers/tests/test_models.py
index fef8144..6343834 100644
--- a/trackers/tests/test_models.py
+++ b/trackers/tests/test_models.py
@@ -21,21 +21,22 @@ from ..models import Product, Ticket
 
 class ProductTest(TestCase):
     """Tests for Product model"""
+
     def setUp(self):
         Product.objects.create(
-            prefix='BHD',
-            name='Bloodhound Legacy',
-            description='The original Apache Bloodhound',
+            prefix="BHD",
+            name="Bloodhound Legacy",
+            description="The original Apache Bloodhound",
         )
         Product.objects.create(
-            prefix='BH',
-            name='Bloodhound',
-            description='The future of Apache Bloodhound',
+            prefix="BH",
+            name="Bloodhound",
+            description="The future of Apache Bloodhound",
         )
 
     def test_product_name(self):
-        bhd = Product.objects.get(prefix='BHD')
-        bh = Product.objects.get(prefix='BH')
+        bhd = Product.objects.get(prefix="BHD")
+        bh = Product.objects.get(prefix="BH")
 
         self.assertEqual(bhd.name, "Bloodhound Legacy")
         self.assertEqual(bh.name, "Bloodhound")
@@ -43,11 +44,12 @@ class ProductTest(TestCase):
 
 class TicketTest(TestCase):
     """Test for Ticket model"""
+
     def setUp(self):
         self.product = Product.objects.create(
-            prefix='BH',
-            name='Bloodhound',
-            description='Apache Bloodhound',
+            prefix="BH",
+            name="Bloodhound",
+            description="Apache Bloodhound",
         )
 
     def test_ticket_create_sets_product_ticket_number(self):
diff --git a/trackers/tests/tests.py b/trackers/tests/tests.py
index 23e10d7..bae346c 100644
--- a/trackers/tests/tests.py
+++ b/trackers/tests/tests.py
@@ -23,13 +23,13 @@ from ..views import home
 
 class HomePageTest(TestCase):
     def test_root_url_resolves_to_home_page_view(self):
-        found = resolve('/')
+        found = resolve("/")
         self.assertEqual(found.func, home)
 
     def test_home_page_returns_expected_html(self):
         request = HttpRequest()
         response = home(request)
 
-        self.assertTrue(response.content.startswith(b'<html>'))
-        self.assertIn(b'<title>Bloodhound Trackers</title>', response.content)
-        self.assertTrue(response.content.endswith(b'</html>'))
+        self.assertTrue(response.content.startswith(b"<html>"))
+        self.assertIn(b"<title>Bloodhound Trackers</title>", response.content)
+        self.assertTrue(response.content.endswith(b"</html>"))
diff --git a/trackers/urls.py b/trackers/urls.py
index c62892f..1265942 100644
--- a/trackers/urls.py
+++ b/trackers/urls.py
@@ -20,6 +20,6 @@ from django.conf.urls import include
 from . import views
 
 urlpatterns = [
-    path('', views.home, name='home'),
-    path('api/', include('trackers.api.urls')),
+    path("", views.home, name="home"),
+    path("api/", include("trackers.api.urls")),
 ]
diff --git a/trackers/views.py b/trackers/views.py
index 0861c17..43e624a 100644
--- a/trackers/views.py
+++ b/trackers/views.py
@@ -19,4 +19,4 @@ from django.http import HttpResponse
 
 
 def home(request):
-    return HttpResponse('<html><title>Bloodhound Trackers</title></html>')
+    return HttpResponse("<html><title>Bloodhound Trackers</title></html>")

Reply via email to