kopia lustrzana https://github.com/OpenDroneMap/WebODM
Tags persistence, system tags, sort by tag
rodzic
8df0e9a96e
commit
a7b09ee3fa
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
class TagsField(serializers.JSONField):
|
||||
def to_representation(self, tags):
|
||||
return [t for t in tags.split(" ") if t != ""]
|
||||
|
||||
def to_internal_value(self, tags):
|
||||
return " ".join([t.strip() for t in tags])
|
|
@ -20,6 +20,7 @@ from nodeodm import status_codes
|
|||
from nodeodm.models import ProcessingNode
|
||||
from worker import tasks as worker_tasks
|
||||
from .common import get_and_check_project, get_asset_download_filename
|
||||
from .tags import TagsField
|
||||
from app.security import path_traversal_check
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -41,6 +42,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||
processing_node_name = serializers.SerializerMethodField()
|
||||
can_rerun_from = serializers.SerializerMethodField()
|
||||
statistics = serializers.SerializerMethodField()
|
||||
tags = TagsField()
|
||||
|
||||
def get_processing_node_name(self, obj):
|
||||
if obj.processing_node is not None:
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.27 on 2023-03-07 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0032_task_epsg'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='tags',
|
||||
field=models.TextField(blank=True, db_index=True, default='', help_text='Project tags', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='tags',
|
||||
field=models.TextField(blank=True, db_index=True, default='', help_text='Task tags', verbose_name='Tags'),
|
||||
),
|
||||
]
|
|
@ -25,6 +25,7 @@ class Project(models.Model):
|
|||
description = models.TextField(default="", blank=True, help_text=_("More in-depth description of the project"), verbose_name=_("Description"))
|
||||
created_at = models.DateTimeField(default=timezone.now, help_text=_("Creation date"), verbose_name=_("Created at"))
|
||||
deleting = models.BooleanField(db_index=True, default=False, help_text=_("Whether this project has been marked for deletion. Projects that have running tasks need to wait for tasks to be properly cleaned up before they can be deleted."), verbose_name=_("Deleting"))
|
||||
tags = models.TextField(db_index=True, default="", blank=True, help_text=_("Project tags"), verbose_name=_("Tags"))
|
||||
|
||||
def delete(self, *args):
|
||||
# No tasks?
|
||||
|
|
|
@ -276,6 +276,7 @@ class Task(models.Model):
|
|||
partial = models.BooleanField(default=False, help_text=_("A flag indicating whether this task is currently waiting for information or files to be uploaded before being considered for processing."), verbose_name=_("Partial"))
|
||||
potree_scene = fields.JSONField(default=dict, blank=True, help_text=_("Serialized potree scene information used to save/load measurements and camera view angle"), verbose_name=_("Potree Scene"))
|
||||
epsg = models.IntegerField(null=True, default=None, blank=True, help_text=_("EPSG code of the dataset (if georeferenced)"), verbose_name="EPSG")
|
||||
tags = models.TextField(db_index=True, default="", blank=True, help_text=_("Task tags"), verbose_name=_("Tags"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Task")
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
export default {
|
||||
userTags: function(tags){
|
||||
// Tags starting with a "_" are considered hidden or system tags
|
||||
// and should not be displayed to end users via the UI
|
||||
if (Array.isArray(tags)){
|
||||
return tags.filter(t => !t.startsWith("_"));
|
||||
}else return [];
|
||||
},
|
||||
|
||||
systemTags: function(tags){
|
||||
// Tags starting with a "_" are considered hidden or system tags
|
||||
// and should not be displayed to end users via the UI
|
||||
if (Array.isArray(tags)){
|
||||
return tags.filter(t => t.startsWith("_"));
|
||||
}else return [];
|
||||
},
|
||||
|
||||
combine: function(user, system){
|
||||
if (Array.isArray(user) && Array.isArray(system)){
|
||||
return user.concat(system);
|
||||
}else throw Error("Invalid parameters");
|
||||
}
|
||||
}
|
|
@ -46,12 +46,13 @@ class EditTaskForm extends React.Component {
|
|||
processingNodes: [],
|
||||
selectedPreset: null,
|
||||
presets: [],
|
||||
tags: Utils.clone(props.task.tags),
|
||||
|
||||
editingPreset: false,
|
||||
|
||||
loadingTaskName: false,
|
||||
|
||||
showTagsField: true // TODO false
|
||||
showTagsField: !!props.task.tags.length
|
||||
};
|
||||
|
||||
this.handleNameChange = this.handleNameChange.bind(this);
|
||||
|
@ -357,12 +358,13 @@ class EditTaskForm extends React.Component {
|
|||
}
|
||||
|
||||
getTaskInfo(){
|
||||
const { name, selectedNode, selectedPreset } = this.state;
|
||||
const { name, selectedNode, selectedPreset, tags } = this.state;
|
||||
|
||||
return {
|
||||
name: name !== "" ? name : this.namePlaceholder,
|
||||
selectedNode: selectedNode,
|
||||
options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options)
|
||||
options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options),
|
||||
tags
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -560,7 +562,7 @@ class EditTaskForm extends React.Component {
|
|||
tagsField = (<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Tags")}</label>
|
||||
<div className="col-sm-10">
|
||||
<TagsField ref={domNode => this.tagsField = domNode}/>
|
||||
<TagsField onUpdate={(tags) => this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,9 @@ class ProjectListItem extends React.Component {
|
|||
},{
|
||||
key: "name",
|
||||
label: _("Name")
|
||||
},{
|
||||
key: "tags",
|
||||
label: _("Tags")
|
||||
}];
|
||||
|
||||
this.toggleTaskList = this.toggleTaskList.bind(this);
|
||||
|
|
|
@ -3,27 +3,35 @@ import '../css/TagsField.scss';
|
|||
import PropTypes from 'prop-types';
|
||||
import update from 'immutability-helper';
|
||||
import { _ } from '../classes/gettext';
|
||||
import Tags from '../classes/Tags';
|
||||
|
||||
class TagsField extends React.Component {
|
||||
static defaultProps = {
|
||||
tags: ["abc", "123", "xyz", "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbb", "ccccccccccc", "dddddddddddd"]
|
||||
tags: [],
|
||||
onUpdate: () => {}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string)
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
onUpdate: PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tags: props.tags
|
||||
userTags: Tags.userTags(props.tags),
|
||||
systemTags: Tags.systemTags(props.tags)
|
||||
}
|
||||
|
||||
this.dzList = [];
|
||||
this.domTags = [];
|
||||
}
|
||||
|
||||
componentDidUpdate(){
|
||||
this.props.onUpdate(Tags.combine(this.state.userTags, this.state.systemTags));
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.restoreDropzones();
|
||||
}
|
||||
|
@ -52,7 +60,7 @@ class TagsField extends React.Component {
|
|||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === "Tab" || e.key === "Enter" || e.key === ","){
|
||||
if (e.key === "Tab" || e.key === "Enter" || e.key === "," || e.key === " "){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.addTag();
|
||||
|
@ -71,18 +79,21 @@ class TagsField extends React.Component {
|
|||
return e => {
|
||||
e.stopPropagation();
|
||||
|
||||
this.setState(update(this.state, { tags: { $splice: [[idx, 1]] } }));
|
||||
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
|
||||
}
|
||||
}
|
||||
|
||||
addTag = () => {
|
||||
const text = this.inputText.innerText;
|
||||
if (text !== ""){
|
||||
// Check for dulicates
|
||||
if (this.state.tags.indexOf(text) === -1){
|
||||
this.setState(update(this.state, {
|
||||
tags: {$push: [text]}
|
||||
}));
|
||||
// Do not allow system tags
|
||||
if (!text.startsWith("_")){
|
||||
// Check for dulicates
|
||||
if (this.state.userTags.indexOf(text) === -1){
|
||||
this.setState(update(this.state, {
|
||||
userTags: {$push: [text]}
|
||||
}));
|
||||
}
|
||||
}
|
||||
this.inputText.innerText = "";
|
||||
}
|
||||
|
@ -102,23 +113,23 @@ class TagsField extends React.Component {
|
|||
const dragTag = e.dataTransfer.getData("application/tag");
|
||||
const [moveTag, side] = this.findClosestTag(e.clientX, e.clientY);
|
||||
|
||||
const { tags } = this.state;
|
||||
const { userTags } = this.state;
|
||||
if (moveTag){
|
||||
const dragIdx = tags.indexOf(dragTag);
|
||||
const moveIdx = tags.indexOf(moveTag);
|
||||
const dragIdx = userTags.indexOf(dragTag);
|
||||
const moveIdx = userTags.indexOf(moveTag);
|
||||
if (dragIdx !== -1 && moveIdx !== -1){
|
||||
if (dragIdx === moveIdx) return;
|
||||
else{
|
||||
// Put drag tag in front of move tag
|
||||
let insertIdx = side === "right" ? moveIdx + 1 : moveIdx;
|
||||
tags.splice(insertIdx, 0, dragTag);
|
||||
for (let i = 0; i < tags.length; i++){
|
||||
if (tags[i] === dragTag && i !== insertIdx){
|
||||
tags.splice(i, 1);
|
||||
userTags.splice(insertIdx, 0, dragTag);
|
||||
for (let i = 0; i < userTags.length; i++){
|
||||
if (userTags[i] === dragTag && i !== insertIdx){
|
||||
userTags.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({tags});
|
||||
this.setState({userTags});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +149,7 @@ class TagsField extends React.Component {
|
|||
let closestTag = null;
|
||||
let minDistX = Infinity, minDistY = Infinity;
|
||||
let rowTagY = null;
|
||||
const { tags } = this.state;
|
||||
const { userTags } = this.state;
|
||||
|
||||
// Find tags in closest row
|
||||
this.domTags.forEach((domTag, i) => {
|
||||
|
@ -164,7 +175,7 @@ class TagsField extends React.Component {
|
|||
let dx = clientX - tagX,
|
||||
sqDistX = dx*dx;
|
||||
if (sqDistX < minDistX){
|
||||
closestTag = tags[i];
|
||||
closestTag = userTags[i];
|
||||
minDistX = sqDistX;
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +183,7 @@ class TagsField extends React.Component {
|
|||
|
||||
let side = "right";
|
||||
if (closestTag){
|
||||
const b = this.domTags[this.state.tags.indexOf(closestTag)].getBoundingClientRect();
|
||||
const b = this.domTags[this.state.userTags.indexOf(closestTag)].getBoundingClientRect();
|
||||
const centerX = b.x + b.width / 2.0;
|
||||
if (clientX < centerX) side = "left";
|
||||
}
|
||||
|
@ -189,7 +200,7 @@ class TagsField extends React.Component {
|
|||
onDrop={this.handleDrop}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
className="form-control tags-field">{this.state.tags.map((tag, i) =>
|
||||
className="form-control tags-field">{this.state.userTags.map((tag, i) =>
|
||||
<div draggable="true" className="tag-badge" key={i} ref={domNode => this.domTags[i] = domNode}
|
||||
onClick={this.stop}
|
||||
onDragStart={this.handleDragStart(tag)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import TaskPluginActionButtons from './TaskPluginActionButtons';
|
|||
import MoveTaskDialog from './MoveTaskDialog';
|
||||
import PipelineSteps from '../classes/PipelineSteps';
|
||||
import Css from '../classes/Css';
|
||||
import Tags from '../classes/Tags';
|
||||
import Trans from './Trans';
|
||||
import { _, interpolate } from '../classes/gettext';
|
||||
|
||||
|
@ -706,6 +707,7 @@ class TaskListItem extends React.Component {
|
|||
|
||||
let taskActionsIcon = "fa-ellipsis-h";
|
||||
if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw";
|
||||
const userTags = Tags.userTags(task.tags);
|
||||
|
||||
return (
|
||||
<div className="task-list-item">
|
||||
|
@ -719,7 +721,10 @@ class TaskListItem extends React.Component {
|
|||
: ""}
|
||||
<div className="row">
|
||||
<div className="col-sm-5 col-xs-12 name">
|
||||
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
|
||||
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded} className="name-link">{name}</a>
|
||||
{userTags.length > 0 ?
|
||||
userTags.map((t, i) => <div key={i} className="tag-badge small-badge">{t}</div>)
|
||||
: ""}
|
||||
</div>
|
||||
<div className="col-sm-1 col-xs-5 details">
|
||||
<i className="far fa-image"></i> {task.images_count}
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
|
||||
a{
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
a:hover, a:focus, a:active{
|
||||
cursor: pointer;
|
||||
|
|
|
@ -119,4 +119,23 @@
|
|||
.mb{
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tag-badge.small-badge {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
margin-left: 4px;
|
||||
margin-top: -2px;
|
||||
border-radius: 6px;
|
||||
font-size: 90%;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.name-link{
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue