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 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:

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"))
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?

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"))
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")

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: [],
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>);
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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}

Wyświetl plik

@ -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;

Wyświetl plik

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