Commit 42e506c5 authored by Robin Tissot's avatar Robin Tissot
Browse files

Merge branch 'feature/one-block' into 'develop'

Feature/one block

See merge request !43
parents fae4add1 f6382625
......@@ -200,25 +200,39 @@ class LineSerializer(serializers.ModelSerializer):
list_serializer_class = LineListSerializer
class LineMoveSerializer(serializers.ModelSerializer):
index = serializers.IntegerField()
class LineOrderListSerializer(serializers.ListSerializer):
def update(self, qs, validated_data):
# Maps for id->instance and id->data item.
line_mapping = {line.pk: line for line in qs}
data_mapping = {item['pk']: item for item in validated_data}
class Meta:
model = Line
fields = ('index',)
# we can only go down or up (not both)
first_ = qs[0]
down = first_.order < data_mapping[first_.pk]['order']
lines = list(data_mapping.items())
lines.sort(key=lambda l: l[1]['order'])
if down:
# reverse to avoid pushing up already moved lines
lines.reverse()
def __init__(self, *args, line=None, **kwargs):
self.line = line
super().__init__(*args, **kwargs)
for i, (line_id, data) in enumerate(lines):
line = line_mapping.get(line_id, None)
line.to(data['order'])
def move(self):
self.line.to(self.validated_data['index'])
line.document_part.enforce_line_order()
# returns all new ordering for the whole page
data = self.child.__class__(line.document_part.lines.all(), many=True).data
return data
class LineOrderSerializer(serializers.ModelSerializer):
pk = serializers.IntegerField()
order = serializers.IntegerField()
class Meta:
model = Line
fields = ('pk', 'order')
list_serializer_class = LineOrderListSerializer
class DetailedLineSerializer(LineSerializer):
......
......@@ -23,7 +23,6 @@ from api.serializers import (UserOnboardingSerializer,
BlockTypeSerializer,
LineTypeSerializer,
DetailedLineSerializer,
LineMoveSerializer,
LineOrderSerializer,
TranscriptionSerializer,
LineTranscriptionSerializer)
......@@ -254,13 +253,14 @@ class LineViewSet(DocumentPermissionMixin, ModelViewSet):
qs.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=['post'])
@action(detail=False, methods=['post'])
def move(self, request, document_pk=None, part_pk=None, pk=None):
line = get_object_or_404(Line, pk=pk)
serializer = LineMoveSerializer(line=line, data=request.data)
data = request.data.get('lines')
qs = Line.objects.filter(pk__in=[l['pk'] for l in data])
serializer = LineOrderSerializer(qs, data=data, many=True)
if serializer.is_valid():
serializer.move()
return Response({'status': 'moved'})
resp = serializer.save()
return Response(resp, status=200)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
......@@ -327,19 +327,35 @@ class LineTranscriptionViewSet(DocumentPermissionMixin, ModelViewSet):
@action(detail=False, methods=['PUT'])
def bulk_update(self, request, document_pk=None, part_pk=None, pk=None):
lines = request.data.get("lines")
lines = request.data.get('lines')
response = []
errors = []
for line in lines:
lt = get_object_or_404(LineTranscription, pk=line["pk"])
lt.new_version(author=request.user.username,
source=settings.VERSIONING_DEFAULT_SOURCE)
serializer = LineTranscriptionSerializer(lt, data=line, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({'status': 'ok'}, status=200)
if serializer.is_valid():
try:
lt.new_version(author=request.user.username,
source=settings.VERSIONING_DEFAULT_SOURCE)
except NoChangeException:
pass
serializer.save()
response.append(serializer.data)
else:
errors.append(errors)
if errors:
return Response(errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(status=200, data=response)
@action(detail=False, methods=['POST'])
def bulk_delete(self, request, document_pk=None, part_pk=None, pk=None):
lines = request.data.get("lines")
qs = LineTranscription.objects.filter(pk__in=lines)
qs.update(content='')
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT, )
......@@ -763,6 +763,15 @@ class DocumentPart(OrderedModel):
return to_calc
def enforce_line_order(self):
# django-ordered-model doesn't care about unicity and linearity...
lines = self.lines.order_by('order', 'pk')
for i, line in enumerate(lines):
if line.order != i:
logger.debug('Enforcing line order %d : %d' % (line.pk, i))
line.order = i
line.save()
def validate_polygon(value):
if value is None:
......@@ -829,7 +838,7 @@ class Line(OrderedModel): # Versioned,
script = models.CharField(max_length=8, null=True, blank=True) # choices ??
# text direction
order_with_respect_to = 'document_part'
version_ignore_fields = ('document_part', 'order')
# version_ignore_fields = ('document_part', 'order')
typology = models.ForeignKey(LineType, null=True, blank=True,
on_delete=models.SET_NULL)
......
var diploLine = LineBase.extend({
props: ['line', 'ratio'],
computed: {
showregion() {
let idx = this.$parent.part.lines.indexOf(this.line);
if (idx) {
let pr = this.$parent.part.lines[idx - 1].region;
if (this.line.region == pr)
return "";
else
return this.getRegion() + 1 ;
} else {
return this.getRegion() + 1 ;
}
}
},
mounted() {
this.$content = this.$refs.content[0];
Vue.nextTick(function() {
this.$parent.appendLine();
}.bind(this));
},
beforeDestroy() {
let el = this.getEl();
if (el != null) {
el.remove();
}
},
watch: {
'line.order': function(o, n) {
// make sure it's at the right place,
// in case it was just created or the ordering got recalculated
let index = Array.from(this.$el.parentNode.children).indexOf(this.$el);
if (index != this.line.order) {
this.$el.parentNode.insertBefore(
this.$el,
this.$el.parentNode.children[this.line.order]);
/* 'line.order': function(n,o) {
* // make sure it's at the right place,
* // in case it was just created or the ordering got recalculated
* let index = Array.from(this.$el.parentNode.children).indexOf(this.$el);
* console.log(index, this.line.order);
* if (index != this.line.order) {
* this.$el.parentNode.insertBefore(
* this.$el,
* this.$el.parentNode.children[this.line.order]);
* this.setElContent(this.line.currentTrans.content);
* }
* }, */
'line.currentTrans': function(n, o) {
if (n!=undefined && n.content) {
this.setElContent(n.content);
}
}
},
methods: {
startEdit(ev) {
// if we are selecting text we don't want to start editing
// to be able to do multiline selection
if (document.getSelection().toString()) {
return true;
}
this.$content.setAttribute('contenteditable', true);
this.$content.focus(); // needed in case we edit from the panel
this.$parent.setEditLine(this.line);
this.$content.style.backgroundColor = '#F8F8F8';
this.$parent.$parent.blockShortcuts = true;
getEl() {
return this.$parent.editor.querySelector('div:nth-child('+parseInt(this.line.order+1)+')');
},
stopEdit(ev) {
this.$content.setAttribute('contenteditable', false);
this.$content.style.backgroundColor = 'white';
this.$parent.$parent.blockShortcuts = false;
this.pushUpdate();
},
pushUpdate(){
// set content of input to line content
if (this.line.currentTrans.content != this.$content.textContent) {
this.line.currentTrans.content = this.$content.textContent;
this.addToList();
// call save of parent method
this.$parent.toggleSave();
}
},
setContent(content){
let id = this.line.pk;
$("#" + id).text(content);
this.line.currentTrans.content = content;
},
onPaste(e) {
let pastedData = e.clipboardData.getData('text/plain');
let pasted_data_split = pastedData.split('\n');
if (pasted_data_split.length < 2) {
return
} else {
e.preventDefault();
if (pasted_data_split[pasted_data_split.length - 1] == "")
pasted_data_split.pop();
let index = this.$parent.$children.indexOf(this);
for (let i = 1; i < pasted_data_split.length; i++) {
if (this.$parent.$children[index + i]) {
let content = pasted_data_split[i];
let child = this.$parent.$children[index + i];
child.setContent(content);
child.addToList();
} else {
let content = pasted_data_split.slice(i - 1).join('\n');
let child = this.$parent.$children[index + 1];
child.setContent(content);
child.addToList();
}
}
}
setElContent(content) {
let line = this.getEl();
line.textContent = content;
},
addToList(){
if(this.line.currentTrans.pk)
this.$parent.$emit('update:transcription:content', this.line.currentTrans);
else
this.$parent.$emit('create:transcription', this.line.currentTrans);
getRegion() {
return this.$parent.part.regions.findIndex(r => r.pk == this.line.region);
}
}
});
var DiploPanel = BasePanel.extend({
data() { return {
editLine: null,
save: false, //show save button
updatedLines : [],
createdLines : [],
dragging: -1
movedLines:[],
};},
components: {
'diploline': diploLine,
},
watch: {
'part.loaded': function(isLoaded, wasLoaded) {
if (!isLoaded) {
// changed page probably
this.empty();
}
}
},
created() {
this.$on('update:transcription:content', function(linetranscription) {
this.addToUpdatedLines(linetranscription);
});
this.$on('create:transcription', function(linetranscription) {
this.createdLines.push(linetranscription);
});
// vue.js quirck, have to dinamically create the event handler
// call save every 10 seconds after last change
this.debouncedSave = _.debounce(function() {
this.save();
}.bind(this), 10000);
},
mounted() {
Vue.nextTick(function() {
var vm = this ;
vm.sortable = Sortable.create(this.editor, {
disabled: true,
multiDrag: true,
multiDragKey : 'CTRL',
selectedClass: "selected",
animation: 150,
onEnd: function(evt) {
vm.onDraggingEnd(evt);
}
});
}.bind(this));
this.editor = this.$el.querySelector('#diplomatic-lines');
this.sortModeBtn = this.$el.querySelector('#sortMode');
this.saveNotif = this.$el.querySelector('.tools #save-notif');
},
methods:{
toggleSave(){
methods: {
empty() {
while (this.editor.hasChildNodes()) {
this.editor.removeChild(this.editor.lastChild);
}
},
toggleSort() {
if (this.editor.contentEditable === 'true') {
this.editor.contentEditable = 'false';
this.sortable.option('disabled', false);
this.sortModeBtn.classList.remove('btn-info');
this.sortModeBtn.classList.add('btn-success');
} else {
this.editor.contentEditable = 'true';
this.sortable.option('disabled', true);
this.sortModeBtn.classList.remove('btn-success');
this.sortModeBtn.classList.add('btn-info');
}
},
changed() {
this.saveNotif.classList.remove('hide');
this.debouncedSave();
},
appendLine(pos) {
let div = document.createElement('div');
div.appendChild(document.createElement('br'));
if (pos === undefined) {
this.editor.appendChild(div);
} else {
this.editor.insertBefore(div, pos.nextSibling);
}
return div;
},
constrainLineNumber() {
// add lines untill we have enough of them
while (this.editor.childElementCount < this.part.lines.length) {
this.appendLine();
}
// need to add/remove danger indicators
for (let i=0; i<this.editor.childElementCount; i++) {
let line = this.editor.querySelector('div:nth-child('+parseInt(i+1)+')');
if (line === null) {
this.editor.children[i].remove();
continue;
}
if (i<this.part.lines.length) {
line.classList.remove('alert-danger');
line.setAttribute('title', '');
} else if (i>=this.part.lines.length) {
if (line.textContent == '') { // just remove empty lines
line.remove();
} else {
line.classList.add('alert-danger');
line.setAttribute('title', 'More lines than there is in the segmentation!');
}
}
}
},
startEdit(ev) {
this.$parent.blockShortcuts = true;
},
stopEdit(ev) {
this.$parent.blockShortcuts = false;
this.constrainLineNumber();
this.save();
},
onDraggingEnd(ev) {
/*
Finish dragging lines, save new positions
*/
if(ev.newIndicies.length == 0 && ev.newIndex != ev.oldIndex) {
let diploLine = this.$children.find(dl=>dl.line.order==ev.oldIndex);
this.movedLines.push({
"pk": diploLine.line.pk,
"order": ev.newIndex
});
} else {
for(let i=0; i< ev.newIndicies.length; i++) {
let diploLine = this.$children.find(dl=>dl.line.order==ev.oldIndicies[i].index);
this.movedLines.push({
"pk": diploLine.line.pk,
"order": ev.newIndicies[i].index
});
}
}
this.moveLines();
},
moveLines() {
if(this.movedLines.length != 0){
this.$parent.$emit('move:line', this.movedLines, function () {
this.movedLines = [];
}.bind(this));
}
},
save() {
/*
if some lines are modified add them to updatedlines,
new lines add them to createdLines then save
*/
this.saveNotif.classList.add('hide');
this.addToList();
this.bulkUpdate();
this.bulkCreate();
},
setHeight() {
this.$el.querySelector('.content-container').style.maxHeight = Math.round(this.part.image.size[1] * this.ratio) + 'px';
},
editNext(ev) {
ev.preventDefault();
let index = this.part.lines.indexOf(this.editLine);
if(index < this.part.lines.length - 1) {
this.setEditLine(this.part.lines[index + 1]);
let nextLine = this.$children[index + 1];
nextLine.startEdit();
focusNextLine(sel, line) {
if (line.nextSibling) {
let range = document.createRange();
range.setStart(line.nextSibling, 0);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
},
focusPreviousLine(sel, line) {
if (line.previousSibling) {
let range = document.createRange();
range.setStart(line.previousSibling, 0);
sel.removeAllRanges();
sel.addRange(range);
}
},
onKeyPress(ev) {
// arrows needed to avoid skipping empty lines
if (ev.key == 'ArrowDown' && !ev.shiftKey) {
let sel = window.getSelection();
let div = sel.anchorNode.nodeType==Node.TEXT_NODE?sel.anchorNode.parentElement:sel.anchorNode;
this.focusNextLine(sel, div);
ev.preventDefault();
} else if (ev.key == 'ArrowUp' && !ev.shiftKey) {
let sel = window.getSelection();
let div = sel.anchorNode.nodeType==Node.TEXT_NODE?sel.anchorNode.parentElement:sel.anchorNode;
this.focusPreviousLine(sel, div);
ev.preventDefault();
}
},
cleanSource(dirtyText) {
// cleanup html and possibly other tags (?)
var tmp = document.createElement("DIV");
tmp.innerHTML = dirtyText;
let clean = tmp.textContent || tmp.innerText || "";
tmp.remove();
return clean;
},
onPaste(e) {
let pastedData = e.clipboardData.getData('text/plain');
let pasted_data_split = pastedData.split('\n');
if (pasted_data_split.length == 1) {
let content = this.cleanSource(pastedData);
document.execCommand('insertText', false, content);
} else {
const selection = window.getSelection();
let range = selection.getRangeAt(0);
let target = range.startContainer.nodeType==Node.TEXT_NODE?range.startContainer.parentNode:range.startContainer;
let start = Array.prototype.indexOf.call(target.parentNode.children, target);
for (let i = 0; i < pasted_data_split.length; i++) {
let content = pasted_data_split[i];
let child = target.parentNode.children[start+i];
let newDiv;
if (child) {
newDiv = this.appendLine(child);
} else {
newDiv = this.appendLine();
}
// trick to get at least 'some' ctrl+z functionality
range.setStart(newDiv, 0);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("insertText", false, this.cleanSource(content));
}
}
this.constrainLineNumber();
e.preventDefault();
},
showOverlay(ev) {
let target = ev.target.nodeType==Node.TEXT_NODE?ev.target.parentNode:ev.target;
let index = Array.prototype.indexOf.call(target.parentNode.children, target);
if (index > -1 && index < this.$children.length) {
let diploLine = this.$children.find(dl=>dl.line.order==index);
if (diploLine) diploLine.showOverlay();
} else {
this.hideOverlay();
}
},
setEditLine(l) {
this.editLine = l;
hideOverlay() {
if (this.$children.length) this.$children[0].hideOverlay();
},
bulkUpdate(){
bulkUpdate() {
if(this.updatedLines.length){
this.$parent.$emit(
'bulk_update:transcriptions',
......@@ -48,18 +248,37 @@ var DiploPanel = BasePanel.extend({
}.bind(this));
}
},
bulkCreate(){
bulkCreate() {
if(this.createdLines.length){
this.$parent.$emit(
'bulk_create:transcriptions',
this.createdLines,
function () {
this.createdLines = [];
this.createdLines.splice(0, this.createdLines.length);
}.bind(this));
}
},
addToUpdatedLines(lt){
addToList() {
/*
parse all lines if the content changed, add it to updated lines
*/
for(let i=0; i<this.$children.length; i++) {
let currentLine = this.$children[i];
let content = currentLine.getEl().textContent;
if(currentLine.line.currentTrans.content != content){
currentLine.line.currentTrans.content = content;
if(currentLine.line.currentTrans.pk) {
this.addToUpdatedLines(currentLine.line.currentTrans);
} else {
this.createdLines.push(currentLine.line.currentTrans);
}