Sharing feature working, still needs unit testing

pull/357/head
Piero Toffanin 2017-12-03 17:56:30 -05:00
rodzic 39c391451f
commit 2bc61c2633
27 zmienionych plików z 256 dodań i 67 usunięć

Wyświetl plik

@ -9,6 +9,7 @@ from django.db.models.functions import Cast
from django.http import HttpResponse
from wsgiref.util import FileWrapper
from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework.decorators import detail_route
from rest_framework.views import APIView
@ -36,7 +37,7 @@ class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = models.Task
exclude = ('processing_lock', 'console_output', 'orthophoto_extent', 'dsm_extent', 'dtm_extent', )
read_only_fields = ('processing_time', 'status', 'last_error', 'created_at', 'pending_action', 'available_assets', 'public_uuid', )
read_only_fields = ('processing_time', 'status', 'last_error', 'created_at', 'pending_action', 'available_assets', )
class TaskViewSet(viewsets.ViewSet):
"""
@ -171,6 +172,7 @@ class TaskViewSet(viewsets.ViewSet):
class TaskNestedView(APIView):
queryset = models.Task.objects.all().defer('orthophoto_extent', 'dtm_extent', 'dsm_extent', 'console_output', )
permission_classes = (IsAuthenticatedOrReadOnly, )
def get_and_check_task(self, request, pk, project_pk, annotate={}):
try:

Wyświetl plik

@ -447,6 +447,17 @@ class Task(models.Model):
}
}
def get_model_display_params(self):
"""
Subset of a task fields used in the 3D model display view
"""
return {
'id': str(self.id),
'project': self.project.id,
'available_assets': self.available_assets,
'public': self.public
}
def generate_deferred_asset(self, archive, directory):
"""
:param archive: path of the destination .zip file (relative to /assets/ directory)

Wyświetl plik

@ -6,6 +6,37 @@ html, body, section.main, .content, #wrapper, #page-wrapper{
padding-bottom: 8px;
}
#public-wrapper{
margin-left: 12px;
margin-right: 12px;
position: relative;
top: -8px;
}
#iframe{
.map-view{
height: 100%;
.map-type-selector{
z-index: 1000;
position: absolute;
float: none;
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
.switchModeButton{
bottom: 22px;
}
.opacity-slider{
bottom: 10px;
}
}
[data-mapview], .model-view{
height: calc(100vh);
}
}
#navbar-top{
height: 50px;
min-height: 50px;

Wyświetl plik

@ -8,13 +8,15 @@ class MapView extends React.Component {
static defaultProps = {
mapItems: [],
selectedMapType: 'orthophoto',
title: ""
title: "",
public: false
};
static propTypes = {
mapItems: PropTypes.array.isRequired, // list of dictionaries where each dict is a {mapType: 'orthophoto', url: <tiles.json>},
selectedMapType: PropTypes.oneOf(['orthophoto', 'dsm', 'dtm']),
title: PropTypes.string,
public: PropTypes.bool
};
constructor(props){
@ -101,7 +103,8 @@ class MapView extends React.Component {
tiles={this.state.tiles}
showBackground={true}
opacity={opacity}
mapType={this.state.selectedMapType} />
mapType={this.state.selectedMapType}
public={this.props.public} />
<div className="opacity-slider theme-secondary">
Opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
</div>

Wyświetl plik

@ -4,6 +4,7 @@ import ErrorMessage from './components/ErrorMessage';
import SwitchModeButton from './components/SwitchModeButton';
import AssetDownloadButtons from './components/AssetDownloadButtons';
import Standby from './components/Standby';
import ShareButton from './components/ShareButton';
import PropTypes from 'prop-types';
import $ from 'jquery';
@ -15,11 +16,13 @@ import Potree from './vendor/potree';
class ModelView extends React.Component {
static defaultProps = {
task: null
task: null,
public: false
};
static propTypes = {
task: PropTypes.object.isRequired, // The object should contain two keys: {id: <taskId>, project: <projectId>}
public: PropTypes.bool // Is the view being displayed via a shared link?
};
constructor(props){
@ -35,6 +38,7 @@ class ModelView extends React.Component {
this.modelReference = null;
this.toggleTexturedModel = this.toggleTexturedModel.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
}
assetsPath(){
@ -66,6 +70,11 @@ class ModelView extends React.Component {
return 'odm_textured_model.mtl';
}
handleMouseDown(e){
// Make sure the share popup closes
this.shareButton.hidePopup();
}
componentDidMount() {
let container = this.container;
@ -179,7 +188,6 @@ class ModelView extends React.Component {
className="container"
style={{height: "100%", width: "100%", position: "relative"}}
onContextMenu={(e) => {e.preventDefault();}}>
<div
id="potree_render_area"
ref={(domNode) => { this.container = domNode; }}>
@ -190,7 +198,7 @@ class ModelView extends React.Component {
</div>
</div>
<div id="potree_sidebar_container">
<div id="potree_sidebar_container" onMouseDown={this.handleMouseDown}>
<div id="sidebar_root"
className="navmenu navmenu-default navmenu-fixed-left unselectable">
@ -210,10 +218,23 @@ class ModelView extends React.Component {
task={this.props.task}
direction="down"
buttonClass="btn-secondary" />
{showSwitchModeButton ?
<SwitchModeButton
task={this.props.task}
type="modelToMap" /> : ""}
<div className="action-buttons-row">
{(!this.props.public) ?
<ShareButton
ref={(ref) => { this.shareButton = ref; }}
task={this.props.task}
popupPlacement="bottom"
linksTarget="3d"
/>
: ""}
{showSwitchModeButton ?
<SwitchModeButton
public={this.props.public}
style={{marginLeft: this.props.public ? '0' : '76px'}}
task={this.props.task}
type="modelToMap" /> : ""}
</div>
</div>
<div className="accordion">

Wyświetl plik

@ -33,7 +33,7 @@ class ClipboardInput extends React.Component{
onBlur={() => { this.setState({showCopied: false}); }}
/>
<div style={{position: 'relative', 'width': '100%'}}>
<div className={"copied theme-background-success " + (this.state.showCopied ? "show" : "")}>Copied to clipboard!</div>
<div className={"copied theme-background-success " + (this.state.showCopied ? "show" : "")}>Copied to clipboard</div>
</div>
</div>);
}

Wyświetl plik

@ -24,7 +24,8 @@ class Map extends React.Component {
minzoom: 0,
showBackground: false,
opacity: 100,
mapType: "orthophoto"
mapType: "orthophoto",
public: false
};
static propTypes = {
@ -33,7 +34,8 @@ class Map extends React.Component {
showBackground: PropTypes.bool,
tiles: PropTypes.array.isRequired,
opacity: PropTypes.number,
mapType: PropTypes.oneOf(['orthophoto', 'dsm', 'dtm'])
mapType: PropTypes.oneOf(['orthophoto', 'dsm', 'dtm']),
public: PropTypes.bool
};
constructor(props) {
@ -276,15 +278,17 @@ class Map extends React.Component {
<div className="actionButtons">
{this.state.singleTask !== null ?
{(!this.props.public && this.state.singleTask !== null) ?
<ShareButton
ref={(ref) => { this.shareButton = ref; }}
task={this.state.singleTask}
linksTarget="map"
/>
: ""}
<SwitchModeButton
task={this.state.singleTask}
type="mapToModel" />
type="mapToModel"
public={this.props.public} />
</div>
</div>
);

Wyświetl plik

@ -7,9 +7,12 @@ import $ from 'jquery';
class ShareButton extends React.Component {
static defaultProps = {
task: null,
popupPlacement: 'top'
};
static propTypes = {
task: PropTypes.object.isRequired
task: PropTypes.object.isRequired,
linksTarget: PropTypes.oneOf(['map', '3d']).isRequired,
popupPlacement: PropTypes.string
}
constructor(props){
@ -37,14 +40,17 @@ class ShareButton extends React.Component {
}
render() {
return (
<div className="shareButton" onClick={e => { e.stopPropagation(); }}>
{this.state.showPopup ?
<SharePopup
const popup = <SharePopup
task={this.state.task}
taskChanged={this.handleTaskChanged}
/>
: ""}
placement={this.props.popupPlacement}
linksTarget={this.props.linksTarget}
/>;
return (
<div className="shareButton" onClick={e => { e.stopPropagation(); }}>
{this.props.popupPlacement === 'top' && this.state.showPopup ?
popup : ""}
<button
ref={(domNode) => { this.shareButton = domNode; }}
type="button"
@ -52,6 +58,8 @@ class ShareButton extends React.Component {
className={"shareButton btn btn-sm " + (this.state.task.public ? "btn-primary" : "btn-secondary")}>
<i className="fa fa-share-alt"></i> Share
</button>
{this.props.popupPlacement === 'bottom' && this.state.showPopup ?
popup : ""}
</div>
);
}

Wyświetl plik

@ -8,9 +8,12 @@ import ClipboardInput from './ClipboardInput';
class SharePopup extends React.Component{
static propTypes = {
task: PropTypes.object.isRequired,
linksTarget: PropTypes.oneOf(['map', '3d']).isRequired,
placement: PropTypes.string,
taskChanged: PropTypes.func
};
static defaultProps = {
placement: 'top',
taskChanged: () => {}
};
@ -57,11 +60,12 @@ class SharePopup extends React.Component{
}
render(){
const shareLink = Utils.absoluteUrl(`/public/task/${this.state.task.id}/map/`);
const iframeUrl = Utils.absoluteUrl(`public/task/${this.state.task.id}/iframe/`);
const shareLink = Utils.absoluteUrl(`/public/task/${this.state.task.id}/${this.props.linksTarget}/`);
const iframeUrl = Utils.absoluteUrl(`public/task/${this.state.task.id}/iframe/${this.props.linksTarget}/`);
const iframeCode = `<iframe>${iframeUrl}</iframe>`;
return (<div className="sharePopup popover top in">
return (<div onMouseDown={e => { e.stopPropagation(); }}
className={"sharePopup popover in " + this.props.placement}>
<div className="arrow"></div>
<h3 className="popover-title theme-background-highlight">Share This Task</h3>
<div className="popover-content theme-secondary">
@ -77,8 +81,7 @@ class SharePopup extends React.Component{
type="checkbox"
checked={this.state.task.public}
onChange={() => {}}
/>
Enabled
/> Enabled
</label>
</div>
<div className={"share-links " + (this.state.task.public ? "show" : "")}>

Wyświetl plik

@ -5,12 +5,16 @@ import PropTypes from 'prop-types';
class SwitchModeButton extends React.Component {
static defaultProps = {
task: null,
type: "mapToModel"
type: "mapToModel",
public: false,
style: {}
};
static propTypes = {
task: PropTypes.object, // The object should contain two keys: {id: <taskId>, project: <projectId>}
type: PropTypes.string // Either "mapToModel" or "modelToMap"
type: PropTypes.string, // Either "mapToModel" or "modelToMap"
public: PropTypes.bool, // Whether to use public or private URLs
style: PropTypes.object
};
constructor(props){
@ -24,8 +28,13 @@ class SwitchModeButton extends React.Component {
handleClick(){
if (this.props.task){
const prefix = this.props.type === 'mapToModel' ? '3d' : 'map';
location.href = `/${prefix}/project/${this.props.task.project}/task/${this.props.task.id}/`;
const target = this.props.type === 'mapToModel' ? '3d' : 'map';
let url = this.props.public ?
`../${target}/`
: `/${target}/project/${this.props.task.project}/task/${this.props.task.id}/`;
location.href = url;
}
}
@ -40,6 +49,7 @@ class SwitchModeButton extends React.Component {
render() {
return (
<button
style={this.props.style}
onClick={this.handleClick}
type="button"
className={"switchModeButton btn btn-sm btn-secondary " + (!this.props.task ? "hide" : "")}>

Wyświetl plik

@ -5,7 +5,7 @@ const taskMock = require('../../tests/utils/MockLoader').load("task.json");
describe('<ShareButton />', () => {
it('renders without exploding', () => {
const wrapper = shallow(<ShareButton task={taskMock} />);
const wrapper = shallow(<ShareButton task={taskMock} linksTarget="map" />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -5,7 +5,7 @@ const taskMock = require('../../tests/utils/MockLoader').load("task.json");
describe('<SharePopup />', () => {
it('renders without exploding', () => {
const wrapper = shallow(<SharePopup task={taskMock} />);
const wrapper = shallow(<SharePopup task={taskMock} linksTarget="map" />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -1,6 +1,6 @@
.clipboardInput{
text-align: center;
.copied{
position: absolute;
font-size: 70%;

Wyświetl plik

@ -21,4 +21,10 @@
.popup-opacity-slider{
margin-bottom: 6px;
}
.shareButton{
z-index: 2000;
bottom: -11px;
right: 38px;
}
}

Wyświetl plik

@ -67,6 +67,15 @@
}
}
}
.action-buttons-row{
margin-top: 12px;
& > *{
display: inline-block;
margin-right: 8px;
}
}
}

Wyświetl plik

@ -1,9 +1,5 @@
.shareButton{
position: absolute;
z-index: 2000;
bottom: -11px;
right: 38px;
position: absolute;
button{
border-width: 1px;
}

Wyświetl plik

@ -1,12 +1,21 @@
.sharePopup{
position: relative;
top: -32px;
display: block;
&.top{
top: -32px;
}
&.bottom{
top: 32px;
}
&.popover.top > .arrow{
left: auto;
right: 60px;
}
&.popover.bottom > .arrow{
left: 25px;
}
h3.popover-title{
padding-top: 8px;

Wyświetl plik

@ -33,6 +33,7 @@
</style>
</head>
<body data-admin-utc-offset="{% now "Z" %}">
{% block body %}
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
@ -78,6 +79,7 @@
{% autoescape off %}
{% get_footer %}
{% endautoescape %}
{% endblock %}
</body>
<script src="{% static 'app/js/vendor/metisMenu.min.js' %}"></script>
<script>

Wyświetl plik

@ -0,0 +1,14 @@
{% extends "app/public/base.html" %}
{% block content %}
{% load render_bundle from webpack_loader %}
{% render_bundle 'ModelView' attrs='async' %}
<h3><i class="fa fa-cube"></i> {{title}}</h3>
<div data-modelview
{% for key, value in params %}
data-{{key}}="{{value}}"
{% endfor %}
></div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,12 @@
{% extends "app/public/iframe_base.html" %}
{% block content %}
{% load render_bundle from webpack_loader %}
{% render_bundle 'ModelView' attrs='async' %}
<div data-modelview
{% for key, value in params %}
data-{{key}}="{{value}}"
{% endfor %}
></div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,9 @@
{% extends "app/base.html" %}
{% block page-wrapper %}
{{ SETTINGS.theme.html_after_header|safe }}
<div id="public-wrapper">
{% block content %}{% endblock %}
</div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "app/base.html" %}
{% block body %}
<div id="iframe">
{% block content %}{% endblock %}
</div>
{% endblock %}

Wyświetl plik

@ -1,7 +1,6 @@
{% extends "app/base.html" %}
{% load i18n %}
{% extends "app/public/base.html" %}
{% block page-wrapper %}
{% block content %}
{% load render_bundle from webpack_loader %}
{% render_bundle 'MapView' attrs='async' %}
@ -10,6 +9,4 @@
data-{{key}}="{{value}}"
{% endfor %}
></div>
{{ SETTINGS.theme.html_after_header|safe }}
{% endblock %}
{% endblock %}

Wyświetl plik

@ -0,0 +1,11 @@
{% extends "app/public/iframe_base.html" %}
{% block content %}
{% load render_bundle from webpack_loader %}
{% render_bundle 'MapView' attrs='async' %}
<div data-mapview
{% for key, value in params %}
data-{{key}}="{{value}}"
{% endfor %}
></div>
{% endblock %}

Wyświetl plik

@ -13,7 +13,10 @@ urlpatterns = [
url(r'^map/project/(?P<project_pk>[^/.]+)/$', private_views.map, name='map'),
url(r'^3d/project/(?P<project_pk>[^/.]+)/task/(?P<task_pk>[^/.]+)/$', private_views.model_display, name='model_display'),
url(r'^public/map/(?P<task_public_uuid>[^/.]+)/$', public_views.map, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/map/$', public_views.map, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/map/$', public_views.map_iframe, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/3d/$', public_views.model_display, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/3d/$', public_views.model_display_iframe, name='public_map'),
url(r'^processingnode/([\d]+)/$', private_views.processing_node, name='processing_node'),

Wyświetl plik

@ -59,7 +59,8 @@ def map(request, project_pk=None, task_pk=None):
'title': title,
'params': {
'map-items': json.dumps(mapItems),
'title': title
'title': title,
'public': 'false'
}.items()
})
@ -80,15 +81,12 @@ def model_display(request, project_pk=None, task_pk=None):
raise Http404()
return render(request, 'app/3d_model_display.html', {
'title': title,
'params': {
'task': json.dumps({
'id': str(task.id),
'project': project.id,
'available_assets': task.available_assets
})
}.items()
})
'title': title,
'params': {
'task': json.dumps(task.get_model_display_params()),
'public': 'false'
}.items()
})
@login_required

Wyświetl plik

@ -7,23 +7,46 @@ from django.shortcuts import render
from app.models import Task
def get_public_task(public_uuid):
def get_public_task(task_pk):
"""
Get a task and raise a 404 if it's not public
"""
task = get_object_or_404(Task, public_uuid=public_uuid)
#if not task.public:
# raise Http404()
task = get_object_or_404(Task, pk=task_pk)
if not task.public:
raise Http404()
return task
def handle_map(request, template, task_pk=None, hide_title=False):
task = get_public_task(task_pk)
def map(request, task_public_uuid=None):
task = get_public_task(task_public_uuid)
return render(request, 'app/map.html', {
return render(request, template, {
'title': _("Map"),
'params': {
'map-items': json.dumps([task.get_map_items()]),
'title': task.name
'title': task.name if not hide_title else '',
'public': 'true'
}.items()
})
})
def map(request, task_pk=None):
return handle_map(request, 'app/public/map.html', task_pk, False)
def map_iframe(request, task_pk=None):
return handle_map(request, 'app/public/map_iframe.html', task_pk, True)
def handle_model_display(request, template, task_pk=None):
task = get_public_task(task_pk)
return render(request, template, {
'title': task.name,
'params': {
'task': json.dumps(task.get_model_display_params()),
'public': 'true'
}.items()
})
def model_display(request, task_pk=None):
return handle_model_display(request, 'app/public/3d_model_display.html', task_pk)
def model_display_iframe(request, task_pk=None):
return handle_model_display(request, 'app/public/3d_model_display_iframe.html', task_pk)