kopia lustrzana https://github.com/OpenDroneMap/WebODM
Project tags persistence
rodzic
a7b09ee3fa
commit
26acc6ea1d
|
@ -10,6 +10,7 @@ from django.db.models import Q
|
|||
|
||||
from app import models
|
||||
from .tasks import TaskIDsSerializer
|
||||
from .tags import TagsField, parse_tags_input
|
||||
from .common import get_and_check_project
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
@ -23,6 +24,7 @@ class ProjectSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
created_at = serializers.ReadOnlyField()
|
||||
permissions = serializers.SerializerMethodField()
|
||||
tags = TagsField(required=False)
|
||||
|
||||
def get_permissions(self, obj):
|
||||
if 'request' in self.context:
|
||||
|
@ -67,7 +69,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
|
||||
return None
|
||||
return super().paginate_queryset(queryset)
|
||||
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def duplicate(self, request, pk=None):
|
||||
"""
|
||||
|
@ -104,6 +106,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
with transaction.atomic():
|
||||
project.name = request.data.get('name', '')
|
||||
project.description = request.data.get('description', '')
|
||||
project.tags = TagsField().to_internal_value(parse_tags_input(request.data.get('tags', [])))
|
||||
project.save()
|
||||
|
||||
form_perms = request.data.get('permissions')
|
||||
|
@ -148,6 +151,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
except User.DoesNotExist as e:
|
||||
return Response({'error': _("Invalid user in permissions list")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except AttributeError as e:
|
||||
print(e)
|
||||
return Response({'error': _("Invalid permissions")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({'success': True}, status=status.HTTP_200_OK)
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
from rest_framework import serializers
|
||||
import json
|
||||
|
||||
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])
|
||||
return " ".join([t.strip() for t in tags])
|
||||
|
||||
def parse_tags_input(tags):
|
||||
if tags is None:
|
||||
return []
|
||||
|
||||
if isinstance(tags, str):
|
||||
try:
|
||||
r = json.loads(tags)
|
||||
if isinstance(r, list):
|
||||
return r
|
||||
else:
|
||||
raise Exception("Invalid tags string")
|
||||
except:
|
||||
return []
|
||||
elif isinstance(tags, list):
|
||||
return list(map(str, tags))
|
||||
else:
|
||||
return []
|
|
@ -42,7 +42,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||
processing_node_name = serializers.SerializerMethodField()
|
||||
can_rerun_from = serializers.SerializerMethodField()
|
||||
statistics = serializers.SerializerMethodField()
|
||||
tags = TagsField()
|
||||
tags = TagsField(required=False)
|
||||
|
||||
def get_processing_node_name(self, obj):
|
||||
if obj.processing_node is not None:
|
||||
|
|
|
@ -28,10 +28,12 @@ class Dashboard extends React.Component {
|
|||
return $.ajax({
|
||||
url: `/api/projects/`,
|
||||
type: 'POST',
|
||||
data: {
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
name: project.name,
|
||||
description: project.descr
|
||||
}
|
||||
description: project.descr,
|
||||
tags: project.tags
|
||||
})
|
||||
}).done(() => {
|
||||
this.projectList.refresh();
|
||||
});
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import '../css/EditProjectDialog.scss';
|
||||
import FormDialog from './FormDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import EditPermissionsPanel from './EditPermissionsPanel';
|
||||
import TagsField from './TagsField';
|
||||
import { _ } from '../classes/gettext';
|
||||
|
||||
class EditProjectDialog extends React.Component {
|
||||
|
@ -10,6 +12,7 @@ class EditProjectDialog extends React.Component {
|
|||
projectName: "",
|
||||
projectDescr: "",
|
||||
projectId: -1,
|
||||
projectTags: [],
|
||||
title: _("New Project"),
|
||||
saveLabel: _("Create Project"),
|
||||
savingLabel: _("Creating project..."),
|
||||
|
@ -25,6 +28,7 @@ class EditProjectDialog extends React.Component {
|
|||
projectName: PropTypes.string,
|
||||
projectDescr: PropTypes.string,
|
||||
projectId: PropTypes.number,
|
||||
projectTags: PropTypes.array,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
|
@ -46,7 +50,9 @@ class EditProjectDialog extends React.Component {
|
|||
name: props.projectName,
|
||||
descr: props.projectDescr !== null ? props.projectDescr : "",
|
||||
duplicating: false,
|
||||
error: ""
|
||||
tags: props.projectTags,
|
||||
error: "",
|
||||
showTagsField: !!props.projectTags.length
|
||||
};
|
||||
|
||||
this.reset = this.reset.bind(this);
|
||||
|
@ -60,6 +66,8 @@ class EditProjectDialog extends React.Component {
|
|||
name: this.props.projectName,
|
||||
descr: this.props.projectDescr,
|
||||
duplicating: false,
|
||||
tags: this.props.projectTags,
|
||||
showTagsField: !!this.props.projectTags.length,
|
||||
error: ""
|
||||
});
|
||||
}
|
||||
|
@ -68,6 +76,7 @@ class EditProjectDialog extends React.Component {
|
|||
const res = {
|
||||
name: this.state.name,
|
||||
descr: this.state.descr,
|
||||
tags: this.state.tags
|
||||
};
|
||||
|
||||
if (this.editPermissionsPanel){
|
||||
|
@ -128,7 +137,26 @@ class EditProjectDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
toggleTagsField = () => {
|
||||
if (!this.state.showTagsField){
|
||||
setTimeout(() => {
|
||||
if (this.tagsField) this.tagsField.focus();
|
||||
}, 0);
|
||||
}
|
||||
this.setState({showTagsField: !this.state.showTagsField});
|
||||
}
|
||||
|
||||
render(){
|
||||
let tagsField = "";
|
||||
if (this.state.showTagsField){
|
||||
tagsField = (<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Tags")}</label>
|
||||
<div className="col-sm-10">
|
||||
<TagsField onUpdate={(tags) => this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormDialog {...this.props}
|
||||
getFormData={this.getFormData}
|
||||
|
@ -137,12 +165,16 @@ class EditProjectDialog extends React.Component {
|
|||
leftButtons={this.props.showDuplicate ? [<button key="duplicate" disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
|
||||
ref={(domNode) => { this.dialog = domNode; }}>
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<div className="form-group">
|
||||
<div className="form-group edit-project-dialog">
|
||||
<label className="col-sm-2 control-label">{_("Name")}</label>
|
||||
<div className="col-sm-10">
|
||||
<div className="col-sm-10 name-fields">
|
||||
<input type="text" className="form-control" ref={(domNode) => { this.nameInput = domNode; }} value={this.state.name} onChange={this.handleChange('name')} onKeyPress={e => this.dialog.handleEnter(e)} />
|
||||
<button type="button" title={_("Add tags")} onClick={this.toggleTagsField} className="btn btn-sm btn-secondary toggle-tags">
|
||||
<i className="fa fa-tag"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{tagsField}
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Description (optional)")}</label>
|
||||
<div className="col-sm-10">
|
||||
|
|
|
@ -13,6 +13,7 @@ import csrf from '../django/csrf';
|
|||
import HistoryNav from '../classes/HistoryNav';
|
||||
import PropTypes from 'prop-types';
|
||||
import ResizeModes from '../classes/ResizeModes';
|
||||
import Tags from '../classes/Tags';
|
||||
import exifr from '../vendor/exifr';
|
||||
import { _, interpolate } from '../classes/gettext';
|
||||
import $ from 'jquery';
|
||||
|
@ -397,6 +398,7 @@ class ProjectListItem extends React.Component {
|
|||
data: JSON.stringify({
|
||||
name: project.name,
|
||||
description: project.descr,
|
||||
tags: project.tags,
|
||||
permissions: project.permissions
|
||||
}),
|
||||
dataType: 'json',
|
||||
|
@ -494,7 +496,7 @@ class ProjectListItem extends React.Component {
|
|||
const { refreshing, data } = this.state;
|
||||
const numTasks = data.tasks.length;
|
||||
const canEdit = this.hasPermission("change");
|
||||
|
||||
const userTags = Tags.userTags(data.tags);
|
||||
|
||||
return (
|
||||
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
|
||||
|
@ -514,6 +516,7 @@ class ProjectListItem extends React.Component {
|
|||
projectName={data.name}
|
||||
projectDescr={data.description}
|
||||
projectId={data.id}
|
||||
projectTags={data.tags}
|
||||
saveAction={this.updateProject}
|
||||
showPermissions={this.hasPermission("change")}
|
||||
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
||||
|
@ -556,6 +559,9 @@ class ProjectListItem extends React.Component {
|
|||
|
||||
<div className="project-name">
|
||||
{data.name}
|
||||
{userTags.length > 0 ?
|
||||
userTags.map((t, i) => <div key={i} className="tag-badge small-badge">{t}</div>)
|
||||
: ""}
|
||||
</div>
|
||||
<div className="project-description">
|
||||
{data.description}
|
||||
|
|
|
@ -64,6 +64,8 @@ class TagsField extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.addTag();
|
||||
}else if (e.key === "Backspace" && this.inputText.innerText === ""){
|
||||
this.removeTag(this.state.userTags.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,19 +77,26 @@ class TagsField extends React.Component {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
removeTag = idx => {
|
||||
handleRemoveTag = idx => {
|
||||
return e => {
|
||||
e.stopPropagation();
|
||||
|
||||
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
|
||||
this.removeTag(idx);
|
||||
}
|
||||
}
|
||||
|
||||
removeTag = idx => {
|
||||
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
|
||||
}
|
||||
|
||||
addTag = () => {
|
||||
const text = this.inputText.innerText;
|
||||
let text = this.inputText.innerText;
|
||||
if (text !== ""){
|
||||
// Do not allow system tags
|
||||
if (!text.startsWith("_")){
|
||||
|
||||
// Only lower case text allowed
|
||||
text = text.toLowerCase();
|
||||
|
||||
// Check for dulicates
|
||||
if (this.state.userTags.indexOf(text) === -1){
|
||||
this.setState(update(this.state, {
|
||||
|
@ -204,7 +213,7 @@ class TagsField extends React.Component {
|
|||
<div draggable="true" className="tag-badge" key={i} ref={domNode => this.domTags[i] = domNode}
|
||||
onClick={this.stop}
|
||||
onDragStart={this.handleDragStart(tag)}
|
||||
onDragEnd={this.handleDragEnd}>{tag} <a href="javascript:void(0)" onClick={this.removeTag(i)}>×</a> </div>
|
||||
onDragEnd={this.handleDragEnd}>{tag} <a href="javascript:void(0)" onClick={this.handleRemoveTag(i)}>×</a> </div>
|
||||
)}
|
||||
<div className="inputText" contentEditable="true" ref={(domNode) => this.inputText = domNode}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.edit-project-dialog{
|
||||
.name-fields{
|
||||
display: flex;
|
||||
.btn.toggle-tags{
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
input[type="text"]{
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -105,4 +105,19 @@
|
|||
.task-filters{
|
||||
float: right;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue