Commit 874e8a25 authored by Robin Tissot's avatar Robin Tissot
Browse files

Fixes duplicate annotation deletion and working on annotation before page reload.

parent cba54783
......@@ -179,21 +179,23 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
def test_list_document_with_tasks(self):
# Creating a new Document that self.doc.owner shouldn't see
other_doc = self.factory.make_document(project=self.factory.make_project(name="Test API"))
report = other_doc.reports.create(user=other_doc.owner, label="Fake report")
report.start()
report1 = other_doc.reports.create(user=other_doc.owner, label="Fake report")
report1.start()
report2 = self.doc.reports.create(user=self.doc.owner, label="Fake report")
report2.start()
self.client.force_login(self.doc.owner)
with self.assertNumQueries(6):
resp = self.client.get(reverse('api:document-tasks'))
self.assertEqual(resp.status_code, 200)
json = resp.json()
self.assertEqual(resp.status_code, 200)
self.assertEqual(json['count'], 1)
self.assertEqual(json['results'], [{
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 6, 'Canceled': 0},
'tasks_stats': {'Queued': 0, 'Running': 1, 'Crashed': 0, 'Finished': 0, 'Canceled': 0},
'last_started_task': self.doc.reports.latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
......@@ -204,6 +206,8 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
other_doc = self.factory.make_document(project=self.factory.make_project(name="Test API"))
report = other_doc.reports.create(user=other_doc.owner, label="Fake report")
report.start()
report2 = self.doc.reports.create(user=self.doc.owner, label="Fake report")
report2.start()
self.client.force_login(self.doc.owner)
with self.assertNumQueries(8):
......@@ -224,7 +228,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 6, 'Canceled': 0},
'tasks_stats': {'Queued': 0, 'Running': 1, 'Crashed': 0, 'Finished': 0, 'Canceled': 0},
'last_started_task': self.doc.reports.latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
},
])
......@@ -243,6 +247,8 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
other_doc = self.factory.make_document(project=self.factory.make_project(name="Test API"))
report = other_doc.reports.create(user=other_doc.owner, label="Fake report")
report.start()
report2 = self.doc.reports.create(user=self.doc.owner, label="Fake report")
report2.start()
self.client.force_login(self.doc.owner)
with self.assertNumQueries(6):
......@@ -256,7 +262,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 6, 'Canceled': 0},
'tasks_stats': {'Queued': 0, 'Running': 1, 'Crashed': 0, 'Finished': 0, 'Canceled': 0},
'last_started_task': self.doc.reports.latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
......@@ -381,7 +387,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 1, 'Running': 1, 'Crashed': 0, 'Finished': 6, 'Canceled': 0},
'tasks_stats': {'Queued': 1, 'Running': 1, 'Crashed': 0, 'Finished': 0, 'Canceled': 0},
'last_started_task': self.doc.reports.filter(started_at__isnull=False).latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
......@@ -409,7 +415,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 6, 'Canceled': 2},
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 0, 'Canceled': 2},
'last_started_task': self.doc.reports.filter(started_at__isnull=False).latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
model.refresh_from_db()
......@@ -440,7 +446,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 1, 'Running': 1, 'Crashed': 0, 'Finished': 6, 'Canceled': 0},
'tasks_stats': {'Queued': 1, 'Running': 1, 'Crashed': 0, 'Finished': 0, 'Canceled': 0},
'last_started_task': self.doc.reports.filter(started_at__isnull=False).latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
......@@ -468,7 +474,7 @@ class DocumentViewSetTestCase(CoreFactoryTestCase):
'pk': self.doc.pk,
'name': self.doc.name,
'owner': self.doc.owner.username,
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 6, 'Canceled': 2},
'tasks_stats': {'Queued': 0, 'Running': 0, 'Crashed': 0, 'Finished': 0, 'Canceled': 2},
'last_started_task': self.doc.reports.filter(started_at__isnull=False).latest('started_at').started_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
}])
model.refresh_from_db()
......@@ -523,7 +529,7 @@ class PartViewSetTestCase(CoreFactoryTestCase):
self.client.force_login(self.user)
uri = reverse('api:part-list',
kwargs={'document_pk': self.part.document.pk})
with self.assertNumQueries(44):
with self.assertNumQueries(26):
img = self.factory.make_image_file()
resp = self.client.post(uri, {
'image': SimpleUploadedFile(
......
......@@ -470,7 +470,7 @@ class PartViewSet(DocumentPermissionMixin, ModelViewSet):
document_part = DocumentPart.objects.get(pk=pk)
angle = self.request.data.get('angle')
if angle:
document_part.rotate(angle)
document_part.rotate(angle, user=self.request.user)
return Response({'status': 'done'}, status=200)
else:
return Response({'error': "Post an angle."},
......
......@@ -210,19 +210,7 @@ class Annotation(models.Model):
abstract = True
def as_w3c(self):
if self.taxonomy.marker_type == AnnotationTaxonomy.MARKER_TYPE_RECTANGLE:
selector = {
"conformsTo": "http://www.w3.org/TR/media-frags/",
"type": "FragmentSelector",
"value": "xywh=pixel:{x},{y},{w},{h}".format(
x=self.coordinates[0][0],
y=self.coordinates[0][1],
w=self.coordinates[1][0] - self.coordinates[0][0],
h=self.coordinates[1][1] - self.coordinates[0][1],
),
}
elif self.taxonomy.marker_type == AnnotationTaxonomy.MARKER_TYPE_POLYGON:
if self.taxonomy.marker_type in [AnnotationTaxonomy.MARKER_TYPE_RECTANGLE, AnnotationTaxonomy.MARKER_TYPE_POLYGON]:
selector = {
'type': 'SvgSelector',
'value': '<svg><polygon points="{pts}"></polygon></svg>'.format(
......@@ -1254,7 +1242,7 @@ class DocumentPart(ExportModelOperationsMixin("DocumentPart"), OrderedModel):
return to_calc
def rotate(self, angle):
def rotate(self, angle, user=None):
"""
Rotates everything in this document part around the center by a given angle (in degrees):
images, lines and regions.
......@@ -1306,9 +1294,11 @@ class DocumentPart(ExportModelOperationsMixin("DocumentPart"), OrderedModel):
self.save()
# we need this one right away
get_thumbnailer(self.image).get_thumbnail(
settings.THUMBNAIL_ALIASES[""]["large"]
)
generate_part_thumbnails.delay(instance_pk=self.pk)
# rotate lines
for line in self.lines.all():
......@@ -1334,6 +1324,16 @@ class DocumentPart(ExportModelOperationsMixin("DocumentPart"), OrderedModel):
]
region.save()
# rotate img annotations
for annotation in self.imageannotation_set.prefetch_related('taxonomy'):
poly = affinity.rotate(Polygon(annotation.coordinates), angle, origin=center)
annotation.coordinates = [
(int(x - offset[0]), int(y - offset[1]))
for x, y in poly.exterior.coords
]
annotation.save()
def crop(self, x1, y1, x2, y2):
"""
Crops the image outside the rectangle defined
......
......@@ -45,15 +45,6 @@ def generate_part_thumbnails(instance_pk=None, user_pk=None, **kwargs):
if not getattr(settings, 'THUMBNAIL_ENABLE', True):
return
if user_pk:
try:
user = User.objects.get(pk=user_pk)
# If quotas are enforced, assert that the user still has free CPU minutes
if not settings.DISABLE_QUOTAS and user.cpu_minutes_limit() is not None:
assert user.has_free_cpu_minutes(), f"User {user.id} doesn't have any CPU minutes left"
except User.DoesNotExist:
user = None
try:
DocumentPart = apps.get_model('core', 'DocumentPart')
part = DocumentPart.objects.get(pk=instance_pk)
......
......@@ -16,7 +16,7 @@ def create_task_reporting(sender, body, **kwargs):
task_kwargs = body[1]
# If the reporting is disabled for this task we don't need to execute following code
if sender in settings.REPORTING_TASKS_BLACKLIST:
if sender.name in settings.REPORTING_TASKS_BLACKLIST:
return
User = get_user_model()
......@@ -97,7 +97,6 @@ def end_task_reporting(task_id, task, *args, **kwargs):
# If the reporting is disabled for this task we don't need to execute following code
if task.name in settings.REPORTING_TASKS_BLACKLIST:
return
TaskReport = apps.get_model('reporting', 'TaskReport')
try:
......
......@@ -232,6 +232,11 @@ CELERY_TASK_ROUTES = {
REPORTING_TASKS_BLACKLIST = [
'users.tasks.async_email',
# if the user still has disk space but no cpu quota it will just slow everything down
# to forbid thumbnails creation or image compression.
'core.tasks.convert',
'core.tasks.lossless_compression',
'core.tasks.generate_part_thumbnails'
]
CHANNEL_LAYERS = {
......
......@@ -410,6 +410,10 @@ form.inline-form {
/* overflow: hidden; */
}
.tools .fas {
line-height: 1.5;
}
i.panel-icon {
vertical-align: top;
}
......
......@@ -37,7 +37,7 @@ export var BasePanel = {
},
updateView() {}
}
}
};
export var LineBase = {
props: ['line', 'ratio'],
......@@ -67,7 +67,7 @@ export var LineBase = {
return this.line.mask.map(pt => Math.round(pt[0]*this.ratio)+','+Math.round(pt[1]*this.ratio)).join(' ');
},
}
}
};
var KeyValueWidget = function(args) {
// Annotorious/recogito-js widget to a key/value input in the annotation modal.
......@@ -178,7 +178,7 @@ export var AnnoPanel = {
widgets.push({widget: KeyValueWidget,
name: compo.name,
values: compo.allowed_values});
})
});
this.anno.widgets = widgets;
},
......@@ -191,7 +191,7 @@ export var AnnoPanel = {
return {
'component': annotation.taxonomy.components.find(c => 'attribute-'+c.name == b.purpose).pk,
'value': b.value
}
};
})
};
}
......
......@@ -124,6 +124,9 @@ export default Vue.extend({
vm.onDraggingEnd(evt);
}
});
// update heights and set ratio
this.refresh();
}.bind(this));
this.initAnnotations();
......@@ -216,23 +219,6 @@ export default Vue.extend({
formatter: textAnnoFormatter.bind(this)
});
// deal with annotation disappearing (user deleted the whole text)
const annotatonHighlighRemove = function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.removedNodes) {
for (let node of mutation.removedNodes) {
if (node.classList && node.classList.contains('r6o-annotation')) {
if (this.$store.state.transcriptions && this.$store.state.transcriptions.transcriptionsLoaded) {
this.$store.dispatch('textAnnotations/delete', node.dataset.id);
}
}
}
}
}
}.bind(this);
const observer = new MutationObserver(annotatonHighlighRemove);
observer.observe(this.$refs.diplomaticLines, {childList: true, subtree: true});
const isEditorOpen = function(mutationsList, observer) {
// let's hope for no race condition with the contenteditable focusin/out...
for (let mutation of mutationsList) {
......@@ -246,13 +232,15 @@ export default Vue.extend({
const editorObserver = new MutationObserver(isEditorOpen);
editorObserver.observe(this.anno._appContainerEl, {childList: true});
this.anno.on('createAnnotation', async function(annotation) {
this.anno.on('createAnnotation', async function(annotation, overrideId) {
annotation.taxonomy = this.currentTaxonomy;
let offsets = annotation.target.selector.find(e => e.type == 'TextPositionSelector');
let body = this.getAPITextAnnotationBody(annotation, offsets);
body.transcription = this.$store.state.transcriptions.selectedTranscription;
const newAnno = await this.$store.dispatch('textAnnotations/create', body);
annotation.id = newAnno.pk;
overrideId(newAnno.pk);
// updates actual object (annotation is just a copy)
this.anno.addAnnotation(annotation);
}.bind(this));
this.anno.on('updateAnnotation', function(annotation) {
......@@ -263,8 +251,9 @@ export default Vue.extend({
}.bind(this));
this.anno.on('selectAnnotation', function(annotation) {
this.enableTaxonomy(annotation.taxonomy);
this.setTextAnnoTaxonomy(annotation.taxonomy);
if (this.currentTaxonomy != annotation.taxonomy) {
this.toggleTaxonomy(annotation.taxonomy);
}
}.bind(this));
this.anno.on('deleteAnnotation', function(annotation) {
......@@ -413,12 +402,21 @@ export default Vue.extend({
/*
if some lines are modified add them to updatedlines,
new lines add them to createdLines then save
*/
*/
this.$refs.saveNotif.classList.add('hide');
this.addToList();
this.bulkUpdate();
var updated = this.bulkUpdate();
this.bulkCreate();
this.recalculateAnnotationSelectors();
updated.then(function(value) {
if (value > 0) this.recalculateAnnotationSelectors();
}.bind(this));
// check if some annotations were completely deleted by the erasing the text
for (let annotation of this.$store.state.textAnnotations.all) {
let annoEl = document.querySelector('.r6o-annotation[data-id="'+annotation.pk+'"]');
if (annoEl === null) this.$store.dispatch('textAnnotations/delete', annotation.pk);
}
},
focusNextLine(sel, line) {
......@@ -620,14 +618,16 @@ export default Vue.extend({
},
async bulkUpdate() {
if(this.updatedLines.length){
var toUpdate = this.updatedLines.length;
if(toUpdate) {
await this.$store.dispatch('transcriptions/bulkUpdate', this.updatedLines);
this.updatedLines = [];
}
return toUpdate;
},
async bulkCreate() {
if(this.createdLines.length){
if(this.createdLines.length) {
await this.$store.dispatch('transcriptions/bulkCreate', this.createdLines);
this.createdLines = [];
}
......
......@@ -237,7 +237,7 @@ export default Vue.extend({
imageLoaded: false,
colorMode: "color", // color - binary - grayscale
undoManager: new UndoManager(),
isWorking: false,
isWorking: false
};
},
components: {
......
<template>
<div class="col panel">
<loading :active.sync="isWorking" :is-full-page="false" />
<div class="tools">
<i title="Source Panel" class="panel-icon fas fa-eye"></i>
<a v-bind:href="$store.state.parts.image.uri" target="_blank">
......@@ -59,6 +60,7 @@ import { assign } from 'lodash'
import { BasePanel } from '../../src/editor/mixins.js';
import { AnnoPanel } from '../../src/editor/mixins.js';
import { Annotorious } from '@recogito/annotorious';
import Loading from "vue-loading-overlay";
const rectangleRegExp = new RegExp(/(?<x>\d+)(?:\.\d+)?,(?<y>\d+)(?:\.\d+)?,(?<w>\d+)(?:\.\d+)?,(?<h>\d+)(?:\.\d+)?/);
const polygonRegExp = new RegExp(/(?<x>\d+)(?:\.\d+)?,(?<y>\d+)(?:\.\d+)?/g);
......@@ -67,8 +69,12 @@ export default Vue.extend({
mixins: [BasePanel, AnnoPanel],
props: ['fullsizeimage'],
data() { return {
imageLoaded: false
imageLoaded: false,
isWorking: false
};},
components: {
loading: Loading,
},
computed: {
imageSrc() {
let src = !this.fullsizeimage
......@@ -113,7 +119,15 @@ export default Vue.extend({
},
methods: {
async rotate(angle) {
await this.$store.dispatch('parts/rotate', angle);
try {
this.isWorking = true;
await this.$store.dispatch('parts/rotate', angle);
this.loadAnnotations();
} catch {
// oh well
} finally {
this.isWorking = false;
}
},
async onImageLoaded() {
......@@ -123,12 +137,15 @@ export default Vue.extend({
getCoordinatesFromW3C(annotation) {
var coordinates = [];
if (annotation.taxonomy.marker_type == 'Rectangle') {
if (annotation.target.selector.type == 'FragmentSelector') {
// looks like xywh=pixel:133.98072814941406,144.94607543945312,169.30674743652344,141.2919921875"
let m = annotation.target.selector.value.match(rectangleRegExp).groups;
coordinates = [[parseInt(m.x), parseInt(m.y)],
[parseInt(m.x)+parseInt(m.w), parseInt(m.y)+parseInt(m.h)]];
} else if (annotation.taxonomy.marker_type == 'Polygon') {
[parseInt(m.x)+parseInt(m.w), parseInt(m.y)],
[parseInt(m.x)+parseInt(m.w), parseInt(m.y)+parseInt(m.h)],
[parseInt(m.x), parseInt(m.y)+parseInt(m.h)]
];
} else if (annotation.target.selector.type == 'SvgSelector') {
// looks like <svg><polygon points=\"168.08567810058594,230.20848083496094 422.65484619140625,242.38882446289062 198.5365447998047,361.75616455078125\"></polygon></svg>
let matches = annotation.target.selector.value.matchAll(polygonRegExp);
for (let m of matches) {
......@@ -158,6 +175,7 @@ export default Vue.extend({
async fetchAnnotations() {
await this.$store.dispatch('imageAnnotations/fetch');
if (this.imageLoaded) this.loadAnnotations();
},
initAnnotations() {
......@@ -221,8 +239,12 @@ export default Vue.extend({
}.bind(this));
this.anno.on('selectAnnotation', function(annotation) {
this.enableTaxonomy(annotation.taxonomy);
this.setAnnoTaxonomy(annotation.taxonomy);
if (this.currentTaxonomy != annotation.taxonomy) {
this.toggleTaxonomy(annotation.taxonomy);
// have to use this trick to make it editable..
this.anno.selectAnnotation();
this.anno.selectAnnotation(annotation);
}
}.bind(this));
this.anno.on('deleteAnnotation', function(annotation) {
......
......@@ -129,10 +129,6 @@ export default Vue.extend({
return 'lightgrey';
}
},
maskPoints() {
if (this.line == null || !this.line.mask) return '';
return this.line.mask.map(pt => Math.round(pt[0]*this.ratio)+','+Math.round(pt[1]*this.ratio)).join(' ');
},
fakeBaseline() {
// create a fake path based on the mask,
var min = this.line.mask.reduce((minPt, curPt) => (curPt[0] < minPt[0]) ? curPt : minPt);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment