diff --git a/HISTORY.rst b/HISTORY.rst index 17d8cb091c..67702d16fe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,7 @@ XX (2017-XX-XX) =============== - Add Django-mathfilters. - Improve render of documents with no pages. +- Add SANE scanner document source. 2.3 (2017-06-08) ================ diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index acfb5d6724..fd5a5a0d9c 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -24,10 +24,11 @@ from .handlers import ( from .links import ( link_document_create_multiple, link_setup_sources, link_setup_source_check_now, link_setup_source_create_imap_email, - link_setup_source_create_pop3_email, link_setup_source_create_watch_folder, - link_setup_source_create_webform, link_setup_source_create_staging_folder, - link_setup_source_delete, link_setup_source_edit, link_setup_source_logs, - link_staging_file_delete, link_upload_version + link_setup_source_create_pop3_email, link_setup_source_create_sane_scanner, + link_setup_source_create_watch_folder, link_setup_source_create_webform, + link_setup_source_create_staging_folder, link_setup_source_delete, + link_setup_source_edit, link_setup_source_logs, link_staging_file_delete, + link_upload_version ) from .widgets import StagingFileThumbnailWidget @@ -44,6 +45,7 @@ class SourcesApp(MayanAppConfig): IMAPEmail = self.get_model('IMAPEmail') Source = self.get_model('Source') SourceLog = self.get_model('SourceLog') + SaneScanner = self.get_model('SaneScanner') StagingFolderSource = self.get_model('StagingFolderSource') WatchFolderSource = self.get_model('WatchFolderSource') WebFormSource = self.get_model('WebFormSource') @@ -119,8 +121,8 @@ class SourcesApp(MayanAppConfig): link_setup_source_edit, link_setup_source_delete, link_transformation_list, link_setup_source_logs ), sources=( - POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource, - WebFormSource + POP3Email, IMAPEmail, SaneScanner, StagingFolderSource, + WatchFolderSource, WebFormSource ) ) menu_object.bind_links( @@ -133,6 +135,7 @@ class SourcesApp(MayanAppConfig): menu_secondary.bind_links( links=( link_setup_sources, link_setup_source_create_webform, + link_setup_source_create_sane_scanner, link_setup_source_create_staging_folder, link_setup_source_create_pop3_email, link_setup_source_create_imap_email, diff --git a/mayan/apps/sources/exceptions.py b/mayan/apps/sources/exceptions.py new file mode 100644 index 0000000000..6e0d974b48 --- /dev/null +++ b/mayan/apps/sources/exceptions.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals + + +class SourceException(Exception): + """ + Base sources warning + """ + pass diff --git a/mayan/apps/sources/forms.py b/mayan/apps/sources/forms.py index 5a6848511c..8b555d345e 100644 --- a/mayan/apps/sources/forms.py +++ b/mayan/apps/sources/forms.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from documents.forms import DocumentForm from .models import ( - IMAPEmail, POP3Email, StagingFolderSource, WebFormSource, + IMAPEmail, POP3Email, SaneScanner, StagingFolderSource, WebFormSource, WatchFolderSource ) @@ -79,6 +79,19 @@ class WebFormUploadFormHTML5(WebFormUploadForm): ) +class SaneScannerUploadForm(UploadBaseForm): + pass + + +class SaneScannerSetupForm(forms.ModelForm): + class Meta: + fields = ( + 'label', 'device_name', 'mode', 'resolution', 'source', + 'adf_mode', 'enabled' + ) + model = SaneScanner + + class WebFormSetupForm(forms.ModelForm): class Meta: fields = ('label', 'enabled', 'uncompress') diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index 185c706f0f..793e3a59bf 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -10,7 +10,7 @@ from navigation import Link from .literals import ( SOURCE_CHOICE_WEB_FORM, SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3, - SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH + SOURCE_CHOICE_SANE_SCANNER, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH ) from .permissions import ( permission_sources_setup_create, permission_sources_setup_delete, @@ -59,6 +59,11 @@ link_setup_source_create_webform = Link( text=_('Add new webform source'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_WEB_FORM ) +link_setup_source_create_sane_scanner = Link( + permissions=(permission_sources_setup_create,), + text=_('Add new SANE scanner'), view='sources:setup_source_create', + args='"%s"' % SOURCE_CHOICE_SANE_SCANNER +) link_setup_source_delete = Link( permissions=(permission_sources_setup_delete,), tags='dangerous', text=_('Delete'), view='sources:setup_source_delete', diff --git a/mayan/apps/sources/literals.py b/mayan/apps/sources/literals.py index 6605994b2c..b16d9480e6 100644 --- a/mayan/apps/sources/literals.py +++ b/mayan/apps/sources/literals.py @@ -2,6 +2,32 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ +SCANNER_SOURCE_FLATBED = 'flatbed' +SCANNER_SOURCE_ADF = 'Automatic Document Feeder' + +SCANNER_SOURCE_CHOICES = ( + (SCANNER_SOURCE_FLATBED, _('Flatbed')), + (SCANNER_SOURCE_ADF, _('Document feeder')), +) + +SCANNER_ADF_MODE_SIMPLEX = 'simplex' +SCANNER_ADF_MODE_DUPLEX = 'duplex' + +SCANNER_ADF_MODE_CHOICES = ( + (SCANNER_ADF_MODE_SIMPLEX, _('Simplex')), + (SCANNER_ADF_MODE_DUPLEX, _('Duplex')), +) + +SCANNER_MODE_LINEART = 'lineart' +SCANNER_MODE_MONOCHROME = 'monochrome' +SCANNER_MODE_COLOR = 'color' + +SCANNER_MODE_CHOICES = ( + (SCANNER_MODE_LINEART, _('Lineart')), + (SCANNER_MODE_MONOCHROME, _('Monochrome')), + (SCANNER_MODE_COLOR, _('Color')) +) + SOURCE_UNCOMPRESS_CHOICE_Y = 'y' SOURCE_UNCOMPRESS_CHOICE_N = 'n' SOURCE_UNCOMPRESS_CHOICE_ASK = 'a' @@ -22,8 +48,10 @@ SOURCE_CHOICE_STAGING = 'staging' SOURCE_CHOICE_WATCH = 'watch' SOURCE_CHOICE_EMAIL_POP3 = 'pop3' SOURCE_CHOICE_EMAIL_IMAP = 'imap' +SOURCE_CHOICE_SANE_SCANNER = 'sane' SOURCE_CHOICES = ( + (SOURCE_CHOICE_SANE_SCANNER, _('Scanner')), (SOURCE_CHOICE_WEB_FORM, _('Web form')), (SOURCE_CHOICE_STAGING, _('Staging folder')), (SOURCE_CHOICE_WATCH, _('Watch folder')), diff --git a/mayan/apps/sources/migrations/0011_sanescanner.py b/mayan/apps/sources/migrations/0011_sanescanner.py new file mode 100644 index 0000000000..892f519057 --- /dev/null +++ b/mayan/apps/sources/migrations/0011_sanescanner.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-05 04:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0010_auto_20151001_0055'), + ] + + operations = [ + migrations.CreateModel( + name='SaneScanner', + fields=[ + ('interactivesource_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sources.InteractiveSource')), + ('device_name', models.CharField(help_text='Device name as returned by the SANE backend.', max_length=255, verbose_name='Device name')), + ], + options={ + 'verbose_name': 'SANE Scanner', + 'verbose_name_plural': 'SANE Scanners', + }, + bases=('sources.interactivesource',), + ), + ] diff --git a/mayan/apps/sources/migrations/0012_auto_20170205_0743.py b/mayan/apps/sources/migrations/0012_auto_20170205_0743.py new file mode 100644 index 0000000000..ec4c6b2468 --- /dev/null +++ b/mayan/apps/sources/migrations/0012_auto_20170205_0743.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-05 07:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0011_sanescanner'), + ] + + operations = [ + migrations.AddField( + model_name='sanescanner', + name='mode', + field=models.CharField(choices=[('lineart', 'Lineart'), ('monochrome', 'Monochrome'), ('color', 'Color')], default='color', max_length=16, verbose_name='Mode'), + ), + migrations.AddField( + model_name='sanescanner', + name='resolution', + field=models.PositiveIntegerField(default=300, help_text='Sets the resolution of the scanned image in DPI (dots per inch).', verbose_name='Resolution'), + ), + ] diff --git a/mayan/apps/sources/migrations/0013_auto_20170206_0710.py b/mayan/apps/sources/migrations/0013_auto_20170206_0710.py new file mode 100644 index 0000000000..b435b64535 --- /dev/null +++ b/mayan/apps/sources/migrations/0013_auto_20170206_0710.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-06 07:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0012_auto_20170205_0743'), + ] + + operations = [ + migrations.AddField( + model_name='sanescanner', + name='adf_mode', + field=models.CharField(blank=True, choices=[('simplex', 'Simples'), ('duplex', 'Duplex')], default='simplex', help_text='Selects the document feeder mode (simplex/duplex). If this option is not supported by your scanner, leave it blank.', max_length=16, verbose_name='ADF mode'), + ), + migrations.AddField( + model_name='sanescanner', + name='source', + field=models.CharField(blank=True, choices=[('flatbed', 'Flatbed'), ('document-feeder', 'Document feeder')], default='flatbed', help_text='Selects the scan source (such as a document-feeder). If this option is not supported by your scanner, leave it blank.', max_length=16, verbose_name='Paper source'), + ), + migrations.AlterField( + model_name='sanescanner', + name='mode', + field=models.CharField(blank=True, choices=[('lineart', 'Lineart'), ('monochrome', 'Monochrome'), ('color', 'Color')], default='color', help_text='Selects the scan mode (e.g., lineart, monochrome, or color). If this option is not supported by your scanner, leave it blank.', max_length=16, verbose_name='Mode'), + ), + migrations.AlterField( + model_name='sanescanner', + name='resolution', + field=models.PositiveIntegerField(blank=True, help_text='Sets the resolution of the scanned image in DPI (dots per inch). Typical value is 200. If this option is not supported by your scanner, leave it blank.', verbose_name='Resolution'), + ), + ] diff --git a/mayan/apps/sources/migrations/0014_auto_20170206_0722.py b/mayan/apps/sources/migrations/0014_auto_20170206_0722.py new file mode 100644 index 0000000000..46b3017b1e --- /dev/null +++ b/mayan/apps/sources/migrations/0014_auto_20170206_0722.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-06 07:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0013_auto_20170206_0710'), + ] + + operations = [ + migrations.AlterField( + model_name='sanescanner', + name='adf_mode', + field=models.CharField(blank=True, choices=[('simplex', 'Simplex'), ('duplex', 'Duplex')], default='simplex', help_text='Selects the document feeder mode (simplex/duplex). If this option is not supported by your scanner, leave it blank.', max_length=16, verbose_name='ADF mode'), + ), + migrations.AlterField( + model_name='sanescanner', + name='source', + field=models.CharField(blank=True, choices=[('flatbed', 'Flatbed'), ('"Automatic Document Feeder"', 'Document feeder')], default='flatbed', help_text='Selects the scan source (such as a document-feeder). If this option is not supported by your scanner, leave it blank.', max_length=32, verbose_name='Paper source'), + ), + ] diff --git a/mayan/apps/sources/migrations/0015_auto_20170206_0835.py b/mayan/apps/sources/migrations/0015_auto_20170206_0835.py new file mode 100644 index 0000000000..8fe1339be6 --- /dev/null +++ b/mayan/apps/sources/migrations/0015_auto_20170206_0835.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-06 08:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0014_auto_20170206_0722'), + ] + + operations = [ + migrations.AlterField( + model_name='sanescanner', + name='source', + field=models.CharField(blank=True, choices=[('flatbed', 'Flatbed'), ('Automatic Document Feeder', 'Document feeder')], default='flatbed', help_text='Selects the scan source (such as a document-feeder). If this option is not supported by your scanner, leave it blank.', max_length=32, verbose_name='Paper source'), + ), + ] diff --git a/mayan/apps/sources/models.py b/mayan/apps/sources/models.py index a70344dec9..234a4b985a 100644 --- a/mayan/apps/sources/models.py +++ b/mayan/apps/sources/models.py @@ -8,19 +8,28 @@ import json import logging import os import poplib +import subprocess +import sh import yaml +try: + scanimage = sh.Command('/usr/bin/scanimage') +except sh.CommandNotFound: + scanimage = None + from django.core.exceptions import ValidationError from django.core.files import File from django.core.files.base import ContentFile from django.db import models, transaction from django.utils.encoding import python_2_unicode_compatible +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from model_utils.managers import InheritanceManager from common.compressed_files import CompressedFile, NotACompressedFile +from common.utils import TemporaryFile from converter.literals import DIMENSION_SEPARATOR from converter.models import Transformation from djcelery.models import PeriodicTask, IntervalSchedule @@ -30,15 +39,20 @@ from metadata.api import save_metadata_list, set_bulk_metadata from metadata.models import MetadataType from tags.models import Tag -from .classes import Attachment, SourceUploadedFile, StagingFile +from .classes import Attachment, PseudoFile, SourceUploadedFile, StagingFile +from .exceptions import SourceException from .literals import ( DEFAULT_INTERVAL, DEFAULT_POP3_TIMEOUT, DEFAULT_IMAP_MAILBOX, - DEFAULT_METADATA_ATTACHMENT_NAME, SOURCE_CHOICES, SOURCE_CHOICE_STAGING, - SOURCE_CHOICE_WATCH, SOURCE_CHOICE_WEB_FORM, - SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, SOURCE_UNCOMPRESS_CHOICES, - SOURCE_UNCOMPRESS_CHOICE_N, SOURCE_UNCOMPRESS_CHOICE_Y, - SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3 + DEFAULT_METADATA_ATTACHMENT_NAME, SCANNER_ADF_MODE_CHOICES, + SCANNER_ADF_MODE_SIMPLEX, SCANNER_MODE_COLOR, SCANNER_MODE_CHOICES, + SCANNER_SOURCE_CHOICES, SCANNER_SOURCE_FLATBED, + SOURCE_CHOICES, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH, + SOURCE_CHOICE_WEB_FORM, SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, + SOURCE_UNCOMPRESS_CHOICES, SOURCE_UNCOMPRESS_CHOICE_N, + SOURCE_UNCOMPRESS_CHOICE_Y, SOURCE_CHOICE_EMAIL_IMAP, + SOURCE_CHOICE_EMAIL_POP3, SOURCE_CHOICE_SANE_SCANNER, ) +from .settings import setting_scanimage_path logger = logging.getLogger(__name__) @@ -153,6 +167,106 @@ class InteractiveSource(Source): verbose_name_plural = _('Interactive sources') +class SaneScanner(InteractiveSource): + can_compress = False + is_interactive = True + source_type = SOURCE_CHOICE_SANE_SCANNER + + device_name = models.CharField( + max_length=255, + help_text=_('Device name as returned by the SANE backend.'), + verbose_name=_('Device name') + ) + mode = models.CharField( + blank=True, choices=SCANNER_MODE_CHOICES, default=SCANNER_MODE_COLOR, + help_text=_( + 'Selects the scan mode (e.g., lineart, monochrome, or color). ' + 'If this option is not supported by your scanner, leave it blank.' + ), max_length=16, verbose_name=_('Mode') + ) + resolution = models.PositiveIntegerField( + blank=True, help_text=_( + 'Sets the resolution of the scanned image in DPI (dots per inch). ' + 'Typical value is 200. If this option is not supported by your ' + 'scanner, leave it blank.' + ), verbose_name=_('Resolution') + ) + source = models.CharField( + blank=True, choices=SCANNER_SOURCE_CHOICES, + default=SCANNER_SOURCE_FLATBED, help_text=_( + 'Selects the scan source (such as a document-feeder). If this ' + 'option is not supported by your scanner, leave it blank.' + ), max_length=32, + verbose_name=_('Paper source') + ) + adf_mode = models.CharField( + blank=True, choices=SCANNER_ADF_MODE_CHOICES, + default=SCANNER_ADF_MODE_SIMPLEX, help_text=_( + 'Selects the document feeder mode (simplex/duplex). If this ' + 'option is not supported by your scanner, leave it blank.' + ), max_length=16, verbose_name=_('ADF mode') + ) + + class Meta: + verbose_name = _('SANE Scanner') + verbose_name_plural = _('SANE Scanners') + + def clean_up_upload_file(self, upload_file_object): + pass + + def get_upload_file_object(self, form_data): + temporary_file_object = TemporaryFile() + command_line = [ + setting_scanimage_path.value, '-d', self.device_name, + '--format', 'tiff', + ] + + if self.resolution: + command_line.extend( + ['--resolution', '{}'.format(self.resolution)] + ) + + if self.mode: + command_line.extend( + ['--mode', self.mode] + ) + + if self.source: + command_line.extend( + ['--source', self.source] + ) + + if self.adf_mode: + command_line.extend( + ['--adf-mode', self.adf_mode] + ) + + filestderr = TemporaryFile() + + try: + logger.debug('Scan command line: %s', command_line) + subprocess.check_call( + command_line, stdout=temporary_file_object, stderr=filestderr + ) + except subprocess.CalledProcessError: + filestderr.seek(0) + error_message = filestderr.read() + logger.error( + 'Exception while scanning from source:%s ; %s', self, + error_message + ) + + message = _('Error while scanning; %s') % error_message + self.logs.create(message=message) + raise SourceException(message) + else: + return SourceUploadedFile( + source=self, file=PseudoFile( + file=temporary_file_object, name='scan {}'.format(now()) + ) + ) + + class StagingFolderSource(InteractiveSource): """ The Staging folder source is interactive but instead of displaying an @@ -169,6 +283,7 @@ class StagingFolderSource(InteractiveSource): convert the scanned files into Mayan EDMS documents. Staging folders are useful when many users share a few networked scanners. """ + can_compress = True is_interactive = True source_type = SOURCE_CHOICE_STAGING @@ -257,6 +372,7 @@ class WebFormSource(InteractiveSource): documents from their own computers such as when each user has their own scanner. """ + can_compress = True is_interactive = True source_type = SOURCE_CHOICE_WEB_FORM diff --git a/mayan/apps/sources/settings.py b/mayan/apps/sources/settings.py new file mode 100644 index 0000000000..6a57bb2c93 --- /dev/null +++ b/mayan/apps/sources/settings.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from smart_settings import Namespace + +namespace = Namespace(name='sources', label=_('Sources')) + +setting_scanimage_path = namespace.add_setting( + global_name='SOURCE_SCANIMAGE_PATH', default='/usr/bin/scanimage', + help_text=_( + 'File path to the scanimage program used to control image scanners.' + ), + is_path=True +) diff --git a/mayan/apps/sources/utils.py b/mayan/apps/sources/utils.py index 68ff27fe72..83548762ec 100644 --- a/mayan/apps/sources/utils.py +++ b/mayan/apps/sources/utils.py @@ -1,14 +1,16 @@ from .forms import ( - POP3EmailSetupForm, IMAPEmailSetupForm, - StagingFolderSetupForm, StagingUploadForm, WatchFolderSetupForm, - WebFormSetupForm, WebFormUploadForm + POP3EmailSetupForm, IMAPEmailSetupForm, SaneScannerSetupForm, + SaneScannerUploadForm, StagingFolderSetupForm, StagingUploadForm, + WatchFolderSetupForm, WebFormSetupForm, WebFormUploadForm ) from .literals import ( - SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3, SOURCE_CHOICE_STAGING, - SOURCE_CHOICE_WATCH, SOURCE_CHOICE_WEB_FORM + SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3, + SOURCE_CHOICE_SANE_SCANNER, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH, + SOURCE_CHOICE_WEB_FORM ) from .models import ( - IMAPEmail, POP3Email, StagingFolderSource, WatchFolderSource, WebFormSource + IMAPEmail, POP3Email, SaneScanner, StagingFolderSource, WatchFolderSource, + WebFormSource ) @@ -23,6 +25,8 @@ def get_class(source_type): return POP3Email elif source_type == SOURCE_CHOICE_EMAIL_IMAP: return IMAPEmail + elif source_type == SOURCE_CHOICE_SANE_SCANNER: + return SaneScanner def get_form_class(source_type): @@ -36,6 +40,8 @@ def get_form_class(source_type): return POP3EmailSetupForm elif source_type == SOURCE_CHOICE_EMAIL_IMAP: return IMAPEmailSetupForm + elif source_type == SOURCE_CHOICE_SANE_SCANNER: + return SaneScannerSetupForm def get_upload_form_class(source_type): @@ -43,3 +49,5 @@ def get_upload_form_class(source_type): return WebFormUploadForm elif source_type == SOURCE_CHOICE_STAGING: return StagingUploadForm + elif source_type == SOURCE_CHOICE_SANE_SCANNER: + return SaneScannerUploadForm diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index 1067745980..a4b3e95694 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -24,17 +24,13 @@ from documents.tasks import task_upload_new_version from metadata.api import decode_metadata_from_url from navigation import Link +from .exceptions import SourceException from .forms import ( - NewDocumentForm, NewVersionForm, WebFormUploadForm, - WebFormUploadFormHTML5 -) -from .literals import ( - SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WEB_FORM, - SOURCE_UNCOMPRESS_CHOICE_ASK, - SOURCE_UNCOMPRESS_CHOICE_Y + NewDocumentForm, NewVersionForm, WebFormUploadForm, WebFormUploadFormHTML5 ) +from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK, SOURCE_UNCOMPRESS_CHOICE_Y from .models import ( - InteractiveSource, Source, StagingFolderSource, WebFormSource + InteractiveSource, Source, SaneScanner, StagingFolderSource ) from .permissions import ( permission_sources_setup_create, permission_sources_setup_delete, @@ -88,27 +84,10 @@ class UploadBaseView(MultiFormView): @staticmethod def get_active_tab_links(document=None): - tab_links = [] - - web_forms = WebFormSource.objects.filter(enabled=True) - for web_form in web_forms: - tab_links.append( - UploadBaseView.get_tab_link_for_source(web_form, document) - ) - - staging_folders = StagingFolderSource.objects.filter(enabled=True) - for staging_folder in staging_folders: - tab_links.append( - UploadBaseView.get_tab_link_for_source( - staging_folder, document - ) - ) - - return { - 'tab_links': tab_links, - SOURCE_CHOICE_WEB_FORM: web_forms, - SOURCE_CHOICE_STAGING: staging_folders, - } + return [ + UploadBaseView.get_tab_link_for_source(source, document) + for source in InteractiveSource.objects.filter(enabled=True).select_subclasses() + ] def dispatch(self, request, *args, **kwargs): if 'source_id' in kwargs: @@ -163,6 +142,16 @@ class UploadBaseView(MultiFormView): } }, ] + elif isinstance(self.source, SaneScanner): + subtemplates_list.append({ + 'name': 'sources/upload_multiform_subtemplate.html', + 'context': { + 'forms': context['forms'], + 'is_multipart': True, + 'title': _('Document properties'), + 'submit_label': _('Scan'), + }, + }) else: subtemplates_list.append({ 'name': 'sources/upload_multiform_subtemplate.html', @@ -173,8 +162,8 @@ class UploadBaseView(MultiFormView): }, }) - menu_facet.bound_links['sources:upload_interactive'] = self.tab_links['tab_links'] - menu_facet.bound_links['sources:upload_version'] = self.tab_links['tab_links'] + menu_facet.bound_links['sources:upload_interactive'] = self.tab_links + menu_facet.bound_links['sources:upload_version'] = self.tab_links context.update({ 'subtemplates_list': subtemplates_list, @@ -206,57 +195,63 @@ class UploadInteractiveView(UploadBaseView): ).dispatch(request, *args, **kwargs) def forms_valid(self, forms): - if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: - expand = forms['source_form'].cleaned_data.get('expand') - else: - if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: - expand = True + if self.source.can_compress: + if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: + expand = forms['source_form'].cleaned_data.get('expand') else: - expand = False - - uploaded_file = self.source.get_upload_file_object( - forms['source_form'].cleaned_data - ) - - shared_uploaded_file = SharedUploadedFile.objects.create( - file=uploaded_file.file - ) - - label = None - - if 'document_type_available_filenames' in forms['document_form'].cleaned_data: - if forms['document_form'].cleaned_data['document_type_available_filenames']: - label = forms['document_form'].cleaned_data['document_type_available_filenames'].filename - - if not self.request.user.is_anonymous(): - user_id = self.request.user.pk + if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: + expand = True + else: + expand = False else: - user_id = None + expand = False try: - self.source.clean_up_upload_file(uploaded_file) - except Exception as exception: - messages.error(self.request, exception) - - task_source_handle_upload.apply_async(kwargs=dict( - description=forms['document_form'].cleaned_data.get('description'), - document_type_id=self.document_type.pk, - expand=expand, - label=label, - language=forms['document_form'].cleaned_data.get('language'), - metadata_dict_list=decode_metadata_from_url(self.request.GET), - shared_uploaded_file_id=shared_uploaded_file.pk, - source_id=self.source.pk, - tag_ids=self.request.GET.getlist('tags'), - user_id=user_id, - )) - messages.success( - self.request, - _( - 'New document queued for uploaded and will be available ' - 'shortly.' + uploaded_file = self.source.get_upload_file_object( + forms['source_form'].cleaned_data + ) + except SourceException as exception: + messages.error(self.request, exception) + else: + shared_uploaded_file = SharedUploadedFile.objects.create( + file=uploaded_file.file + ) + + label = None + + if 'document_type_available_filenames' in forms['document_form'].cleaned_data: + if forms['document_form'].cleaned_data['document_type_available_filenames']: + label = forms['document_form'].cleaned_data['document_type_available_filenames'].filename + + if not self.request.user.is_anonymous(): + user_id = self.request.user.pk + else: + user_id = None + + try: + self.source.clean_up_upload_file(uploaded_file) + except Exception as exception: + messages.error(self.request, exception) + + task_source_handle_upload.apply_async(kwargs=dict( + description=forms['document_form'].cleaned_data.get('description'), + document_type_id=self.document_type.pk, + expand=expand, + label=label, + language=forms['document_form'].cleaned_data.get('language'), + metadata_dict_list=decode_metadata_from_url(self.request.GET), + shared_uploaded_file_id=shared_uploaded_file.pk, + source_id=self.source.pk, + tag_ids=self.request.GET.getlist('tags'), + user_id=user_id, + )) + messages.success( + self.request, + _( + 'New document queued for uploaded and will be available ' + 'shortly.' + ) ) - ) return HttpResponseRedirect( '{}?{}'.format( @@ -266,12 +261,15 @@ class UploadInteractiveView(UploadBaseView): ) def create_source_form_form(self, **kwargs): + if hasattr(self.source, 'uncompress'): + show_expand = self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK + else: + show_expand = False + return self.get_form_classes()['source_form']( prefix=kwargs['prefix'], source=self.source, - show_expand=( - self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK - ), + show_expand=show_expand, data=kwargs.get('data', None), files=kwargs.get('files', None), ) @@ -301,7 +299,7 @@ class UploadInteractiveView(UploadBaseView): context['title'] = _( 'Upload a local document from source: %s' ) % self.source.label - if not isinstance(self.source, StagingFolderSource): + if not isinstance(self.source, StagingFolderSource) and not isinstance(self.source, SaneScanner): context['subtemplates_list'][0]['context'].update( { 'form_action': '{}?{}'.format( @@ -349,38 +347,42 @@ class UploadInteractiveVersionView(UploadBaseView): ).dispatch(request, *args, **kwargs) def forms_valid(self, forms): - uploaded_file = self.source.get_upload_file_object( - forms['source_form'].cleaned_data - ) - - shared_uploaded_file = SharedUploadedFile.objects.create( - file=uploaded_file.file - ) - try: - self.source.clean_up_upload_file(uploaded_file) - except Exception as exception: - messages.error(self.request, exception) - - if not self.request.user.is_anonymous(): - user_id = self.request.user.pk - else: - user_id = None - - task_upload_new_version.apply_async(kwargs=dict( - shared_uploaded_file_id=shared_uploaded_file.pk, - document_id=self.document.pk, - user_id=user_id, - comment=forms['document_form'].cleaned_data.get('comment') - )) - - messages.success( - self.request, - _( - 'New document version queued for uploaded and will be ' - 'available shortly.' + uploaded_file = self.source.get_upload_file_object( + forms['source_form'].cleaned_data ) - ) + except SourceException as exception: + messages.error(self.request, exception) + else: + shared_uploaded_file = SharedUploadedFile.objects.create( + file=uploaded_file.file + ) + + try: + self.source.clean_up_upload_file(uploaded_file) + except Exception as exception: + messages.error(self.request, exception) + + if not self.request.user.is_anonymous(): + user_id = self.request.user.pk + else: + user_id = None + + task_upload_new_version.apply_async(kwargs=dict( + shared_uploaded_file_id=shared_uploaded_file.pk, + document_id=self.document.pk, + user_id=user_id, + comment=forms['document_form'].cleaned_data.get('comment') + )) + + messages.success( + self.request, + _( + 'New document version queued for uploaded and will be ' + 'available shortly.' + ) + ) + return HttpResponseRedirect( reverse( 'documents:document_version_list', args=(self.document.pk,)