kopia lustrzana https://github.com/OpenDroneMap/WebODM
Unit tests, minor fixes
rodzic
e7142a6e8d
commit
68a4dd6dbb
|
@ -1,5 +1,5 @@
|
|||
from abc import ABC
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
||||
from app.models import PluginDatum
|
||||
import logging
|
||||
|
||||
|
@ -29,6 +29,8 @@ class DataStore(ABC):
|
|||
# This should never happen
|
||||
logger.warning("A plugin data store for the {} plugin returned multiple objects. This is potentially bad. The plugin developer needs to fix this! The data store will not be changed.".format(self.namespace))
|
||||
PluginDatum.objects.filter(key=self.db_key(key), user=self.user).delete()
|
||||
except ValidationError as e:
|
||||
raise InvalidDataStoreValue(e)
|
||||
|
||||
def get_value(self, type, key, default=None):
|
||||
datum = self.get_datum(key)
|
||||
|
@ -83,3 +85,7 @@ class UserDataStore(DataStore):
|
|||
|
||||
class GlobalDataStore(DataStore):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDataStoreValue(Exception):
|
||||
pass
|
|
@ -170,7 +170,10 @@ def get_dynamic_script_handler(script_path, callback=None, **kwargs):
|
|||
|
||||
with open(script_path) as f:
|
||||
tmpl = Template(f.read())
|
||||
return HttpResponse(tmpl.substitute(template_params))
|
||||
try:
|
||||
return HttpResponse(tmpl.substitute(template_params))
|
||||
except TypeError as e:
|
||||
return HttpResponse("Template substitution failed with params: {}. {}".format(str(template_params), e))
|
||||
|
||||
return handleRequest
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class PluginBase(ABC):
|
|||
"""
|
||||
return UserDataStore(self.get_name(), user)
|
||||
|
||||
def get_global_data_store(self, user):
|
||||
def get_global_data_store(self):
|
||||
"""
|
||||
Helper function to instantiate a user data store associated
|
||||
with this plugin
|
||||
|
|
|
@ -1 +1 @@
|
|||
from app.api.tasks import TaskNestedView as TaskView
|
||||
from app.api.tasks import TaskNestedView as TaskView
|
||||
|
|
|
@ -14,6 +14,14 @@ export default class ApiFactory{
|
|||
// We could just use events, but methods
|
||||
// are more robust as we can detect more easily if
|
||||
// things break
|
||||
|
||||
// TODO: we should consider refactoring this code
|
||||
// to use functions instead of events. Originally
|
||||
// we chose to use events because that would have
|
||||
// decreased coupling, but since all API pubsub activity
|
||||
// evolved to require a call to the PluginsAPI object, we might have
|
||||
// added a bunch of complexity for no real advantage here.
|
||||
|
||||
const addEndpoint = (obj, eventName, preTrigger = () => {}) => {
|
||||
const emitResponse = response => {
|
||||
// Timeout needed for modules that have no dependencies
|
||||
|
|
|
@ -16,11 +16,12 @@ import worker
|
|||
from django.utils import timezone
|
||||
from app.models import Project, Task, ImageUpload
|
||||
from app.models.task import task_directory_path, full_task_directory_path
|
||||
from app.plugins.signals import task_completed, task_removed, task_removing
|
||||
from app.tests.classes import BootTransactionTestCase
|
||||
from nodeodm import status_codes
|
||||
from nodeodm.models import ProcessingNode, OFFLINE_MINUTES
|
||||
from app.testwatch import testWatch
|
||||
from .utils import start_processing_node, clear_test_media_root
|
||||
from .utils import start_processing_node, clear_test_media_root, catch_signal
|
||||
|
||||
# We need to test the task API in a TransactionTestCase because
|
||||
# task processing happens on a separate thread, and normal TestCases
|
||||
|
@ -279,9 +280,17 @@ class TestApiTask(BootTransactionTestCase):
|
|||
time.sleep(DELAY)
|
||||
|
||||
# Calling process pending tasks should finish the process
|
||||
worker.tasks.process_pending_tasks()
|
||||
task.refresh_from_db()
|
||||
self.assertTrue(task.status == status_codes.COMPLETED)
|
||||
# and invoke the plugins completed signal
|
||||
with catch_signal(task_completed) as handler:
|
||||
worker.tasks.process_pending_tasks()
|
||||
task.refresh_from_db()
|
||||
self.assertTrue(task.status == status_codes.COMPLETED)
|
||||
|
||||
handler.assert_called_with(
|
||||
sender=Task,
|
||||
task_id=task.id,
|
||||
signal=task_completed,
|
||||
)
|
||||
|
||||
# Can download assets
|
||||
for asset in list(task.ASSETS_MAP.keys()):
|
||||
|
@ -362,9 +371,15 @@ class TestApiTask(BootTransactionTestCase):
|
|||
task.refresh_from_db()
|
||||
self.assertTrue(task.status == status_codes.CANCELED)
|
||||
|
||||
# Remove a task
|
||||
res = client.post("/api/projects/{}/tasks/{}/remove/".format(project.id, task.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||
# Remove a task and verify that it calls the proper plugins signals
|
||||
with catch_signal(task_removing) as h1:
|
||||
with catch_signal(task_removed) as h2:
|
||||
res = client.post("/api/projects/{}/tasks/{}/remove/".format(project.id, task.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||
|
||||
h1.assert_called_once_with(sender=Task, task_id=task.id, signal=task_removing)
|
||||
h2.assert_called_once_with(sender=Task, task_id=task.id, signal=task_removed)
|
||||
|
||||
# task is processed right away
|
||||
|
||||
# Has been removed along with assets
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import os
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client
|
||||
from rest_framework import status
|
||||
|
||||
from app.models import Project
|
||||
from app.models import Task
|
||||
from app.plugins import UserDataStore
|
||||
from app.plugins import get_plugin_by_name
|
||||
from app.plugins.data_store import InvalidDataStoreValue
|
||||
from .classes import BootTestCase
|
||||
from app.plugins.grass_engine import grass, GrassEngineException
|
||||
|
||||
|
@ -28,6 +33,9 @@ class TestPlugins(BootTestCase):
|
|||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTemplateUsed(res, 'plugins/test/templates/app.html')
|
||||
|
||||
# Form was rendered correctly
|
||||
self.assertContains(res, '<input type="text" name="testField" class="form-control" required id="id_testField" />', count=1, status_code=200, html=True)
|
||||
|
||||
# It uses regex properly
|
||||
res = client.get('/plugins/test/app_mountpoint/a')
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
@ -48,6 +56,37 @@ class TestPlugins(BootTestCase):
|
|||
test_plugin = get_plugin_by_name("test")
|
||||
self.assertTrue(os.path.exists(test_plugin.get_path("public/node_modules")))
|
||||
|
||||
# A webpack file and build directory have been created as a
|
||||
# result of the build_jsx_components directive
|
||||
self.assertTrue(os.path.exists(test_plugin.get_path("public/webpack.config.js")))
|
||||
self.assertTrue(os.path.exists(test_plugin.get_path("public/build")))
|
||||
self.assertTrue(os.path.exists(test_plugin.get_path("public/build/component.js")))
|
||||
|
||||
# Test task view
|
||||
user = User.objects.get(username="testuser")
|
||||
project = Project.objects.get(owner=user)
|
||||
task = Task.objects.create(project=project, name="Test")
|
||||
client.logout()
|
||||
|
||||
# Cannot see the task view without logging-in
|
||||
res = client.get('/plugins/test/task/{}/'.format(task.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
client.login(username='testuser', password='test1234')
|
||||
res = client.get('/plugins/test/task/{}/'.format(task.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertContains(res, str(task.id))
|
||||
|
||||
# Test dynamic script
|
||||
res = client.get('/plugins/test/app_dynamic_script.js')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(res.content.decode('utf-8') == '') # Empty
|
||||
|
||||
res = client.get('/plugins/test/app_dynamic_script.js?print=1')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(res.content.decode('utf-8') == "console.log('Hello WebODM');") # Empty
|
||||
|
||||
|
||||
def test_grass_engine(self):
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
grass_scripts_dir = os.path.join(cwd, "grass_scripts")
|
||||
|
@ -86,3 +125,63 @@ class TestPlugins(BootTestCase):
|
|||
|
||||
with self.assertRaises(GrassEngineException):
|
||||
ctx.execute(os.path.join(grass_scripts_dir, "nonexistant_script.grass"))
|
||||
|
||||
|
||||
def test_plugin_datastore(self):
|
||||
test_plugin = get_plugin_by_name("test")
|
||||
user = User.objects.get(username='testuser')
|
||||
other_user = User.objects.get(username='testuser2')
|
||||
|
||||
uds = test_plugin.get_user_data_store(user)
|
||||
other_uds = test_plugin.get_user_data_store(other_user)
|
||||
gds = test_plugin.get_global_data_store()
|
||||
|
||||
# No key
|
||||
self.assertFalse(uds.has_key('mykey'))
|
||||
|
||||
# Default value works
|
||||
self.assertTrue(uds.get_string('mykey', 'default') == 'default')
|
||||
|
||||
# Still no key should have been added
|
||||
self.assertFalse(uds.has_key('mykey'))
|
||||
|
||||
# Add key
|
||||
(object, created) = uds.set_string('mykey', 'value')
|
||||
self.assertTrue(object.string_value == 'value')
|
||||
self.assertTrue(created)
|
||||
self.assertTrue(uds.has_key('mykey'))
|
||||
|
||||
# Key is not visible in global datastore
|
||||
self.assertFalse(gds.has_key('mykey'))
|
||||
|
||||
# Key is not visible in another user's data store
|
||||
self.assertFalse(other_uds.has_key('mykey'))
|
||||
|
||||
# Key is not visible in another's plugin data store
|
||||
# for the same user
|
||||
other_puds = UserDataStore('test2', user)
|
||||
self.assertFalse(other_puds.has_key('mykey'))
|
||||
|
||||
# Deleting a non-existing key return False
|
||||
self.assertFalse(uds.del_key('nonexistant'))
|
||||
|
||||
# Deleting a valid key returns True
|
||||
self.assertTrue(uds.del_key('mykey'))
|
||||
self.assertFalse(uds.has_key('mykey'))
|
||||
|
||||
# Various data types setter/getter work
|
||||
uds.set_int('myint', 5)
|
||||
self.assertTrue(uds.get_int('myint') == 5)
|
||||
|
||||
uds.set_float('myfloat', 10.0)
|
||||
self.assertTrue(uds.get_float('myfloat', 50.0) == 10.0)
|
||||
|
||||
uds.set_bool('mybool', True)
|
||||
self.assertTrue(uds.get_bool('mybool'))
|
||||
|
||||
uds.set_json('myjson', {'test': 123})
|
||||
self.assertTrue('test' in uds.get_json('myjson'))
|
||||
|
||||
# Invalid types
|
||||
self.assertRaises(InvalidDataStoreValue, uds.set_bool, 'invalidbool', 5)
|
||||
|
||||
|
|
|
@ -6,6 +6,11 @@ import subprocess
|
|||
|
||||
import logging
|
||||
|
||||
from unittest import mock
|
||||
from contextlib import contextmanager
|
||||
|
||||
import random
|
||||
|
||||
from webodm import settings
|
||||
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
@ -26,4 +31,13 @@ def clear_test_media_root():
|
|||
logger.info("Cleaning up {}".format(settings.MEDIA_ROOT))
|
||||
shutil.rmtree(settings.MEDIA_ROOT)
|
||||
else:
|
||||
logger.warning("We did not remove MEDIA_ROOT because we couldn't find a _test suffix in its path.")
|
||||
logger.warning("We did not remove MEDIA_ROOT because we couldn't find a _test suffix in its path.")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_signal(signal):
|
||||
"""Catch django signal and return the mocked call."""
|
||||
handler = mock.Mock()
|
||||
signal.connect(handler, dispatch_uid=str(random.random()))
|
||||
yield handler
|
||||
signal.disconnect(handler)
|
|
@ -1,6 +1,5 @@
|
|||
PluginsAPI.Dashboard.addTaskActionButton([
|
||||
'openaerialmap/build/ShareButton.js',
|
||||
'openaerialmap/build/ShareButton.css'
|
||||
'openaerialmap/build/ShareButton.js'
|
||||
],function(args, ShareButton){
|
||||
var task = args.task;
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class Plugin(PluginBase):
|
|||
|
||||
return [
|
||||
MountPoint('$', self.home_view()),
|
||||
MountPoint('main.js', self.get_dynamic_script(
|
||||
MountPoint('main.js$', self.get_dynamic_script(
|
||||
'load_buttons.js',
|
||||
load_buttons_cb
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './ShareButton.scss';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ShareDialog from './ShareDialog';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.oam-share-button{
|
||||
|
||||
}
|
|
@ -26,10 +26,4 @@
|
|||
{% include "app/plugins/templates/form.html" %}
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-save fa-fw"></i> Set Token</i></button>
|
||||
</form>
|
||||
|
||||
<!--{% if form.token.value %}
|
||||
<div class="oam-form">
|
||||
<h4>Advanced Settings</h4>
|
||||
</div>
|
||||
{% endif %}-->
|
||||
{% endblock %}
|
|
@ -0,0 +1 @@
|
|||
console.log('Hello ${name}');
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "POSM GCP Interface",
|
||||
"webodmMinVersion": "0.5.0",
|
||||
"webodmMinVersion": "0.6.0",
|
||||
"description": "A plugin to create GCP files from images",
|
||||
"version": "0.1.0",
|
||||
"author": "Piero Toffanin",
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from app.plugins import PluginBase, Menu, MountPoint
|
||||
from app.plugins.views import TaskView
|
||||
from django.shortcuts import render
|
||||
from django import forms
|
||||
|
||||
class TestForm(forms.Form):
|
||||
testField = forms.CharField(label='Test')
|
||||
|
||||
|
||||
class TestTaskView(TaskView):
|
||||
def get(self, request, pk=None):
|
||||
task = self.get_and_check_task(request, pk)
|
||||
return Response(task.id, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
|
||||
|
@ -12,9 +27,24 @@ class Plugin(PluginBase):
|
|||
def include_css_files(self):
|
||||
return ['test.css']
|
||||
|
||||
def build_jsx_components(self):
|
||||
return ['component.jsx']
|
||||
|
||||
def app_mount_points(self):
|
||||
# Show script only if '?print=1' is set
|
||||
def dynamic_cb(request):
|
||||
if 'print' in request.GET:
|
||||
return {'name': 'WebODM'} # Test template substitution
|
||||
else:
|
||||
return False
|
||||
|
||||
return [
|
||||
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {'title': 'Test'}))
|
||||
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {
|
||||
'title': 'Test',
|
||||
'test_form': TestForm()
|
||||
})),
|
||||
MountPoint('task/(?P<pk>[^/.]+)/', TestTaskView.as_view()),
|
||||
MountPoint('/app_dynamic_script.js$', self.get_dynamic_script('dynamic.js', dynamic_cb))
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react'; // can import React
|
||||
|
||||
module.exports = {
|
||||
es6func: () => {} // ES6 transpiler works
|
||||
};
|
|
@ -2,4 +2,10 @@
|
|||
|
||||
{% block content %}
|
||||
Hello world!
|
||||
|
||||
<form>
|
||||
{% csrf_token %}
|
||||
{% include "app/plugins/templates/form.html" with form=test_form %}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
{% endblock %}
|
Ładowanie…
Reference in New Issue