diff --git a/.gitignore b/.gitignore index fdc0811..846d90e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ target/ # editors *.komodoproject +.vscode # other *.DS_Store* diff --git a/.travis.yml b/.travis.yml index c904476..a12a574 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,43 @@ language: python -sudo: false cache: pip +dist: focal python: - - "3.4" - - "2.7" + - "3.8" env: - - DJANGO="django>=1.11,<1.12" - - DJANGO="django>=2.0,<2.1" + - DJANGO="django~=3.1" -matrix: - exclude: - - python: "2.7" - env: DJANGO="django>=2.0,<2.1" +addons: + apt: + packages: + - sqlite3 + - fping + - gdal-bin + - libproj-dev + - libgeos-dev + - libspatialite-dev + - spatialite-bin + - libsqlite3-mod-spatialite + +services: + - redis-server branches: only: - master -install: +before_install: + - pip install -U "pip==20.2.4" wheel setuptools - pip install $DJANGO - - python setup.py -q develop - pip install -r requirements-test.txt +install: + - pip install -e . + script: - - openwisp-utils-qa-checks --skip-checkmigrations - - coverage run --source=owm_legacy runtests.py + - ./run-qa-checks + - coverage run -a --source=owm_legacy runtests.py after_success: coveralls diff --git a/README.rst b/README.rst index d45ae89..b1f5098 100644 --- a/README.rst +++ b/README.rst @@ -3,17 +3,31 @@ django-owm-legacy .. image:: https://travis-ci.org/openwisp/django-owm-legacy.svg :target: https://travis-ci.org/openwisp/django-owm-legacy + :alt: CI build status .. image:: https://coveralls.io/repos/openwisp/django-owm-legacy/badge.svg :target: https://coveralls.io/r/openwisp/django-owm-legacy + :alt: Test Coverage .. image:: https://requires.io/github/openwisp/django-owm-legacy/requirements.svg?branch=master :target: https://requires.io/github/openwisp/django-owm-legacy/requirements/?branch=master :alt: Requirements Status +.. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg + :target: https://gitter.im/openwisp/general + :alt: Chat + .. image:: https://badge.fury.io/py/django-owm-legacy.svg :target: http://badge.fury.io/py/django-owm-legacy +.. image:: https://pepy.tech/badge/django-own-legacy + :target: https://pepy.tech/project/django-own-legacy + :alt: Downloads + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://pypi.org/project/black/ + :alt: code style: black + ------------ Legacy features of OpenWISP Manager reimplemented in django for `OpenWISP2 @@ -54,19 +68,46 @@ If you want to contribute, install your cloned fork: Setup (integrate in an existing django project) ----------------------------------------------- -Add ``django_netjsonconfig``, ``sortedm2m`` and ``owm_legacy`` to ``INSTALLED_APPS``: +Add ``openwisp_controller`` and ``owm_legacy`` to ``INSTALLED_APPS`` as follow: .. code-block:: python INSTALLED_APPS = [ - # other apps - 'django_netjsonconfig', + # ... + 'django.contrib.sites', + # allauth + 'allauth', + 'allauth.account', + 'django_extensions', + # openwisp2 modules + 'openwisp_controller.config', + 'openwisp_controller.pki', + 'openwisp_controller.geo', + 'openwisp_controller.connection', + 'openwisp_users', + # openwisp2 admin theme + # (must be loaded here) + 'openwisp_utils.admin_theme', + 'django.contrib.admin', + 'django.forms', + # other dependencies 'sortedm2m', 'reversion', - 'owm_legacy' + 'leaflet', + 'flat_json_widget', + 'owm_legacy', # ... ] +Other settings needed in ``settings.py``: + +.. code-block:: python + + EXTENDED_APPS = ('django_x509', 'django_loci') + + AUTH_USER_MODEL = 'openwisp_users.User' + SITE_ID = 1 + Your ``urls.py`` should look like the following: .. code-block:: python @@ -80,8 +121,8 @@ Your ``urls.py`` should look like the following: urlpatterns = [ url(r'^admin/', include(admin.site.urls)), - url(r'^', include('django_netjsonconfig.controller.urls', namespace='controller')), - url(r'^', include('owm_legacy.urls', namespace='owm')), + url(r'^', include('openwisp_controller.urls', namespace='controller')), + url(r'^', include('owm_legacy.urls', namespace='owm_legacy')), ] urlpatterns += staticfiles_urlpatterns() @@ -93,7 +134,14 @@ Install sqlite: .. code-block:: shell - sudo apt-get install sqlite3 libsqlite3-dev + sudo apt install -y sqlite3 libsqlite3-dev openssl libssl-dev + sudo apt install -y gdal-bin libproj-dev libgeos-dev libspatialite-dev libsqlite3-mod-spatialite + +Launch Redis: + +.. code-block:: shell + + docker-compose up -d redis Install your forked repo: @@ -117,6 +165,12 @@ Create database: ./manage.py migrate ./manage.py createsuperuser +Launch celery worker (for background jobs): + +.. code-block:: shell + + celery -A openwisp2 worker -l info + Launch development server: .. code-block:: shell @@ -149,15 +203,7 @@ checksums and download configuration archives. Contributing ------------ -1. Announce your intentions in the `OpenWISP Mailing List `_ -2. Fork this repo and install it -3. Follow `PEP8, Style Guide for Python Code`_ -4. Write code -5. Write tests for your code -6. Ensure all tests pass -7. Ensure test coverage is not under 90% -8. Document your changes -9. Send pull request +Please refer to the `OpenWISP contributing guidelines `_. .. _PEP8, Style Guide for Python Code: http://www.python.org/dev/peps/pep-0008/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b10a9da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3" + +services: + redis: + image: redis:5.0-alpine + ports: + - "6379:6379" + entrypoint: redis-server --appendonly yes diff --git a/owm_legacy/settings.py b/owm_legacy/settings.py index 6ced9e7..23af82f 100644 --- a/owm_legacy/settings.py +++ b/owm_legacy/settings.py @@ -1,4 +1,5 @@ from django.conf import settings -ALLOWED_SUBNETS = getattr(settings, 'OWM_LEGACY_ALLOWED_SUBNETS', ['10.8.0.0/16', - '127.0.0.1/32']) +ALLOWED_SUBNETS = getattr( + settings, 'OWM_LEGACY_ALLOWED_SUBNETS', ['10.8.0.0/16', '127.0.0.1/32'] +) diff --git a/owm_legacy/tests.py b/owm_legacy/tests.py index 5d91b0d..e0c798a 100644 --- a/owm_legacy/tests.py +++ b/owm_legacy/tests.py @@ -1,53 +1,68 @@ from time import sleep -from django.urls import reverse from django.test import TestCase +from django.urls import reverse -from django_netjsonconfig.models import Config, Device -from django_netjsonconfig.tests import CreateConfigMixin +from openwisp_controller.config.models import Config, Device +from openwisp_controller.config.tests import CreateConfigMixin +from openwisp_users.tests.utils import TestOrganizationMixin from owm_legacy.settings import ALLOWED_SUBNETS -class TestOwmLegacy(CreateConfigMixin, TestCase): +class TestOwmLegacy(CreateConfigMixin, TestOrganizationMixin, TestCase): """ tests for owm_legacy """ + config_model = Config device_model = Device def test_get_config_md5(self): c = self._create_config() - response = self.client.get(reverse('owm:get_config_md5', args=[c.mac_address])) - self.assertEqual(response['Content-Disposition'], 'attachment; filename={0}'.format(self.TEST_MAC_ADDRESS)) + response = self.client.get( + reverse('owm_legacy:get_config_md5', args=[c.mac_address]) + ) + self.assertEqual( + response['Content-Disposition'], + 'attachment; filename={0}'.format(self.TEST_MAC_ADDRESS), + ) self.assertEqual(len(response.content), 32) checksum1 = response.content sleep(1) - response = self.client.get(reverse('owm:get_config_md5', args=[c.mac_address])) + response = self.client.get( + reverse('owm_legacy:get_config_md5', args=[c.mac_address]) + ) checksum2 = response.content self.assertEqual(checksum1, checksum2) c.refresh_from_db() - self.assertIsNotNone(c.last_ip) + self.assertIsNotNone(c.device.last_ip) def test_get_config(self): d = self._create_device(name='test') c = self._create_config(device=d) - response = self.client.get(reverse('owm:get_config', args=[c.mac_address])) - self.assertEqual(response['Content-Disposition'], 'attachment; filename=test.tar.gz') + response = self.client.get( + reverse('owm_legacy:get_config', args=[c.mac_address]) + ) + self.assertEqual( + response['Content-Disposition'], 'attachment; filename=test.tar.gz' + ) def test_last_ip(self): c = self._create_config() - response = self.client.get(reverse('owm:get_config', args=[c.mac_address])) + self.client.get(reverse('owm_legacy:get_config', args=[c.mac_address])) c.refresh_from_db() - self.assertIsNotNone(c.last_ip) + self.assertIsNotNone(c.device.last_ip) def test_status(self): c = self._create_config() - response = self.client.get(reverse('owm:get_config', args=[c.mac_address])) + self.client.get(reverse('owm_legacy:get_config', args=[c.mac_address])) c.refresh_from_db() - self.assertEqual(c.status, 'running') + self.assertEqual(c.status, 'applied') def test_forbidden_ip(self): ALLOWED_SUBNETS.remove('127.0.0.1/32') - response = self.client.get(reverse('owm:get_config', args=['00:11:22:33:44:55'])) + response = self.client.get( + reverse('owm_legacy:get_config', args=['00:11:22:33:44:55']) + ) self.assertEqual(response.status_code, 403) ALLOWED_SUBNETS.append('127.0.0.1/32') diff --git a/owm_legacy/urls.py b/owm_legacy/urls.py index 8be25f7..667d120 100644 --- a/owm_legacy/urls.py +++ b/owm_legacy/urls.py @@ -5,10 +5,10 @@ app_name = "owm_legacy" urlpatterns = [ - url(r'^get_config/(?P[^/^.]+).md5$', + url( + r'^get_config/(?P[^/^.]+).md5$', views.get_config_md5, - name='get_config_md5'), - url(r'^get_config/(?P[^/^.]+)$', - views.get_config, - name='get_config'), + name='get_config_md5', + ), + url(r'^get_config/(?P[^/^.]+)$', views.get_config, name='get_config'), ] diff --git a/owm_legacy/views.py b/owm_legacy/views.py index 582bbbb..06a445d 100644 --- a/owm_legacy/views.py +++ b/owm_legacy/views.py @@ -1,20 +1,14 @@ -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.shortcuts import get_object_or_404 -from django_netjsonconfig.utils import send_config, send_file, update_last_ip +from openwisp_controller.config.models import Config +from openwisp_controller.config.utils import ( + send_device_config, + send_file, + update_last_ip, +) from .utils import forbid_unallowed -if 'django_netjsonconfig' in settings.INSTALLED_APPS: - from django_netjsonconfig.models import Config -elif 'openwisp_controller.config' in settings.INSTALLED_APPS: # pragma: nocover - from openwisp_controller.config.models import Config -else: # pragma: nocover - raise ImproperlyConfigured('django-owm-legacy depends on django-netjsonconfig or ' - 'openwisp_controller.config, but neither is present ' - 'in settings.INSTALLED_APPS') - def get_config_md5(request, mac_address): """ @@ -22,7 +16,7 @@ def get_config_md5(request, mac_address): """ forbid_unallowed(request) config = get_object_or_404(Config, device__mac_address__iexact=mac_address) - update_last_ip(config, request) + update_last_ip(config.device, request) return send_file(mac_address, config.checksum) @@ -32,5 +26,5 @@ def get_config(request, mac_address): """ forbid_unallowed(request) config = get_object_or_404(Config, device__mac_address__iexact=mac_address) - config.set_status_running(save=False) - return send_config(config, request) + config.set_status_applied() + return send_device_config(config, request) diff --git a/requirements-test.txt b/requirements-test.txt index dd7240b..05ffd16 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,3 @@ coveralls -openwisp-utils[qa]>=0.4.1 +openwisp-utils[qa]~=0.7.0 +django-redis diff --git a/requirements.txt b/requirements.txt index 81fd00b..50630e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -django-netjsonconfig>=0.8.0,<0.9.0 +openwisp-controller~=0.8 diff --git a/run-qa-checks b/run-qa-checks new file mode 100755 index 0000000..a7b5776 --- /dev/null +++ b/run-qa-checks @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +openwisp-qa-check \ + --skip-checkmigrations + diff --git a/runtests.py b/runtests.py index 86a831e..3a0d06d 100755 --- a/runtests.py +++ b/runtests.py @@ -5,10 +5,11 @@ import sys sys.path.insert(0, "tests") -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings") if __name__ == "__main__": from django.core.management import execute_from_command_line + args = sys.argv args.insert(1, "test") args.insert(2, "owm_legacy") diff --git a/setup.cfg b/setup.cfg index 3c6e79c..cf41613 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,20 @@ [bdist_wheel] universal=1 + +[isort] +known_third_party = django +known_first_party = openwisp_controller, openwisp_users +default_section = THIRDPARTY +line_length=88 +multi_line_output=3 +use_parentheses=True +include_trailing_comma=True +force_grid_wrap=0 + +[flake8] +# W503: line break before or after operator +# W504: line break after or after operator +# W605: invalid escape sequence +ignore = W605, W503, W504 +exclude = ./tests/*settings*.py +max-line-length = 88 diff --git a/setup.py b/setup.py index ae38d17..9e7078d 100755 --- a/setup.py +++ b/setup.py @@ -60,5 +60,5 @@ def get_install_requires(): 'Topic :: System :: Networking', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', - ] + ], ) diff --git a/tests/local_settings.example.py b/tests/local_settings.example.py index 602f896..949008b 100644 --- a/tests/local_settings.example.py +++ b/tests/local_settings.example.py @@ -1,7 +1,7 @@ # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS # BUT DO NOT COMMIT -#DATABASES = { +# DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': 'netjsonconfig.db', @@ -10,4 +10,4 @@ # 'HOST': '', # 'PORT': '' # }, -#} +# } diff --git a/tests/manage.py b/tests/manage.py index 9811d22..dd989d3 100755 --- a/tests/manage.py +++ b/tests/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'openwisp2.settings') from django.core.management import execute_from_command_line diff --git a/tests/openwisp2/__init__.py b/tests/openwisp2/__init__.py new file mode 100644 index 0000000..cd04264 --- /dev/null +++ b/tests/openwisp2/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ['celery_app'] diff --git a/tests/openwisp2/celery.py b/tests/openwisp2/celery.py new file mode 100644 index 0000000..a9ff385 --- /dev/null +++ b/tests/openwisp2/celery.py @@ -0,0 +1,11 @@ +import os + +from celery import Celery + +# Replace 'openwisp2.settings' with path to your settings.py should be relative +# from the location where celery command is executed. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'openwisp2.settings') + +app = Celery('openwisp2') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py new file mode 100644 index 0000000..5c634ce --- /dev/null +++ b/tests/openwisp2/settings.py @@ -0,0 +1,137 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +DEBUG = True + +ALLOWED_HOSTS = [] + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.spatialite', + 'NAME': 'owm-legacy.db', + } +} + +SECRET_KEY = 'fn)t*+$)ugeyip6-#txyy$5wf2ervc0d2n#h)qb)y5@ly$t*@w' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.gis', + 'django.contrib.sites', + # allauth + 'allauth', + 'allauth.account', + 'django_extensions', + # openwisp2 modules + 'openwisp_controller.config', + 'openwisp_controller.pki', + 'openwisp_controller.geo', + 'openwisp_controller.connection', + 'openwisp_users', + # openwisp2 admin theme + # (must be loaded here) + 'openwisp_utils.admin_theme', + 'django.contrib.admin', + 'django.forms', + # other dependencies + 'sortedm2m', + 'reversion', + 'leaflet', + 'flat_json_widget', + 'owm_legacy', +] + + +EXTENDED_APPS = ('django_x509', 'django_loci') + +AUTH_USER_MODEL = 'openwisp_users.User' +SITE_ID = 1 + +STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'openwisp_utils.staticfiles.DependencyFinder', +] + +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 = 'openwisp2.urls' + +TIME_ZONE = 'Europe/Rome' +LANGUAGE_CODE = 'en-gb' +USE_TZ = True +USE_I18N = False +USE_L10N = False +STATIC_URL = '/static/' +CORS_ORIGIN_ALLOW_ALL = True + + +ASGI_APPLICATION = 'openwisp_controller.geo.channels.routing.channel_routing' +CHANNEL_LAYERS = { + # in production you should use another channel layer backend + 'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}, +} + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'OPTIONS': { + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'openwisp_utils.loaders.DependencyLoader', + ], + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'openwisp_utils.admin_theme.context_processor.menu_items', + 'openwisp_notifications.context_processors.notification_api_settings', + ], + }, + } +] + +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + + +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://localhost/0', + 'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient',}, + } +} + +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' +SESSION_CACHE_ALIAS = 'default' + +CELERY_BROKER_URL = 'redis://localhost/1' + +# Workaround for stalled migrate command +CELERY_BROKER_TRANSPORT_OPTIONS = { + 'max_retries': 10, +} + + +# local settings must be imported before test runner +# otherwise they'll be ignored +try: + from local_settings import * +except ImportError: + pass diff --git a/tests/urls.py b/tests/openwisp2/urls.py similarity index 69% rename from tests/urls.py rename to tests/openwisp2/urls.py index c7d93bf..724e1a8 100644 --- a/tests/urls.py +++ b/tests/openwisp2/urls.py @@ -7,7 +7,8 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^', include('owm_legacy.urls', namespace='owm')), + url(r'', include('openwisp_controller.urls')), + url(r'^', include('owm_legacy.urls', namespace='owm_legacy')), ] urlpatterns += staticfiles_urlpatterns() diff --git a/tests/settings.py b/tests/settings.py deleted file mode 100644 index f10f636..0000000 --- a/tests/settings.py +++ /dev/null @@ -1,72 +0,0 @@ -import os - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - -DEBUG = True - -ALLOWED_HOSTS = [] - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'owm-legacy.db', - } -} - -SECRET_KEY = 'fn)t*+$)ugeyip6-#txyy$5wf2ervc0d2n#h)qb)y5@ly$t*@w' - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_x509', - 'django_netjsonconfig', - 'sortedm2m', - 'owm_legacy', - 'django.contrib.admin', -] - -MIDDLEWARE_CLASSES = [ - '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 = 'urls' - -TIME_ZONE = 'Europe/Rome' -LANGUAGE_CODE = 'en-gb' -USE_TZ = True -USE_I18N = False -USE_L10N = False -STATIC_URL = '/static/' -CORS_ORIGIN_ALLOW_ALL = True - -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', - ], - }, - }, -] - - -# local settings must be imported before test runner otherwise they'll be ignored -try: - from local_settings import * -except ImportError: - pass