Add experimental SANE scanner source.

This commit is contained in:
Roberto Rosario
2017-02-05 04:02:12 -04:00
parent b58fa7e241
commit 25f1f7d067
10 changed files with 227 additions and 52 deletions

View File

@@ -24,6 +24,7 @@ from .handlers import (
from .links import ( from .links import (
link_document_create_multiple, link_setup_sources, link_document_create_multiple, link_setup_sources,
link_setup_source_create_imap_email, link_setup_source_create_pop3_email, link_setup_source_create_imap_email, 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_watch_folder, link_setup_source_create_webform,
link_setup_source_create_staging_folder, link_setup_source_delete, link_setup_source_create_staging_folder, link_setup_source_delete,
link_setup_source_edit, link_setup_source_logs, link_staging_file_delete, link_setup_source_edit, link_setup_source_logs, link_staging_file_delete,
@@ -44,6 +45,7 @@ class SourcesApp(MayanAppConfig):
IMAPEmail = self.get_model('IMAPEmail') IMAPEmail = self.get_model('IMAPEmail')
Source = self.get_model('Source') Source = self.get_model('Source')
SourceLog = self.get_model('SourceLog') SourceLog = self.get_model('SourceLog')
SaneScanner = self.get_model('SaneScanner')
StagingFolderSource = self.get_model('StagingFolderSource') StagingFolderSource = self.get_model('StagingFolderSource')
WatchFolderSource = self.get_model('WatchFolderSource') WatchFolderSource = self.get_model('WatchFolderSource')
WebFormSource = self.get_model('WebFormSource') WebFormSource = self.get_model('WebFormSource')
@@ -119,8 +121,8 @@ class SourcesApp(MayanAppConfig):
link_setup_source_edit, link_setup_source_delete, link_setup_source_edit, link_setup_source_delete,
link_transformation_list, link_setup_source_logs link_transformation_list, link_setup_source_logs
), sources=( ), sources=(
POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource, POP3Email, IMAPEmail, SaneScanner, StagingFolderSource,
WebFormSource WatchFolderSource, WebFormSource
) )
) )
menu_object.bind_links( menu_object.bind_links(
@@ -129,6 +131,7 @@ class SourcesApp(MayanAppConfig):
menu_secondary.bind_links( menu_secondary.bind_links(
links=( links=(
link_setup_sources, link_setup_source_create_webform, link_setup_sources, link_setup_source_create_webform,
link_setup_source_create_sane_scanner,
link_setup_source_create_staging_folder, link_setup_source_create_staging_folder,
link_setup_source_create_pop3_email, link_setup_source_create_pop3_email,
link_setup_source_create_imap_email, link_setup_source_create_imap_email,

View File

@@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from documents.forms import DocumentForm from documents.forms import DocumentForm
from .models import ( from .models import (
IMAPEmail, POP3Email, StagingFolderSource, WebFormSource, IMAPEmail, POP3Email, SaneScanner, StagingFolderSource, WebFormSource,
WatchFolderSource WatchFolderSource
) )
@@ -79,6 +79,16 @@ class WebFormUploadFormHTML5(WebFormUploadForm):
) )
class SaneScannerUploadForm(UploadBaseForm):
pass
class SaneScannerSetupForm(forms.ModelForm):
class Meta:
fields = ('label', 'device_name', 'mode', 'resolution', 'enabled')
model = SaneScanner
class WebFormSetupForm(forms.ModelForm): class WebFormSetupForm(forms.ModelForm):
class Meta: class Meta:
fields = ('label', 'enabled', 'uncompress') fields = ('label', 'enabled', 'uncompress')

View File

@@ -10,7 +10,7 @@ from navigation import Link
from .literals import ( from .literals import (
SOURCE_CHOICE_WEB_FORM, SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3, 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 ( from .permissions import (
permission_sources_setup_create, permission_sources_setup_delete, 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', text=_('Add new webform source'), view='sources:setup_source_create',
args='"%s"' % SOURCE_CHOICE_WEB_FORM 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( link_setup_source_delete = Link(
permissions=(permission_sources_setup_delete,), tags='dangerous', permissions=(permission_sources_setup_delete,), tags='dangerous',
text=_('Delete'), view='sources:setup_source_delete', text=_('Delete'), view='sources:setup_source_delete',

View File

@@ -2,6 +2,16 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
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_Y = 'y'
SOURCE_UNCOMPRESS_CHOICE_N = 'n' SOURCE_UNCOMPRESS_CHOICE_N = 'n'
SOURCE_UNCOMPRESS_CHOICE_ASK = 'a' SOURCE_UNCOMPRESS_CHOICE_ASK = 'a'
@@ -22,8 +32,10 @@ SOURCE_CHOICE_STAGING = 'staging'
SOURCE_CHOICE_WATCH = 'watch' SOURCE_CHOICE_WATCH = 'watch'
SOURCE_CHOICE_EMAIL_POP3 = 'pop3' SOURCE_CHOICE_EMAIL_POP3 = 'pop3'
SOURCE_CHOICE_EMAIL_IMAP = 'imap' SOURCE_CHOICE_EMAIL_IMAP = 'imap'
SOURCE_CHOICE_SANE_SCANNER = 'sane'
SOURCE_CHOICES = ( SOURCE_CHOICES = (
(SOURCE_CHOICE_SANE_SCANNER, _('Scanner')),
(SOURCE_CHOICE_WEB_FORM, _('Web form')), (SOURCE_CHOICE_WEB_FORM, _('Web form')),
(SOURCE_CHOICE_STAGING, _('Staging folder')), (SOURCE_CHOICE_STAGING, _('Staging folder')),
(SOURCE_CHOICE_WATCH, _('Watch folder')), (SOURCE_CHOICE_WATCH, _('Watch folder')),

View File

@@ -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',),
),
]

View File

@@ -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'),
),
]

View File

@@ -8,19 +8,28 @@ import json
import logging import logging
import os import os
import poplib import poplib
import subprocess
import sh
import yaml import yaml
try:
scanimage = sh.Command('/usr/bin/scanimage')
except sh.CommandNotFound:
scanimage = None
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import models, transaction from django.db import models, transaction
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from common.compressed_files import CompressedFile, NotACompressedFile from common.compressed_files import CompressedFile, NotACompressedFile
from common.utils import TemporaryFile
from converter.literals import DIMENSION_SEPARATOR from converter.literals import DIMENSION_SEPARATOR
from converter.models import Transformation from converter.models import Transformation
from djcelery.models import PeriodicTask, IntervalSchedule from djcelery.models import PeriodicTask, IntervalSchedule
@@ -30,15 +39,17 @@ from metadata.api import save_metadata_list, set_bulk_metadata
from metadata.models import MetadataType from metadata.models import MetadataType
from tags.models import Tag from tags.models import Tag
from .classes import Attachment, SourceUploadedFile, StagingFile from .classes import Attachment, PseudoFile, SourceUploadedFile, StagingFile
from .literals import ( from .literals import (
DEFAULT_INTERVAL, DEFAULT_POP3_TIMEOUT, DEFAULT_IMAP_MAILBOX, DEFAULT_INTERVAL, DEFAULT_POP3_TIMEOUT, DEFAULT_IMAP_MAILBOX,
DEFAULT_METADATA_ATTACHMENT_NAME, SOURCE_CHOICES, SOURCE_CHOICE_STAGING, DEFAULT_METADATA_ATTACHMENT_NAME, SCANNER_MODE_COLOR, SCANNER_MODE_CHOICES,
SOURCE_CHOICE_WATCH, SOURCE_CHOICE_WEB_FORM, SOURCE_CHOICES,SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH,
SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, SOURCE_UNCOMPRESS_CHOICES, SOURCE_CHOICE_WEB_FORM, SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES,
SOURCE_UNCOMPRESS_CHOICE_N, SOURCE_UNCOMPRESS_CHOICE_Y, SOURCE_UNCOMPRESS_CHOICES, SOURCE_UNCOMPRESS_CHOICE_N,
SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3 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__) logger = logging.getLogger(__name__)
@@ -153,7 +164,64 @@ class InteractiveSource(Source):
verbose_name_plural = _('Interactive sources') 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(
choices=SCANNER_MODE_CHOICES, default=SCANNER_MODE_COLOR,
max_length=16, verbose_name=_('Mode')
)
resolution = models.PositiveIntegerField(
default=300, help_text=_(
'Sets the resolution of the scanned image in DPI (dots per inch).'
), verbose_name=_('Resolution')
)
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()
try:
command_line = [
setting_scanimage_path.value, '-d', self.device_name,
'--resolution', '{}'.format(self.resolution), '--mode',
self.mode, '--format', 'tiff'
]
logger.debug('Scan command line: %s', command_line)
result = subprocess.check_call(
command_line, stdout=temporary_file_object
)
except subprocess.CalledProcessError as exception:
logger.error(
'Exception while scanning from source:%s ; %s', self,
exception
)
self.logs.create(
message=_('Error while scanning; %s') % exception
)
return SourceUploadedFile(
source=self, file=PseudoFile(
file=temporary_file_object, name='scan {}'.format(now())
)
)
class StagingFolderSource(InteractiveSource): class StagingFolderSource(InteractiveSource):
can_compress = True
is_interactive = True is_interactive = True
source_type = SOURCE_CHOICE_STAGING source_type = SOURCE_CHOICE_STAGING
@@ -234,6 +302,7 @@ class StagingFolderSource(InteractiveSource):
class WebFormSource(InteractiveSource): class WebFormSource(InteractiveSource):
can_compress = True
is_interactive = True is_interactive = True
source_type = SOURCE_CHOICE_WEB_FORM source_type = SOURCE_CHOICE_WEB_FORM

View File

@@ -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
)

View File

@@ -1,14 +1,16 @@
from .forms import ( from .forms import (
POP3EmailSetupForm, IMAPEmailSetupForm, POP3EmailSetupForm, IMAPEmailSetupForm, SaneScannerSetupForm,
StagingFolderSetupForm, StagingUploadForm, WatchFolderSetupForm, SaneScannerUploadForm, StagingFolderSetupForm, StagingUploadForm,
WebFormSetupForm, WebFormUploadForm WatchFolderSetupForm, WebFormSetupForm, WebFormUploadForm
) )
from .literals import ( from .literals import (
SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_EMAIL_IMAP, SOURCE_CHOICE_EMAIL_POP3,
SOURCE_CHOICE_WATCH, SOURCE_CHOICE_WEB_FORM SOURCE_CHOICE_SANE_SCANNER, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WATCH,
SOURCE_CHOICE_WEB_FORM
) )
from .models import ( 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 return POP3Email
elif source_type == SOURCE_CHOICE_EMAIL_IMAP: elif source_type == SOURCE_CHOICE_EMAIL_IMAP:
return IMAPEmail return IMAPEmail
elif source_type == SOURCE_CHOICE_SANE_SCANNER:
return SaneScanner
def get_form_class(source_type): def get_form_class(source_type):
@@ -36,6 +40,8 @@ def get_form_class(source_type):
return POP3EmailSetupForm return POP3EmailSetupForm
elif source_type == SOURCE_CHOICE_EMAIL_IMAP: elif source_type == SOURCE_CHOICE_EMAIL_IMAP:
return IMAPEmailSetupForm return IMAPEmailSetupForm
elif source_type == SOURCE_CHOICE_SANE_SCANNER:
return SaneScannerSetupForm
def get_upload_form_class(source_type): def get_upload_form_class(source_type):
@@ -43,3 +49,5 @@ def get_upload_form_class(source_type):
return WebFormUploadForm return WebFormUploadForm
elif source_type == SOURCE_CHOICE_STAGING: elif source_type == SOURCE_CHOICE_STAGING:
return StagingUploadForm return StagingUploadForm
elif source_type == SOURCE_CHOICE_SANE_SCANNER:
return SaneScannerUploadForm

View File

@@ -25,16 +25,17 @@ from metadata.api import decode_metadata_from_url
from navigation import Link from navigation import Link
from .forms import ( from .forms import (
NewDocumentForm, NewVersionForm, WebFormUploadForm, NewDocumentForm, NewVersionForm, SaneScannerUploadForm, WebFormUploadForm,
WebFormUploadFormHTML5 WebFormUploadFormHTML5
) )
from .literals import ( from .literals import (
SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WEB_FORM, SOURCE_CHOICE_STAGING, SOURCE_CHOICE_SANE_SCANNER, SOURCE_CHOICE_WEB_FORM,
SOURCE_UNCOMPRESS_CHOICE_ASK, SOURCE_UNCOMPRESS_CHOICE_ASK,
SOURCE_UNCOMPRESS_CHOICE_Y SOURCE_UNCOMPRESS_CHOICE_Y
) )
from .models import ( from .models import (
InteractiveSource, Source, StagingFolderSource, WebFormSource InteractiveSource, Source, SaneScanner, StagingFolderSource,
WebFormSource
) )
from .permissions import ( from .permissions import (
permission_sources_setup_create, permission_sources_setup_delete, permission_sources_setup_create, permission_sources_setup_delete,
@@ -88,27 +89,10 @@ class UploadBaseView(MultiFormView):
@staticmethod @staticmethod
def get_active_tab_links(document=None): def get_active_tab_links(document=None):
tab_links = [] return [
UploadBaseView.get_tab_link_for_source(source, document)
web_forms = WebFormSource.objects.filter(enabled=True) for source in InteractiveSource.objects.filter(enabled=True).select_subclasses()
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,
}
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if 'source_id' in kwargs: if 'source_id' in kwargs:
@@ -163,6 +147,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: else:
subtemplates_list.append({ subtemplates_list.append({
'name': 'sources/upload_multiform_subtemplate.html', 'name': 'sources/upload_multiform_subtemplate.html',
@@ -173,8 +167,8 @@ class UploadBaseView(MultiFormView):
}, },
}) })
menu_facet.bound_links['sources:upload_interactive'] = 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['tab_links'] menu_facet.bound_links['sources:upload_version'] = self.tab_links
context.update({ context.update({
'subtemplates_list': subtemplates_list, 'subtemplates_list': subtemplates_list,
@@ -206,13 +200,16 @@ class UploadInteractiveView(UploadBaseView):
).dispatch(request, *args, **kwargs) ).dispatch(request, *args, **kwargs)
def forms_valid(self, forms): def forms_valid(self, forms):
if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: if self.source.can_compress:
expand = forms['source_form'].cleaned_data.get('expand') if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK:
else: expand = forms['source_form'].cleaned_data.get('expand')
if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y:
expand = True
else: else:
expand = False if self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y:
expand = True
else:
expand = False
else:
expand = False
uploaded_file = self.source.get_upload_file_object( uploaded_file = self.source.get_upload_file_object(
forms['source_form'].cleaned_data forms['source_form'].cleaned_data
@@ -260,12 +257,15 @@ class UploadInteractiveView(UploadBaseView):
return HttpResponseRedirect(self.request.get_full_path()) return HttpResponseRedirect(self.request.get_full_path())
def create_source_form_form(self, **kwargs): 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']( return self.get_form_classes()['source_form'](
prefix=kwargs['prefix'], prefix=kwargs['prefix'],
source=self.source, source=self.source,
show_expand=( show_expand=show_expand,
self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK
),
data=kwargs.get('data', None), data=kwargs.get('data', None),
files=kwargs.get('files', None), files=kwargs.get('files', None),
) )
@@ -295,7 +295,7 @@ class UploadInteractiveView(UploadBaseView):
context['title'] = _( context['title'] = _(
'Upload a local document from source: %s' 'Upload a local document from source: %s'
) % self.source.label ) % 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( context['subtemplates_list'][0]['context'].update(
{ {
'form_action': self.request.get_full_path(), 'form_action': self.request.get_full_path(),