diff --git a/CHANGES.rst b/CHANGES.rst index 817a4fd90c..7312536562 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,15 +6,16 @@ Changelog **Added** - **Removed** - **Changed** - **Fixed** +- #445 Fix AR Add Form: No sample points are found if a sample type was set + +**Security** + 1.1.7 (2017-12-01) ------------------ diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index e7e52b30e7..0bcd547f3d 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -3,14 +3,17 @@ # Copyright 2011-2016 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. -from bika.lims.permissions import * +import ast +import json + +from zope.interface import implements + +from Products.AdvancedQuery import Or, MatchRegexp, Generic +from Products.CMFCore.utils import getToolByName + from bika.lims.utils import to_utf8 as _c from bika.lims.utils import to_unicode as _u from bika.lims.interfaces import IReferenceWidgetVocabulary -from Products.AdvancedQuery import And, Or, MatchRegexp, Generic -from Products.CMFCore.utils import getToolByName -from zope.interface import implements -import json class DefaultReferenceWidgetVocabulary(object): @@ -29,8 +32,13 @@ def __call__(self, result=None, specification=None, **kwargs): # lookup objects from ZODB catalog_name = _c(self.request.get('catalog_name', 'portal_catalog')) catalog = getToolByName(self.context, catalog_name) - base_query = json.loads(_c(self.request['base_query'])) - search_query = json.loads(_c(self.request.get('search_query', "{}"))) + + # N.B. We don't use json.loads to avoid unicode conversion, which will + # fail in the catalog search for some cases + # see: https://github.com/senaite/bika.lims/issues/443 + base_query = ast.literal_eval(self.request['base_query']) + search_query = ast.literal_eval(self.request.get('search_query', "{}")) + # first with all queries contentFilter = dict((k, v) for k, v in base_query.items()) contentFilter.update(search_query) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index c07e44a765..f29caeda86 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1118,12 +1118,14 @@ def get_sampletype_info(self, obj): # catalog queries for UI field filtering filter_queries = { "samplepoint": { - "getSampleTypeTitle": obj.Title(), + "getSampleTypeTitles": [obj.Title(), ''], "getClientUID": [client_uid, bika_samplepoints_uid], + "sort_order": "descending", }, "specification": { - "getSampleTypeTitle": obj.Title(), + "getSampleTypeTitles": [obj.Title(), ''], "getClientUID": [client_uid, bika_analysisspecs_uid], + "sort_order": "descending", } } info["filter_queries"] = filter_queries diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index 576a264a5b..f9713ea770 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -7,10 +7,25 @@ from AccessControl import ClassSecurityInfo from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin -from Products.Archetypes.public import * +from Products.Archetypes.public import BaseContent +from Products.Archetypes.public import BooleanField +from Products.Archetypes.public import BooleanWidget +from Products.Archetypes.public import ComputedField +from Products.Archetypes.public import ComputedWidget +from Products.Archetypes.public import DisplayList +from Products.Archetypes.public import FileWidget +from Products.Archetypes.public import ReferenceField +from Products.Archetypes.public import Schema +from Products.Archetypes.public import StringField +from Products.Archetypes.public import StringWidget +from Products.Archetypes.public import registerType from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import safe_unicode + +from plone.app.blob.field import FileField as BlobFileField + from bika.lims import bikaMessageFactory as _ +from bika.lims import deprecated from bika.lims.browser.fields import CoordinateField from bika.lims.browser.fields import DurationField from bika.lims.browser.widgets import CoordinateWidget @@ -18,83 +33,92 @@ from bika.lims.browser.widgets.referencewidget import ReferenceWidget as brw from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from plone.app.blob.field import FileField as BlobFileField + schema = BikaSchema.copy() + Schema(( - CoordinateField('Latitude', - schemata = 'Location', + CoordinateField( + 'Latitude', + schemata='Location', widget=CoordinateWidget( label=_("Latitude"), description=_("Enter the Sample Point's latitude in degrees 0-90, minutes 0-59, seconds 0-59 and N/S indicator"), ), ), - CoordinateField('Longitude', - schemata = 'Location', + + CoordinateField( + 'Longitude', + schemata='Location', widget=CoordinateWidget( label=_("Longitude"), description=_("Enter the Sample Point's longitude in degrees 0-180, minutes 0-59, seconds 0-59 and E/W indicator"), ), ), - StringField('Elevation', - schemata = 'Location', + + StringField( + 'Elevation', + schemata='Location', widget=StringWidget( label=_("Elevation"), description=_("The height or depth at which the sample has to be taken"), ), ), - DurationField('SamplingFrequency', + + DurationField( + 'SamplingFrequency', vocabulary_display_path_bound=sys.maxint, widget=DurationWidget( label=_("Sampling Frequency"), description=_("If a sample is taken periodically at this sample point, enter frequency here, e.g. weekly"), ), ), - ReferenceField('SampleTypes', - required = 0, - multiValued = 1, - allowed_types = ('SampleType',), - vocabulary = 'SampleTypesVocabulary', - relationship = 'SamplePointSampleType', - widget = brw( + + ReferenceField( + 'SampleTypes', + required=0, + multiValued=1, + allowed_types=('SampleType',), + vocabulary='SampleTypesVocabulary', + relationship='SamplePointSampleType', + widget=brw( label=_("Sample Types"), - description =_("The list of sample types that can be collected " - "at this sample point. If no sample types are " - "selected, then all sample types are available."), + description=_("The list of sample types that can be collected " + "at this sample point. If no sample types are " + "selected, then all sample types are available."), ), ), - ComputedField( - 'SampleTypeTitle', - expression="[o.Title() for o in context.getSampleTypes()]", - widget = ComputedWidget( - visible=False, - ) - ), - BooleanField('Composite', + + BooleanField( + 'Composite', default=False, widget=BooleanWidget( label=_("Composite"), - description =_( + description=_( "Check this box if the samples taken at this point are 'composite' " "and put together from more than one sub sample, e.g. several surface " "samples from a dam mixed together to be a representative sample for the dam. " "The default, unchecked, indicates 'grab' samples"), ), ), - BlobFileField('AttachmentFile', - widget = FileWidget( + + BlobFileField( + 'AttachmentFile', + widget=FileWidget( label=_("Attachment"), ), ), )) + schema['description'].widget.visible = True schema['description'].schemata = 'default' + class SamplePoint(BaseContent, HistoryAwareMixin): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True + def _renameAfterCreation(self, check_auto_id=False): from bika.lims.idserver import renameAfterCreation renameAfterCreation(self) @@ -102,6 +126,28 @@ def _renameAfterCreation(self, check_auto_id=False): def Title(self): return safe_unicode(self.getField('title').get(self)).encode('utf-8') + def getSampleTypeTitles(self): + """Returns a list of sample type titles + """ + sample_types = self.getSampleTypes() + sample_type_titles = map(lambda obj: obj.Title(), sample_types) + + # N.B. This is used only for search purpose, because the catalog does + # not add an entry to the Keywordindex for an empty list. + # + # => This "empty" category allows to search for values with a certain + # sample type set OR with no sample type set. + # (see bika.lims.browser.analysisrequest.add2.get_sampletype_info) + if not sample_type_titles: + return [""] + return sample_type_titles + + @deprecated("Please use getSampleTypeTitles instead") + def getSampleTypeTitle(self): + """Returns a comma separated list of sample type titles + """ + return ",".join(self.getSampleTypeTitles()) + def SampleTypesVocabulary(self): from bika.lims.content.sampletype import SampleTypes return SampleTypes(self, allow_blank=False) @@ -112,14 +158,14 @@ def setSampleTypes(self, value, **kw): It's done strangely, because it may be required to behave strangely. """ bsc = getToolByName(self, 'bika_setup_catalog') - ## convert value to objects + # convert value to objects if value and type(value) == str: - value = [bsc(UID=value)[0].getObject(),] + value = [bsc(UID=value)[0].getObject(), ] elif value and type(value) in (list, tuple) and type(value[0]) == str: value = [bsc(UID=uid)[0].getObject() for uid in value if uid] if not type(value) in (list, tuple): - value = [value,] - ## Find all SampleTypes that were removed + value = [value, ] + # Find all SampleTypes that were removed existing = self.Schema()['SampleTypes'].get(self) removed = existing and [s for s in existing if s not in value] or [] added = value and [s for s in value if s not in existing] or [] @@ -136,7 +182,7 @@ def setSampleTypes(self, value, **kw): st.setSamplePoints(samplepoints) for st in added: - st.setSamplePoints(list(st.getSamplePoints()) + [self,]) + st.setSamplePoints(list(st.getSamplePoints()) + [self, ]) return ret @@ -146,19 +192,22 @@ def getSampleTypes(self, **kw): def getClientUID(self): return self.aq_parent.UID() + registerType(SamplePoint, PROJECTNAME) + +@deprecated("bika.lims.content.samplepoint.SamplePoints function will be removed in senaite.core 1.2.0") def SamplePoints(self, instance=None, allow_blank=True, lab_only=True): instance = instance or self bsc = getToolByName(instance, 'bika_setup_catalog') items = [] contentFilter = { - 'portal_type' : 'SamplePoint', - 'inactive_state' :'active', - 'sort_on' : 'sortable_title'} + 'portal_type': 'SamplePoint', + 'inactive_state': 'active', + 'sort_on': 'sortable_title'} if lab_only: lab_path = instance.bika_setup.bika_samplepoints.getPhysicalPath() - contentFilter['path'] = {"query": "/".join(lab_path), "level" : 0 } + contentFilter['path'] = {"query": "/".join(lab_path), "level": 0} for sp in bsc(contentFilter): sp = sp.getObject() if sp.aq_parent.portal_type == 'Client': @@ -166,5 +215,5 @@ def SamplePoints(self, instance=None, allow_blank=True, lab_only=True): else: sp_title = sp.Title() items.append((sp.UID(), sp_title)) - items = allow_blank and [['','']] + list(items) or list(items) + items = allow_blank and [['', '']] + list(items) or list(items) return DisplayList(items) diff --git a/bika/lims/setuphandlers.py b/bika/lims/setuphandlers.py index 6a1d4031a4..6b8af87f87 100644 --- a/bika/lims/setuphandlers.py +++ b/bika/lims/setuphandlers.py @@ -479,7 +479,8 @@ def addColumn(cat, col): addIndex(bsc, 'getPrice', 'FieldIndex') addIndex(bsc, 'getSamplePointTitle', 'KeywordIndex') addIndex(bsc, 'getSamplePointUID', 'FieldIndex') - addIndex(bsc, 'getSampleTypeTitle', 'KeywordIndex') + addIndex(bsc, 'getSampleTypeTitle', 'FieldIndex') + addIndex(bsc, 'getSampleTypeTitles', 'KeywordIndex') addIndex(bsc, 'getSampleTypeUID', 'FieldIndex') addIndex(bsc, 'getServiceUID', 'FieldIndex') addIndex(bsc, 'getServiceUIDs', 'KeywordIndex') diff --git a/bika/lims/upgrade/configure.zcml b/bika/lims/upgrade/configure.zcml index 369480ea99..6a9ffa3e20 100644 --- a/bika/lims/upgrade/configure.zcml +++ b/bika/lims/upgrade/configure.zcml @@ -79,4 +79,5 @@ destination="1.1.8" handler="bika.lims.upgrade.v01_01_008.upgrade" profile="bika.lims:default"/> + diff --git a/bika/lims/upgrade/v01_01_008.py b/bika/lims/upgrade/v01_01_008.py index 641115cee5..6c92c2bbbe 100644 --- a/bika/lims/upgrade/v01_01_008.py +++ b/bika/lims/upgrade/v01_01_008.py @@ -1,5 +1,7 @@ +# -*- coding: utf-8 -*- + +from bika.lims import api from bika.lims import logger -from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING from bika.lims.config import PROJECTNAME as product from bika.lims.upgrade import upgradestep from bika.lims.upgrade.utils import UpgradeUtils @@ -7,6 +9,13 @@ version = '1.1.8' # Remember version number in metadata.xml and setup.py profile = 'profile-{0}:default'.format(product) +INDEXES = [ + # catalog, id, indexed attribute, type + ("bika_setup_catalog", "getSampleTypeTitle", "", "FieldIndex"), + ("bika_setup_catalog", "getSampleTypeTitles", "", "KeywordIndex"), +] + + @upgradestep(product, version) def upgrade(tool): portal = tool.aq_inner.aq_parent @@ -21,7 +30,45 @@ def upgrade(tool): logger.info("Upgrading {0}: {1} -> {2}".format(product, ver_from, version)) # -------- ADD YOUR STUFF HERE -------- + upgrade_indexes() logger.info("{0} upgraded to version {1}".format(product, version)) return True + + +def upgrade_indexes(): + logger.info("Fixing broken calculations (re-assignment of dependents)...") + + to_index = [] + for catalog, name, attribute, meta_type in INDEXES: + c = api.get_tool(catalog) + + # get the index from the catalog + index = c._catalog.indexes.get(name, None) + + # continue if the index exists and has the right meta type + if index and index.meta_type == meta_type: + logger.info("*** Index '{}' of type '{}' is already in catalog '{}'" + .format(name, meta_type, catalog)) + continue + + # remove the existing index with the wrong meta_type + if index is not None: + logger.info("*** Removing index '{}' from catalog '{}'" + .format(name, catalog)) + c._catalog.delIndex(name) + + # add the index with the right meta_type + logger.info("*** Adding index '{}' of type '{}' to catalog '{}'" + .format(name, meta_type, catalog)) + c.addIndex(name, meta_type) + to_index.append((catalog, name)) + + for catalog, name in to_index: + c = api.get_tool(catalog) + logger.info("*** Indexing new index '{}' of catalog {} ..." + .format(name, catalog)) + c.manage_reindexIndex(name) + logger.info("*** Indexing new index '{}' of catalog {} [DONE]" + .format(name, catalog)) \ No newline at end of file