<commit_before>from AccessControl import ClassSecurityInfo
from Products.ATExtensions.ateapi import RecordsField
from bika.lims.browser.widgets import RecordsWidget
from Products.Archetypes.public import *
from Products.Archetypes.references import HoldingReference
from Products.CMFCore.permissions import View, ModifyPortalContent
from Products.CMFCore.utils import getToolByName
from bika.lims import PMF, bikaMessageFactory as _
from bika.lims.config import *
from bika.lims.content.bikaschema import BikaFolderSchema
from bika.lims.interfaces import IBikaSetup
from bika.lims.interfaces import IHaveNoBreadCrumbs
from bika.lims.browser.widgets import DurationWidget
from bika.lims.browser.fields import DurationField
from plone.app.folder import folder
from zope.interface import implements
import sys
from bika.lims.locales import COUNTRIES

class PrefixesField(RecordsField):
    """a list of prefixes per portal_type"""
    _properties = RecordsField._properties.copy()
    _properties.update({
        'type' : 'prefixes',
        'subfields' : ('portal_type', 'prefix', 'padding'),
        'subfield_labels':{'portal_type': 'Portal type',
                           'prefix': 'Prefix',
                           'padding': 'Padding',
                           },
        'subfield_readonly':{'portal_type': False,
                             'prefix': False,
                             'padding': False,
                            },
        'subfield_sizes':{'portal_type':32,
                          'prefix': 12,
                          'padding':12},
    })
    security = ClassSecurityInfo()

LABEL_AUTO_OPTIONS = DisplayList((
    ('None', _('None')),
    ('register', _('Register')),
    ('receive', _('Receive')),
))

LABEL_AUTO_SIZES = DisplayList((
    ('small', _('Small')),
    ('normal', _('Normal')),
))

schema = BikaFolderSchema.copy() + Schema((
    IntegerField('PasswordLifetime',
        schemata = "Security",
        required = 1,
        default = 0,
        widget = IntegerWidget(
            label = _("Password lifetime"),
            description = _("The number of days before a password expires. 0 disables password expiry"),
        )
    ),
    IntegerField('AutoLogOff',
        schemata = "Security",
        required = 1,
        default = 0,
        widget = IntegerWidget(
            label = _("Automatic log-off"),
            description = _("The number of minutes before a user is automatically logged off. "
                            "0 disables automatic log-off"),
        )
    ),
    BooleanField(
        'AllowClerksToEditClients',
        schemata="Security",
        default=False,
        widget=BooleanWidget(
            label=_("Allow Lab Clerks to create and edit clients"),
        )
    ),
    BooleanField(
        'RestrictWorksheetUsersAccess',
        schemata="Security",
        default=True,
        widget=BooleanWidget(
            label=_("Allow access to worksheets only to assigned analysts"),
            description=_("If unticked, analysts will have access to all worksheets.")
        )
    ),
    BooleanField(
        'RestrictWorksheetManagement',
        schemata="Security",
        default=True,
        widget=BooleanWidget(
            label=_("Only lab managers can create and manage worksheets"),
            description=_("If unticked, analysts and lab clerks will "
                          "be able to manage Worksheets, too. If the "
                          "users have restricted access only to those "
                          "worksheets for which they are assigned, "
                          "this option will be ticked and readonly.")
        )
    ),
    BooleanField(
        'ShowPrices',
        schemata="Accounting",
        default=True,
        widget=BooleanWidget(
            label=_("Include and display pricing information"),
        )
    ),
    StringField('Currency',
        schemata = "Accounting",
        required = 1,
        vocabulary = CURRENCIES,
        default = 'ZAR',
        widget = SelectionWidget(
            label = _("Currency"),
            description = _("Select the currency the site will use to display "
                            "prices."),
            format='select',
        )
    ),
    StringField('DefaultCountry',
        schemata = "Accounting",
        required = 1,
        vocabulary = 'getCountries',
        default = '',
        widget = SelectionWidget(
            label = _("Country"),
            description = _("Select the country the site will show by default"),
            format='select',
        )
    ),
    FixedPointField('MemberDiscount',
        schemata = "Accounting",
        default = '33.33',
        widget = DecimalWidget(
            label = _("Member discount %"),
            description = _("The discount percentage entered here, is applied to the prices for clients "
                            "flagged as 'members', normally co-operative members or associates deserving "
                            "of this discount"),
        )
    ),
    FixedPointField('VAT',
        schemata = "Accounting",
        default = '14.00',
        widget = DecimalWidget(
            label = _("VAT %"),
            description = _("Enter percentage value eg. 14.0. This percentage is applied system wide "
                            "but can be overwrittem on individual items"),
        )
    ),
    IntegerField('MinimumResults',
        schemata = "Results Reports",
        required = 1,
        default = 5,
        widget = IntegerWidget(
            label = _("Minimum number of results for QC stats calculations"),
            description = _("Using too few data points does not make statistical sense. "
                            "Set an acceptable minimum number of results before QC statistics "
                            "will be calculated and plotted"),
        )
    ),
    BooleanField('IncludePreviousFromBatch',
        schemata = "Results Reports",
        default = False,
        widget = BooleanWidget(
            label = _("Include Previous Results From Batch"),
            description = _("If there are previous results for a service in the "
                            "same batch of Analysis Requests, they will be displayed "
                            "in the report.")
        )
    ),
    IntegerField('BatchEmail',
        schemata = "Results Reports",
        required = 1,
        default = 5,
        widget = IntegerWidget(
            label = _("Maximum columns per results email"),
            description = _("Set the maximum number of analysis requests per results email. "
                            "Too many columns per email are difficult to read for some clients "
                            "who prefer fewer results per email"),
        )
    ),
    TextField('ResultFooter',
        schemata = "Results Reports",
        default_content_type = 'text/plain',
        allowed_content_types= ('text/plain', ),
        default_output_type="text/plain",
        default="",
        widget = TextAreaWidget(
            label = _('Result Footer'),
            description = _("This text will be appended to results reports."),
            append_only = False,
        ),
    ),
##    IntegerField('BatchFax',
##        schemata = "Results Reports",
##        required = 1,
##        default = 4,
##        widget = IntegerWidget(
##            label = _("Maximum columns per results fax"),
##            description = _("Too many AR columns per fax will see the font size minimised and could "
##                            "render faxes illegible. 4 ARs maximum per page is recommended"),
##        )
##    ),
##    StringField('SMSGatewayAddress',
##        schemata = "Results Reports",
##        required = 0,
##        widget = StringWidget(
##            label = _("SMS Gateway Email Address"),
##            description = _("The email to SMS gateway address. Either a complete email address, "
##                            "or just the domain, e.g. '@2way.co.za', the contact's mobile phone "
##                            "number will be prepended to"),
##        )
##    ),
    BooleanField('SamplingWorkflowEnabled',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Enable the Sampling workflow"),
            description = _("Select this to activate the sample collection workflow steps.")
        ),
    ),
    BooleanField(
        'ShowPartitions',
        schemata = "Analyses",
        default = True,
        widget = BooleanWidget(
            label = _("Display individual sample partitions "),
            description = _("Turn this on if you want to work with sample partitions")
        ),
    ),
    BooleanField('CategoriseAnalysisServices',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Categorise analysis services"),
            description = _("Group analysis services by category in the LIMS "
                            "tables, helpful when the list is long")
        ),
    ),
    BooleanField('EnableARSpecs',
        schemata = "Analyses",
        default = True,
        widget = BooleanWidget(
            label = _("Enable AR Specifications"),
            description=_(
                "Analysis specifications which are edited directly on the "
                "Analysis Request."),
        ),
    ),
    StringField('DefaultARSpecs',
        schemata = "Analyses",
        default = 'ar_specs',
        vocabulary = DEFAULT_AR_SPECS,
        widget = SelectionWidget(
            label = _("Default AR Specifications"),
            description = _(
                "Choose the default specifications used for all AR views "
                "to display alerts and notifications.  These will also be "
                "applied when an AR is published in permanent media, "
                "e.g. PDF."),
            format='select',
        )
    ),
    BooleanField('EnableAnalysisRemarks',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Add a remarks field to all analyses"),
        ),
    ),
    ReferenceField('DryMatterService',
        schemata = "Analyses",
        required = 0,
        vocabulary_display_path_bound = sys.maxint,
        allowed_types = ('AnalysisService',),
        relationship = 'SetupDryAnalysisService',
        vocabulary = 'getAnalysisServices',
        referenceClass = HoldingReference,
        widget = ReferenceWidget(
            label = _("Dry matter analysis"),
            description = _("The analysis to be used for determining dry matter."),
        )
    ),
    LinesField('ARImportOption',
        schemata = "Analyses",
        vocabulary = ARIMPORT_OPTIONS,
        widget = MultiSelectionWidget(
            visible = False,
            label = _("AR Import options"),
            description = _("'Classic' indicates importing analysis requests per sample and "
                            "analysis service selection. With 'Profiles', analysis profile keywords "
                            "are used to select multiple analysis services together"),
        )
    ),
    StringField('ARAttachmentOption',
        schemata = "Analyses",
        default = 'p',
        vocabulary = ATTACHMENT_OPTIONS,
        widget = SelectionWidget(
            label = _("AR Attachment Option"),
            description = _("The system wide default configuration to indicate "
                            "whether file attachments are required, permitted or not "
                            "per analysis request"),
            format='select',
        )
    ),
    StringField('AnalysisAttachmentOption',
        schemata = "Analyses",
        default = 'p',
        vocabulary = ATTACHMENT_OPTIONS,
        widget = SelectionWidget(
            label = _("Analysis Attachment Option"),
            description = _("Same as the above, but sets the default on analysis services. "
                            "This setting can be set per individual analysis on its "
                            "own configuration"),
            format='select',
        )
    ),
    DurationField('DefaultSampleLifetime',
        schemata = "Analyses",
        required = 1,
        default = {"days":30, "hours":0, "minutes":0},
        widget = DurationWidget(
            label = _("Default sample retention period"),
            description = _("The number of days before a sample expires and cannot be analysed "
                            "any more. This setting can be overwritten per individual sample type "
                            "in the sample types setup"),
        )
    ),
    StringField('AutoPrintLabels',
        schemata = "Labels",
        vocabulary = LABEL_AUTO_OPTIONS,
        widget = SelectionWidget(
            format = 'select',
            label = _("Automatic label printing"),
            description = _("Select 'Register' if you want labels to be automatically printed when "
                            "new ARs or sample records are created. Select 'Receive' to print labels "
                            "when ARs or Samples are received. Select 'None' to disable automatic printing"),
        )
    ),
    StringField('AutoLabelSize',
        schemata = "Labels",
        vocabulary = LABEL_AUTO_SIZES,
        widget = SelectionWidget(
            format = 'select',
            label = _("Label sizes"),
            description = _("Select the which label to print when automatic label printing is enabled"),
        )
    ),
    PrefixesField('Prefixes',
        schemata = "ID Server",
        default = [{'portal_type': 'ARImport', 'prefix': 'AI', 'padding': '4'},
                   {'portal_type': 'AnalysisRequest', 'prefix': 'client', 'padding': '0'},
                   {'portal_type': 'Client', 'prefix': 'client', 'padding': '0'},
                   {'portal_type': 'Batch', 'prefix': 'batch', 'padding': '0'},
                   {'portal_type': 'DuplicateAnalysis', 'prefix': 'DA', 'padding': '0'},
                   {'portal_type': 'Invoice', 'prefix': 'I', 'padding': '4'},
                   {'portal_type': 'ReferenceAnalysis', 'prefix': 'RA', 'padding': '4'},
                   {'portal_type': 'ReferenceSample', 'prefix': 'RS', 'padding': '4'},
                   {'portal_type': 'SupplyOrder', 'prefix': 'O', 'padding': '3'},
                   {'portal_type': 'Worksheet', 'prefix': 'WS', 'padding': '4'},
                   {'portal_type': 'Pricelist', 'prefix': 'PL', 'padding': '4'},
                   ],
#        fixedSize=8,
        widget=RecordsWidget(
            label = _("Prefixes"),
            description = _("Define the prefixes for the unique sequential IDs the system issues for "
                            "objects. In the 'Padding' field, indicate with how many leading zeros the "
                            "numbers must be padded. E.g. a prefix of WS for worksheets with padding of "
                            "4, will see them numbered from WS-0001 to WS-9999. NB: Note that samples "
                            "and analysis requests are prefixed with sample type abbreviations and are "
                            "not configured in this table - their padding can be set in the specified "
                            "fields below"),
            allowDelete=False,
        )
    ),
    BooleanField('YearInPrefix',
        schemata = "ID Server",
        default = False,
        widget = BooleanWidget(
            label = _("Include year in ID prefix"),
            description = _("Adds a two-digit year after the ID prefix")
        ),
    ),
    IntegerField('SampleIDPadding',
        schemata = "ID Server",
        required = 1,
        default = 4,
        widget = IntegerWidget(
            label = _("Sample ID Padding"),
            description = _("The length of the zero-padding for Sample IDs"),
        )
    ),
    IntegerField('ARIDPadding',
        schemata = "ID Server",
        required = 1,
        default = 2,
        widget = IntegerWidget(
            label = _("AR ID Padding"),
            description = _("The length of the zero-padding for the AR number in AR IDs"),
        )
    ),
    BooleanField('ExternalIDServer',
        schemata = "ID Server",
        default = False,
        widget = BooleanWidget(
            label = _("Use external ID server"),
            description = _("Check this if you want to use a separate ID server. "
                            "Prefixes are configurable separately in each Bika site")
        ),
    ),
    StringField('IDServerURL',
        schemata = "ID Server",
        widget = StringWidget(
            label = _("ID Server URL"),
            description = _("The full URL: http://URL/path:port")

        ),
    ),
))

schema['title'].validators = ()
schema['title'].widget.visible = False
# Update the validation layer after change the validator in runtime
schema['title']._validationLayer()

class BikaSetup(folder.ATFolder):
    security = ClassSecurityInfo()
    schema = schema
    implements(IBikaSetup, IHaveNoBreadCrumbs)

    def getAttachmentsPermitted(self):
        """ are any attachments permitted """
        if self.getARAttachmentOption() in ['r', 'p'] \
        or self.getAnalysisAttachmentOption() in ['r', 'p']:
            return True
        else:
            return False

    def getARAttachmentsPermitted(self):
        """ are AR attachments permitted """
        if self.getARAttachmentOption() == 'n':
            return False
        else:
            return True

    def getAnalysisAttachmentsPermitted(self):
        """ are analysis attachments permitted """
        if self.getAnalysisAttachmentOption() == 'n':
            return False
        else:
            return True

    def getAnalysisServices(self):
        """
        """
        bsc = getToolByName(self, 'bika_setup_catalog')
        items = [('','')] + [(o.UID, o.Title) for o in
                               bsc(portal_type='AnalysisService',
                                   inactive_state = 'active')]
        items.sort(lambda x,y: cmp(x[1], y[1]))
        return DisplayList(list(items))

    def getPrefixFor(self, portal_type):
        """Return the prefix for a portal_type.
        If not found, simply uses the portal_type itself
        """
        prefix = [p for p in self.getPrefixes() if p['portal_type'] == portal_type]
        if prefix:
            return prefix[0]['prefix']
        else:
            return portal_type

    def getCountries(self):
        items = [(x['ISO'], x['Country']) for x in COUNTRIES]
        items.sort(lambda x,y: cmp(x[1], y[1]))
        return items

registerType(BikaSetup, PROJECTNAME)
<commit_msg>Added decimal mark selector in bika_setup<commit_after>from AccessControl import ClassSecurityInfo
from Products.ATExtensions.ateapi import RecordsField
from bika.lims.browser.widgets import RecordsWidget
from Products.Archetypes.public import *
from Products.Archetypes.references import HoldingReference
from Products.CMFCore.permissions import View, ModifyPortalContent
from Products.CMFCore.utils import getToolByName
from bika.lims import PMF, bikaMessageFactory as _
from bika.lims.config import *
from bika.lims.content.bikaschema import BikaFolderSchema
from bika.lims.interfaces import IBikaSetup
from bika.lims.interfaces import IHaveNoBreadCrumbs
from bika.lims.browser.widgets import DurationWidget
from bika.lims.browser.fields import DurationField
from plone.app.folder import folder
from zope.interface import implements
import sys
from bika.lims.locales import COUNTRIES

class PrefixesField(RecordsField):
    """a list of prefixes per portal_type"""
    _properties = RecordsField._properties.copy()
    _properties.update({
        'type' : 'prefixes',
        'subfields' : ('portal_type', 'prefix', 'padding'),
        'subfield_labels':{'portal_type': 'Portal type',
                           'prefix': 'Prefix',
                           'padding': 'Padding',
                           },
        'subfield_readonly':{'portal_type': False,
                             'prefix': False,
                             'padding': False,
                            },
        'subfield_sizes':{'portal_type':32,
                          'prefix': 12,
                          'padding':12},
    })
    security = ClassSecurityInfo()

LABEL_AUTO_OPTIONS = DisplayList((
    ('None', _('None')),
    ('register', _('Register')),
    ('receive', _('Receive')),
))

LABEL_AUTO_SIZES = DisplayList((
    ('small', _('Small')),
    ('normal', _('Normal')),
))

schema = BikaFolderSchema.copy() + Schema((
    IntegerField('PasswordLifetime',
        schemata = "Security",
        required = 1,
        default = 0,
        widget = IntegerWidget(
            label = _("Password lifetime"),
            description = _("The number of days before a password expires. 0 disables password expiry"),
        )
    ),
    IntegerField('AutoLogOff',
        schemata = "Security",
        required = 1,
        default = 0,
        widget = IntegerWidget(
            label = _("Automatic log-off"),
            description = _("The number of minutes before a user is automatically logged off. "
                            "0 disables automatic log-off"),
        )
    ),
    BooleanField(
        'AllowClerksToEditClients',
        schemata="Security",
        default=False,
        widget=BooleanWidget(
            label=_("Allow Lab Clerks to create and edit clients"),
        )
    ),
    BooleanField(
        'RestrictWorksheetUsersAccess',
        schemata="Security",
        default=True,
        widget=BooleanWidget(
            label=_("Allow access to worksheets only to assigned analysts"),
            description=_("If unticked, analysts will have access to all worksheets.")
        )
    ),
    BooleanField(
        'RestrictWorksheetManagement',
        schemata="Security",
        default=True,
        widget=BooleanWidget(
            label=_("Only lab managers can create and manage worksheets"),
            description=_("If unticked, analysts and lab clerks will "
                          "be able to manage Worksheets, too. If the "
                          "users have restricted access only to those "
                          "worksheets for which they are assigned, "
                          "this option will be ticked and readonly.")
        )
    ),
    BooleanField(
        'ShowPrices',
        schemata="Accounting",
        default=True,
        widget=BooleanWidget(
            label=_("Include and display pricing information"),
        )
    ),
    StringField('Currency',
        schemata = "Accounting",
        required = 1,
        vocabulary = CURRENCIES,
        default = 'ZAR',
        widget = SelectionWidget(
            label = _("Currency"),
            description = _("Select the currency the site will use to display "
                            "prices."),
            format='select',
        )
    ),
    StringField('DefaultCountry',
        schemata = "Accounting",
        required = 1,
        vocabulary = 'getCountries',
        default = '',
        widget = SelectionWidget(
            label = _("Country"),
            description = _("Select the country the site will show by default"),
            format='select',
        )
    ),
    FixedPointField('MemberDiscount',
        schemata = "Accounting",
        default = '33.33',
        widget = DecimalWidget(
            label = _("Member discount %"),
            description = _("The discount percentage entered here, is applied to the prices for clients "
                            "flagged as 'members', normally co-operative members or associates deserving "
                            "of this discount"),
        )
    ),
    FixedPointField('VAT',
        schemata = "Accounting",
        default = '14.00',
        widget = DecimalWidget(
            label = _("VAT %"),
            description = _("Enter percentage value eg. 14.0. This percentage is applied system wide "
                            "but can be overwrittem on individual items"),
        )
    ),
    StringField('DecimalMark',
        schemata = "Results Reports",
        default = "dot",
        vocabulary = ["comma","dot"],
        widget = SelectionWidget(
            label = _("Select Decimal Mark"),
            description = _("Select your prefered decimal mark from the dropdown list."),
            format = 'select',
        )
    ),
    IntegerField('MinimumResults',
        schemata = "Results Reports",
        required = 1,
        default = 5,
        widget = IntegerWidget(
            label = _("Minimum number of results for QC stats calculations"),
            description = _("Using too few data points does not make statistical sense. "
                            "Set an acceptable minimum number of results before QC statistics "
                            "will be calculated and plotted"),
        )
    ),
    BooleanField('IncludePreviousFromBatch',
        schemata = "Results Reports",
        default = False,
        widget = BooleanWidget(
            label = _("Include Previous Results From Batch"),
            description = _("If there are previous results for a service in the "
                            "same batch of Analysis Requests, they will be displayed "
                            "in the report.")
        )
    ),
    IntegerField('BatchEmail',
        schemata = "Results Reports",
        required = 1,
        default = 5,
        widget = IntegerWidget(
            label = _("Maximum columns per results email"),
            description = _("Set the maximum number of analysis requests per results email. "
                            "Too many columns per email are difficult to read for some clients "
                            "who prefer fewer results per email"),
        )
    ),
    TextField('ResultFooter',
        schemata = "Results Reports",
        default_content_type = 'text/plain',
        allowed_content_types= ('text/plain', ),
        default_output_type="text/plain",
        default="",
        widget = TextAreaWidget(
            label = _('Result Footer'),
            description = _("This text will be appended to results reports."),
            append_only = False,
        ),
    ),
##    IntegerField('BatchFax',
##        schemata = "Results Reports",
##        required = 1,
##        default = 4,
##        widget = IntegerWidget(
##            label = _("Maximum columns per results fax"),
##            description = _("Too many AR columns per fax will see the font size minimised and could "
##                            "render faxes illegible. 4 ARs maximum per page is recommended"),
##        )
##    ),
##    StringField('SMSGatewayAddress',
##        schemata = "Results Reports",
##        required = 0,
##        widget = StringWidget(
##            label = _("SMS Gateway Email Address"),
##            description = _("The email to SMS gateway address. Either a complete email address, "
##                            "or just the domain, e.g. '@2way.co.za', the contact's mobile phone "
##                            "number will be prepended to"),
##        )
##    ),
    BooleanField('SamplingWorkflowEnabled',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Enable the Sampling workflow"),
            description = _("Select this to activate the sample collection workflow steps.")
        ),
    ),
    BooleanField(
        'ShowPartitions',
        schemata = "Analyses",
        default = True,
        widget = BooleanWidget(
            label = _("Display individual sample partitions "),
            description = _("Turn this on if you want to work with sample partitions")
        ),
    ),
    BooleanField('CategoriseAnalysisServices',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Categorise analysis services"),
            description = _("Group analysis services by category in the LIMS "
                            "tables, helpful when the list is long")
        ),
    ),
    BooleanField('EnableARSpecs',
        schemata = "Analyses",
        default = True,
        widget = BooleanWidget(
            label = _("Enable AR Specifications"),
            description=_(
                "Analysis specifications which are edited directly on the "
                "Analysis Request."),
        ),
    ),
    StringField('DefaultARSpecs',
        schemata = "Analyses",
        default = 'ar_specs',
        vocabulary = DEFAULT_AR_SPECS,
        widget = SelectionWidget(
            label = _("Default AR Specifications"),
            description = _(
                "Choose the default specifications used for all AR views "
                "to display alerts and notifications.  These will also be "
                "applied when an AR is published in permanent media, "
                "e.g. PDF."),
            format='select',
        )
    ),
    BooleanField('EnableAnalysisRemarks',
        schemata = "Analyses",
        default = False,
        widget = BooleanWidget(
            label = _("Add a remarks field to all analyses"),
        ),
    ),
    ReferenceField('DryMatterService',
        schemata = "Analyses",
        required = 0,
        vocabulary_display_path_bound = sys.maxint,
        allowed_types = ('AnalysisService',),
        relationship = 'SetupDryAnalysisService',
        vocabulary = 'getAnalysisServices',
        referenceClass = HoldingReference,
        widget = ReferenceWidget(
            label = _("Dry matter analysis"),
            description = _("The analysis to be used for determining dry matter."),
        )
    ),
    LinesField('ARImportOption',
        schemata = "Analyses",
        vocabulary = ARIMPORT_OPTIONS,
        widget = MultiSelectionWidget(
            visible = False,
            label = _("AR Import options"),
            description = _("'Classic' indicates importing analysis requests per sample and "
                            "analysis service selection. With 'Profiles', analysis profile keywords "
                            "are used to select multiple analysis services together"),
        )
    ),
    StringField('ARAttachmentOption',
        schemata = "Analyses",
        default = 'p',
        vocabulary = ATTACHMENT_OPTIONS,
        widget = SelectionWidget(
            label = _("AR Attachment Option"),
            description = _("The system wide default configuration to indicate "
                            "whether file attachments are required, permitted or not "
                            "per analysis request"),
            format='select',
        )
    ),
    StringField('AnalysisAttachmentOption',
        schemata = "Analyses",
        default = 'p',
        vocabulary = ATTACHMENT_OPTIONS,
        widget = SelectionWidget(
            label = _("Analysis Attachment Option"),
            description = _("Same as the above, but sets the default on analysis services. "
                            "This setting can be set per individual analysis on its "
                            "own configuration"),
            format='select',
        )
    ),
    DurationField('DefaultSampleLifetime',
        schemata = "Analyses",
        required = 1,
        default = {"days":30, "hours":0, "minutes":0},
        widget = DurationWidget(
            label = _("Default sample retention period"),
            description = _("The number of days before a sample expires and cannot be analysed "
                            "any more. This setting can be overwritten per individual sample type "
                            "in the sample types setup"),
        )
    ),
    StringField('AutoPrintLabels',
        schemata = "Labels",
        vocabulary = LABEL_AUTO_OPTIONS,
        widget = SelectionWidget(
            format = 'select',
            label = _("Automatic label printing"),
            description = _("Select 'Register' if you want labels to be automatically printed when "
                            "new ARs or sample records are created. Select 'Receive' to print labels "
                            "when ARs or Samples are received. Select 'None' to disable automatic printing"),
        )
    ),
    StringField('AutoLabelSize',
        schemata = "Labels",
        vocabulary = LABEL_AUTO_SIZES,
        widget = SelectionWidget(
            format = 'select',
            label = _("Label sizes"),
            description = _("Select the which label to print when automatic label printing is enabled"),
        )
    ),
    PrefixesField('Prefixes',
        schemata = "ID Server",
        default = [{'portal_type': 'ARImport', 'prefix': 'AI', 'padding': '4'},
                   {'portal_type': 'AnalysisRequest', 'prefix': 'client', 'padding': '0'},
                   {'portal_type': 'Client', 'prefix': 'client', 'padding': '0'},
                   {'portal_type': 'Batch', 'prefix': 'batch', 'padding': '0'},
                   {'portal_type': 'DuplicateAnalysis', 'prefix': 'DA', 'padding': '0'},
                   {'portal_type': 'Invoice', 'prefix': 'I', 'padding': '4'},
                   {'portal_type': 'ReferenceAnalysis', 'prefix': 'RA', 'padding': '4'},
                   {'portal_type': 'ReferenceSample', 'prefix': 'RS', 'padding': '4'},
                   {'portal_type': 'SupplyOrder', 'prefix': 'O', 'padding': '3'},
                   {'portal_type': 'Worksheet', 'prefix': 'WS', 'padding': '4'},
                   {'portal_type': 'Pricelist', 'prefix': 'PL', 'padding': '4'},
                   ],
#        fixedSize=8,
        widget=RecordsWidget(
            label = _("Prefixes"),
            description = _("Define the prefixes for the unique sequential IDs the system issues for "
                            "objects. In the 'Padding' field, indicate with how many leading zeros the "
                            "numbers must be padded. E.g. a prefix of WS for worksheets with padding of "
                            "4, will see them numbered from WS-0001 to WS-9999. NB: Note that samples "
                            "and analysis requests are prefixed with sample type abbreviations and are "
                            "not configured in this table - their padding can be set in the specified "
                            "fields below"),
            allowDelete=False,
        )
    ),
    BooleanField('YearInPrefix',
        schemata = "ID Server",
        default = False,
        widget = BooleanWidget(
            label = _("Include year in ID prefix"),
            description = _("Adds a two-digit year after the ID prefix")
        ),
    ),
    IntegerField('SampleIDPadding',
        schemata = "ID Server",
        required = 1,
        default = 4,
        widget = IntegerWidget(
            label = _("Sample ID Padding"),
            description = _("The length of the zero-padding for Sample IDs"),
        )
    ),
    IntegerField('ARIDPadding',
        schemata = "ID Server",
        required = 1,
        default = 2,
        widget = IntegerWidget(
            label = _("AR ID Padding"),
            description = _("The length of the zero-padding for the AR number in AR IDs"),
        )
    ),
    BooleanField('ExternalIDServer',
        schemata = "ID Server",
        default = False,
        widget = BooleanWidget(
            label = _("Use external ID server"),
            description = _("Check this if you want to use a separate ID server. "
                            "Prefixes are configurable separately in each Bika site")
        ),
    ),
    StringField('IDServerURL',
        schemata = "ID Server",
        widget = StringWidget(
            label = _("ID Server URL"),
            description = _("The full URL: http://URL/path:port")

        ),
    ),
))

schema['title'].validators = ()
schema['title'].widget.visible = False
# Update the validation layer after change the validator in runtime
schema['title']._validationLayer()

class BikaSetup(folder.ATFolder):
    security = ClassSecurityInfo()
    schema = schema
    implements(IBikaSetup, IHaveNoBreadCrumbs)

    def getAttachmentsPermitted(self):
        """ are any attachments permitted """
        if self.getARAttachmentOption() in ['r', 'p'] \
        or self.getAnalysisAttachmentOption() in ['r', 'p']:
            return True
        else:
            return False

    def getARAttachmentsPermitted(self):
        """ are AR attachments permitted """
        if self.getARAttachmentOption() == 'n':
            return False
        else:
            return True

    def getAnalysisAttachmentsPermitted(self):
        """ are analysis attachments permitted """
        if self.getAnalysisAttachmentOption() == 'n':
            return False
        else:
            return True

    def getAnalysisServices(self):
        """
        """
        bsc = getToolByName(self, 'bika_setup_catalog')
        items = [('','')] + [(o.UID, o.Title) for o in
                               bsc(portal_type='AnalysisService',
                                   inactive_state = 'active')]
        items.sort(lambda x,y: cmp(x[1], y[1]))
        return DisplayList(list(items))

    def getPrefixFor(self, portal_type):
        """Return the prefix for a portal_type.
        If not found, simply uses the portal_type itself
        """
        prefix = [p for p in self.getPrefixes() if p['portal_type'] == portal_type]
        if prefix:
            return prefix[0]['prefix']
        else:
            return portal_type

    def getCountries(self):
        items = [(x['ISO'], x['Country']) for x in COUNTRIES]
        items.sort(lambda x,y: cmp(x[1], y[1]))
        return items

registerType(BikaSetup, PROJECTNAME)
<|endoftext|>


<commit_before>#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Marek Marczykowski-Górecki
#                                        <marmarek@invisiblethingslab.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#

import os
import subprocess
import tempfile
import time
import unittest
from contextlib import suppress

from distutils import spawn

import asyncio

import sys

import qubes.tests

class TC_04_DispVM(qubes.tests.SystemTestCase):

    def setUp(self):
        super(TC_04_DispVM, self).setUp()
        self.init_default_template()
        self.disp_base = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('dvm'),
            label='red',
        )
        self.loop.run_until_complete(self.disp_base.create_on_disk())
        self.disp_base.template_for_dispvms = True
        self.app.default_dispvm = self.disp_base
        self.testvm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('vm'),
            label='red',
        )
        self.loop.run_until_complete(self.testvm.create_on_disk())
        self.app.save()

    def tearDown(self):
        self.app.default_dispvm = None
        super(TC_04_DispVM, self).tearDown()

    def test_002_cleanup(self):
        self.loop.run_until_complete(self.testvm.start())

        try:
            (stdout, _) = self.loop.run_until_complete(
                self.testvm.run_for_stdio("qvm-run-vm --dispvm bash",
                    input=b"echo test; qubesdb-read /name; echo ERROR\n"))
        except subprocess.CalledProcessError as err:
            self.fail('qvm-run-vm failed with {} code, stderr: {}'.format(
                err.returncode, err.stderr))
        lines = stdout.decode('ascii').splitlines()
        self.assertEqual(lines[0], "test")
        dispvm_name = lines[1]
        # wait for actual DispVM destruction
        self.loop.run_until_complete(asyncio.sleep(5))
        self.assertNotIn(dispvm_name, self.app.domains)

    def test_003_cleanup_destroyed(self):
        """
        Check if DispVM is properly removed even if it terminated itself (#1660)
        :return:
        """

        self.loop.run_until_complete(self.testvm.start())

        p = self.loop.run_until_complete(
            self.testvm.run("qvm-run-vm --dispvm bash; true",
                stdin=subprocess.PIPE, stdout=subprocess.PIPE))
        p.stdin.write(b"qubesdb-read /name\n")
        p.stdin.write(b"echo ERROR\n")
        p.stdin.write(b"sudo poweroff\n")
        # do not close p.stdin on purpose - wait to automatic disconnect when
        #  domain is destroyed
        timeout = 70
        lines_task = asyncio.ensure_future(p.stdout.read())
        self.loop.run_until_complete(asyncio.wait_for(p.wait(), timeout))
        self.loop.run_until_complete(lines_task)
        lines = lines_task.result().splitlines()
        self.assertTrue(lines, 'No output received from DispVM')
        dispvm_name = lines[0]
        self.assertNotEquals(dispvm_name, b"ERROR")

        self.assertNotIn(dispvm_name, self.app.domains)

class TC_20_DispVMMixin(object):

    def setUp(self):
        super(TC_20_DispVMMixin, self).setUp()
        self.init_default_template(self.template)
        self.disp_base = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('dvm'),
            label='red', template_for_dispvms=True,
        )
        self.loop.run_until_complete(self.disp_base.create_on_disk())
        self.app.default_dispvm = self.disp_base
        self.app.save()

    def tearDown(self):
        self.app.default_dispvm = None
        super(TC_20_DispVMMixin, self).tearDown()

    def test_010_simple_dvm_run(self):
        dispvm = self.loop.run_until_complete(
            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base))
        try:
            self.loop.run_until_complete(dispvm.start())
            (stdout, _) = self.loop.run_until_complete(
                dispvm.run_service_for_stdio('qubes.VMShell',
                    input=b"echo test"))
            self.assertEqual(stdout, b"test\n")
        finally:
            self.loop.run_until_complete(dispvm.cleanup())

    @unittest.skipUnless(spawn.find_executable('xdotool'),
                         "xdotool not installed")
    def test_020_gui_app(self):
        dispvm = self.loop.run_until_complete(
            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base))
        try:
            self.loop.run_until_complete(dispvm.start())
            self.loop.run_until_complete(self.wait_for_session(dispvm))
            p = self.loop.run_until_complete(
                dispvm.run_service('qubes.VMShell',
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE))
            # wait for DispVM startup:
            p.stdin.write(b"echo test\n")
            self.loop.run_until_complete(p.stdin.drain())
            l = self.loop.run_until_complete(p.stdout.readline())
            self.assertEqual(l, b"test\n")

            self.assertTrue(dispvm.is_running())
            try:
                window_title = 'user@%s' % (dispvm.name,)
                # close xterm on Return, but after short delay, to allow
                # xdotool to send also keyup event
                p.stdin.write("xterm -e "
                    "\"sh -c 'echo \\\"\033]0;{}\007\\\";read x;"
                              "sleep 0.1;'\"\n".
                    format(window_title).encode())
                self.loop.run_until_complete(p.stdin.drain())
                self.wait_for_window(window_title)

                time.sleep(0.5)
                self.enter_keys_in_window(window_title, ['Return'])
                # Wait for window to close
                self.wait_for_window(window_title, show=False)
                p.stdin.close()
                self.loop.run_until_complete(
                    asyncio.wait_for(p.wait(), 30))
            except:
                with suppress(ProcessLookupError):
                    p.terminate()
                self.loop.run_until_complete(p.wait())
                raise
            finally:
                del p
        finally:
            self.loop.run_until_complete(dispvm.cleanup())
            dispvm_name = dispvm.name
            del dispvm

        # give it a time for shutdown + cleanup
        self.loop.run_until_complete(asyncio.sleep(5))

        self.assertNotIn(dispvm_name, self.app.domains,
                          "DispVM not removed from qubes.xml")

    def _handle_editor(self, winid):
        (window_title, _) = subprocess.Popen(
            ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE).\
            communicate()
        window_title = window_title.decode().strip().\
            replace('(', '\(').replace(')', '\)')
        time.sleep(1)
        if "gedit" in window_title or 'KWrite' in window_title:
            subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
                                   'type', 'Test test 2'])
            subprocess.check_call(['xdotool', 'key', '--window', winid,
                                   'key', 'Return'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool',
                                   'key', 'ctrl+s', 'ctrl+q'])
        elif "LibreOffice" in window_title:
            # wait for actual editor (we've got splash screen)
            search = subprocess.Popen(['xdotool', 'search', '--sync',
                '--onlyvisible', '--all', '--name', '--class', 'disp*|Writer'],
                stdout=subprocess.PIPE,
                                  stderr=open(os.path.devnull, 'w'))
            retcode = search.wait()
            if retcode == 0:
                winid = search.stdout.read().strip()
            time.sleep(0.5)
            subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
                                   'type', 'Test test 2'])
            subprocess.check_call(['xdotool', 'key', '--window', winid,
                                   'key', 'Return'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool',
                                   'key', '--delay', '100', 'ctrl+s',
                'Return', 'ctrl+q'])
        elif "emacs" in window_title:
            subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
                                   'type', 'Test test 2'])
            subprocess.check_call(['xdotool', 'key', '--window', winid,
                                   'key', 'Return'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool',
                                   'key', 'ctrl+x', 'ctrl+s'])
            subprocess.check_call(['xdotool',
                                   'key', 'ctrl+x', 'ctrl+c'])
        elif "vim" in window_title or "user@" in window_title:
            subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
                                   'key', 'i', 'type', 'Test test 2'])
            subprocess.check_call(['xdotool', 'key', '--window', winid,
                                   'key', 'Return'])
            subprocess.check_call(
                ['xdotool',
                 'key', 'Escape', 'colon', 'w', 'q', 'Return'])
        else:
            self.fail("Unknown editor window: {}".format(window_title))

    @unittest.skipUnless(spawn.find_executable('xdotool'),
                         "xdotool not installed")
    def test_030_edit_file(self):
        self.testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
                                     name=self.make_vm_name('vm1'),
                                     label='red',
                                     template=self.app.domains[self.template])
        self.loop.run_until_complete(self.testvm1.create_on_disk())
        self.app.save()

        self.loop.run_until_complete(self.testvm1.start())
        self.loop.run_until_complete(
            self.testvm1.run_for_stdio("echo test1 > /home/user/test.txt"))

        p = self.loop.run_until_complete(
            self.testvm1.run("qvm-open-in-dvm /home/user/test.txt",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT))

        # if first 5 windows isn't expected editor, there is no hope
        winid = None
        for _ in range(5):
            try:
                winid = self.wait_for_window('disp[0-9]*', search_class=True)
            except Exception as e:
                try:
                    self.loop.run_until_complete(asyncio.wait_for(p.wait(), 1))
                except asyncio.TimeoutError:
                    raise e
                else:
                    stdout = self.loop.run_until_complete(p.stdout.read())
                    self.fail(
                        'qvm-open-in-dvm exited prematurely with {}: {}'.format(
                            p.returncode, stdout))
            # get window title
            (window_title, _) = subprocess.Popen(
                ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
                communicate()
            window_title = window_title.decode().strip()
            # ignore LibreOffice splash screen and window with no title
            # set yet
            if window_title and not window_title.startswith("LibreOffice")\
                    and not window_title == 'VMapp command' \
                    and 'whonixcheck' not in window_title \
                    and not window_title == 'NetworkManager Applet':
                break
            self.loop.run_until_complete(asyncio.sleep(1))
            winid = None
        if winid is None:
            self.fail('Timeout waiting for editor window')

        time.sleep(0.5)
        self._handle_editor(winid)
        self.loop.run_until_complete(p.communicate())
        (test_txt_content, _) = self.loop.run_until_complete(
            self.testvm1.run_for_stdio("cat /home/user/test.txt"))
        # Drop BOM if added by editor
        if test_txt_content.startswith(b'\xef\xbb\xbf'):
            test_txt_content = test_txt_content[3:]
        self.assertEqual(test_txt_content, b"Test test 2\ntest1\n")

def create_testcases_for_templates():
    return qubes.tests.create_testcases_for_templates('TC_20_DispVM',
        TC_20_DispVMMixin, qubes.tests.SystemTestCase,
        module=sys.modules[__name__])

def load_tests(loader, tests, pattern):
    tests.addTests(loader.loadTestsFromNames(
        create_testcases_for_templates()))
    return tests

qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)
<commit_msg>tests/dispvm: make _handle_editor handle read-only files too<commit_after>#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Marek Marczykowski-Górecki
#                                        <marmarek@invisiblethingslab.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#

import os
import subprocess
import tempfile
import time
import unittest
from contextlib import suppress

from distutils import spawn

import asyncio

import sys

import qubes.tests

class TC_04_DispVM(qubes.tests.SystemTestCase):

    def setUp(self):
        super(TC_04_DispVM, self).setUp()
        self.init_default_template()
        self.disp_base = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('dvm'),
            label='red',
        )
        self.loop.run_until_complete(self.disp_base.create_on_disk())
        self.disp_base.template_for_dispvms = True
        self.app.default_dispvm = self.disp_base
        self.testvm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('vm'),
            label='red',
        )
        self.loop.run_until_complete(self.testvm.create_on_disk())
        self.app.save()

    def tearDown(self):
        self.app.default_dispvm = None
        super(TC_04_DispVM, self).tearDown()

    def test_002_cleanup(self):
        self.loop.run_until_complete(self.testvm.start())

        try:
            (stdout, _) = self.loop.run_until_complete(
                self.testvm.run_for_stdio("qvm-run-vm --dispvm bash",
                    input=b"echo test; qubesdb-read /name; echo ERROR\n"))
        except subprocess.CalledProcessError as err:
            self.fail('qvm-run-vm failed with {} code, stderr: {}'.format(
                err.returncode, err.stderr))
        lines = stdout.decode('ascii').splitlines()
        self.assertEqual(lines[0], "test")
        dispvm_name = lines[1]
        # wait for actual DispVM destruction
        self.loop.run_until_complete(asyncio.sleep(5))
        self.assertNotIn(dispvm_name, self.app.domains)

    def test_003_cleanup_destroyed(self):
        """
        Check if DispVM is properly removed even if it terminated itself (#1660)
        :return:
        """

        self.loop.run_until_complete(self.testvm.start())

        p = self.loop.run_until_complete(
            self.testvm.run("qvm-run-vm --dispvm bash; true",
                stdin=subprocess.PIPE, stdout=subprocess.PIPE))
        p.stdin.write(b"qubesdb-read /name\n")
        p.stdin.write(b"echo ERROR\n")
        p.stdin.write(b"sudo poweroff\n")
        # do not close p.stdin on purpose - wait to automatic disconnect when
        #  domain is destroyed
        timeout = 70
        lines_task = asyncio.ensure_future(p.stdout.read())
        self.loop.run_until_complete(asyncio.wait_for(p.wait(), timeout))
        self.loop.run_until_complete(lines_task)
        lines = lines_task.result().splitlines()
        self.assertTrue(lines, 'No output received from DispVM')
        dispvm_name = lines[0]
        self.assertNotEquals(dispvm_name, b"ERROR")

        self.assertNotIn(dispvm_name, self.app.domains)

class TC_20_DispVMMixin(object):

    def setUp(self):
        super(TC_20_DispVMMixin, self).setUp()
        self.init_default_template(self.template)
        self.disp_base = self.app.add_new_vm(qubes.vm.appvm.AppVM,
            name=self.make_vm_name('dvm'),
            label='red', template_for_dispvms=True,
        )
        self.loop.run_until_complete(self.disp_base.create_on_disk())
        self.app.default_dispvm = self.disp_base
        self.app.save()

    def tearDown(self):
        self.app.default_dispvm = None
        super(TC_20_DispVMMixin, self).tearDown()

    def test_010_simple_dvm_run(self):
        dispvm = self.loop.run_until_complete(
            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base))
        try:
            self.loop.run_until_complete(dispvm.start())
            (stdout, _) = self.loop.run_until_complete(
                dispvm.run_service_for_stdio('qubes.VMShell',
                    input=b"echo test"))
            self.assertEqual(stdout, b"test\n")
        finally:
            self.loop.run_until_complete(dispvm.cleanup())

    @unittest.skipUnless(spawn.find_executable('xdotool'),
                         "xdotool not installed")
    def test_020_gui_app(self):
        dispvm = self.loop.run_until_complete(
            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base))
        try:
            self.loop.run_until_complete(dispvm.start())
            self.loop.run_until_complete(self.wait_for_session(dispvm))
            p = self.loop.run_until_complete(
                dispvm.run_service('qubes.VMShell',
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE))
            # wait for DispVM startup:
            p.stdin.write(b"echo test\n")
            self.loop.run_until_complete(p.stdin.drain())
            l = self.loop.run_until_complete(p.stdout.readline())
            self.assertEqual(l, b"test\n")

            self.assertTrue(dispvm.is_running())
            try:
                window_title = 'user@%s' % (dispvm.name,)
                # close xterm on Return, but after short delay, to allow
                # xdotool to send also keyup event
                p.stdin.write("xterm -e "
                    "\"sh -c 'echo \\\"\033]0;{}\007\\\";read x;"
                              "sleep 0.1;'\"\n".
                    format(window_title).encode())
                self.loop.run_until_complete(p.stdin.drain())
                self.wait_for_window(window_title)

                time.sleep(0.5)
                self.enter_keys_in_window(window_title, ['Return'])
                # Wait for window to close
                self.wait_for_window(window_title, show=False)
                p.stdin.close()
                self.loop.run_until_complete(
                    asyncio.wait_for(p.wait(), 30))
            except:
                with suppress(ProcessLookupError):
                    p.terminate()
                self.loop.run_until_complete(p.wait())
                raise
            finally:
                del p
        finally:
            self.loop.run_until_complete(dispvm.cleanup())
            dispvm_name = dispvm.name
            del dispvm

        # give it a time for shutdown + cleanup
        self.loop.run_until_complete(asyncio.sleep(5))

        self.assertNotIn(dispvm_name, self.app.domains,
                          "DispVM not removed from qubes.xml")

    def _handle_editor(self, winid, copy=False):
        (window_title, _) = subprocess.Popen(
            ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE).\
            communicate()
        window_title = window_title.decode().strip().\
            replace('(', '\(').replace(')', '\)')
        time.sleep(1)
        if "gedit" in window_title or 'KWrite' in window_title:
            subprocess.check_call(
                ['xdotool', 'windowactivate', '--sync', winid])
            if copy:
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'ctrl+a', 'ctrl+c',
                                       'ctrl+shift+c'])
            else:
                subprocess.check_call(['xdotool', 'type', 'Test test 2'])
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'Return'])
                time.sleep(0.5)
                subprocess.check_call(['xdotool', 'key', 'ctrl+s'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool', 'key', 'ctrl+q'])
        elif "LibreOffice" in window_title:
            # wait for actual editor (we've got splash screen)
            search = subprocess.Popen(['xdotool', 'search', '--sync',
                '--onlyvisible', '--all', '--name', '--class', 'disp*|Writer'],
                stdout=subprocess.PIPE,
                                  stderr=open(os.path.devnull, 'w'))
            retcode = search.wait()
            if retcode == 0:
                winid = search.stdout.read().strip()
            time.sleep(0.5)
            subprocess.check_call(
                ['xdotool', 'windowactivate', '--sync', winid])
            if copy:
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'ctrl+a', 'ctrl+c',
                                       'ctrl+shift+c'])
            else:
                subprocess.check_call(['xdotool', 'type', 'Test test 2'])
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'Return'])
                time.sleep(0.5)
                subprocess.check_call(['xdotool',
                                       'key', '--delay', '100', 'ctrl+s',
                                       'Return'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool', 'key', 'ctrl+q'])
        elif "emacs" in window_title:
            subprocess.check_call(
                ['xdotool', 'windowactivate', '--sync', winid])
            if copy:
                subprocess.check_call(['xdotool',
                                       'key', 'ctrl+x', 'h', 'alt+w',
                                       'ctrl+shift+c'])
            else:
                subprocess.check_call(['xdotool', 'type', 'Test test 2'])
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'Return'])
                time.sleep(0.5)
                subprocess.check_call(['xdotool',
                                       'key', 'ctrl+x', 'ctrl+s'])
            time.sleep(0.5)
            subprocess.check_call(['xdotool',
                                   'key', 'ctrl+x', 'ctrl+c'])
        elif "vim" in window_title or "user@" in window_title:
            subprocess.check_call(
                ['xdotool', 'windowactivate', '--sync', winid])
            if copy:
                raise NotImplementedError('copy not implemented for vim')
            else:
                subprocess.check_call(
                    ['xdotool', 'key', 'i', 'type', 'Test test 2'])
                subprocess.check_call(['xdotool', 'key', '--window', winid,
                                       'key', 'Return'])
                subprocess.check_call(
                    ['xdotool',
                     'key', 'Escape', 'colon', 'w', 'q', 'Return'])
        else:
            self.fail("Unknown editor window: {}".format(window_title))

    @unittest.skipUnless(spawn.find_executable('xdotool'),
                         "xdotool not installed")
    def test_030_edit_file(self):
        self.testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
                                     name=self.make_vm_name('vm1'),
                                     label='red',
                                     template=self.app.domains[self.template])
        self.loop.run_until_complete(self.testvm1.create_on_disk())
        self.app.save()

        self.loop.run_until_complete(self.testvm1.start())
        self.loop.run_until_complete(
            self.testvm1.run_for_stdio("echo test1 > /home/user/test.txt"))

        p = self.loop.run_until_complete(
            self.testvm1.run("qvm-open-in-dvm /home/user/test.txt",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT))

        # if first 5 windows isn't expected editor, there is no hope
        winid = None
        for _ in range(5):
            try:
                winid = self.wait_for_window('disp[0-9]*', search_class=True)
            except Exception as e:
                try:
                    self.loop.run_until_complete(asyncio.wait_for(p.wait(), 1))
                except asyncio.TimeoutError:
                    raise e
                else:
                    stdout = self.loop.run_until_complete(p.stdout.read())
                    self.fail(
                        'qvm-open-in-dvm exited prematurely with {}: {}'.format(
                            p.returncode, stdout))
            # get window title
            (window_title, _) = subprocess.Popen(
                ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
                communicate()
            window_title = window_title.decode().strip()
            # ignore LibreOffice splash screen and window with no title
            # set yet
            if window_title and not window_title.startswith("LibreOffice")\
                    and not window_title == 'VMapp command' \
                    and 'whonixcheck' not in window_title \
                    and not window_title == 'NetworkManager Applet':
                break
            self.loop.run_until_complete(asyncio.sleep(1))
            winid = None
        if winid is None:
            self.fail('Timeout waiting for editor window')

        time.sleep(0.5)
        self._handle_editor(winid)
        self.loop.run_until_complete(p.communicate())
        (test_txt_content, _) = self.loop.run_until_complete(
            self.testvm1.run_for_stdio("cat /home/user/test.txt"))
        # Drop BOM if added by editor
        if test_txt_content.startswith(b'\xef\xbb\xbf'):
            test_txt_content = test_txt_content[3:]
        self.assertEqual(test_txt_content, b"Test test 2\ntest1\n")

def create_testcases_for_templates():
    return qubes.tests.create_testcases_for_templates('TC_20_DispVM',
        TC_20_DispVMMixin, qubes.tests.SystemTestCase,
        module=sys.modules[__name__])

def load_tests(loader, tests, pattern):
    tests.addTests(loader.loadTestsFromNames(
        create_testcases_for_templates()))
    return tests

qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)
<|endoftext|>


<commit_before>#-*- coding: utf-8 -*-

import wrapper

##########################
# COORDINATE SYSTEM TAGS #
##########################

CARTESIAN   = 'cartesian'
POLAR       = 'polar'
SPHERICAL   = 'spherical'
CYLINDRICAL = 'cylindrical'

COORD_SYSTEM_TAGS = (CARTESIAN, POLAR, SPHERICAL, CYLINDRICAL)

########################
# SUPPORTED DIMENSIONS #
########################

SUPPORTED_DIMENSIONS = {
	CARTESIAN:      (1, 2, 3),
	CYLINDRICAL:    (3,),
	POLAR:          (2,),
	SPHERICAL:      (3,),
}

#############
# CELL TAGS #
#############

LINE_TAG          = 'linear'
TRIANGLE_TAG      = 'triangular'
TETRAHEDRON_TAG   = 'tetrahedral'
QUADRILATERAL_TAG = 'quadrilateral'
HEXAHEDRON_TAG    = 'hexahedral'

CELL_TAGS = (LINE_TAG, TRIANGLE_TAG, TETRAHEDRON_TAG, QUADRILATERAL_TAG, HEXAHEDRON_TAG)

#######################
# CONFIGURATION CLASS #
#######################

class Configuration(object):
	def __init__(self, cell_tag, coord_system, dim=None):
		super(Configuration, self).__init__()

		# Set numeric_type to double
		self._numeric_type = 'double'

		# Set cell_tag
		if cell_tag not in CELL_TAGS:
			raise ValueError('Unsupported cell tag')
		else:
			self._cell_tag = cell_tag

		# Set coord_system
		if coord_system not in COORD_SYSTEM_TAGS:
			raise ValueError('Unsupported coordinate system tag')
		else:
			self._coord_system = coord_system

		# Set dim
		if dim is not None:
			if not isinstance(dim, int):
				raise TypeError('Dimension must be an integer')

			if dim not in SUPPORTED_DIMENSIONS[coord_system]:
				raise ValueError('Unsupported dimension: %(dim)s' % locals())

			self._dim = dim
		else:
			if len(SUPPORTED_DIMENSIONS[coord_system]) == 1:
				self._dim = SUPPORTED_DIMENSIONS[coord_system][0]
			else:
				raise TypeError('Dimension has not been specified')

	@property
	def numeric_type(self):
		return self._numeric_type

	@property
	def coord_system(self):
		return self._coord_system

	@property
	def cell_tag(self):
		return self._cell_tag

	@property
	def dim(self):
		return self._dim

	@property
	def point_type(self):
		"""Return appropriate point type based in this configuration"""
		classname = ''.join(['Point', self.coord_system.title(), str(self.dim), 'D'])
		return _wrapper.__getattribute__(classname)

	@property
	def domain_type(self):
		"""Return appropriate domain type based in this configuration"""
		classname = ''.join([self.cell_tag.title(), self.coord_system.title(), str(self.dim), 'D_Domain'])
		return _wrapper.__getattribute__(classname)

	@property
	def segmentation_type(self):
		"""Return appropriate segmentation type based in this configuration"""
		classname = ''.join([self.cell_tag.title(), self.coord_system.title(), str(self.dim), 'D_Segmentation'])
		return _wrapper.__getattribute__(classname)

	def make_point(self, *args, **kwargs):
		"""Create a new point based in this configuration"""
		PointType = self.point_type
		return PointType(*args, **kwargs)

	def make_domain(self, *args, **kwargs):
		"""Create a new domain based in this configuration"""
		DomainType = self.domain_type
		return DomainType(*args, **kwargs)

	def make_segmentation(self, *args, **kwargs):
		"""Create a new segmentation based in this configuration"""
		SegmentationType = self.segmentation_type
		return SegmentationType(*args, **kwargs)

#########################
# COMMON CONFIGURATIONS #
#########################

linear_1d        = Configuration(LINE_TAG, CARTESIAN, 1)
linear_2d        = Configuration(LINE_TAG, CARTESIAN, 2)
linear_3d        = Configuration(LINE_TAG, CARTESIAN, 3)
triangular_2d    = Configuration(TRIANGLE_TAG, CARTESIAN, 2)
triangular_3d    = Configuration(TRIANGLE_TAG, CARTESIAN, 3)
tetrahedral_3d   = Configuration(TETRAHEDRON_TAG, CARTESIAN, 3)
quadrilateral_2d = Configuration(QUADRILATERAL_TAG, CARTESIAN, 2)
quadrilateral_3d = Configuration(QUADRILATERAL_TAG, CARTESIAN, 3)
hexahedral_3d    = Configuration(HEXAHEDRON_TAG, CARTESIAN, 3)
<commit_msg>Document module 'viennagrid.config'.<commit_after>#-*- coding: utf-8 -*-

import wrapper

##########################
# COORDINATE SYSTEM TAGS #
##########################

CARTESIAN   = 'cartesian'
POLAR       = 'polar'
SPHERICAL   = 'spherical'
CYLINDRICAL = 'cylindrical'

COORD_SYSTEM_TAGS = (CARTESIAN, POLAR, SPHERICAL, CYLINDRICAL)

########################
# SUPPORTED DIMENSIONS #
########################

SUPPORTED_DIMENSIONS = {
	CARTESIAN:      (1, 2, 3),
	CYLINDRICAL:    (3,),
	POLAR:          (2,),
	SPHERICAL:      (3,),
}

#############
# CELL TAGS #
#############

LINE_TAG          = 'linear'
TRIANGLE_TAG      = 'triangular'
TETRAHEDRON_TAG   = 'tetrahedral'
QUADRILATERAL_TAG = 'quadrilateral'
HEXAHEDRON_TAG    = 'hexahedral'

CELL_TAGS = (LINE_TAG, TRIANGLE_TAG, TETRAHEDRON_TAG, QUADRILATERAL_TAG, HEXAHEDRON_TAG)

#######################
# CONFIGURATION CLASS #
#######################

class Configuration(object):
	"""
	This class stores the necessary information to characterize the configuration of a domain:

	* numeric type (data type used for number representation)
	* coordinate system
	* dimension of the space
	* cell tag (string that identifies which type of cell the domain consists of)

	and provides useful functions for retrieving the appropriate types for points, domains and segmentations.
	"""
	def __init__(self, cell_tag, coord_system, dim=None):
		super(Configuration, self).__init__()

		# Set numeric_type to double
		self._numeric_type = 'double'

		# Set cell_tag
		if cell_tag not in CELL_TAGS:
			raise ValueError('Unsupported cell tag')
		else:
			self._cell_tag = cell_tag

		# Set coord_system
		if coord_system not in COORD_SYSTEM_TAGS:
			raise ValueError('Unsupported coordinate system tag')
		else:
			self._coord_system = coord_system

		# Set dim
		if dim is not None:
			if not isinstance(dim, int):
				raise TypeError('Dimension must be an integer')

			if dim not in SUPPORTED_DIMENSIONS[coord_system]:
				raise ValueError('Unsupported dimension: %(dim)s' % locals())

			self._dim = dim
		else:
			if len(SUPPORTED_DIMENSIONS[coord_system]) == 1:
				self._dim = SUPPORTED_DIMENSIONS[coord_system][0]
			else:
				raise TypeError('Dimension has not been specified')

	@property
	def numeric_type(self):
		"""Return the data type used for number representation (this is always the string `'double'`)."""
		return self._numeric_type

	@property
	def coord_system(self):
		"""Return the coordinate system tag."""
		return self._coord_system

	@property
	def cell_tag(self):
		"""Return the cell tag."""
		return self._cell_tag

	@property
	def dim(self):
		"""Return the dimension of the space as an integer."""
		return self._dim

	@property
	def point_type(self):
		"""Return appropriate point type based in this configuration"""
		classname = ''.join(['Point', self.coord_system.title(), str(self.dim), 'D'])
		return _wrapper.__getattribute__(classname)

	@property
	def domain_type(self):
		"""Return appropriate domain type based in this configuration"""
		classname = ''.join([self.cell_tag.title(), self.coord_system.title(), str(self.dim), 'D_Domain'])
		return _wrapper.__getattribute__(classname)

	@property
	def segmentation_type(self):
		"""Return appropriate segmentation type based in this configuration"""
		classname = ''.join([self.cell_tag.title(), self.coord_system.title(), str(self.dim), 'D_Segmentation'])
		return _wrapper.__getattribute__(classname)

	def make_point(self, *args, **kwargs):
		"""
		Create a new low-level (:mod:`viennagrid.wrapper`) point based in this configuration

		As arguments you can pass the coordinates of the point to create, or nothing at all.
		If you specify the coordinates of the point, the number of arguments must match the dimension of the space
		(i.e. the number of coordinates that the are needed to describe the location of the point in the space).
		"""
		PointType = self.point_type
		return PointType(*args, **kwargs)

	def make_domain(self):
		"""Create a new low-level (:mod:`viennagrid.wrapper`) domain based in this configuration"""
		DomainType = self.domain_type
		return DomainType()

	def make_segmentation(self, domain):
		"""
		Create a new low-level (:mod:`viennagrid.wrapper`) segmentation based in this configuration.

		:param domain: Domain on which to base the segmentation.
		:type domain: A domain class from :mod:`viennagrid.wrapper` which matches this configuration class
		"""
		SegmentationType = self.segmentation_type
		return SegmentationType(*args, **kwargs)

#########################
# COMMON CONFIGURATIONS #
#########################

linear_1d        = Configuration(LINE_TAG, CARTESIAN, 1)
linear_2d        = Configuration(LINE_TAG, CARTESIAN, 2)
linear_3d        = Configuration(LINE_TAG, CARTESIAN, 3)
triangular_2d    = Configuration(TRIANGLE_TAG, CARTESIAN, 2)
triangular_3d    = Configuration(TRIANGLE_TAG, CARTESIAN, 3)
tetrahedral_3d   = Configuration(TETRAHEDRON_TAG, CARTESIAN, 3)
quadrilateral_2d = Configuration(QUADRILATERAL_TAG, CARTESIAN, 2)
quadrilateral_3d = Configuration(QUADRILATERAL_TAG, CARTESIAN, 3)
hexahedral_3d    = Configuration(HEXAHEDRON_TAG, CARTESIAN, 3)
<|endoftext|>