Tags persistence, system tags, sort by tag

pull/1297/head
Piero Toffanin 2023-03-07 11:56:17 -05:00
rodzic 8df0e9a96e
commit a7b09ee3fa
12 zmienionych plików z 130 dodań i 32 usunięć

8
app/api/tags.py 100644
Wyświetl plik

@ -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])

Wyświetl plik

@ -20,6 +20,7 @@ from nodeodm import status_codes
from nodeodm.models import ProcessingNode from nodeodm.models import ProcessingNode
from worker import tasks as worker_tasks from worker import tasks as worker_tasks
from .common import get_and_check_project, get_asset_download_filename from .common import get_and_check_project, get_asset_download_filename
from .tags import TagsField
from app.security import path_traversal_check from app.security import path_traversal_check
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -41,6 +42,7 @@ class TaskSerializer(serializers.ModelSerializer):
processing_node_name = serializers.SerializerMethodField() processing_node_name = serializers.SerializerMethodField()
can_rerun_from = serializers.SerializerMethodField() can_rerun_from = serializers.SerializerMethodField()
statistics = serializers.SerializerMethodField() statistics = serializers.SerializerMethodField()
tags = TagsField()
def get_processing_node_name(self, obj): def get_processing_node_name(self, obj):
if obj.processing_node is not None: if obj.processing_node is not None:

Wyświetl plik

@ -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'),
),
]

Wyświetl plik

@ -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")) 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")) 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")) 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): def delete(self, *args):
# No tasks? # No tasks?

Wyświetl plik

@ -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")) 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")) 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") 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: class Meta:
verbose_name = _("Task") verbose_name = _("Task")

Wyświetl plik

@ -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");
}
}

Wyświetl plik

@ -46,12 +46,13 @@ class EditTaskForm extends React.Component {
processingNodes: [], processingNodes: [],
selectedPreset: null, selectedPreset: null,
presets: [], presets: [],
tags: Utils.clone(props.task.tags),
editingPreset: false, editingPreset: false,
loadingTaskName: false, loadingTaskName: false,
showTagsField: true // TODO false showTagsField: !!props.task.tags.length
}; };
this.handleNameChange = this.handleNameChange.bind(this); this.handleNameChange = this.handleNameChange.bind(this);
@ -357,12 +358,13 @@ class EditTaskForm extends React.Component {
} }
getTaskInfo(){ getTaskInfo(){
const { name, selectedNode, selectedPreset } = this.state; const { name, selectedNode, selectedPreset, tags } = this.state;
return { return {
name: name !== "" ? name : this.namePlaceholder, name: name !== "" ? name : this.namePlaceholder,
selectedNode: selectedNode, 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"> tagsField = (<div className="form-group">
<label className="col-sm-2 control-label">{_("Tags")}</label> <label className="col-sm-2 control-label">{_("Tags")}</label>
<div className="col-sm-10"> <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>
</div>); </div>);
} }

Wyświetl plik

@ -49,6 +49,9 @@ class ProjectListItem extends React.Component {
},{ },{
key: "name", key: "name",
label: _("Name") label: _("Name")
},{
key: "tags",
label: _("Tags")
}]; }];
this.toggleTaskList = this.toggleTaskList.bind(this); this.toggleTaskList = this.toggleTaskList.bind(this);

Wyświetl plik

@ -3,27 +3,35 @@ import '../css/TagsField.scss';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import update from 'immutability-helper'; import update from 'immutability-helper';
import { _ } from '../classes/gettext'; import { _ } from '../classes/gettext';
import Tags from '../classes/Tags';
class TagsField extends React.Component { class TagsField extends React.Component {
static defaultProps = { static defaultProps = {
tags: ["abc", "123", "xyz", "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbb", "ccccccccccc", "dddddddddddd"] tags: [],
onUpdate: () => {}
}; };
static propTypes = { static propTypes = {
tags: PropTypes.arrayOf(PropTypes.string) tags: PropTypes.arrayOf(PropTypes.string),
onUpdate: PropTypes.func
}; };
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
tags: props.tags userTags: Tags.userTags(props.tags),
systemTags: Tags.systemTags(props.tags)
} }
this.dzList = []; this.dzList = [];
this.domTags = []; this.domTags = [];
} }
componentDidUpdate(){
this.props.onUpdate(Tags.combine(this.state.userTags, this.state.systemTags));
}
componentWillUnmount(){ componentWillUnmount(){
this.restoreDropzones(); this.restoreDropzones();
} }
@ -52,7 +60,7 @@ class TagsField extends React.Component {
} }
handleKeyDown = e => { 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.addTag(); this.addTag();
@ -71,18 +79,21 @@ class TagsField extends React.Component {
return e => { return e => {
e.stopPropagation(); e.stopPropagation();
this.setState(update(this.state, { tags: { $splice: [[idx, 1]] } })); this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
} }
} }
addTag = () => { addTag = () => {
const text = this.inputText.innerText; const text = this.inputText.innerText;
if (text !== ""){ if (text !== ""){
// Check for dulicates // Do not allow system tags
if (this.state.tags.indexOf(text) === -1){ if (!text.startsWith("_")){
this.setState(update(this.state, { // Check for dulicates
tags: {$push: [text]} if (this.state.userTags.indexOf(text) === -1){
})); this.setState(update(this.state, {
userTags: {$push: [text]}
}));
}
} }
this.inputText.innerText = ""; this.inputText.innerText = "";
} }
@ -102,23 +113,23 @@ class TagsField extends React.Component {
const dragTag = e.dataTransfer.getData("application/tag"); const dragTag = e.dataTransfer.getData("application/tag");
const [moveTag, side] = this.findClosestTag(e.clientX, e.clientY); const [moveTag, side] = this.findClosestTag(e.clientX, e.clientY);
const { tags } = this.state; const { userTags } = this.state;
if (moveTag){ if (moveTag){
const dragIdx = tags.indexOf(dragTag); const dragIdx = userTags.indexOf(dragTag);
const moveIdx = tags.indexOf(moveTag); const moveIdx = userTags.indexOf(moveTag);
if (dragIdx !== -1 && moveIdx !== -1){ if (dragIdx !== -1 && moveIdx !== -1){
if (dragIdx === moveIdx) return; if (dragIdx === moveIdx) return;
else{ else{
// Put drag tag in front of move tag // Put drag tag in front of move tag
let insertIdx = side === "right" ? moveIdx + 1 : moveIdx; let insertIdx = side === "right" ? moveIdx + 1 : moveIdx;
tags.splice(insertIdx, 0, dragTag); userTags.splice(insertIdx, 0, dragTag);
for (let i = 0; i < tags.length; i++){ for (let i = 0; i < userTags.length; i++){
if (tags[i] === dragTag && i !== insertIdx){ if (userTags[i] === dragTag && i !== insertIdx){
tags.splice(i, 1); userTags.splice(i, 1);
break; break;
} }
} }
this.setState({tags}); this.setState({userTags});
} }
} }
} }
@ -138,7 +149,7 @@ class TagsField extends React.Component {
let closestTag = null; let closestTag = null;
let minDistX = Infinity, minDistY = Infinity; let minDistX = Infinity, minDistY = Infinity;
let rowTagY = null; let rowTagY = null;
const { tags } = this.state; const { userTags } = this.state;
// Find tags in closest row // Find tags in closest row
this.domTags.forEach((domTag, i) => { this.domTags.forEach((domTag, i) => {
@ -164,7 +175,7 @@ class TagsField extends React.Component {
let dx = clientX - tagX, let dx = clientX - tagX,
sqDistX = dx*dx; sqDistX = dx*dx;
if (sqDistX < minDistX){ if (sqDistX < minDistX){
closestTag = tags[i]; closestTag = userTags[i];
minDistX = sqDistX; minDistX = sqDistX;
} }
} }
@ -172,7 +183,7 @@ class TagsField extends React.Component {
let side = "right"; let side = "right";
if (closestTag){ 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; const centerX = b.x + b.width / 2.0;
if (clientX < centerX) side = "left"; if (clientX < centerX) side = "left";
} }
@ -189,7 +200,7 @@ class TagsField extends React.Component {
onDrop={this.handleDrop} onDrop={this.handleDrop}
onDragOver={this.handleDragOver} onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter} 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} <div draggable="true" className="tag-badge" key={i} ref={domNode => this.domTags[i] = domNode}
onClick={this.stop} onClick={this.stop}
onDragStart={this.handleDragStart(tag)} onDragStart={this.handleDragStart(tag)}

Wyświetl plik

@ -12,6 +12,7 @@ import TaskPluginActionButtons from './TaskPluginActionButtons';
import MoveTaskDialog from './MoveTaskDialog'; import MoveTaskDialog from './MoveTaskDialog';
import PipelineSteps from '../classes/PipelineSteps'; import PipelineSteps from '../classes/PipelineSteps';
import Css from '../classes/Css'; import Css from '../classes/Css';
import Tags from '../classes/Tags';
import Trans from './Trans'; import Trans from './Trans';
import { _, interpolate } from '../classes/gettext'; import { _, interpolate } from '../classes/gettext';
@ -706,6 +707,7 @@ class TaskListItem extends React.Component {
let taskActionsIcon = "fa-ellipsis-h"; let taskActionsIcon = "fa-ellipsis-h";
if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw"; if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw";
const userTags = Tags.userTags(task.tags);
return ( return (
<div className="task-list-item"> <div className="task-list-item">
@ -719,7 +721,10 @@ class TaskListItem extends React.Component {
: ""} : ""}
<div className="row"> <div className="row">
<div className="col-sm-5 col-xs-12 name"> <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>
<div className="col-sm-1 col-xs-5 details"> <div className="col-sm-1 col-xs-5 details">
<i className="far fa-image"></i> {task.images_count} <i className="far fa-image"></i> {task.images_count}

Wyświetl plik

@ -19,10 +19,10 @@
margin-bottom: 8px; margin-bottom: 8px;
border-radius: 6px; border-radius: 6px;
a{ a{
margin-top: 2px; margin-top: 2px;
font-weight: bold; font-weight: bold;
padding-bottom: 5px;
} }
a:hover, a:focus, a:active{ a:hover, a:focus, a:active{
cursor: pointer; cursor: pointer;

Wyświetl plik

@ -119,4 +119,23 @@
.mb{ .mb{
margin-bottom: 12px; 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;
}
} }