Commit 1abf223d authored by Robin Tissot's avatar Robin Tissot
Browse files

Merge branch 'develop' of gitlab.inria.fr:scripta/escriptorium into develop

parents 24b89ceb 82771f20
......@@ -81,14 +81,6 @@ class UserOnboardingSerializer(serializers.ModelSerializer):
model = User
fields = ('onboarding',)
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def complete(self):
self.user.onboarding = self.validated_data['onboarding']
self.user.save()
class BlockTypeSerializer(serializers.ModelSerializer):
class Meta:
......
......@@ -11,6 +11,26 @@ from django.urls import reverse
from core.models import Block, Line, Transcription, LineTranscription
from core.tests.factory import CoreFactoryTestCase
class UserViewSetTestCase(CoreFactoryTestCase):
def setUp(self):
super().setUp()
def test_onboarding(self):
user = self.factory.make_user()
self.client.force_login(user)
uri = reverse('api:user-onboarding')
resp = self.client.put(uri, {
'onboarding' : 'False',
}, content_type='application/json')
user.refresh_from_db()
self.assertEqual(resp.status_code, 200)
self.assertEqual(user.onboarding, False)
class DocumentViewSetTestCase(CoreFactoryTestCase):
def setUp(self):
......
......@@ -14,7 +14,7 @@ from api.views import (DocumentViewSet,
router = routers.DefaultRouter()
router.register(r'documents', DocumentViewSet)
router.register(r'users', UserViewSet)
router.register(r'user', UserViewSet)
router.register(r'types/block', BlockTypeViewSet)
router.register(r'types/line', LineTypeViewSet)
documents_router = routers.NestedSimpleRouter(router, r'documents', lookup='document')
......
......@@ -50,9 +50,9 @@ class UserViewSet(ModelViewSet):
@action(detail=False, methods=['put'])
def onboarding(self, request):
serializer = UserOnboardingSerializer(data=request.data, user=self.request.user)
serializer = UserOnboardingSerializer(self.request.user,data=request.data, partial=True)
if serializer.is_valid(raise_exception=True):
serializer.complete()
serializer.save()
return Response(status=status.HTTP_200_OK)
......
......@@ -163,6 +163,6 @@ const TranscriptionModal = Vue.component('transcriptionmodal', {
} else {
overlay.style.display = 'none';
}
},
}
},
});
......@@ -147,8 +147,6 @@ class DocumentImages(LoginRequiredMixin, DocumentMixin, DetailView):
context['process_form'] = DocumentProcessForm(self.object, self.request.user)
context['import_form'] = ImportForm(self.object, self.request.user)
context['export_form'] = ExportForm(self.object, self.request.user)
context['onboarding'] = self.request.user.onboarding
return context
......
......@@ -32,7 +32,7 @@ class MyUserAdmin(UserAdmin):
add_form = MyUserCreationForm
list_display = UserAdmin.list_display + ('last_login',)
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('fields',)}), # second fields refers to research fields
(None, {'fields': ('fields','onboarding')}), # second fields refers to research fields
)
......
......@@ -21,7 +21,7 @@ class User(AbstractUser):
fields = models.ManyToManyField('ResearchField', blank=True)
onboarding = models.BooleanField(
_('first connection'),
_('Show onboarding'),
default=True
)
......
......@@ -102,6 +102,7 @@ class Profile(SuccessMessageMixin, UpdateView):
page_number = self.request.GET.get('page')
context['is_paginated'] = paginator.count != 0
context['page_obj'] = paginator.get_page(page_number)
return context
class ContactUsView(SuccessMessageMixin, CreateView):
......
......@@ -644,5 +644,8 @@ i.panel-icon {
}
#contact-form{
width: 100%;
width: 100%;
}
#reset-onboarding{
margin-top: 5%;
}
\ No newline at end of file
var document_images_intro = introJs();
document_images_intro.setOptions({
steps: [
{
element: '#nav-doc-tab',
intro: 'Update Document description (Name, Text direction or metadata).<br>',
position: 'bottom'
},
{
element: '#nav-edit-tab',
intro: 'Edit Document Part. Panels to update transcriptions, baselines and masks',
position: 'bottom'
},
{
element: '#nav-models-tab',
intro: 'Handle Transcription and Segmentation models related to this document.',
position: 'bottom'
}, {
element: '#document-image-dropzone',
intro: 'Upload a new document part image.',
position: 'bottom'
},
{
element: '#import-selected',
intro: 'Import document part. <br> accepted formats : IIIF, Pagexml, Alto.',
position: 'bottom'
},
{
element: '#document-export',
intro: 'Import document part. <br> accepted formats : Text, Pagexml, Alto.',
position: 'bottom'
},
{
element: '#train-selected',
intro: 'Train a Segmentation or Transcription model',
position: 'bottom'
},
{
element: '#binarize-selected',
intro: 'Binarize the color of selected images.',
position: 'bottom'
},
{
element: '#segment-selected',
intro: 'Segment selected images.',
position: 'bottom'
},
{
element: '#transcribe-selected',
intro: 'Transcribe automatically the selected images.',
position: 'top'
},
{
element: '#js-edit',
intro: 'Transcribe, Segment Manually the image.',
position: 'bottom'
},
]});
/*
Just set ONBOARDING_PAGE in the page before the scripts block.super eg:
function exitonboarding() {
$.ajax({type: 'PUT', url:'/api/users/onboarding/',
contentType: "application/json; charset=utf-8",
data:JSON.stringify({
onboarding : "False",
})
const ONBOARDING_PAGE = 'onboarding_document_form';
}).done($.proxy(function(data){
console.log("success",data)
}, this)).fail(function(data) {
alert(data);
*/
if (typeof ONBOARDING_PAGE !== 'undefined') {
var onboarding_page_done = userProfile.get(ONBOARDING_PAGE) || false;
if (!onboarding_page_done) {
var intro = introJs().setOptions({'skipLabel': "Skip"});
intro.oncomplete(function() {
userProfile.set(ONBOARDING_PAGE, true);
})
intro.onexit(function(aa) {
if (!userProfile.get(ONBOARDING_PAGE, true)) {
if (confirm("Are you sure you want to avoid further help?")) {
exitOnboarding();
}
}
});
//document_form
if (ONBOARDING_PAGE == 'onboarding_document_form') {
intro.setOptions({
steps: [
{
element: '#nav-doc-tab',
intro: 'Update Document description (Name, Text direction or metadata).',
position: 'bottom',
},
{
element: '#nav-img-tab',
intro: 'Upload images and changes their orders, import and export transcriptions, launch mass automatic segmentation or transcription.',
position: 'bottom',
},
{
element: '#nav-edit-tab',
intro: 'Panels to update transcriptions, baselines and masks.',
position: 'bottom',
},
{
element: '#nav-models-tab',
intro: 'Handle Transcription and Segmentation models related to this document.',
position: 'bottom'
},
]
});
} else if (ONBOARDING_PAGE == 'onboarding_images') {
intro.setOptions({
steps: [
{
element: '#import-selected',
intro: 'Import images, segmentation and/or transcriptions. <br> accepted formats : IIIF, Pagexml, Alto.',
position: 'bottom'
},
{
element: '#document-export',
intro: 'Export segmentation and/or transcriptions. <br> accepted formats : Text, Pagexml, Alto.',
position: 'bottom'
},
{
element: '#train-selected',
intro: 'Train a Segmentation or Transcription model.',
position: 'bottom'
},
{
element: '#binarize-selected',
intro: 'Binarize the color of selected images.',
position: 'bottom'
},
{
element: '#segment-selected',
intro: 'Segment selected images.',
position: 'bottom'
},
{
element: '#transcribe-selected',
intro: 'Transcribe automatically the selected images.',
position: 'left'
},
{
element: "#cards-container",
intro: 'This shows all the images for your manuscript. You can select one or multiple images for training, segmentation, transcribing, or export. Clicking on the [fas fa-edit] icon allows you to edit the segmentation and text. The [fa-align-left] icon shows you if the page has been segmented (green = yes, black = no, ‘pulsing’ green = segmentation in progress). The blue progress bar shows the amount of text that has been entered.\n',
position: 'top'
}
]
});
} else if (ONBOARDING_PAGE == 'onboarding_edit') {
intro.setOptions({
steps: [
// {
// element: '#document-transcriptions',
// intro: 'Here you can select which transcription to display. You may have several transcriptions for a given page,<br> for instance a manual one and one created automatically, or two different editions that you have imported.',
// position: 'bottom',
// },
{
element: '#seg-panel-btn',
intro: 'In this panel you can manually segment the image or correct the segmentation. You can draw regions or lines onto the image, or change existing lines or regions,<br> and you can also add categories to the different regions and lines (‘main text’, ‘marginal gloss’, ‘page number’ etc.)',
position: 'left',
},
{
element: '#trans-panel-btn',
intro: 'In this pane you can enter or correct a transcription line-by-line.<br> Clicking on a line of text will bring up a window showing the image of that line, and a box where you can enter or correct the transcription.',
position: 'left',
},
{
element: '#diplo-panel-btn',
intro: 'This shows another form for entering transcription.<br> Here you can enter and work with multiple lines at a time, for instance copying and pasting a block of text from another source.\n.',
position: 'left',
},
]
});
} else if (ONBOARDING_PAGE == 'onboarding_models') {
intro.setOptions({
doneLabel: null,
steps: [{
element: '#models-table',
intro: 'Here you manage Transcription and Segmentation models related to this document.',
position: 'bottom'
}]
});
}
document.addEventListener('DOMContentLoaded', function() {
intro.start();
});
}
}
function exitOnboarding() {
$.ajax({
type: 'PUT',
url: '/api/user/onboarding/',
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
onboarding: "False",
})
});
}
......@@ -117,7 +117,7 @@
padding: 10px;
background-color: white;
min-width: 200px;
max-width: 300px;
max-width: 100%;
border-radius: 3px;
box-shadow: 0 1px 10px rgba(0,0,0,.4);
-webkit-transition: opacity 0.1s ease-out;
......
......@@ -27,6 +27,7 @@
<link href="{% static '/vendor/fontawesome/all.min.css' %}" rel="stylesheet" type="text/css">
<link href="{% static '/css/escriptorium.css' %}" rel="stylesheet" type="text/css">
<link href="{% static '/css/rtl.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'vendor/introjs/introjs.css' %}" rel="stylesheet" type="text/css">
{% endcompress %}
{% endblock styles %}
......@@ -112,6 +113,10 @@
<script src="{% static 'js/messages.js' %}"></script>
{% include 'includes/messages.html' %}
<script src="{% static 'js/profile.js' %}"></script>
{% if user.onboarding %}
<script src="{% static 'vendor/introjs/intro.js' %}"></script>
<script src="{% static 'js/onboarding.js' %}"></script>
{% endif %}
{% endif %}
{% endblock scripts %}
</body>
......
......@@ -166,6 +166,11 @@
{% endblock %}
{% block scripts %}
<script type="text/javascript">
{% if user.onboarding %}
const ONBOARDING_PAGE = 'onboarding_document_form';
{% endif %}
</script>
{{ block.super }}
<script src="{% static 'js/document_form.js' %}"></script>
<script src="{% static 'js/help.js' %}"></script>
......
......@@ -4,7 +4,6 @@
{% block styles %}
<link href="{% static 'vendor/dropzone/basic.min.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'vendor/dropzone/dropzone.min.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'vendor/introjs/introjs.css' %}" rel="stylesheet" type="text/css">
{# custom jquery ui build with draggable and resizable only #}
<link href="{% static 'vendor/jquery/jquery-ui.min.css' %}" rel="stylesheet" type="text/css">
......@@ -113,42 +112,31 @@
{% endblock %}
{% block scripts %}
{{ block.super }}
<script type="text/javascript">
'use strict';
const DOCUMENT_ID = {{ document.pk }};
const onboarding = "{{ onboarding }}";
$(document).ready(function() {
// join the ws room
msgSocket.addEventListener('open', function(ev) {
msgSocket.send('{"type": "join-room", "object_cls": "document", "object_pk": {{ document.pk }}}');
});
});
'use strict';
const DOCUMENT_ID = {{ document.pk }};
{% if user.onboarding %}
const ONBOARDING_PAGE = 'onboarding_images';
{% endif %}
</script>
{% compress js file document_images %}
<script src="{% static 'vendor/dropzone/dropzone.min.js' %}"></script>
<script src="{% static 'vendor/jquery/jquery-ui.min.js' %}"></script>
<script src="{% static 'vendor/introjs/intro.js' %}"></script>
<script src="{% static 'js/image_cards.js' %}"></script>
<script src="{% static 'js/lazyload.js' %}"></script>
<script src="{% static 'js/help.js' %}"></script>
<script src="{% static 'js/onboarding.js' %}"></script>
{% endcompress %}
{{ block.super }}
<script type="text/javascript">
if (onboarding == "True")
{
document_images_intro.start();
document_images_intro.onexit(function() {
exitonboarding();
});
document_images_intro.oncomplete(function() {
exitonboarding();
});
'use strict';
}
$(document).ready(function() {
// join the ws room
msgSocket.addEventListener('open', function(ev) {
msgSocket.send('{"type": "join-room", "object_cls": "document", "object_pk": {{ document.pk }}}');
});
});
</script>
{% compress js file document_images %}
<script src="{% static 'vendor/dropzone/dropzone.min.js' %}"></script>
<script src="{% static 'vendor/jquery/jquery-ui.min.js' %}"></script>
<script src="{% static 'js/image_cards.js' %}"></script>
<script src="{% static 'js/lazyload.js' %}"></script>
<script src="{% static 'js/help.js' %}"></script>
{% endcompress %}
{% endblock %}
{% extends 'base.html' %}
{% load i18n bootstrap %}
{% load i18n staticfiles bootstrap %}
{% block head_title %}{% if object %}{% trans "Update a Document" %}{% else %}{% trans "Create a new Document" %}{% endif %}{% endblock %}
{% block body %}
......
......@@ -62,26 +62,30 @@
{% endblock %}
{% block extra_nav %}
<div class="nav-item ml-auto">
<div class="nav-item ml-auto" id="toggle-panels">
<button type="button"
id="source-panel-btn"
@click="togglePanel"
data-target="source"
class="open-panel nav-item btn"
v-bind:class="[ show.source ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Source Image" %}"><i class="click-through fas fa-eye"></i></button>
<button type="button"
id="seg-panel-btn"
@click="togglePanel"
data-target="segmentation"
class="open-panel nav-item btn"
v-bind:class="[ show.segmentation ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Segmentation" %}"><i class="click-through fas fa-align-left"></i></button>
<button type="button"
id="trans-panel-btn"
@click="togglePanel"
data-target="visualisation"
class="open-panel nav-item btn"
v-bind:class="[ show.visualisation ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Transcription" %}"><i class="click-through fas fa-language"></i></button>
<button type="button"
id="diplo-panel-btn"
@click="togglePanel"
data-target="diplomatic"
class="open-panel nav-item btn"
......@@ -171,6 +175,7 @@
<SegmentationPanel v-if="show.segmentation && part.loaded"
v-bind:fullsizeimage="fullsizeimage"
v-bind:part="part"
id="segmentation-panel"
ref="segPanel" inline-template>
<div class="col panel">
<div class="tools">
......@@ -333,6 +338,7 @@
<keep-alive>
<VisuPanel v-if="show.visualisation && part.loaded"
v-bind:part="part"
id="transcription-panel"
ref="visuPanel" inline-template>
<div class="col panel">
<div class="tools">
......@@ -538,8 +544,7 @@
</TranscriptionModal>
</div>
</VisuPanel>
</keep-alive>
</keep-alive>
<keep-alive>
<DiploPanel id="diplo-panel"
v-bind:part="part"
......@@ -601,17 +606,23 @@
</a>
{% endif %}
</div>
</div>
{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script type="text/javascript">
const READ_DIRECTION = '{{document.read_direction}}';
const TEXT_DIRECTION = '{{document.main_script.text_direction}}';
const DOCUMENT_ID = '{{document.id}}';
var PART_ID = {{part.id}}; // can be changed with next & previous pages
</script>
{% block scripts %}
<script type="text/javascript">
const READ_DIRECTION = '{{document.read_direction}}';
const TEXT_DIRECTION = '{{document.main_script.text_direction}}';
const DOCUMENT_ID = '{{document.id}}';
var PART_ID = {{part.id}}; // can be changed with next & previous pages
{% if user.onboarding %}
const ONBOARDING_PAGE = "onboarding_edit";
{% endif %}
var models_url = "{% url 'document-models' document_pk=document.pk %}";
</script>
{{ block.super }}
<script type="text/javascript">
'use strict';
......
......@@ -35,13 +35,13 @@
<i class="fas fa-save"></i>
</button>
{% endif %}
{% if model.file %}
<a href="{{ model.file.url }}" class="btn btn-sm btn-primary" title="{% trans "Download" %}" role="button">
<i class="fas fa-file-download"></i>
</a>
{% endif %}
{% if model.owner == request.user and not model.training %}
<form method="POST" class="inline-form" action="{% url 'model-delete' model.pk %}?next={{request.path}}">
{% csrf_token %}
......@@ -54,7 +54,7 @@
</tr>
{% for version in model.history %}
<tr id="model-version-{{version.revision}}" class="versions-{{model.pk}} collapse">
<td title="{% trans "Model name" %}" class="pl-5">{{ version.name }} (epoch #{{version.training_epoch}})</td>
<td></td>
<td title="{% trans "Accuracy" %}">{{ version.accuracy_percent|floatformat:1 }}%</td>
......@@ -62,7 +62,7 @@
<td>
<a href="{{ version.file.url }}">
<button type="button" class="btn btn-sm btn-primary" title="{% trans "Download" %}">
<i class="fas fa-file-download"></i>
<i class="fas fa-file-download"></i>
</button>
</td>
</tr>
......@@ -72,6 +72,13 @@
{% endblock %}
{% block scripts %}
<script type="text/javascript">
'use strict';
{% if user.onboarding %}
const ONBOARDING_PAGE = "onboarding_models";
{% endif %}
</script>