diff --git a/app/migrations/0020_plugindatum.py b/app/migrations/0020_plugindatum.py new file mode 100644 index 00000000..5ba16d63 --- /dev/null +++ b/app/migrations/0020_plugindatum.py @@ -0,0 +1,30 @@ +# Generated by Django 2.0.3 on 2018-07-24 21:01 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('app', '0019_remove_task_processing_lock'), + ] + + operations = [ + migrations.CreateModel( + name='PluginDatum', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, help_text='Setting key', max_length=255)), + ('int_value', models.IntegerField(blank=True, default=None, help_text='Integer value', null=True)), + ('float_value', models.FloatField(blank=True, default=None, help_text='Float value', null=True)), + ('bool_value', models.NullBooleanField(default=None, help_text='Bool value')), + ('string_value', models.TextField(blank=True, default=None, help_text='String value', null=True)), + ('json_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='JSON value', null=True)), + ('user', models.ForeignKey(help_text='The user this setting belongs to. If NULL, the setting is global.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app/models/__init__.py b/app/models/__init__.py index 7b510c73..677002ef 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,4 +4,5 @@ from .task import Task, validate_task_options, gcp_directory_path from .preset import Preset from .theme import Theme from .setting import Setting +from .plugin_datum import PluginDatum diff --git a/app/models/plugin_datum.py b/app/models/plugin_datum.py new file mode 100644 index 00000000..b60d03d3 --- /dev/null +++ b/app/models/plugin_datum.py @@ -0,0 +1,18 @@ +import logging +from django.db import models +from django.contrib.postgres import fields +from django.contrib.auth.models import User + +logger = logging.getLogger('app.logger') + +class PluginDatum(models.Model): + key = models.CharField(max_length=255, help_text="Setting key", db_index=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, help_text="The user this setting belongs to. If NULL, the setting is global.") + int_value = models.IntegerField(blank=True, null=True, default=None, help_text="Integer value") + float_value = models.FloatField(blank=True, null=True, default=None, help_text="Float value") + bool_value = models.NullBooleanField(blank=True, null=True, default=None, help_text="Bool value") + string_value = models.TextField(blank=True, null=True, default=None, help_text="String value") + json_value = fields.JSONField(default=None, blank=True, null=True, help_text="JSON value") + + def __str__(self): + return self.key diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py index 8bc5b80f..22666a83 100644 --- a/app/plugins/__init__.py +++ b/app/plugins/__init__.py @@ -1,3 +1,4 @@ +from .data_store import UserDataStore, GlobalDataStore from .plugin_base import PluginBase from .menu import Menu from .mount_point import MountPoint diff --git a/app/plugins/data_store.py b/app/plugins/data_store.py new file mode 100644 index 00000000..e8926cb4 --- /dev/null +++ b/app/plugins/data_store.py @@ -0,0 +1,77 @@ +from abc import ABC +from django.core.exceptions import MultipleObjectsReturned +from app.models import PluginDatum +import logging + +logger = logging.getLogger('app.logger') + +class DataStore(ABC): + def __init__(self, namespace, user=None): + """ + :param namespace: Namespace (typically the plugin's name) to use for this datastore + :param user: User tied to this datastore. If None, this is a global data store + """ + self.namespace = namespace + self.user = user + + def db_key(self, key): + return "{}::{}".format(self.namespace, key) + + def get_datum(self, key): + return PluginDatum.objects.filter(key=self.db_key(key), user=self.user).first() + + def set_value(self, type, key, value): + try: + return PluginDatum.objects.update_or_create(key=self.db_key(key), + user=self.user, + defaults={type + '_value': value}) + except MultipleObjectsReturned: + # 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() + + def get_value(self, type, key, default=None): + datum = self.get_datum(key) + return default if datum is None else getattr(datum, type + '_value') + + def get_string(self, key, default=""): + return self.get_value('string', key, default) + + def set_string(self, key, value): + return self.set_value('string', key, value) + + def get_int(self, key, default=0): + return self.get_value('int', key, default) + + def set_int(self, key, value): + return self.set_value('int', key, value) + + def get_float(self, key, default=0.0): + return self.get_value('float', key, default) + + def set_float(self, key, value): + return self.set_value('float', key, value) + + def get_bool(self, key, default=False): + return self.get_value('bool', key, default) + + def set_bool(self, key, value): + return self.set_value('bool', key, value) + + def get_json(self, key, default={}): + return self.get_value('json', key, default) + + def set_json(self, key, value): + return self.set_value('json', key, value) + + def has_key(self, key): + return self.get_datum(key) is not None + + +class UserDataStore(DataStore): + def __init__(self, namespace, user): + super().__init__(namespace, user) + + +class GlobalDataStore(DataStore): + pass diff --git a/app/plugins/mount_point.py b/app/plugins/mount_point.py index e6cf4f2b..0509eedf 100644 --- a/app/plugins/mount_point.py +++ b/app/plugins/mount_point.py @@ -3,7 +3,6 @@ import re class MountPoint: def __init__(self, url, view, *args, **kwargs): """ - :param url: path to mount this view to, relative to plugins directory :param view: Django/DjangoRestFramework view :param args: extra args to pass to url() call diff --git a/app/plugins/plugin_base.py b/app/plugins/plugin_base.py index 83244aa9..a28c1fb9 100644 --- a/app/plugins/plugin_base.py +++ b/app/plugins/plugin_base.py @@ -1,5 +1,6 @@ import logging, os, sys from abc import ABC +from app.plugins import UserDataStore, GlobalDataStore logger = logging.getLogger('app.logger') @@ -23,6 +24,22 @@ class PluginBase(ABC): """ return self.name + def get_user_data_store(self, user): + """ + Helper function to instantiate a user data store associated + with this plugin + :return: UserDataStore + """ + return UserDataStore(self.get_name(), user) + + def get_global_data_store(self, user): + """ + Helper function to instantiate a user data store associated + with this plugin + :return: GlobalDataStore + """ + return GlobalDataStore(self.get_name()) + def get_module_name(self): return self.__class__.__module__ diff --git a/app/plugins/templates/form.html b/app/plugins/templates/form.html new file mode 100644 index 00000000..793f341a --- /dev/null +++ b/app/plugins/templates/form.html @@ -0,0 +1,14 @@ +{% load bootstrap_extras %} + +{% for field in form %} +
+ + {{ field|with_class:'form-control' }} + {% if field.errors %} + {{ field.errors|join:'
' }}
+ {% endif %} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} +
+{% endfor %} \ No newline at end of file diff --git a/package.json b/package.json index d76f627e..470f0e4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "0.5.2", + "version": "0.5.3", "description": "Open Source Drone Image Processing", "main": "index.js", "scripts": { diff --git a/plugins/openaerialmap/__init__.py b/plugins/openaerialmap/__init__.py new file mode 100644 index 00000000..48aad58e --- /dev/null +++ b/plugins/openaerialmap/__init__.py @@ -0,0 +1 @@ +from .plugin import * diff --git a/plugins/openaerialmap/manifest.json b/plugins/openaerialmap/manifest.json new file mode 100644 index 00000000..009a043c --- /dev/null +++ b/plugins/openaerialmap/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "OpenAerialMap", + "webodmMinVersion": "0.5.3", + "description": "A plugin to upload orthophotos to OpenAerialMap", + "version": "0.1.0", + "author": "Piero Toffanin", + "email": "pt@masseranolabs.com", + "repository": "https://github.com/OpenDroneMap/WebODM", + "tags": ["oam", "openaerialmap"], + "homepage": "https://github.com/OpenDroneMap/WebODM", + "experimental": true, + "deprecated": false +} \ No newline at end of file diff --git a/plugins/openaerialmap/plugin.py b/plugins/openaerialmap/plugin.py new file mode 100644 index 00000000..096a1359 --- /dev/null +++ b/plugins/openaerialmap/plugin.py @@ -0,0 +1,45 @@ +from django.contrib import messages +from django.shortcuts import render + +from app.plugins import PluginBase, Menu, MountPoint +from django.contrib.auth.decorators import login_required +from django import forms + +class TokenForm(forms.Form): + token = forms.CharField(label='', required=False, max_length=1024, widget=forms.TextInput(attrs={'placeholder': 'Token'})) + + +class Plugin(PluginBase): + + def main_menu(self): + return [Menu("OpenAerialMap", self.public_url(""), "oam-icon fa fa-fw")] + + def include_css_files(self): + return ['style.css'] + + def app_mount_points(self): + return [ + MountPoint('$', self.home_view()) + ] + + def home_view(self): + @login_required + def home(request): + ds = self.get_user_data_store(request.user) + + # if this is a POST request we need to process the form data + if request.method == 'POST': + form = TokenForm(request.POST) + if form.is_valid(): + ds.set_string('token', form.cleaned_data['token']) + messages.success(request, 'Token updated. Tasks can now be shared to OpenAerialMap.') + + form = TokenForm(initial={'token': ds.get_string('token', default="")}) + + return render(request, self.template_path("app.html"), { + 'title': 'OpenAerialMap', + 'form': form + }) + + return home + diff --git a/plugins/openaerialmap/public/fonts/oamfont.eot b/plugins/openaerialmap/public/fonts/oamfont.eot new file mode 100644 index 00000000..7613984a Binary files /dev/null and b/plugins/openaerialmap/public/fonts/oamfont.eot differ diff --git a/plugins/openaerialmap/public/fonts/oamfont.svg b/plugins/openaerialmap/public/fonts/oamfont.svg new file mode 100644 index 00000000..9d7b7d5a --- /dev/null +++ b/plugins/openaerialmap/public/fonts/oamfont.svg @@ -0,0 +1,11 @@ + + + +Generated by IcoMoon + + + + + + + \ No newline at end of file diff --git a/plugins/openaerialmap/public/fonts/oamfont.ttf b/plugins/openaerialmap/public/fonts/oamfont.ttf new file mode 100644 index 00000000..bbd57acd Binary files /dev/null and b/plugins/openaerialmap/public/fonts/oamfont.ttf differ diff --git a/plugins/openaerialmap/public/fonts/oamfont.woff b/plugins/openaerialmap/public/fonts/oamfont.woff new file mode 100644 index 00000000..d5cd54d9 Binary files /dev/null and b/plugins/openaerialmap/public/fonts/oamfont.woff differ diff --git a/plugins/openaerialmap/public/style.css b/plugins/openaerialmap/public/style.css new file mode 100644 index 00000000..d6d29ac8 --- /dev/null +++ b/plugins/openaerialmap/public/style.css @@ -0,0 +1,37 @@ +@font-face { + font-family: 'oamfont'; + src: url('fonts/oamfont.eot?7ho5xe'); + src: url('fonts/oamfont.eot?7ho5xe#iefix') format('embedded-opentype'), + url('fonts/oamfont.ttf?7ho5xe') format('truetype'), + url('fonts/oamfont.woff?7ho5xe') format('woff'), + url('fonts/oamfont.svg?7ho5xe#oamfont') format('svg'); + font-weight: normal; + font-style: normal; +} + +.oam-icon { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'oamfont' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.oam-icon:before { + content: "\e900"; +} + +.oam-form{ + margin-top: 16px; +} + +.oam-token-form label{ + display: none; +} \ No newline at end of file diff --git a/plugins/openaerialmap/templates/app.html b/plugins/openaerialmap/templates/app.html new file mode 100644 index 00000000..14e7abb3 --- /dev/null +++ b/plugins/openaerialmap/templates/app.html @@ -0,0 +1,31 @@ +{% extends "app/plugins/templates/base.html" %} + + +{% block content %} +

OpenAerialMap

+

OpenAerialMap (OAM) is a set of tools for searching, sharing, and using openly licensed satellite and unmanned aerial vehicle (UAV) imagery.

+ + {% if not form.token.value %} +

To share your results with OAM:

+
    +
  1. Sign-in from map.openaerialmap.org.
  2. +
  3. Navigate to your OAM profile page (click your user's avatar) and press the Request 3rd Party API Token button.
  4. +
  5. Copy and paste the token in the form below.
  6. +
+ {% else %} +

Go To OpenAerialMap

+ {% endif %} + +
+

Token Settings

+ {% csrf_token %} + {% include "app/plugins/templates/form.html" %} + +
+ + +{% endblock %} \ No newline at end of file