diff --git a/app/escriptorium/context_processors.py b/app/escriptorium/context_processors.py new file mode 100644 index 0000000000000000000000000000000000000000..5c226b31c36778f6b5d5cb7d7de0e460a24fa622 --- /dev/null +++ b/app/escriptorium/context_processors.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +def enable_cookie_consent(request): + return {'ENABLE_COOKIE_CONSENT': getattr(settings, + 'ENABLE_COOKIE_CONSENT', + True)} diff --git a/app/escriptorium/settings.py b/app/escriptorium/settings.py index 222e9f0b9fd0f8f2285bcef3322674d535442625..a019a3270f3f6688d5b2fb59b8093d763b7b487a 100644 --- a/app/escriptorium/settings.py +++ b/app/escriptorium/settings.py @@ -95,6 +95,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'escriptorium.context_processors.enable_cookie_consent' ], }, }, @@ -302,6 +303,9 @@ THUMBNAIL_ALIASES = { # 'jpeg': '/usr/bin/jpegoptim -S200 {filename}' # } + +ENABLE_COOKIE_CONSENT = os.getenv('ENABLE_COOKIE_CONSENT', True) + VERSIONING_DEFAULT_SOURCE = 'eScriptorium' VERSION_DATE = os.getenv('VERSION_DATE', '<development>') diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html index abfeca7273ece25e6d9c60c698c017babab89ea4..780cdb3ddb69f4975df0009f0690dde9bd3ccbf3 100644 --- a/app/escriptorium/templates/base.html +++ b/app/escriptorium/templates/base.html @@ -97,7 +97,7 @@ {% block scripts %} <script src="{% static 'main.js' %}" type="text/javascript"></script> <script type="text/javascript"> - const DEBUG = {% if debug %}true{% else %}false{% endif %}; + const DEBUG = {% if debug %}true{% else %}false{% endif %}; </script> {% if user.is_authenticated %} {# no need to open a socket for anonymous users, bots etc #} <script type="text/javascript"> @@ -108,7 +108,12 @@ {% include 'includes/messages.html' %} {% if user.onboarding %} <script type="text/javascript"> - bootOnboarding(); + bootOnboarding(); + </script> + {% endif %} + {% if ENABLE_COOKIE_CONSENT %} + <script type="text/javascript"> + userProfile.getCookieConsent(); </script> {% endif %} {% endif %} diff --git a/app/requirements-dev.txt b/app/requirements-dev.txt new file mode 100644 index 0000000000000000000000000000000000000000..17bb39607363af5893cd80f7583da88daaee047c --- /dev/null +++ b/app/requirements-dev.txt @@ -0,0 +1,2 @@ +django-debug-toolbar==3.2 +django-extensions==3.1.0 diff --git a/docker-compose.override.yml_example b/docker-compose.override.yml_example index 789e18900dfbb920c69c1362847d0dd7f0102862..519b21d512d6bd5e98a89c8eb850afe67e480eb1 100644 --- a/docker-compose.override.yml_example +++ b/docker-compose.override.yml_example @@ -45,9 +45,13 @@ services: ## if need be to correspond to the volume below ### and uncomment this block and the port 443 # volumes: - # - ${PWD}/nginx/ssl.conf:/etc/nginx/conf.d/nginx.conf - # - ${PWD}/nginx/ssl_certificates.conf:/etc/nginx/conf.d/ssl_certificates.conf - # - ${PWD}/nginx/certs/:/etc/certs/ + # - type: bind + # source: $PWD/nginx/ssl.conf + # target: /etc/nginx/conf.d/nginx.conf + # - type: bind + # source: $PWD/nginx/ssl_certificates.conf + # target: /etc/nginx/conf.d/ssl_certificates.conf + # - $PWD/nginx/certs/:/etc/certs/ flower: restart: always diff --git a/docker-compose.yml b/docker-compose.yml index 76df44507314d876d3efbdd1e608ff93f3946b16..b7bbb53b52f4ed4e023f75f5efd68eb81f49f84b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,9 @@ services: environment: - SERVERNAME=${DOMAIN:-localhost} volumes: - - ${PWD}/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf + - type: bind + source: $PWD/nginx/nginx.conf + target: /etc/nginx/conf.d/nginx.conf - static:/usr/src/app/static - media:/usr/src/app/media ports: @@ -67,6 +69,8 @@ services: celery-main: <<: *app + environment: + - OMP_NUM_THREADS=1 command: "celery worker -l INFO -E -A escriptorium -Ofair --prefetch-multiplier 1 -Q default -c ${CELERY_MAIN_CONC:-10} --max-tasks-per-child=10" celery-low-priority: diff --git a/front/package-lock.json b/front/package-lock.json index 97ef7a1c092e6e572b861b5c5827ad06444e976b..02b562ba47ed3693ed2ead761efe0a053371d484 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -828,9 +828,9 @@ "dev": true }, "intro.js": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-0.6.0.tgz", - "integrity": "sha1-D82p9DZOIBiX4RKJ4A3S5S4col8=" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-3.3.1.tgz", + "integrity": "sha512-Y+iMbIzVQou9PDRP5miKa7NNQeUkSA6XFp+atnGiTarEi2jghthSLK+9JUvdM6QvQ9G+bpOEfKd+DNtWJlGb9w==" }, "is-core-module": { "version": "2.2.0", diff --git a/front/package.json b/front/package.json index a55f8701f4953337afc00112aa378faeac93a60b..fc2db0948f2a178370a44c8b87261a3b387f1766 100644 --- a/front/package.json +++ b/front/package.json @@ -16,7 +16,7 @@ "diff": "^4.0.1", "dropzone": "^5.5.1", "file-loader": "^6.2.0", - "intro.js": "^0.6.0", + "intro.js": "^3.3.1", "jquery": "^3.3.1", "jquery-ui-dist": "^1.12.1", "js-cookie": "^2.2.0", diff --git a/front/src/baseline.editor.js b/front/src/baseline.editor.js index 6089f213824cf152f84dad19f9c1e0eeb18e8167..d5764f89bdeab8f087f21be21d37aaf6d6f201ea 100644 --- a/front/src/baseline.editor.js +++ b/front/src/baseline.editor.js @@ -181,7 +181,8 @@ class SegmenterLine { } this.tooltipText = this.type; - this.segmenter.attachTooltip(this, this.baselinePath); + let tooltipTarget = this.baseline !== null ? this.baselinePath : this.maskPath; + this.segmenter.attachTooltip(this, tooltipTarget); this.refresh(); } @@ -874,8 +875,6 @@ export class Segmenter { // this.raster.position = view.center; // this.img.style.display = 'hidden'; - tool.onMouseDown = this.onMouseDown.bind(this); - // hide the tooltip on leaving the area this.canvas.addEventListener('mouseleave', function() { this.tooltip.style.display = 'none'; @@ -883,6 +882,8 @@ export class Segmenter { this.tool = tool; this.tool.activate(); + this.resetToolEvents(); + this.loaded = true; } @@ -958,14 +959,32 @@ export class Segmenter { bindRegionEvents(region) { region.polygonPath.onMouseDown = function(event) { if (event.event.ctrlKey || - this.selecting || + this.spliting || + // this.selecting || isRightClick(event.event) || this.mode != 'regions') return; - this.selecting = region; + + // if what we are clicking on is already selected, + // check there isn't something below + if (region.selected) { + let hit; + for (let i=0; i<this.regions.length; i++) { + if (this.regions[i] != region) { + hit = this.regions[i].polygonPath.hitTest(event.point); + if (hit) { + this.selecting = this.regions[i]; + break; + } + } + } + if (!hit) this.selecting = region; + } else { + this.selecting = region; + } var dragging = region.polygonPath.getNearestLocation(event.point).segment; this.tool.onMouseDrag = function(event) { - this.selecting = false; + this.selecting = region; if (!event.event.shiftKey) { this.movePointInView(dragging.point, event.delta); } @@ -984,6 +1003,7 @@ export class Segmenter { } } this.tool.onMouseUp = function(event) { + this.onMouseUp(event); this.resetToolEvents(); this.updateRegionsFromCanvas(); }.bind(this); @@ -1001,10 +1021,12 @@ export class Segmenter { if (line.baselinePath) { line.baselinePath.onMouseDown = function(event) { if (event.event.ctrlKey || + this.spliting || isRightClick(event.event) || this.mode != 'lines' || this.selecting) return; this.selecting = line; + var hit = line.baselinePath.hitTest(event.point, { segments: true, tolerance: 20 @@ -1028,6 +1050,7 @@ export class Segmenter { }.bind(this); this.tool.onMouseUp = function(event) { + this.onMouseUp(event); this.resetToolEvents(); line.updateDataFromCanvas(); }.bind(this); @@ -1069,6 +1092,7 @@ export class Segmenter { if (line.maskPath) { line.maskPath.onMouseDown = function(event) { if (event.event.ctrlKey || + this.spliting || isRightClick(event.event) || this.selecting || this.mode != 'lines') return; @@ -1095,6 +1119,7 @@ export class Segmenter { }.bind(this); this.tool.onMouseUp = function(event) { + this.onMouseUp(event); this.resetToolEvents(); line.updateDataFromCanvas(); }.bind(this); @@ -1116,9 +1141,9 @@ export class Segmenter { resetToolEvents() { this.tool.onMouseDown = this.onMouseDown.bind(this); + this.tool.onMouseUp = null; //this.onMouseUp.bind(this); this.tool.onMouseDrag = this.onMouseDrag.bind(this); this.tool.onMouseMove = null; - this.tool.onMouseUp = null; } attachTooltip(obj, target) { @@ -1201,39 +1226,14 @@ export class Segmenter { onMouseDown(event) { if (isRightClick(event.event)) return; - if (this.selecting) { - if (this.mode == 'regions') { - // if what we are selecting is already selected, check there isn't something below - for (let i=0; i<this.regions.length; i++) { - if (this.selecting.selected && this.regions[i] != this.selecting) { - let hit = this.regions[i].polygonPath.hitTest(event.point); - if (hit) { - this.selecting = this.regions[i]; - break; - } - } - } - } - // selection - if (event.event.shiftKey) { - this.selecting.toggleSelect(); - this.startLassoSelection(event); - } else { - this.selecting.select(); - this.purgeSelection(this.selecting); - } - this.trigger('baseline-editor:selection', {target: this.selecting, selection: this.selection}); - this.selecting = null; - } else { + if (!this.selecting) { if (event.event.ctrlKey) return; if (this.spliting) { this.startCuter(event); } else if (event.event.shiftKey) { // lasso selection tool this.startLassoSelection(event); - } else if (this.hasSelection()) { - this.purgeSelection(); - } else if (this.mode == 'regions') { + } else if (this.mode == 'regions') { this.startNewRegion(event); } else { // mode = 'lines' // create a new line @@ -1242,6 +1242,23 @@ export class Segmenter { } } + onMouseUp(event) { + if (this.selecting) { + // selection + if (event.event.shiftKey) { + this.selecting.toggleSelect(); + this.startLassoSelection(event); + } else { + this.selecting.select(); + this.purgeSelection(this.selecting); + } + this.trigger('baseline-editor:selection', {target: this.selecting, selection: this.selection}); + this.selecting = null; + } else if (this.hasSelection()) { + this.purgeSelection(); + } + } + getRegionsAt(pt) { // returns all the regions that contains the given point pt return this.regions.filter(r => r.polygonPath.contains(pt)); @@ -2048,7 +2065,7 @@ export class Segmenter { } } else { let newMask = null; - // calculate the normals before splitting + // calculate the normals before spliting let normal1 = line.baselinePath.getNormalAt(intersections[i].offset); let normal2 = line.baselinePath.getNormalAt(intersections[i+1].offset); let split = line.baselinePath.splitAt(intersections[i+1]); diff --git a/front/src/editor/store/lines.js b/front/src/editor/store/lines.js index 65b1f66878746cab5652aca505e083c27a9668d4..a9473ff30fcc59aa48952aa3316bb57148eebbb4 100644 --- a/front/src/editor/store/lines.js +++ b/front/src/editor/store/lines.js @@ -96,7 +96,14 @@ export const mutations = { if (index < 0) return state.all[index] = { ...state.all[index], - currentTrans: { ...state.all[index].currentTrans, pk: lineTrans.pk } + currentTrans: { ...state.all[index].currentTrans, pk: lineTrans.pk }, + transcriptions: { + ...state.all[index].transcriptions, + [lineTrans.transcription]: { + ...state.all[index].transcriptions[lineTrans.transcription], + pk: lineTrans.pk + } + } } } // Force reference update on the whole array diff --git a/front/src/editor/store/transcriptions.js b/front/src/editor/store/transcriptions.js index 36f73392aefc15271e343018ee35e8f615d4eb02..d273299a9854d2463c4b848c8a3b165b6979b842 100644 --- a/front/src/editor/store/transcriptions.js +++ b/front/src/editor/store/transcriptions.js @@ -83,13 +83,13 @@ export const actions = { // then fetch all content page by page let fetchPage = async function(page) { const resp = await api.retrievePage(rootState.document.id, rootState.parts.pk, transcription, page) - + let data = resp.data for (var i=0; i<data.results.length; i++) { let line = rootState.lines.all.find(l=>l.pk == data.results[i].line) commit('lines/setTranscriptions', { pk: line.pk, transcription: data.results[i] }, {root: true}) } - if (data.next) fetchPage(page+1) + if (data.next) await fetchPage(page+1) } await fetchPage(1) }, @@ -162,4 +162,4 @@ export default { state: initialState(), mutations, actions -} \ No newline at end of file +} diff --git a/front/src/messages.js b/front/src/messages.js index 8ae463aa3a22b57dd04302aa46b7c1a6284082f3..05fc0b2aa0527c78f9a135d9bbea635789838e74 100644 --- a/front/src/messages.js +++ b/front/src/messages.js @@ -14,10 +14,12 @@ export class Alert { if (this.links !== undefined) { for (let i=0; i<this.links.length; i++) { let link = $('<div>').html('<a href="'+this.links[i].src+'" >'+this.links[i].text+'</a>'); + if (this.links[i].cssClass) $('a', link).addClass(this.links[i].cssClass); $('.additional', $new).append(link).css('display', 'block'); } } this.$element = $new; + this.htmlElement = this.$element.get(0); $('#alerts-container').append($new); $new.show(); @@ -33,6 +35,7 @@ export class Alert { } else { alerts[id_].incrementCounter(); } + return alerts[id_]; } incrementCounter() { diff --git a/front/src/onboarding.js b/front/src/onboarding.js index a177d27585aed926beb14b81498e5925d0df98bb..e34c678b2b95056228d166820a020b2bac9c9bb3 100644 --- a/front/src/onboarding.js +++ b/front/src/onboarding.js @@ -4,24 +4,29 @@ Just set ONBOARDING_PAGE in the page before the scripts block.super eg: const ONBOARDING_PAGE = 'onboarding_document_form'; */ - export function bootOnboarding() { 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(); + intro.onbeforeexit(function(aa) { + if (intro._currentStep<intro._introItems.length-1) { + if (!userProfile.get(ONBOARDING_PAGE, true)) { + if (confirm("Are you sure you want to avoid further help?")) { + exitOnboarding(); + } else { + return false; + } } + } else { + userProfile.set(ONBOARDING_PAGE, true); } }); - + //document_form if (ONBOARDING_PAGE == 'onboarding_document_form') { intro.setOptions({ @@ -48,7 +53,7 @@ export function bootOnboarding() { }, ] }); - + } else if (ONBOARDING_PAGE == 'onboarding_images') { intro.setOptions({ steps: [ @@ -89,7 +94,7 @@ export function bootOnboarding() { } ] }); - + } else if (ONBOARDING_PAGE == 'onboarding_edit') { intro.setOptions({ steps: [ @@ -125,7 +130,7 @@ export function bootOnboarding() { }] }); } - + document.addEventListener('DOMContentLoaded', function() { intro.start(); }); diff --git a/front/src/profile.js b/front/src/profile.js index 3dc5d0b70a87d89f20ef440348017935cdd3c9a7..c1e67661555e1312f6f8a052719f468d135ef789 100644 --- a/front/src/profile.js +++ b/front/src/profile.js @@ -22,6 +22,20 @@ class Profile { get(key) { return this.settings[key]; } + + getCookieConsent() { + // get cookie consent. + if (!this.get('cookie-consent')) { + let alert = Alert.add('cookie-consent', + "eScriptorium uses cookies to store the user session and local storage to save user interface preferences.", + "warning", + [{src: '', text:'Accept', cssClass: 'btn btn-outline-dark btn-sm mt-2'}]); + alert.htmlElement.querySelector('.additional a').addEventListener('click', function(ev) { + this.set('cookie-consent', true); + return false; + }.bind(this)); + } + } } export var userProfile = new Profile(); diff --git a/front/src/vendor.js b/front/src/vendor.js index a10c138493cefcf1637c0873a83803c61110ddf4..03bfe26d4a0818dac562063e25e0c2f29b9fc57f 100644 --- a/front/src/vendor.js +++ b/front/src/vendor.js @@ -21,7 +21,7 @@ window.Dropzone = require('dropzone/dist/dropzone'); // Intro.js needs to be explicitly set on window, as it's used at boot time // by onboarding.js -window.introJs = require('intro.js/intro').introJs; +window.introJs = require('intro.js'); // moment needs to be explicitly set on window, as it's used at boot time // by trans_modal.js diff --git a/front/vue/components/DiploPanel.vue b/front/vue/components/DiploPanel.vue index 39fd8092018ea2eb78b91c18d693d99591b3da23..4da1265f94f744da00cf08334cadcac945091fd8 100644 --- a/front/vue/components/DiploPanel.vue +++ b/front/vue/components/DiploPanel.vue @@ -270,6 +270,7 @@ export default Vue.extend({ } } + this.$refs.saveNotif.classList.remove('hide'); this.constrainLineNumber(); e.preventDefault(); }, @@ -339,4 +340,4 @@ export default Vue.extend({ </script> <style scoped> -</style> \ No newline at end of file +</style> diff --git a/front/vue/components/TranscriptionModal.vue b/front/vue/components/TranscriptionModal.vue index 1112da13443294bdb0eb174d6ddc7a4dc30d3b1a..640ce673e574cbc89aa637747007f41d7c46f5bd 100644 --- a/front/vue/components/TranscriptionModal.vue +++ b/front/vue/components/TranscriptionModal.vue @@ -202,7 +202,13 @@ export default Vue.extend({ return moment.tz(this.line.currentTrans.version_updated_at, this.timeZone); }, modalImgSrc() { - return this.$store.state.parts.image.uri; + if (this.$store.state.parts.image.uri.endsWith('.tif') || + this.$store.state.parts.image.uri.endsWith('.tiff')) { + // can't display tifs so fallback to large thumbnail + return this.$store.state.parts.image.thumbnails.large; + } else { + return this.$store.state.parts.image.uri; + } }, otherTranscriptions() { let a = Object @@ -250,8 +256,16 @@ export default Vue.extend({ }, getLineAngle() { - let p1 = this.line.baseline[0]; - let p2 = this.line.baseline[this.line.baseline.length-1]; + let p1, p2; + if (this.line.baseline) { + p1 = this.line.baseline[0]; + p2 = this.line.baseline[this.line.baseline.length-1]; + } else { + // fake baseline from left most to right most points in mask + p1 = this.line.mask.reduce((minPt, curPt) => (curPt[0] < minPt[0]) ? curPt : minPt); + p2 = this.line.mask.reduce((maxPt, curPt) => (curPt[0] > maxPt[0]) ? curPt : maxPt); + } + return Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI; }, diff --git a/variables.env_example b/variables.env_example index 8ee2552e6966bd0b57139107f660d38092854de3..a3dabbb55d4630114510d7c2dd6ceaab3ece3c35 100644 --- a/variables.env_example +++ b/variables.env_example @@ -1,9 +1,10 @@ DOMAIN=localhost SECRET_KEY=changeme - +REDIS_HOST=redis +# REDIS_PORT=6379 SQL_HOST=db -SQL_PORT=5432 +# SQL_PORT=5432 # POSTGRES_USER=postgres # POSTGRES_PASSWORD=postgres # POSTGRES_DB=escriptorium