Skip to content

Commit b1f6788

Browse files
authored
Merge pull request #625 from pierotofy/febsprint
February Sprint
2 parents 8ff4b91 + cf7e767 commit b1f6788

31 files changed

+980
-242
lines changed

README.md

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ A free, user-friendly, extendable application and [API](http://docs.webodm.org)
3030
* [Run it natively](#run-it-natively)
3131
* [Run it on the cloud (Google Compute, Amazon AWS)](#run-it-on-the-cloud-google-compute-amazon-aws)
3232

33+
![ezgif-1-c81c8daab2e0](https://user-images.githubusercontent.com/1951843/52976882-3db81d80-3399-11e9-8915-ffb00b062aaf.gif)
3334

34-
![Alt text](https://user-images.githubusercontent.com/1951843/28586405-af18e8cc-7141-11e7-9853-a7feca7c9c6b.gif)
35-
36-
![Alt text](/screenshots/pointcloud.png?raw=true "3D Display")
35+
![ezgif-1-4d8402e295f9](https://user-images.githubusercontent.com/1951843/52976888-43adfe80-3399-11e9-8bc6-1690806131d1.gif)
3736

3837

3938
## Getting Started
@@ -265,25 +264,12 @@ Developer, I'm looking to build an app that displays map results and takes care
265264
Developer, I'm looking to build an app that will stay behind a firewall and just needs raw results | [NodeODM](https://github.com/OpenDroneMap/NodeODM)
266265

267266
## Roadmap
268-
- [X] User Registration / Authentication
269-
- [X] UI mockup
270-
- [X] Task Processing
271-
- [X] 2D Map Display
272-
- [X] 3D Model Display
273-
- [ ] NDVI display
274-
- [X] Volumetric Measurements
275-
- [X] Cluster management and setup.
276-
- [ ] Mission Planner
277-
- [X] Plugins/Webhooks System
278-
- [X] API
279-
- [X] Documentation
280-
- [ ] Android Mobile App
281-
- [ ] iOS Mobile App
282-
- [ ] Processing Nodes Volunteer Network
283-
- [X] Unit Testing
284-
- [X] SSL Support
285-
286-
Don't see a feature that you want? [Help us make it happen](/CONTRIBUTING.md).
267+
268+
We follow a bottom-up approach to decide what new features are added to WebODM. User feedback guides us in the decision making process and we collect such feedback via [improvement requests](https://github.com/OpenDroneMap/WebODM/issues?q=is%3Aopen+is%3Aissue+label%3Aimprovements).
269+
270+
Don't see a feature that you want? [Open a feature request](https://github.com/OpenDroneMap/WebODM/issues) or [help us build it](/CONTRIBUTING.md).
271+
272+
Sometimes we also prioritize work that has received financial backing. If your organization is in the position to financially support the development of a particular feature, [get in touch](https://community.opendronemap.org) and we'll make it happen.
287273

288274
## Getting Help
289275

@@ -304,7 +290,7 @@ There are many ways to contribute back to the project:
304290
- ⭐️ us on GitHub.
305291
- Spread the word about WebODM and OpenDroneMap on social media.
306292
- While we don't accept donations, you can purchase an [installer](https://webodm.org/download#installer) or a [premium support package](https://webodm.org/services#premium-support).
307-
- Become a contributor (see below to get free swag 🤘)
293+
- Become a contributor 🤘
308294

309295
## Become a Contributor
310296

@@ -314,14 +300,10 @@ You don't necessarily need to be a developer to become a contributor. We can use
314300

315301
If you know how to code, we primarily use Python (Django), Javascript (React), HTML and SCSS. See the [Development Quickstart](http://docs.webodm.org/#development-quickstart) and [Contributing](/CONTRIBUTING.md) documents for more information.
316302

317-
To make a contribution, you will need to open a pull request ([here's how](https://github.com/Roshanjossey/first-contributions#fork-this-repository)). To make changes to WebODM, make a clone of the repository and run `./devenv.sh start`.
303+
To make a contribution, you will need to open a pull request ([here's how](https://github.com/Roshanjossey/first-contributions#fork-this-repository)). To make changes to WebODM, make a clone of the repository and run `./webodm.sh start --dev`.
318304

319305
If you have questions visit us on the [forum](http://community.opendronemap.org/c/webodm) and we'll be happy to help you out with your first contribution.
320306

321-
When your first pull request is accepted, don't forget to fill [this form](https://goo.gl/forms/PZkiPPeNKUHNz0qe2) to get your **free** WebODM T-Shirt 🤘
322-
323-
<img src="https://user-images.githubusercontent.com/1951843/36511023-344f86b2-1733-11e8-8cae-236645db407b.png" alt="T-Shirt" width="50%">
324-
325307
## Architecture Overview
326308

327309
WebODM is built with scalability and performance in mind. While the default setup places all databases and applications on the same machine, users can separate its components for increased performance (ex. place a Celery worker on a separate machine for running background tasks).

app/api/tasks.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,32 @@
22
from wsgiref.util import FileWrapper
33

44
import mimetypes
5+
6+
import datetime
57
from django.core.exceptions import ObjectDoesNotExist, SuspiciousFileOperation, ValidationError
68
from django.db import transaction
79
from django.http import FileResponse
810
from django.http import HttpResponse
911
from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers
1012
from rest_framework.decorators import detail_route
11-
from rest_framework.permissions import IsAuthenticatedOrReadOnly
13+
from rest_framework.permissions import AllowAny
1214
from rest_framework.response import Response
1315
from rest_framework.views import APIView
1416

1517
from app import models, pending_actions
18+
from nodeodm import status_codes
1619
from nodeodm.models import ProcessingNode
1720
from worker import tasks as worker_tasks
1821
from .common import get_and_check_project, get_tile_json, path_traversal_check
1922

2023

24+
def flatten_files(request_files):
25+
# MultiValueDict in, flat array of files out
26+
return [file for filesList in map(
27+
lambda key: request_files.getlist(key),
28+
[keys for keys in request_files])
29+
for file in filesList]
30+
2131
class TaskIDsSerializer(serializers.BaseSerializer):
2232
def to_representation(self, obj):
2333
return obj.id
@@ -26,7 +36,6 @@ class TaskSerializer(serializers.ModelSerializer):
2636
project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all())
2737
processing_node = serializers.PrimaryKeyRelatedField(queryset=ProcessingNode.objects.all())
2838
processing_node_name = serializers.SerializerMethodField()
29-
images_count = serializers.SerializerMethodField()
3039
can_rerun_from = serializers.SerializerMethodField()
3140

3241
def get_processing_node_name(self, obj):
@@ -35,9 +44,6 @@ def get_processing_node_name(self, obj):
3544
else:
3645
return None
3746

38-
def get_images_count(self, obj):
39-
return obj.imageupload_set.count()
40-
4147
def get_can_rerun_from(self, obj):
4248
"""
4349
When a task has been associated with a processing node
@@ -142,11 +148,7 @@ def retrieve(self, request, pk=None, project_pk=None):
142148
def create(self, request, project_pk=None):
143149
project = get_and_check_project(request, project_pk, ('change_project', ))
144150

145-
# MultiValueDict in, flat array of files out
146-
files = [file for filesList in map(
147-
lambda key: request.FILES.getlist(key),
148-
[keys for keys in request.FILES])
149-
for file in filesList]
151+
files = flatten_files(request.FILES)
150152

151153
if len(files) <= 1:
152154
raise exceptions.ValidationError(detail="Cannot create task, you need at least 2 images")
@@ -157,6 +159,7 @@ def create(self, request, project_pk=None):
157159

158160
for image in files:
159161
models.ImageUpload.objects.create(task=task, image=image)
162+
task.images_count = len(files)
160163

161164
# Update other parameters such as processing node, task name, etc.
162165
serializer = TaskSerializer(task, data=request.data, partial=True)
@@ -198,7 +201,7 @@ def partial_update(self, request, *args, **kwargs):
198201

199202
class TaskNestedView(APIView):
200203
queryset = models.Task.objects.all().defer('orthophoto_extent', 'dtm_extent', 'dsm_extent', 'console_output', )
201-
permission_classes = (IsAuthenticatedOrReadOnly, )
204+
permission_classes = (AllowAny, )
202205

203206
def get_and_check_task(self, request, pk, annotate={}):
204207
try:
@@ -322,3 +325,43 @@ def get(self, request, pk=None, project_pk=None, unsafe_asset_path=""):
322325
raise exceptions.NotFound("Asset does not exist")
323326

324327
return download_file_response(request, asset_path, 'inline')
328+
329+
"""
330+
Task assets import
331+
"""
332+
class TaskAssetsImport(APIView):
333+
permission_classes = (permissions.AllowAny,)
334+
parser_classes = (parsers.MultiPartParser, parsers.JSONParser, parsers.FormParser,)
335+
336+
def post(self, request, project_pk=None):
337+
project = get_and_check_project(request, project_pk, ('change_project',))
338+
339+
files = flatten_files(request.FILES)
340+
import_url = request.data.get('url', None)
341+
task_name = request.data.get('name', 'Imported Task')
342+
343+
if not import_url and len(files) != 1:
344+
raise exceptions.ValidationError(detail="Cannot create task, you need to upload 1 file")
345+
346+
if import_url and len(files) > 0:
347+
raise exceptions.ValidationError(detail="Cannot create task, either specify a URL or upload 1 file.")
348+
349+
with transaction.atomic():
350+
task = models.Task.objects.create(project=project,
351+
auto_processing_node=False,
352+
name=task_name,
353+
import_url=import_url if import_url else "file://all.zip",
354+
status=status_codes.RUNNING,
355+
pending_action=pending_actions.IMPORT)
356+
task.create_task_directories()
357+
358+
if len(files) > 0:
359+
destination_file = task.assets_path("all.zip")
360+
with open(destination_file, 'wb+') as fd:
361+
for chunk in files[0].chunks():
362+
fd.write(chunk)
363+
364+
worker_tasks.process_task.delay(task.id)
365+
366+
serializer = TaskSerializer(task)
367+
return Response(serializer.data, status=status.HTTP_201_CREATED)

app/api/urls.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from app.api.presets import PresetViewSet
44
from app.plugins import get_api_url_patterns
55
from .projects import ProjectViewSet
6-
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets
6+
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets, TaskAssetsImport
77
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
88
from rest_framework_nested import routers
99
from rest_framework_jwt.views import obtain_jwt_token
@@ -25,10 +25,9 @@
2525

2626
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', TaskTiles.as_view()),
2727
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles\.json$', TaskTilesJson.as_view()),
28-
2928
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
30-
3129
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
30+
url(r'projects/(?P<project_pk>[^/.]+)/tasks/import$', TaskAssetsImport.as_view()),
3231

3332
url(r'^auth/', include('rest_framework.urls')),
3433
url(r'^token-auth/', obtain_jwt_token),
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Generated by Django 2.1.5 on 2019-02-20 18:54
2+
3+
import app.models.task
4+
import colorfield.fields
5+
import django.contrib.postgres.fields
6+
import django.contrib.postgres.fields.jsonb
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('app', '0024_update_task_assets'),
14+
]
15+
16+
operations = [
17+
migrations.AddField(
18+
model_name='task',
19+
name='import_url',
20+
field=models.TextField(blank=True, default='',
21+
help_text='URL this task is imported from (only for imported tasks)'),
22+
),
23+
migrations.AlterField(
24+
model_name='preset',
25+
name='options',
26+
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list,
27+
help_text="Options that define this preset (same format as in a Task's options).",
28+
validators=[app.models.task.validate_task_options]),
29+
),
30+
migrations.AlterField(
31+
model_name='task',
32+
name='available_assets',
33+
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=80), blank=True,
34+
default=list,
35+
help_text='List of available assets to download',
36+
size=None),
37+
),
38+
migrations.AlterField(
39+
model_name='task',
40+
name='options',
41+
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict,
42+
help_text='Options that are being used to process this task',
43+
validators=[app.models.task.validate_task_options]),
44+
),
45+
migrations.AlterField(
46+
model_name='task',
47+
name='pending_action',
48+
field=models.IntegerField(blank=True, choices=[(1, 'CANCEL'), (2, 'REMOVE'), (3, 'RESTART'), (4, 'RESIZE'),
49+
(5, 'IMPORT')], db_index=True,
50+
help_text='A requested action to be performed on the task. The selected action will be performed by the worker at the next iteration.',
51+
null=True),
52+
),
53+
migrations.AlterField(
54+
model_name='theme',
55+
name='header_background',
56+
field=colorfield.fields.ColorField(default='#3498db', help_text="Background color of the site's header.",
57+
max_length=18),
58+
),
59+
migrations.AlterField(
60+
model_name='theme',
61+
name='tertiary',
62+
field=colorfield.fields.ColorField(default='#3498db', help_text='Navigation links.', max_length=18),
63+
),
64+
migrations.AddField(
65+
model_name='task',
66+
name='images_count',
67+
field=models.IntegerField(blank=True, default=0, help_text='Number of images associated with this task'),
68+
),
69+
]

0 commit comments

Comments
 (0)