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
commit 0394b68f7b4bd03e97d4911d358339043fbd67b2 Author: Gary Martin <g...@apache.org> AuthorDate: Mon Jun 4 13:50:34 2018 +0000 add branch for new bh_core experiment git-svn-id: https://svn.apache.org/repos/asf/bloodhound/branches/bh_core_experimental@1832850 13f79535-47bb-0310-9956-ffa450edef68 --- Pipfile | 16 +++++ Pipfile.lock | 144 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 91 ++++++++++++++++++++++++++++ bh_core/__init__.py | 17 ++++++ bh_core/settings.py | 142 +++++++++++++++++++++++++++++++++++++++++++ bh_core/urls.py | 40 ++++++++++++ bh_core/wsgi.py | 33 ++++++++++ functional_tests.py | 39 ++++++++++++ manage.py | 33 ++++++++++ pytest.ini | 2 + trackers/__init__.py | 16 +++++ trackers/admin.py | 21 +++++++ trackers/apps.py | 22 +++++++ trackers/fixtures/empty.yml | 1 + trackers/models.py | 80 ++++++++++++++++++++++++ trackers/tests.py | 93 ++++++++++++++++++++++++++++ trackers/urls.py | 23 +++++++ trackers/views.py | 22 +++++++ 18 files changed, 835 insertions(+) diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e912a7b --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[dev-packages] +selenium = "*" +pytest-django = "*" +PyYAML = "*" + +[packages] +django = ">=2.0.0" +pyyaml = "*" + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..4836a5b --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,144 @@ +{ + "_meta": { + "hash": { + "sha256": "4e791c7fd7b1d7f8749e94bcf56e4f78fb6514b5cf0d3174fe26d91f92cb672d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "django": { + "hashes": [ + "sha256:3eb25c99df1523446ec2dc1b00e25eb2ecbdf42c9d8b0b8b32a204a8db9011f8", + "sha256:69ff89fa3c3a8337015478a1a0744f52a9fef5d12c1efa01a01f99bcce9bf10c" + ], + "index": "pypi", + "version": "==2.0.6" + }, + "pytz": { + "hashes": [ + "sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555", + "sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749" + ], + "version": "==2018.4" + }, + "pyyaml": { + "hashes": [ + "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8", + "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736", + "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f", + "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608", + "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8", + "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab", + "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7", + "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3", + "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1", + "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6", + "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8", + "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4", + "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca", + "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269" + ], + "index": "pypi", + "version": "==3.12" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", + "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" + ], + "version": "==1.1.5" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "more-itertools": { + "hashes": [ + "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", + "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", + "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" + ], + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", + "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", + "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881", + "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" + ], + "version": "==1.5.3" + }, + "pytest": { + "hashes": [ + "sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d", + "sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a" + ], + "version": "==3.6.0" + }, + "pytest-django": { + "hashes": [ + "sha256:534505e0261cc566279032d9d887f844235342806fd63a6925689670fa1b29d7", + "sha256:7501942093db2250a32a4e36826edfc542347bb9b26c78ed0649cdcfd49e5789" + ], + "index": "pypi", + "version": "==3.2.1" + }, + "pyyaml": { + "hashes": [ + "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8", + "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736", + "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f", + "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608", + "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8", + "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab", + "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7", + "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3", + "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1", + "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6", + "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8", + "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4", + "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca", + "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269" + ], + "index": "pypi", + "version": "==3.12" + }, + "selenium": { + "hashes": [ + "sha256:1372101ad23798462038481f92ba1c7fab8385c788b05da6b44318f10ea52422", + "sha256:b8a2630fd858636c894960726ca3c94d8277e516ea3a9d81614fb819a5844764" + ], + "index": "pypi", + "version": "==3.12.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd17d1d --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# New Bloodhound + +## Requirements + +Bloodhound uses pipenv for development process. + +If you have pip installed already, installation can be a simple as + +``` +pip install --user pipenv +``` + +For more information on installing and usage of pipenv, see +https://docs.pipenv.org/. + +Once pipenv is installed, the remaining job of installing should be as simple +as + +``` +pipenv install +``` + +If this doesn't work, it should be done from the same directory as the +`Pipenv` file. + +Though possibly annoying, the commands in this file will assume the use of +`pipenv` but not that the pipenv shell has been activated. + +## Setup + +The basic setup steps to get running are: + +``` +pipenv run python manage.py makemigrations trackers +pipenv run python manage.py migrate +``` + +The above will do the basic database setup. + +Note that currently models are in flux and, for the moment, no support should +be expected for migrations as models change. This will change when basic +models gain stability. + +## Running the development server: + +``` +pipenv run python manage.py runserver +``` + +## Unit Tests + +Unit tests are currently being written with the standard unittest framework. +This may be replaced with pytest. + +The tests may be run with the following command: + +``` +pipenv run python manage.py test +``` + +Fixtures for tests when required can be generated with: + +``` +pipenv python manage.py dumpdata bh-core --format=yaml --indent=2 > bh-core/fixtures/[fixture-name].yaml +``` + +## Integration Tests + +Selenium tests currently require that Firefox is installed and `geckodriver` is +also on the path. One way to do this is (example for 64bit linux distributions): + +``` +BIN_LOCATION="$HOME/.local/bin" +PLATFORM_EXT="linux64.tar.gz" +TMP_DIR=/tmp +LATEST=$(wget -O - https://github.com/mozilla/geckodriver/releases/latest 2>&1 | awk 'match($0, /geckodriver-(v.*)-'"$PLATFORM_EXT"'/, a) {print a[1]; exit}') +wget -N -P "$TMP_DIR" "https://github.com/mozilla/geckodriver/releases/download/$LATEST/geckodriver-$LATEST-$PLATFORM_EXT" +tar -x geckodriver -zf "$TMP_DIR/geckodriver-$LATEST-$PLATFORM_EXT" -O > "$BIN_LOCATION"/geckodriver +chmod +x "$BIN_LOCATION"/geckodriver +``` + +If `$BIN_LOCATION` is on the system path, it should be possible to run the integration tests. + +So, assuming the use of pipenv: + +``` +pipenv run python functional_tests.py +``` + +There are currently not many tests - those that are there are in place to test +the setup above and assume that there will be useful tests in due course. diff --git a/bh_core/__init__.py b/bh_core/__init__.py new file mode 100644 index 0000000..534df97 --- /dev/null +++ b/bh_core/__init__.py @@ -0,0 +1,17 @@ +# 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/bh_core/settings.py b/bh_core/settings.py new file mode 100644 index 0000000..579b660 --- /dev/null +++ b/bh_core/settings.py @@ -0,0 +1,142 @@ +# 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. + +""" +Django settings for bh_core project. + +Generated by 'django-admin startproject' using Django 2.0.3. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.0/ref/settings/ + +SECURITY WARNING: do not use the SECRET_KEY below. This file has only had +minimal changes following on from the point of generation. Do not expect +this project to be production ready at this point! +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# 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' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +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', +] + +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', +] + +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', + ], + }, + }, +] + +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'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.0/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/bh_core/urls.py b/bh_core/urls.py new file mode 100644 index 0000000..9bf7a1c --- /dev/null +++ b/bh_core/urls.py @@ -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. + +"""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')) +""" + +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path('', include('trackers.urls')), + # path('admin/', admin.site.urls), +] diff --git a/bh_core/wsgi.py b/bh_core/wsgi.py new file mode 100644 index 0000000..15905c2 --- /dev/null +++ b/bh_core/wsgi.py @@ -0,0 +1,33 @@ +# 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. + +""" +WSGI config for bh_core project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bh_core.settings") + +application = get_wsgi_application() diff --git a/functional_tests.py b/functional_tests.py new file mode 100644 index 0000000..df49437 --- /dev/null +++ b/functional_tests.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# 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 selenium import webdriver +import unittest + + +class TicketViewTest(unittest.TestCase): + def setUp(self): + self.browser = webdriver.Firefox() + self.browser.implicitly_wait(3) + + def tearDown(self): + self.browser.quit() + + def test_user_can_add_view_and_delete_ticket(self): + self.browser.get('http://localhost:8000') + + self.assertIn('Bloodhound', self.browser.title) + + +if __name__ == '__main__': + unittest.main(warnings='ignore') diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..63afb12 --- /dev/null +++ b/manage.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# 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. + +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bh_core.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..7cf71fa --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE = bh_core.settings diff --git a/trackers/__init__.py b/trackers/__init__.py new file mode 100644 index 0000000..084b296 --- /dev/null +++ b/trackers/__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/admin.py b/trackers/admin.py new file mode 100644 index 0000000..e1f3d8f --- /dev/null +++ b/trackers/admin.py @@ -0,0 +1,21 @@ +# 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 import admin + +# Register your models here. diff --git a/trackers/apps.py b/trackers/apps.py new file mode 100644 index 0000000..7b9d013 --- /dev/null +++ b/trackers/apps.py @@ -0,0 +1,22 @@ +# 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.apps import AppConfig + + +class TrackersConfig(AppConfig): + name = 'trackers' diff --git a/trackers/fixtures/empty.yml b/trackers/fixtures/empty.yml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/trackers/fixtures/empty.yml @@ -0,0 +1 @@ +[] diff --git a/trackers/models.py b/trackers/models.py new file mode 100644 index 0000000..1887e4e --- /dev/null +++ b/trackers/models.py @@ -0,0 +1,80 @@ +# 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. + +import difflib +import functools +import logging +import uuid + +from django.db import models + +logger = logging.getLogger(__name__) + + +class Ticket(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created = models.DateTimeField(auto_now_add=True, editable=False) + + def last_update(self): + last_event = self.changeevent_set.order_by('event_time').last() + return self.created if last_event is None else last_event.event_time + + def add_field_event(self, field, newvalue): + current_lines = self.get_field_value(field).splitlines(keepends=True) + replace_lines = newvalue.splitlines(keepends=True) + result = '\n'.join(difflib.ndiff(current_lines, replace_lines)) + + tfield, created = TicketField.objects.get_or_create(name=field) + c = ChangeEvent(ticket=self, field=tfield, diff=result) + c.save() + + def get_field_value(self, field): + try: + tfield = TicketField.objects.get(name=field) + except TicketField.DoesNotExist as e: + return '' + + event = self.changeevent_set.filter(field=tfield).order_by('event_time').last() + return '' if event is None else event.value() + + +class TicketField(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=32) + +class Label(TicketField): + pass + +class SharedField(TicketField): + pass + +class ChangeEvent(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + ticket = models.ForeignKey(Ticket, models.CASCADE, editable=False, null=False) + field = models.ForeignKey(TicketField, models.CASCADE, editable=False, null=False) + event_time = models.DateTimeField(auto_now_add=True, editable=False) + diff = models.TextField(editable=False) + + def value(self, which=2): + return ''.join(difflib.restore(self.diff.splitlines(keepends=True), which)).strip() + + old_value = functools.partialmethod(value, which=1) + + def __str__(self): + return "Change to: {}; Field: {}; Diff: {}".format( + self.ticket, self.field, self.diff) + diff --git a/trackers/tests.py b/trackers/tests.py new file mode 100644 index 0000000..cbc666b --- /dev/null +++ b/trackers/tests.py @@ -0,0 +1,93 @@ +# 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.http import HttpRequest +from django.test import TestCase +from django.urls import resolve +from trackers.views import home + + +class HomePageTest(TestCase): + def test_root_url_resolves_to_home_page_view(self): + 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>')) + + +from trackers.models import Ticket + +class TicketModelTest(TestCase): + def test_last_update_on_create_returns_created_date(self): + t = Ticket() + t.save() + self.assertEqual(t.created, t.last_update()) + + def test_last_update_returns_last_change_date(self): + # test may be safer with a fixture with an existing ticket to check + t = Ticket() + t.save() + t.add_field_event('summary', "this is the summary") + self.assertNotEqual(t.created, t.last_update()) + + def test_ticket_creation(self): + # Currently simple but may need updates for required fields + pre_count = Ticket.objects.count() + t = Ticket() + t.save() + self.assertEqual(pre_count + 1, Ticket.objects.count()) + + def test_ticket_add_field_event(self): + field = 'summary' + field_value = "this is the summary" + + t = Ticket() + t.save() + t.add_field_event(field, field_value) + + self.assertEqual(t.get_field_value(field), field_value) + + def test_ticket_add_two_single_line_field_events_same_field(self): + field = 'summary' + first_field_value = "this is the summary" + second_field_value = "this is the replacement summary" + + t = Ticket() + t.save() + t.add_field_event(field, first_field_value) + t.add_field_event(field, second_field_value) + + self.assertEqual(t.get_field_value(field), second_field_value) + + def test_ticket_add_two_multiline_field_events_same_field(self): + field = 'summary' + first_field_value = "this is the summary\nwith multiple lines" + second_field_value = "this is the replacement summary\nwith multiple lines" + + t = Ticket() + t.save() + t.add_field_event(field, first_field_value) + t.add_field_event(field, second_field_value) + + self.assertEqual(t.get_field_value(field), second_field_value) + diff --git a/trackers/urls.py b/trackers/urls.py new file mode 100644 index 0000000..a7a1c75 --- /dev/null +++ b/trackers/urls.py @@ -0,0 +1,23 @@ +# 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.urls import path +from . import views + +urlpatterns = [ + path('', views.home, name='home'), +] diff --git a/trackers/views.py b/trackers/views.py new file mode 100644 index 0000000..6e21c4f --- /dev/null +++ b/trackers/views.py @@ -0,0 +1,22 @@ +# 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.http import HttpResponse +from django.shortcuts import render + +def home(request): + return HttpResponse('<html><title>Bloodhound Trackers</title></html>')