From 97524e7b23db415aa45a45556aedaa5b9bc8295a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 19:43:19 +0100 Subject: [PATCH 01/30] Cleanup getClientUID fields (added IClientBindable and ClientBindable) --- bika/lims/browser/analysisrequest/add2.py | 1 + .../widgets/srtemplateartemplateswidget.py | 1 + bika/lims/content/abstractroutineanalysis.py | 32 ++++------ bika/lims/content/analysisprofile.py | 9 +-- bika/lims/content/analysisspec.py | 7 +-- bika/lims/content/artemplate.py | 10 +--- bika/lims/content/attachment.py | 12 +--- bika/lims/content/batch.py | 22 ++----- bika/lims/content/clientbindable.py | 58 +++++++++++++++++++ bika/lims/content/report.py | 25 +------- bika/lims/content/samplepoint.py | 6 +- bika/lims/interfaces/__init__.py | 17 +++++- bika/lims/interfaces/analysis.py | 12 ++-- 13 files changed, 114 insertions(+), 98 deletions(-) create mode 100644 bika/lims/content/clientbindable.py diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index c525e72c6c..ee99845d6e 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1117,6 +1117,7 @@ def get_specification_info(self, obj): "results_range": results_range, "sample_type_uid": obj.getSampleTypeUID(), "sample_type_title": obj.getSampleTypeTitle(), + # FIXME "client_uid": obj.getClientUID(), }) diff --git a/bika/lims/browser/widgets/srtemplateartemplateswidget.py b/bika/lims/browser/widgets/srtemplateartemplateswidget.py index a1d41df0af..fcfae43030 100644 --- a/bika/lims/browser/widgets/srtemplateartemplateswidget.py +++ b/bika/lims/browser/widgets/srtemplateartemplateswidget.py @@ -75,6 +75,7 @@ def isItemAllowed(self, obj): if self.clientUID is None: self.clientUID = self.context.aq_parent.aq_parent.UID() # Only display client's and lab's arts + # FIXME if obj.aq_parent.aq_inner.meta_type == 'Client': obj_client = obj.getClientUID() if obj_client != self.clientUID: diff --git a/bika/lims/content/abstractroutineanalysis.py b/bika/lims/content/abstractroutineanalysis.py index dc21d0c8e0..14985a487c 100644 --- a/bika/lims/content/abstractroutineanalysis.py +++ b/bika/lims/content/abstractroutineanalysis.py @@ -29,6 +29,7 @@ from bika.lims.content.abstractanalysis import AbstractAnalysis from bika.lims.content.abstractanalysis import schema from bika.lims.content.analysisspec import ResultsRangeDict +from bika.lims.content.clientbindable import ClientBindable from bika.lims.content.reflexrule import doReflexRuleAction from bika.lims.interfaces import IAnalysis from bika.lims.interfaces import ICancellable @@ -124,7 +125,7 @@ )) -class AbstractRoutineAnalysis(AbstractAnalysis): +class AbstractRoutineAnalysis(AbstractAnalysis, ClientBindable): implements(IAnalysis, IRequestAnalysis, IRoutineAnalysis, ICancellable) security = ClassSecurityInfo() displayContentsTab = False @@ -166,38 +167,27 @@ def getRequestURL(self): if request: return request.absolute_url_path() - @security.public - def getClientTitle(self): - """Used to populate catalog values. - Returns the Title of the client for this analysis' AR. + def getClient(self): + """Returns the Client this analysis is bound to, if any """ request = self.getRequest() - if request: - client = request.getClient() - if client: - return client.Title() + return request and request.getClient() or None @security.public - def getClientUID(self): + def getClientTitle(self): """Used to populate catalog values. - Returns the UID of the client for this analysis' AR. + Returns the Title of the client for this analysis' AR. """ - request = self.getRequest() - if request: - client = request.getClient() - if client: - return client.UID() + client = self.getClient() + return client and client.Title() or None @security.public def getClientURL(self): """This method is used to populate catalog values Returns the URL of the client for this analysis' AR. """ - request = self.getRequest() - if request: - client = request.getClient() - if client: - return client.absolute_url_path() + client = self.getClient() + return client and client.absolute_url_path() or None @security.public def getClientOrderNumber(self): diff --git a/bika/lims/content/analysisprofile.py b/bika/lims/content/analysisprofile.py index 0bed7e5583..ac0186dfba 100644 --- a/bika/lims/content/analysisprofile.py +++ b/bika/lims/content/analysisprofile.py @@ -33,6 +33,8 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from Products.Archetypes.public import * + +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IAnalysisProfile, IDeactivable from Products.Archetypes.references import HoldingReference from Products.ATExtensions.field import RecordsField @@ -41,6 +43,7 @@ from zope.interface import Interface, implements import sys from bika.lims.interfaces import IAnalysisProfile +from bika.lims.interfaces import IClient schema = BikaSchema.copy() + Schema(( StringField('ProfileKey', @@ -145,7 +148,8 @@ schema['description'].widget.visible = True IdField = schema['id'] -class AnalysisProfile(BaseContent): + +class AnalysisProfile(BaseContent, ClientBindable): security = ClassSecurityInfo() schema = schema displayContentsTab = False @@ -156,9 +160,6 @@ def _renameAfterCreation(self, check_auto_id=False): from bika.lims.idserver import renameAfterCreation renameAfterCreation(self) - def getClientUID(self): - return self.aq_parent.UID() - def getAnalysisServiceSettings(self, uid): """ Returns a dictionary with the settings for the analysis service that match with the uid provided. diff --git a/bika/lims/content/analysisspec.py b/bika/lims/content/analysisspec.py index 790b13129f..a7b5ac90b6 100644 --- a/bika/lims/content/analysisspec.py +++ b/bika/lims/content/analysisspec.py @@ -25,6 +25,7 @@ from bika.lims.browser.widgets import AnalysisSpecificationWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IAnalysisSpec, IDeactivable from Products.Archetypes import atapi from Products.Archetypes.public import BaseFolder @@ -40,6 +41,7 @@ from zope.i18n import translate from zope.interface import implements +from bika.lims.interfaces import IClient schema = Schema(( @@ -131,7 +133,7 @@ schema['title'].required = True -class AnalysisSpec(BaseFolder, HistoryAwareMixin): +class AnalysisSpec(BaseFolder, HistoryAwareMixin, ClientBindable): """Analysis Specification """ implements(IAnalysisSpec, IDeactivable) @@ -208,9 +210,6 @@ def getSampleTypes(self, active_only=True): lambda brain: (brain.UID, brain.Title), results) return DisplayList(sampletypes) - def getClientUID(self): - return self.aq_parent.UID() - atapi.registerType(AnalysisSpec, PROJECTNAME) diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 0109eaa45c..d623757935 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -30,6 +30,7 @@ from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IARTemplate, IDeactivable from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField @@ -299,7 +300,7 @@ schema["title"]._validationLayer() -class ARTemplate(BaseContent): +class ARTemplate(BaseContent, ClientBindable): security = ClassSecurityInfo() schema = schema displayContentsTab = False @@ -326,13 +327,6 @@ def AnalysisProfiles(self, instance=None): items = [["", ""]] + list(items) return DisplayList(items) - def getClientUID(self): - """This populates the getClientUID catalog - If the parent is the system bika_artemplates folder, - then that folder's UID must be returned in this index. - """ - return self.aq_parent.UID() - def getAnalysisServiceSettings(self, uid): """Returns a dictionary with the settings for the analysis service that match with the uid provided. diff --git a/bika/lims/content/attachment.py b/bika/lims/content/attachment.py index 6a9b390842..63da206f2e 100644 --- a/bika/lims/content/attachment.py +++ b/bika/lims/content/attachment.py @@ -27,6 +27,7 @@ from bika.lims.config import ATTACHMENT_REPORT_OPTIONS from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces.analysis import IRequestAnalysis from DateTime import DateTime from plone.app.blob.field import FileField @@ -96,7 +97,7 @@ schema["title"].required = False -class Attachment(BaseFolder): +class Attachment(BaseFolder, ClientBindable): """Attachments are stored per client and can be linked to ARs or Analyses """ security = ClassSecurityInfo() @@ -136,15 +137,6 @@ def getAttachmentTypeUID(self): return "" return api.get_uid(attachment_type) - @security.public - def getClientUID(self): - """Return the UID of the client - """ - client = api.get_parent(self) - if not client: - return "" - return api.get_uid(client) - @security.public def getLinkedRequests(self): """Lookup linked Analysis Requests diff --git a/bika/lims/content/batch.py b/bika/lims/content/batch.py index 52c46c9b05..6f12b1ed27 100644 --- a/bika/lims/content/batch.py +++ b/bika/lims/content/batch.py @@ -30,6 +30,7 @@ from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaFolderSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IBatch from bika.lims.interfaces import ICancellable from bika.lims.interfaces import IClient @@ -142,7 +143,7 @@ def BatchDate(instance): schema.moveField('Client', after='title') -class Batch(ATFolder): +class Batch(ATFolder, ClientBindable): """A Batch combines multiple ARs into a logical unit """ implements(IBatch, ICancellable) @@ -159,8 +160,9 @@ def _renameAfterCreation(self, check_auto_id=False): def getClient(self): """Retrieves the Client the current Batch is assigned to """ - # The schema's field Client is only used to allow the user to assign - # the batch to a client in edit form. The entered value is used in + # We override here getClient from ClientBindable because te schema's + # field Client is only used to allow the user to assign the batch to a + # client in edit form. The entered value is used in # ObjectModifiedEventHandler to move the batch to the Client's folder, # so the value stored in the Schema's is not used anymore # See https://github.com/senaite/senaite.core/pull/1450 @@ -169,20 +171,6 @@ def getClient(self): return client return None - def getClientTitle(self): - client = self.getClient() - if client: - return client.Title() - return "" - - def getClientUID(self): - """This index is required on batches so that batch listings can be - filtered by client - """ - client = self.getClient() - if client: - return client.UID() - def getContactTitle(self): return "" diff --git a/bika/lims/content/clientbindable.py b/bika/lims/content/clientbindable.py new file mode 100644 index 0000000000..1db1072ae2 --- /dev/null +++ b/bika/lims/content/clientbindable.py @@ -0,0 +1,58 @@ +from AccessControl import ClassSecurityInfo +from Products.Archetypes.BaseObject import BaseObject +from zope.interface import implements + +from bika.lims import api +from bika.lims.interfaces import IClient +from bika.lims.interfaces import IClientBindable + + +class ClientBindable(BaseObject): + implements(IClientBindable) + + security = ClassSecurityInfo() + + @security.public + def getClient(self): + """Returns the Client the object is bound to, if any + """ + # Look for the parent + if IClient.providedBy(self.aq_parent): + return self.aq_parent + + # Look in Schema + client_field = self.Schema().get("Client", default=None) + if client_field: + client = client_field.get(self) + client = api.get_object(client, None) + if client and IClient.providedBy(client): + return client + + # No client bound + return None + + @security.public + def getClientUID(self): + """Returns the Client UID the object is bound to, if any + """ + client = self.getClient() + return client and api.get_uid(client) or "" + + @security.public + def getClientID(self): + """Returns the Client ID the object is bound to, if any + """ + client = self.getClient() + return client and api.get_id(client) or "" + + @security.public + def getClientTitle(self): + """Returns the Client Title the object is bound to, if any + """ + client = self.getClient() + return client and client.Title() or "" + + @security.public + def getClientURL(self): + client = self.getClient() + return client and client.absolute_url_path() or "" diff --git a/bika/lims/content/report.py b/bika/lims/content/report.py index 51fe10e17f..ad6ed99c43 100644 --- a/bika/lims/content/report.py +++ b/bika/lims/content/report.py @@ -27,6 +27,7 @@ from bika.lims.content.bikaschema import BikaSchema from bika.lims.config import PROJECTNAME from bika.lims import bikaMessageFactory as _ +from bika.lims.content.clientbindable import ClientBindable from bika.lims.utils import t from bika.lims.browser import ulocalized_time from bika.lims.utils import user_fullname @@ -56,7 +57,7 @@ schema['id'].required = False schema['title'].required = False -class Report(BaseFolder): +class Report(BaseFolder, ClientBindable): security = ClassSecurityInfo() displayContentsTab = False schema = schema @@ -71,13 +72,6 @@ def current_date(self): """ return current date """ return DateTime() - @security.public - def getClientUID(self): - client = self.getClient() - if client: - return client.UID() - return '' - @security.public def getCreatorFullName(self): return user_fullname(self, self.Creator()) @@ -89,19 +83,4 @@ def getFileSize(self): return '%sKb' % (file.get_size() / 1024) return '' - @security.public - def getClientURL(self): - client = self.getClient() - if client: - return client.absolute_url_path() - return '' - - @security.public - def getClientTitle(self): - client = self.getClient() - if client: - return client.Title() - return '' - - atapi.registerType(Report, PROJECTNAME) diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index d0202aaac2..d62ca5c9c5 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -44,6 +44,7 @@ ReferenceWidget as BikaReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IDeactivable from plone.app.blob.field import FileField as BlobFileField from zope.interface import implements @@ -125,7 +126,7 @@ schema['description'].schemata = 'default' -class SamplePoint(BaseContent, HistoryAwareMixin): +class SamplePoint(BaseContent, HistoryAwareMixin, ClientBindable): implements(IDeactivable) security = ClassSecurityInfo() displayContentsTab = False @@ -202,9 +203,6 @@ def setSampleTypes(self, value, **kw): def getSampleTypes(self, **kw): return self.Schema()['SampleTypes'].get(self) - def getClientUID(self): - return self.aq_parent.UID() - registerType(SamplePoint, PROJECTNAME) diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 96d5b93674..26e752cf38 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -1012,4 +1012,19 @@ def get_object_info(self): applied when the value of current field changes. - field_values: contains default values for other fields to be applied when the value of the current field changes. - """ \ No newline at end of file + """ + + +class IClientBindable(Interface): + """Marker interface for objects that can be bound to a Client, either + because they can be added inside a Client folder or because it can be + assigned through a Reference field + """ + + def getClient(self): + """Returns the client this object is bound to, if any + """ + + def getClientUID(self): + """Returns the client UID this object is bound to, if any + """ diff --git a/bika/lims/interfaces/analysis.py b/bika/lims/interfaces/analysis.py index 88bc2ee8c6..f5b61a5926 100644 --- a/bika/lims/interfaces/analysis.py +++ b/bika/lims/interfaces/analysis.py @@ -59,16 +59,16 @@ def getClient(self): :rtype: IClient""" def getClientID(self): - """Returns the UID of the Client assigned to the Analysis Request this - analysis belongs to. Returns None if there is no Request nor a Client + """Returns the ID of the Client assigned to the Analysis Request this + analysis belongs to. Returns empty if there is no Request nor a Client assigned to this analysis - :return: the UID of the Client + :return: the ID of the Client :rtype: str """ def getClientUID(self): """Returns the UID of the Client assigned to the Analysis Request this - analysis belongs to. Returns None if there is no Request nor a Client + analysis belongs to. Returns empty if there is no Request nor a Client assigned to this analysis :return: the UID of the Client :rtype: str @@ -76,7 +76,7 @@ def getClientUID(self): def getClientTitle(self): """Returns the name of the Client assigned to the Analysis Request this - analysis belongs to. Returns None if there is no Request nor a Client + analysis belongs to. Returns empty if there is no Request nor a Client assigned to this analysis :return: the name of the Client :rtype: str @@ -84,7 +84,7 @@ def getClientTitle(self): def getClientURL(self): """Returns the absolute url path of the Client assigned to the Analysis - Request this analysis belongs to. Returns None if there is no Request + Request this analysis belongs to. Returns empty if there is no Request nor a Client assigned to this analysis :return: the url path of the Client :type: str From a33ee843077e6b1e3034a4029bf684f7b9239655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 19:45:20 +0100 Subject: [PATCH 02/30] Cleanup getClientUID search criteria for Add form --- bika/lims/browser/analysisrequest/add2.py | 32 +++++------------------ 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index ee99845d6e..f9d20f4554 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -45,8 +45,9 @@ from bika.lims import logger from bika.lims.api.analysisservice import get_calculation_dependencies_for from bika.lims.api.analysisservice import get_service_dependencies_for -from bika.lims.interfaces import IGetDefaultFieldValueARAddHook, \ - IAddSampleFieldsFlush, IAddSampleObjectInfo +from bika.lims.interfaces import IAddSampleFieldsFlush +from bika.lims.interfaces import IAddSampleObjectInfo +from bika.lims.interfaces import IGetDefaultFieldValueARAddHook from bika.lims.utils import tmpID from bika.lims.utils.analysisrequest import create_analysisrequest as crar from bika.lims.workflow import ActionHandlerPool @@ -826,25 +827,6 @@ def get_client_info(self, obj): # UID of the client uid = api.get_uid(obj) - # Bika Setup folder - bika_setup = api.get_bika_setup() - - # bika samplepoints - bika_samplepoints = bika_setup.bika_samplepoints - bika_samplepoints_uid = api.get_uid(bika_samplepoints) - - # bika artemplates - bika_artemplates = bika_setup.bika_artemplates - bika_artemplates_uid = api.get_uid(bika_artemplates) - - # bika analysisprofiles - bika_analysisprofiles = bika_setup.bika_analysisprofiles - bika_analysisprofiles_uid = api.get_uid(bika_analysisprofiles) - - # bika analysisspecs - bika_analysisspecs = bika_setup.bika_analysisspecs - bika_analysisspecs_uid = api.get_uid(bika_analysisspecs) - # catalog queries for UI field filtering filter_queries = { "Contact": { @@ -857,16 +839,16 @@ def get_client_info(self, obj): "getParentUID": [uid] }, "SamplePoint": { - "getClientUID": [uid, bika_samplepoints_uid], + "getClientUID": [uid, ""], }, "Template": { - "getClientUID": [uid, bika_artemplates_uid], + "getClientUID": [uid, ""], }, "Profiles": { - "getClientUID": [uid, bika_analysisprofiles_uid], + "getClientUID": [uid, ""], }, "Specification": { - "getClientUID": [uid, bika_analysisspecs_uid], + "getClientUID": [uid, ""], }, "SamplingRound": { "getParentUID": [uid], From 49a07b6efa8cb9e55a15a8a01175df90c7af42c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 19:51:35 +0100 Subject: [PATCH 03/30] No fixme anymore --- bika/lims/browser/analysisrequest/add2.py | 1 - bika/lims/browser/widgets/srtemplateartemplateswidget.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index f9d20f4554..aa756577c7 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1099,7 +1099,6 @@ def get_specification_info(self, obj): "results_range": results_range, "sample_type_uid": obj.getSampleTypeUID(), "sample_type_title": obj.getSampleTypeTitle(), - # FIXME "client_uid": obj.getClientUID(), }) diff --git a/bika/lims/browser/widgets/srtemplateartemplateswidget.py b/bika/lims/browser/widgets/srtemplateartemplateswidget.py index fcfae43030..a1d41df0af 100644 --- a/bika/lims/browser/widgets/srtemplateartemplateswidget.py +++ b/bika/lims/browser/widgets/srtemplateartemplateswidget.py @@ -75,7 +75,6 @@ def isItemAllowed(self, obj): if self.clientUID is None: self.clientUID = self.context.aq_parent.aq_parent.UID() # Only display client's and lab's arts - # FIXME if obj.aq_parent.aq_inner.meta_type == 'Client': obj_client = obj.getClientUID() if obj_client != self.clientUID: From 7fe9b8e948ddf3d31bbff47fb20beb9dbc6aa6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 20:08:31 +0100 Subject: [PATCH 04/30] Sanitize AnalysisRequest content type regarding Client fields --- bika/lims/content/analysisrequest.py | 49 ++++++++-------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 0113fb8977..b753364bb2 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -49,9 +49,12 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.analysisspec import ResultsRangeDict from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IAnalysisRequest from bika.lims.interfaces import IAnalysisRequestPartition +from bika.lims.interfaces import IBatch from bika.lims.interfaces import ICancellable +from bika.lims.interfaces import IClient from bika.lims.interfaces import ISubmitted from bika.lims.permissions import FieldEditBatch from bika.lims.permissions import FieldEditClient @@ -1097,14 +1100,6 @@ }, ), ), - # TODO-catalog: move all these computed fields to methods - ComputedField( - 'ClientUID', - expression='here.aq_parent.UID()', - widget=ComputedWidget( - visible=False, - ), - ), ComputedField( 'SampleTypeTitle', @@ -1192,27 +1187,6 @@ "if here.getBatch() else ''", widget=ComputedWidget(visible=False), ), - ComputedField( - 'ClientUID', - expression="here.getClient().UID() if here.getClient() else ''", - widget=ComputedWidget(visible=False), - ), - ComputedField( - 'ClientID', - expression="here.getClient().getClientID() if here.getClient() else ''", - widget=ComputedWidget(visible=False), - ), - ComputedField( - 'ClientTitle', - expression="here.getClient().Title() if here.getClient() else ''", - widget=ComputedWidget(visible=False), - ), - ComputedField( - 'ClientURL', - expression="here.getClient().absolute_url_path() " \ - "if here.getClient() else ''", - widget=ComputedWidget(visible=False), - ), ComputedField( 'ContactUsername', expression="here.getContact().getUsername() " \ @@ -1419,7 +1393,7 @@ schema.moveField("PrimaryAnalysisRequest", before="Client") -class AnalysisRequest(BaseFolder): +class AnalysisRequest(BaseFolder, ClientBindable): implements(IAnalysisRequest, ICancellable) security = ClassSecurityInfo() displayContentsTab = False @@ -1458,14 +1432,17 @@ def Description(self): return safe_unicode(descr).encode('utf-8') def getClient(self): - if self.aq_parent.portal_type == 'Client': + """Returns the client this object is bound to. We override getClient + from ClientBindable because the "Client" schema field is only used to + allow the user to set the client while creating the Sample through + Sample Add form, but cannot be changed afterwards. The Sample is + created directly inside the selected client folder on submit + """ + if IClient.providedBy(self.aq_parent): return self.aq_parent - if self.aq_parent.portal_type == 'Batch': + if IBatch.providedBy(self.aq_parent): return self.aq_parent.getClient() - return '' - - def getClientPath(self): - return "/".join(self.aq_parent.getPhysicalPath()) + return None def getProfilesTitle(self): return [profile.Title() for profile in self.getProfiles()] From 44532fcb9fef34755d6a37c9d428e83d0896cde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 20:08:46 +0100 Subject: [PATCH 05/30] BatchReferenceWidgetVocabulary -> ClientBindableReferenceWidgetVocabulary --- bika/lims/adapters/configure.zcml | 9 ++++----- bika/lims/adapters/referencewidgetvocabulary.py | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/bika/lims/adapters/configure.zcml b/bika/lims/adapters/configure.zcml index eadc8ad223..bdbd4fd577 100644 --- a/bika/lims/adapters/configure.zcml +++ b/bika/lims/adapters/configure.zcml @@ -19,14 +19,13 @@ provides="bika.lims.interfaces.IReferenceWidgetVocabulary" /> - + + factory="bika.lims.adapters.referencewidgetvocabulary.ClientBindableReferenceWidgetVocabulary" + provides="bika.lims.interfaces.IReferenceWidgetVocabulary" /> + + + + + diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index c2242fcd5a..912156cd0b 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -22,6 +22,8 @@ from bika.lims import api from bika.lims import logger +from bika.lims.interfaces import IClient +from bika.lims.interfaces import IClientBindable from bika.lims.interfaces import IReferenceWidgetVocabulary, IAnalysisRequest from bika.lims.utils import to_unicode as _u from bika.lims.utils import to_utf8 as _c @@ -259,24 +261,38 @@ def __call__(self): return brains -class ClientBindableReferenceWidgetVocabulary(DefaultReferenceWidgetVocabulary): +class ClientBoundReferenceWidgetVocabulary(DefaultReferenceWidgetVocabulary): + """Injects search criteria (filters) in the query when the current context + is, belongs or is associated to a Client + """ def get_raw_query(self): """Returns the raw query to use for current search, based on the base query + update query """ - query = super(ClientBindableReferenceWidgetVocabulary, self).get_raw_query() - if IAnalysisRequest.providedBy(self.context): - + query = super(ClientBoundReferenceWidgetVocabulary, self).get_raw_query() + client_uid = self.get_client_uid(self.context) + if client_uid: # Apply the search criteria for this client - client = self.context.getClient() if "Contact" in self.get_portal_types(query): - query["getParentUID"] = [api.get_uid(client)] + query["getParentUID"] = [client_uid] else: - query["getClientUID"] = [api.get_uid(client), ""] + query["getClientUID"] = [client_uid, ""] return query + def get_client_uid(self, obj): + """Returns the client uid the current object belongs to + """ + if IClient.providedBy(obj): + return api.get_uid(obj) + elif IClientBindable.providedBy(obj): + return obj.getClientUID() + elif api.is_portal(obj): + return None + return self.get_client_uid(obj.aq_parent) + + def get_portal_types(self, query): """Return the list of portal types from the query passed-in """ diff --git a/bika/lims/browser/client/__init__.py b/bika/lims/browser/client/__init__.py index 25b917dfd0..e44f0293ec 100644 --- a/bika/lims/browser/client/__init__.py +++ b/bika/lims/browser/client/__init__.py @@ -30,5 +30,3 @@ from views.contacts import ClientContactsView from views.contacts import ClientContactVocabularyFactory from views.samplingrounds import ClientSamplingRoundsView - -from ajax import ReferenceWidgetVocabulary diff --git a/bika/lims/browser/client/ajax.py b/bika/lims/browser/client/ajax.py deleted file mode 100644 index d0de763154..0000000000 --- a/bika/lims/browser/client/ajax.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of SENAITE.CORE. -# -# SENAITE.CORE is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, version 2. -# -# This program 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 General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 51 -# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Copyright 2018-2019 by it's authors. -# Some rights reserved, see README and LICENSE. - -import json - -from bika.lims.adapters.referencewidgetvocabulary import \ - DefaultReferenceWidgetVocabulary - - -class ReferenceWidgetVocabulary(DefaultReferenceWidgetVocabulary): - """Implements IReferenceWidgetVocabulary for Clients - """ - - def __call__(self): - base_query = json.loads(self.request['base_query']) - portal_type = base_query.get('portal_type', []) - if 'Contact' in portal_type: - base_query['getClientUID'] = [self.context.UID(), ] - # If ensure_ascii is false, a result may be a unicode instance. This - # usually happens if the input contains unicode strings or the encoding - # parameter is used. - # see: https://github.com/senaite/senaite.core/issues/605 - self.request['base_query'] = json.dumps(base_query, ensure_ascii=False) - return DefaultReferenceWidgetVocabulary.__call__(self) diff --git a/bika/lims/browser/client/configure.zcml b/bika/lims/browser/client/configure.zcml index 49898e54b3..3edad7d5d4 100644 --- a/bika/lims/browser/client/configure.zcml +++ b/bika/lims/browser/client/configure.zcml @@ -11,13 +11,6 @@ name="getContacts" /> - - Date: Fri, 22 Nov 2019 22:00:44 +0100 Subject: [PATCH 10/30] Make ARReport ClientBindable --- bika/lims/content/arreport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bika/lims/content/arreport.py b/bika/lims/content/arreport.py index eb82fea40c..4a1f36320a 100644 --- a/bika/lims/content/arreport.py +++ b/bika/lims/content/arreport.py @@ -26,6 +26,7 @@ from bika.lims.browser.widgets import ReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientbindable import ClientBindable from bika.lims.interfaces import IARReport from plone.app.blob.field import BlobField from Products.Archetypes import atapi @@ -136,7 +137,7 @@ schema["title"].required = False -class ARReport(BaseFolder): +class ARReport(BaseFolder, ClientBindable): """An AnalysisRequest report, containing the report itself in pdf and html format. It includes information about the date when was published, from whom, the report recipients (and their emails) and the publication mode From ae47c2023f226e996a196875a2a60607f6fce1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 22:32:41 +0100 Subject: [PATCH 11/30] Omit search filter for non-bindable portal_types --- bika/lims/adapters/configure.zcml | 19 +++++---- .../adapters/referencewidgetvocabulary.py | 40 ++++++++++++++----- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/bika/lims/adapters/configure.zcml b/bika/lims/adapters/configure.zcml index 69136e0f7b..0b97c5af30 100644 --- a/bika/lims/adapters/configure.zcml +++ b/bika/lims/adapters/configure.zcml @@ -19,20 +19,23 @@ provides="bika.lims.interfaces.IReferenceWidgetVocabulary" /> - - + + - - + factory=".referencewidgetvocabulary.ClientAwareReferenceWidgetVocabulary" + provides="bika.lims.interfaces.IReferenceWidgetVocabulary" /> diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index 912156cd0b..e418357398 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -261,26 +261,48 @@ def __call__(self): return brains -class ClientBoundReferenceWidgetVocabulary(DefaultReferenceWidgetVocabulary): +class ClientAwareReferenceWidgetVocabulary(DefaultReferenceWidgetVocabulary): """Injects search criteria (filters) in the query when the current context is, belongs or is associated to a Client """ + # portal_types that might be bound to a client + client_bound_types = [ + "Contact", + "Batch", + "AnalysisProfile", + "AnalysisSpec", + "ARTemplate", + "SamplePoint" + ] + def get_raw_query(self): """Returns the raw query to use for current search, based on the base query + update query """ - query = super(ClientBoundReferenceWidgetVocabulary, self).get_raw_query() - client_uid = self.get_client_uid(self.context) - if client_uid: - # Apply the search criteria for this client - if "Contact" in self.get_portal_types(query): - query["getParentUID"] = [client_uid] - else: - query["getClientUID"] = [client_uid, ""] + query = super(ClientAwareReferenceWidgetVocabulary, self).get_raw_query() + + if self.requires_client_filter(query): + client_uid = self.get_client_uid(self.context) + if client_uid: + # Apply the search criteria for this client + # Contact is the only object bound to a Client that is stored + # in portal_catalog. And in this catalog, getClientUID does not + # exist, rather getParentUID + if "Contact" in self.get_portal_types(query): + query["getParentUID"] = [client_uid] + else: + query["getClientUID"] = [client_uid, ""] return query + def requires_client_filter(self, query): + """Returns whether the query passed in requires a filter by client + """ + portal_types = self.get_portal_types(query) + bound = map(lambda pt: pt in self.client_bound_types, portal_types) + return any(bound) + def get_client_uid(self, obj): """Returns the client uid the current object belongs to """ From 779254b3dc0dd0000dd4b6c75e3eacba57dd2d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 22:34:05 +0100 Subject: [PATCH 12/30] Sample edit form - some selection widgets empty (CHANGES) --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index dca13e2ddb..22f21c619d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,7 @@ Changelog **Fixed** +- #1477 Sample edit form - some selection widgets empty - #1474 Adding Control Reference to Worksheet causes print fail - #1473 Hidden settings of analysis services lost on Sample creation - #1472 Secondary samples - removal of analysis profile not possible From 28674eee609cbe0464322cf3587d4bd04e90f735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 22:47:03 +0100 Subject: [PATCH 13/30] Typo --- bika/lims/upgrade/v01_03_003.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bika/lims/upgrade/v01_03_003.py b/bika/lims/upgrade/v01_03_003.py index 65304cbde7..06ea20cb28 100644 --- a/bika/lims/upgrade/v01_03_003.py +++ b/bika/lims/upgrade/v01_03_003.py @@ -50,12 +50,13 @@ def upgrade(tool): setup.runImportStepFromProfile(profile, "propertiestool") # Reindex client's related fields (getClientUID, getClientTitle, etc.) - # https://github.com/senaite/senaite.core/pull/1476 + # https://github.com/senaite/senaite.core/pull/1477 reindex_client_fields(portal) logger.info("{0} upgraded to version {1}".format(product, version)) return True + def reindex_client_fields(portal): logger.info("Reindexing client fields ...") fields_to_reindex = [ From 4283628da4c0473ffe01f50b40406e6fde4ce78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 23:06:54 +0100 Subject: [PATCH 14/30] Fix test --- bika/lims/content/clientbindable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bika/lims/content/clientbindable.py b/bika/lims/content/clientbindable.py index 5f896508a6..0e472194e7 100644 --- a/bika/lims/content/clientbindable.py +++ b/bika/lims/content/clientbindable.py @@ -55,7 +55,7 @@ def getClientID(self): """Returns the Client ID the object is bound to, if any """ client = self.getClient() - return client and api.get_id(client) or "" + return client and client.getClientID() or "" @security.public def getClientTitle(self): From f78ac4c38922d89a3d44e268fba4a9c4bbd59fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 23:28:21 +0100 Subject: [PATCH 15/30] Removal of unnecessary functions (provided by ClientBindable) --- bika/lims/content/abstractroutineanalysis.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bika/lims/content/abstractroutineanalysis.py b/bika/lims/content/abstractroutineanalysis.py index 14985a487c..0f304d45ba 100644 --- a/bika/lims/content/abstractroutineanalysis.py +++ b/bika/lims/content/abstractroutineanalysis.py @@ -173,22 +173,6 @@ def getClient(self): request = self.getRequest() return request and request.getClient() or None - @security.public - def getClientTitle(self): - """Used to populate catalog values. - Returns the Title of the client for this analysis' AR. - """ - client = self.getClient() - return client and client.Title() or None - - @security.public - def getClientURL(self): - """This method is used to populate catalog values - Returns the URL of the client for this analysis' AR. - """ - client = self.getClient() - return client and client.absolute_url_path() or None - @security.public def getClientOrderNumber(self): """Used to populate catalog values. From 1f8369d1adf9b210341051f08af5acbcc3ee7be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 22 Nov 2019 23:46:48 +0100 Subject: [PATCH 16/30] Cleanup of code that is no longer necessary --- bika/lims/browser/analysisrequest/add2.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index 21e5538091..7c88d1920e 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -996,17 +996,6 @@ def get_sampletype_info(self, obj): """ info = self.get_base_info(obj) - # Bika Setup folder - bika_setup = api.get_bika_setup() - - # bika samplepoints - bika_samplepoints = bika_setup.bika_samplepoints - bika_samplepoints_uid = api.get_uid(bika_samplepoints) - - # bika analysisspecs - bika_analysisspecs = bika_setup.bika_analysisspecs - bika_analysisspecs_uid = api.get_uid(bika_analysisspecs) - # client client = self.get_client() client_uid = client and api.get_uid(client) or "" From 0472ab010b0f57c1f712afc14efb1a5e71015a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 25 Nov 2019 11:30:27 +0100 Subject: [PATCH 17/30] ClientBindable -> ClientAwareMixin --- bika/lims/adapters/referencewidgetvocabulary.py | 4 ++-- bika/lims/content/abstractroutineanalysis.py | 4 ++-- bika/lims/content/analysisprofile.py | 4 ++-- bika/lims/content/analysisrequest.py | 6 +++--- bika/lims/content/analysisspec.py | 4 ++-- bika/lims/content/arreport.py | 4 ++-- bika/lims/content/artemplate.py | 4 ++-- bika/lims/content/attachment.py | 4 ++-- bika/lims/content/batch.py | 6 +++--- bika/lims/content/clientbindable.py | 6 +++--- bika/lims/content/report.py | 4 ++-- bika/lims/content/samplepoint.py | 4 ++-- bika/lims/interfaces/__init__.py | 2 +- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index e418357398..7f0d2d4381 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -23,7 +23,7 @@ from bika.lims import api from bika.lims import logger from bika.lims.interfaces import IClient -from bika.lims.interfaces import IClientBindable +from bika.lims.interfaces import IClientAwareMixin from bika.lims.interfaces import IReferenceWidgetVocabulary, IAnalysisRequest from bika.lims.utils import to_unicode as _u from bika.lims.utils import to_utf8 as _c @@ -308,7 +308,7 @@ def get_client_uid(self, obj): """ if IClient.providedBy(obj): return api.get_uid(obj) - elif IClientBindable.providedBy(obj): + elif IClientAwareMixin.providedBy(obj): return obj.getClientUID() elif api.is_portal(obj): return None diff --git a/bika/lims/content/abstractroutineanalysis.py b/bika/lims/content/abstractroutineanalysis.py index 0f304d45ba..2afe999fe0 100644 --- a/bika/lims/content/abstractroutineanalysis.py +++ b/bika/lims/content/abstractroutineanalysis.py @@ -29,7 +29,7 @@ from bika.lims.content.abstractanalysis import AbstractAnalysis from bika.lims.content.abstractanalysis import schema from bika.lims.content.analysisspec import ResultsRangeDict -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.content.reflexrule import doReflexRuleAction from bika.lims.interfaces import IAnalysis from bika.lims.interfaces import ICancellable @@ -125,7 +125,7 @@ )) -class AbstractRoutineAnalysis(AbstractAnalysis, ClientBindable): +class AbstractRoutineAnalysis(AbstractAnalysis, ClientAwareMixin): implements(IAnalysis, IRequestAnalysis, IRoutineAnalysis, ICancellable) security = ClassSecurityInfo() displayContentsTab = False diff --git a/bika/lims/content/analysisprofile.py b/bika/lims/content/analysisprofile.py index ac0186dfba..31e6871a9c 100644 --- a/bika/lims/content/analysisprofile.py +++ b/bika/lims/content/analysisprofile.py @@ -34,7 +34,7 @@ from bika.lims.content.bikaschema import BikaSchema from Products.Archetypes.public import * -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IAnalysisProfile, IDeactivable from Products.Archetypes.references import HoldingReference from Products.ATExtensions.field import RecordsField @@ -149,7 +149,7 @@ IdField = schema['id'] -class AnalysisProfile(BaseContent, ClientBindable): +class AnalysisProfile(BaseContent, ClientAwareMixin): security = ClassSecurityInfo() schema = schema displayContentsTab = False diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index b753364bb2..b176914067 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -49,7 +49,7 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.analysisspec import ResultsRangeDict from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IAnalysisRequest from bika.lims.interfaces import IAnalysisRequestPartition from bika.lims.interfaces import IBatch @@ -1393,7 +1393,7 @@ schema.moveField("PrimaryAnalysisRequest", before="Client") -class AnalysisRequest(BaseFolder, ClientBindable): +class AnalysisRequest(BaseFolder, ClientAwareMixin): implements(IAnalysisRequest, ICancellable) security = ClassSecurityInfo() displayContentsTab = False @@ -1433,7 +1433,7 @@ def Description(self): def getClient(self): """Returns the client this object is bound to. We override getClient - from ClientBindable because the "Client" schema field is only used to + from ClientAwareMixin because the "Client" schema field is only used to allow the user to set the client while creating the Sample through Sample Add form, but cannot be changed afterwards. The Sample is created directly inside the selected client folder on submit diff --git a/bika/lims/content/analysisspec.py b/bika/lims/content/analysisspec.py index a7b5ac90b6..5cbf68286b 100644 --- a/bika/lims/content/analysisspec.py +++ b/bika/lims/content/analysisspec.py @@ -25,7 +25,7 @@ from bika.lims.browser.widgets import AnalysisSpecificationWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IAnalysisSpec, IDeactivable from Products.Archetypes import atapi from Products.Archetypes.public import BaseFolder @@ -133,7 +133,7 @@ schema['title'].required = True -class AnalysisSpec(BaseFolder, HistoryAwareMixin, ClientBindable): +class AnalysisSpec(BaseFolder, HistoryAwareMixin, ClientAwareMixin): """Analysis Specification """ implements(IAnalysisSpec, IDeactivable) diff --git a/bika/lims/content/arreport.py b/bika/lims/content/arreport.py index 4a1f36320a..79d17db527 100644 --- a/bika/lims/content/arreport.py +++ b/bika/lims/content/arreport.py @@ -26,7 +26,7 @@ from bika.lims.browser.widgets import ReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IARReport from plone.app.blob.field import BlobField from Products.Archetypes import atapi @@ -137,7 +137,7 @@ schema["title"].required = False -class ARReport(BaseFolder, ClientBindable): +class ARReport(BaseFolder, ClientAwareMixin): """An AnalysisRequest report, containing the report itself in pdf and html format. It includes information about the date when was published, from whom, the report recipients (and their emails) and the publication mode diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index d623757935..4f31e280cd 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -30,7 +30,7 @@ from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IARTemplate, IDeactivable from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField @@ -300,7 +300,7 @@ schema["title"]._validationLayer() -class ARTemplate(BaseContent, ClientBindable): +class ARTemplate(BaseContent, ClientAwareMixin): security = ClassSecurityInfo() schema = schema displayContentsTab = False diff --git a/bika/lims/content/attachment.py b/bika/lims/content/attachment.py index 63da206f2e..a7a99ac5ce 100644 --- a/bika/lims/content/attachment.py +++ b/bika/lims/content/attachment.py @@ -27,7 +27,7 @@ from bika.lims.config import ATTACHMENT_REPORT_OPTIONS from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces.analysis import IRequestAnalysis from DateTime import DateTime from plone.app.blob.field import FileField @@ -97,7 +97,7 @@ schema["title"].required = False -class Attachment(BaseFolder, ClientBindable): +class Attachment(BaseFolder, ClientAwareMixin): """Attachments are stored per client and can be linked to ARs or Analyses """ security = ClassSecurityInfo() diff --git a/bika/lims/content/batch.py b/bika/lims/content/batch.py index 6f12b1ed27..5fde49c0a0 100644 --- a/bika/lims/content/batch.py +++ b/bika/lims/content/batch.py @@ -30,7 +30,7 @@ from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaFolderSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IBatch from bika.lims.interfaces import ICancellable from bika.lims.interfaces import IClient @@ -143,7 +143,7 @@ def BatchDate(instance): schema.moveField('Client', after='title') -class Batch(ATFolder, ClientBindable): +class Batch(ATFolder, ClientAwareMixin): """A Batch combines multiple ARs into a logical unit """ implements(IBatch, ICancellable) @@ -160,7 +160,7 @@ def _renameAfterCreation(self, check_auto_id=False): def getClient(self): """Retrieves the Client the current Batch is assigned to """ - # We override here getClient from ClientBindable because te schema's + # We override here getClient from ClientAwareMixin because te schema's # field Client is only used to allow the user to assign the batch to a # client in edit form. The entered value is used in # ObjectModifiedEventHandler to move the batch to the Client's folder, diff --git a/bika/lims/content/clientbindable.py b/bika/lims/content/clientbindable.py index 0e472194e7..3588894a9d 100644 --- a/bika/lims/content/clientbindable.py +++ b/bika/lims/content/clientbindable.py @@ -4,11 +4,11 @@ from bika.lims import api from bika.lims.interfaces import IClient -from bika.lims.interfaces import IClientBindable +from bika.lims.interfaces import IClientAwareMixin -class ClientBindable(BaseObject): - implements(IClientBindable) +class ClientAwareMixin(BaseObject): + implements(IClientAwareMixin) security = ClassSecurityInfo() diff --git a/bika/lims/content/report.py b/bika/lims/content/report.py index ad6ed99c43..fb865566de 100644 --- a/bika/lims/content/report.py +++ b/bika/lims/content/report.py @@ -27,7 +27,7 @@ from bika.lims.content.bikaschema import BikaSchema from bika.lims.config import PROJECTNAME from bika.lims import bikaMessageFactory as _ -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.utils import t from bika.lims.browser import ulocalized_time from bika.lims.utils import user_fullname @@ -57,7 +57,7 @@ schema['id'].required = False schema['title'].required = False -class Report(BaseFolder, ClientBindable): +class Report(BaseFolder, ClientAwareMixin): security = ClassSecurityInfo() displayContentsTab = False schema = schema diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index d62ca5c9c5..9e37051266 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -44,7 +44,7 @@ ReferenceWidget as BikaReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientBindable +from bika.lims.content.clientbindable import ClientAwareMixin from bika.lims.interfaces import IDeactivable from plone.app.blob.field import FileField as BlobFileField from zope.interface import implements @@ -126,7 +126,7 @@ schema['description'].schemata = 'default' -class SamplePoint(BaseContent, HistoryAwareMixin, ClientBindable): +class SamplePoint(BaseContent, HistoryAwareMixin, ClientAwareMixin): implements(IDeactivable) security = ClassSecurityInfo() displayContentsTab = False diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 26e752cf38..c668cd52a2 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -1015,7 +1015,7 @@ def get_object_info(self): """ -class IClientBindable(Interface): +class IClientAwareMixin(Interface): """Marker interface for objects that can be bound to a Client, either because they can be added inside a Client folder or because it can be assigned through a Reference field From 71e40397fa1d870d9cc6c815f6f18860fcb2a0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 25 Nov 2019 13:36:58 +0100 Subject: [PATCH 18/30] Better way of returning a Client from acquisition chain --- .../adapters/referencewidgetvocabulary.py | 29 ++++------- bika/lims/content/clientbindable.py | 20 ++------ bika/lims/utils/__init__.py | 50 +++++++++++++++++++ 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index 7f0d2d4381..3e681de45a 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -20,14 +20,14 @@ import json +from zope.interface import implements + from bika.lims import api from bika.lims import logger -from bika.lims.interfaces import IClient -from bika.lims.interfaces import IClientAwareMixin -from bika.lims.interfaces import IReferenceWidgetVocabulary, IAnalysisRequest +from bika.lims.interfaces import IReferenceWidgetVocabulary +from bika.lims.utils import get_client from bika.lims.utils import to_unicode as _u from bika.lims.utils import to_utf8 as _c -from zope.interface import implements class DefaultReferenceWidgetVocabulary(object): @@ -282,8 +282,11 @@ def get_raw_query(self): """ query = super(ClientAwareReferenceWidgetVocabulary, self).get_raw_query() - if self.requires_client_filter(query): - client_uid = self.get_client_uid(self.context) + if self.is_client_aware(query): + + client = get_client(self.context) + client_uid = client and api.get_uid(client) or None + if client_uid: # Apply the search criteria for this client # Contact is the only object bound to a Client that is stored @@ -296,25 +299,13 @@ def get_raw_query(self): return query - def requires_client_filter(self, query): + def is_client_aware(self, query): """Returns whether the query passed in requires a filter by client """ portal_types = self.get_portal_types(query) bound = map(lambda pt: pt in self.client_bound_types, portal_types) return any(bound) - def get_client_uid(self, obj): - """Returns the client uid the current object belongs to - """ - if IClient.providedBy(obj): - return api.get_uid(obj) - elif IClientAwareMixin.providedBy(obj): - return obj.getClientUID() - elif api.is_portal(obj): - return None - return self.get_client_uid(obj.aq_parent) - - def get_portal_types(self, query): """Return the list of portal types from the query passed-in """ diff --git a/bika/lims/content/clientbindable.py b/bika/lims/content/clientbindable.py index 3588894a9d..42397a89cf 100644 --- a/bika/lims/content/clientbindable.py +++ b/bika/lims/content/clientbindable.py @@ -5,6 +5,7 @@ from bika.lims import api from bika.lims.interfaces import IClient from bika.lims.interfaces import IClientAwareMixin +from bika.lims.utils import chain class ClientAwareMixin(BaseObject): @@ -16,10 +17,10 @@ class ClientAwareMixin(BaseObject): def getClient(self): """Returns the Client the object is bound to, if any """ - # Look for the parent - client = self._infere_client() - if client: - return client + # Look in the acquisition chain + for obj in chain(self): + if IClient.providedBy(obj): + return obj # Look in Schema client_field = self.Schema().get("Client", default=None) @@ -32,17 +33,6 @@ def getClient(self): # No client bound return None - def _infere_client(self, obj=None): - """Inferes the client the object belongs to by walking through parents - """ - if not obj: - obj = self - if IClient.providedBy(obj): - return obj - elif api.is_portal(obj): - return None - return self._infere_client(obj.aq_parent) - @security.public def getClientUID(self): """Returns the Client UID the object is bound to, if any diff --git a/bika/lims/utils/__init__.py b/bika/lims/utils/__init__.py index 457dc53e03..475d8e3730 100644 --- a/bika/lims/utils/__init__.py +++ b/bika/lims/utils/__init__.py @@ -26,6 +26,8 @@ from AccessControl import ModuleSecurityInfo from AccessControl import allow_module from AccessControl import getSecurityManager +from Acquisition import aq_inner +from Acquisition import aq_parent from email import Encoders from time import time @@ -51,6 +53,9 @@ from zope.i18n import translate from zope.i18n.locales import locales +from bika.lims.interfaces import IClient +from bika.lims.interfaces import IClientAwareMixin + ModuleSecurityInfo('email.Utils').declarePublic('formataddr') allow_module('csv') @@ -899,3 +904,48 @@ def to_choices(display_list): "ResultValue": item[0], "ResultText": item[1]}, display_list.items()) + + +def chain(obj): + """Generator to walk the acquistion chain of object, considering that it + could be a function. + """ + + # Walk up the acquisition chain of the object, to be able to check + # each one for IWorkspace. + + # If the thing we are accessing is actually a bound method on an + # instance, then after we've checked the method itself, get the + # instance it's bound to using im_self, so that we can continue to + # walk up the acquistion chain from it (incidentally, this is why we + # can't juse use aq_chain()). + + context = aq_inner(obj) + + while context is not None: + yield context + + func_object = getattr(context, 'im_self', None) + if func_object is not None: + context = aq_inner(func_object) + else: + # Don't use aq_inner() since portal_factory (and probably other) + # things, depends on being able to wrap itself in a fake context. + context = aq_parent(context) + + +def get_client(obj): + """Returns the client the object passed-in belongs to, if any + + This walks the acquisition chain up until we find something which + provides either IClient or IClientAwareMixin + """ + for obj in chain(obj): + if IClient.providedBy(obj): + return obj + elif IClientAwareMixin.providedBy(obj): + # ClientAwareMixin can return a Client, even if there is no client + # in the acquisition chain + return obj.getClient() + + return None From a423dd4fa28d8c75d04d630a7fa0cde4a9b0402d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 25 Nov 2019 13:52:27 +0100 Subject: [PATCH 19/30] Replace clientbindable namespace --- bika/lims/content/abstractroutineanalysis.py | 2 +- bika/lims/content/analysisprofile.py | 2 +- bika/lims/content/analysisrequest.py | 2 +- bika/lims/content/analysisspec.py | 2 +- bika/lims/content/arreport.py | 2 +- bika/lims/content/artemplate.py | 2 +- bika/lims/content/attachment.py | 2 +- bika/lims/content/batch.py | 2 +- bika/lims/content/{clientbindable.py => clientawaremixin.py} | 0 bika/lims/content/report.py | 2 +- bika/lims/content/samplepoint.py | 2 +- 11 files changed, 10 insertions(+), 10 deletions(-) rename bika/lims/content/{clientbindable.py => clientawaremixin.py} (100%) diff --git a/bika/lims/content/abstractroutineanalysis.py b/bika/lims/content/abstractroutineanalysis.py index 2afe999fe0..8e160b40d7 100644 --- a/bika/lims/content/abstractroutineanalysis.py +++ b/bika/lims/content/abstractroutineanalysis.py @@ -29,7 +29,7 @@ from bika.lims.content.abstractanalysis import AbstractAnalysis from bika.lims.content.abstractanalysis import schema from bika.lims.content.analysisspec import ResultsRangeDict -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.content.reflexrule import doReflexRuleAction from bika.lims.interfaces import IAnalysis from bika.lims.interfaces import ICancellable diff --git a/bika/lims/content/analysisprofile.py b/bika/lims/content/analysisprofile.py index 31e6871a9c..42c4f2a8a6 100644 --- a/bika/lims/content/analysisprofile.py +++ b/bika/lims/content/analysisprofile.py @@ -34,7 +34,7 @@ from bika.lims.content.bikaschema import BikaSchema from Products.Archetypes.public import * -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IAnalysisProfile, IDeactivable from Products.Archetypes.references import HoldingReference from Products.ATExtensions.field import RecordsField diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index b176914067..c9e20c53e5 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -49,7 +49,7 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.analysisspec import ResultsRangeDict from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IAnalysisRequest from bika.lims.interfaces import IAnalysisRequestPartition from bika.lims.interfaces import IBatch diff --git a/bika/lims/content/analysisspec.py b/bika/lims/content/analysisspec.py index 5cbf68286b..114d74ba7d 100644 --- a/bika/lims/content/analysisspec.py +++ b/bika/lims/content/analysisspec.py @@ -25,7 +25,7 @@ from bika.lims.browser.widgets import AnalysisSpecificationWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IAnalysisSpec, IDeactivable from Products.Archetypes import atapi from Products.Archetypes.public import BaseFolder diff --git a/bika/lims/content/arreport.py b/bika/lims/content/arreport.py index 79d17db527..b6ca51e35f 100644 --- a/bika/lims/content/arreport.py +++ b/bika/lims/content/arreport.py @@ -26,7 +26,7 @@ from bika.lims.browser.widgets import ReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IARReport from plone.app.blob.field import BlobField from Products.Archetypes import atapi diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 4f31e280cd..4e61f9b88a 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -30,7 +30,7 @@ from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IARTemplate, IDeactivable from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField diff --git a/bika/lims/content/attachment.py b/bika/lims/content/attachment.py index a7a99ac5ce..2b25511687 100644 --- a/bika/lims/content/attachment.py +++ b/bika/lims/content/attachment.py @@ -27,7 +27,7 @@ from bika.lims.config import ATTACHMENT_REPORT_OPTIONS from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces.analysis import IRequestAnalysis from DateTime import DateTime from plone.app.blob.field import FileField diff --git a/bika/lims/content/batch.py b/bika/lims/content/batch.py index 5fde49c0a0..1d9969af83 100644 --- a/bika/lims/content/batch.py +++ b/bika/lims/content/batch.py @@ -30,7 +30,7 @@ from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaFolderSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IBatch from bika.lims.interfaces import ICancellable from bika.lims.interfaces import IClient diff --git a/bika/lims/content/clientbindable.py b/bika/lims/content/clientawaremixin.py similarity index 100% rename from bika/lims/content/clientbindable.py rename to bika/lims/content/clientawaremixin.py diff --git a/bika/lims/content/report.py b/bika/lims/content/report.py index fb865566de..1f2ee267be 100644 --- a/bika/lims/content/report.py +++ b/bika/lims/content/report.py @@ -27,7 +27,7 @@ from bika.lims.content.bikaschema import BikaSchema from bika.lims.config import PROJECTNAME from bika.lims import bikaMessageFactory as _ -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.utils import t from bika.lims.browser import ulocalized_time from bika.lims.utils import user_fullname diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index 9e37051266..c15c44536f 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -44,7 +44,7 @@ ReferenceWidget as BikaReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientbindable import ClientAwareMixin +from bika.lims.content.clientawaremixin import ClientAwareMixin from bika.lims.interfaces import IDeactivable from plone.app.blob.field import FileField as BlobFileField from zope.interface import implements From a03c694a5363693f5e8af37ba7fd2f9dfa16e334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 25 Nov 2019 15:00:52 +0100 Subject: [PATCH 20/30] Use six for isinstance --- bika/lims/adapters/referencewidgetvocabulary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bika/lims/adapters/referencewidgetvocabulary.py b/bika/lims/adapters/referencewidgetvocabulary.py index 3e681de45a..f2420e1efb 100644 --- a/bika/lims/adapters/referencewidgetvocabulary.py +++ b/bika/lims/adapters/referencewidgetvocabulary.py @@ -19,6 +19,7 @@ # Some rights reserved, see README and LICENSE. import json +import six from zope.interface import implements @@ -310,6 +311,6 @@ def get_portal_types(self, query): """Return the list of portal types from the query passed-in """ portal_types = query.get("portal_type", []) - if isinstance(portal_types, basestring): + if isinstance(portal_types, six.string_types): portal_types = [portal_types] return portal_types From dbd7edafc9349a5064bbecddf3558cfe532d7607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Tue, 26 Nov 2019 17:50:19 +0100 Subject: [PATCH 21/30] Filter Templates field when Sample Type is selected in Sample Add form When user selects a SampleType in Add Sample Form, Templates that have a Sample Type assigned, but not the same as the one selected, are not displayed for selection. This commit also includes some clean-up of indexes and metadata that are no longer used. --- CHANGES.rst | 1 + bika/lims/browser/analysisrequest/add2.py | 19 ++++-- bika/lims/content/samplepoint.py | 20 ++---- bika/lims/setuphandlers.py | 8 +-- bika/lims/upgrade/v01_03_003.py | 74 +++++++++++++++++++++++ 5 files changed, 95 insertions(+), 27 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bdcb34d790..6309d5ffb3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Changelog **Added** +- #1481 Filter Templates field when Sample Type is selected in Sample Add form - #1466 Support for "readonly" and "hidden" visibility modes in ReferenceWidget diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index 7c88d1920e..ebbb6cf7be 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1008,16 +1008,24 @@ def get_sampletype_info(self, obj): }) # catalog queries for UI field filtering + sample_type_uid = api.get_uid(obj) filter_queries = { + # Display Sample Points that have this sample type assigned plus + # those that do not have a sample type assigned "SamplePoint": { - "getSampleTypeTitles": [obj.Title(), ''], + "getSampleTypeUID": [sample_type_uid, ''], "getClientUID": [client_uid, ""], - "sort_order": "descending", }, + # Display Specifications that have this sample type assigned only "Specification": { - "getSampleTypeTitle": obj.Title(), + "getSampleTypeUID": sample_type_uid, + "getClientUID": [client_uid, ""], + }, + # Display AR Templates that have this sample type assigned plus + # those that do not have a sample type assigned + "Template": { + "getSampleTypeUID": [api.get_uid(obj), ""], "getClientUID": [client_uid, ""], - "sort_order": "descending", } } info["filter_queries"] = filter_queries @@ -1147,7 +1155,8 @@ def ajax_get_flush_settings(self): ], "SampleType": [ "SamplePoint", - "Specification" + "Specification", + "Template", ], "PrimarySample": [ "Batch" diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index c15c44536f..9e88666e76 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -141,26 +141,14 @@ 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 + def getSampleTypeTitle(self): + """Returns a comma separated 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 - - def getSampleTypeTitle(self): - """Returns a comma separated list of sample type titles - """ - return ",".join(self.getSampleTypeTitles()) + return "" + return ",".join(sample_type_titles) def SampleTypesVocabulary(self): from bika.lims.content.sampletype import SampleTypes diff --git a/bika/lims/setuphandlers.py b/bika/lims/setuphandlers.py index 7390a44a24..6b06472309 100644 --- a/bika/lims/setuphandlers.py +++ b/bika/lims/setuphandlers.py @@ -149,8 +149,6 @@ ("bika_catalog", "getExpiryDate", "", "DateIndex"), ("bika_catalog", "getId", "", "FieldIndex"), ("bika_catalog", "getReferenceDefinitionUID", "", "FieldIndex"), - ("bika_catalog", "getSampleTypeTitle", "", "FieldIndex"), - ("bika_catalog", "getSampleTypeUID", "", "FieldIndex"), ("bika_catalog", "getSupportedServices", "", "KeywordIndex"), ("bika_catalog", "id", "getId", "FieldIndex"), ("bika_catalog", "isValid", "", "BooleanIndex"), @@ -202,8 +200,9 @@ ("bika_setup_catalog", "getPrice", "", "FieldIndex"), ("bika_setup_catalog", "getSamplePointTitle", "", "KeywordIndex"), ("bika_setup_catalog", "getSamplePointUID", "", "FieldIndex"), + # Sorting of listings: Sample Points, Specifications ("bika_setup_catalog", "getSampleTypeTitle", "", "FieldIndex"), - ("bika_setup_catalog", "getSampleTypeTitles", "", "KeywordIndex"), + # Filter in Add2: Sample Points, Specifications, Templates ("bika_setup_catalog", "getSampleTypeUID", "", "FieldIndex"), ("bika_setup_catalog", "getServiceUID", "", "FieldIndex"), ("bika_setup_catalog", "getServiceUIDs", "", "KeywordIndex"), @@ -238,7 +237,6 @@ ("bika_catalog", "getClientTitle"), ("bika_catalog", "getClientID"), ("bika_catalog", "getClientBatchID"), - ("bika_catalog", "getSampleTypeTitle"), ("bika_catalog", "getDateReceived"), ("bika_catalog", "getDateSampled"), ("bika_catalog", "review_state"), @@ -283,8 +281,6 @@ ("bika_setup_catalog", "getPrice"), ("bika_setup_catalog", "getSamplePointTitle"), ("bika_setup_catalog", "getSamplePointUID"), - ("bika_setup_catalog", "getSampleTypeTitle"), - ("bika_setup_catalog", "getSampleTypeUID"), ("bika_setup_catalog", "getServiceUID"), ("bika_setup_catalog", "getTotalPrice"), ("bika_setup_catalog", "getUnit"), diff --git a/bika/lims/upgrade/v01_03_003.py b/bika/lims/upgrade/v01_03_003.py index 06ea20cb28..90fa46c731 100644 --- a/bika/lims/upgrade/v01_03_003.py +++ b/bika/lims/upgrade/v01_03_003.py @@ -29,6 +29,39 @@ version = "1.3.3" # Remember version number in metadata.xml and setup.py profile = "profile-{0}:default".format(product) +INDEXES_TO_REMOVE = [ + # Only used in add2 to filter Sample Points by Sample Type when a Sample + # Type was selected. Now, getSampleTypeUID is used instead because of + # https://github.com/senaite/senaite.core/pull/1481 + ("bika_setup_catalog", "getSampleTypeTitles"), + + # Only used for when Sample and SamplePartition objects + # existed. The following are the portal types stored in bika_catalog: + # Batch, BatchFolder and ReferenceSample + # and there are no searches by getSampleTypeTitle for none of them + ("bika_catalog", "getSampleTypeTitle"), + + # Not used neither for searches nor filtering of any of the content types + # stored in bika_catalog (Batch, BatchFolder and ReferenceSample) + ("bika_catalog", "getSampleTypeUID") +] + +METADATA_TO_REMOVE = [ + # Not used anywhere. In SamplePoints and Specifications listings, the + # SampleType object is waken-up instead of calling the metadata + ("bika_setup_catalog", "getSampleTypeTitle"), + + # getSampleTypeUID (as metadata field) is only used for analyses and + # samples (AnalysisRequest), and none of the two are stored in setup_catalog + ("bika_setup_catalog", "getSampleTypeUID"), + + # Only used for when Sample and SamplePartition objects existed. + # The following are the portal types stored in bika_catalog: + # Batch, BatchFolder and ReferenceSample + # and "getSampleTypeTitle" metadata is not used for none of them + ("bika_catalog", "getSampleTypeTitle") +] + @upgradestep(product, version) def upgrade(tool): @@ -49,6 +82,11 @@ def upgrade(tool): # https://github.com/senaite/senaite.core/pull/1469 setup.runImportStepFromProfile(profile, "propertiestool") + # Remove stale indexes and metadata + # https://github.com/senaite/senaite.core/pull/1481 + remove_stale_indexes(portal) + remove_stale_metadata(portal) + # Reindex client's related fields (getClientUID, getClientTitle, etc.) # https://github.com/senaite/senaite.core/pull/1477 reindex_client_fields(portal) @@ -87,3 +125,39 @@ def reindex_client_fields(portal): obj.reindexObject(idxs=fields_to_reindex) logger.info("Reindexing client fields ... [DONE]") + + +def remove_stale_indexes(portal): + logger.info("Removing stale indexes ...") + for catalog, index in INDEXES_TO_REMOVE: + del_index(catalog, index) + logger.info("Removing stale indexes ... [DONE]") + + +def remove_stale_metadata(portal): + logger.info("Removing stale metadata ...") + for catalog, column in METADATA_TO_REMOVE: + del_metadata(catalog, column) + logger.info("Removing stale metadata ... [DONE]") + + +def del_index(catalog_id, index_name): + logger.info("Removing '{}' index from '{}' ..." + .format(index_name, catalog_id)) + catalog = api.get_tool(catalog_id) + if index_name not in catalog.indexes(): + logger.info("Index '{}' not in catalog '{}' [SKIP]" + .format(index_name, catalog_id)) + return + catalog.delIndex(index_name) + + +def del_metadata(catalog_id, column): + logger.info("Removing '{}' metadata from '{}' ..." + .format(column, catalog_id)) + catalog = api.get_tool(catalog_id) + if column not in catalog.schema(): + logger.info("Metadata '{}' not in catalog '{}' [SKIP]" + .format(column, catalog_id)) + return + catalog.delColumn(column) From ba9473067e9718fe2c4befbcb38abbcbd193d506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 27 Nov 2019 22:47:00 +0100 Subject: [PATCH 22/30] getSampleTypeUID index refactoring --- bika/lims/browser/analysisrequest/add2.py | 4 +- bika/lims/catalog/indexers/bikasetup.py | 17 +++++ bika/lims/catalog/indexers/configure.zcml | 3 + bika/lims/content/analysisspec.py | 35 ++-------- bika/lims/content/artemplate.py | 10 +-- bika/lims/content/samplepoint.py | 54 ++++------------ bika/lims/content/sampletype.py | 79 +++++++++++++++++------ bika/lims/interfaces/__init__.py | 14 ++++ bika/lims/setuphandlers.py | 2 +- bika/lims/upgrade/v01_03_003.py | 35 +++++++++- 10 files changed, 148 insertions(+), 105 deletions(-) create mode 100644 bika/lims/catalog/indexers/bikasetup.py diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index a9745ea8e3..0bef224842 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1018,7 +1018,7 @@ def get_sampletype_info(self, obj): # Display Sample Points that have this sample type assigned plus # those that do not have a sample type assigned "SamplePoint": { - "getSampleTypeUID": [sample_type_uid, ''], + "getSampleTypeUID": [sample_type_uid, None], "getClientUID": [client_uid, ""], }, # Display Specifications that have this sample type assigned only @@ -1029,7 +1029,7 @@ def get_sampletype_info(self, obj): # Display AR Templates that have this sample type assigned plus # those that do not have a sample type assigned "Template": { - "getSampleTypeUID": [sample_type_uid, ""], + "getSampleTypeUID": [sample_type_uid, None], "getClientUID": [client_uid, ""], } } diff --git a/bika/lims/catalog/indexers/bikasetup.py b/bika/lims/catalog/indexers/bikasetup.py new file mode 100644 index 0000000000..dfb2958256 --- /dev/null +++ b/bika/lims/catalog/indexers/bikasetup.py @@ -0,0 +1,17 @@ +from Products.Archetypes.interfaces import IBaseObject +from plone.indexer import indexer + +from bika.lims.interfaces import IBikaSetupCatalog +from bika.lims.interfaces import ISampleTypeAwareMixin + + +@indexer(ISampleTypeAwareMixin, IBikaSetupCatalog) +def getSampleTypeUID(instance): + """Returns the list of SampleType UIDs the instance is assigned to + + This is a KeywordIndex, so it will be indexed as a list, even if only one + SampleType can be assigned to the instance. Moreover, if the instance has no + SampleType assigned, it returns a tuple with a None value. This allows + searches for `MissingValue` entries too. + """ + return instance.getSampleTypeUID() or (None, ) diff --git a/bika/lims/catalog/indexers/configure.zcml b/bika/lims/catalog/indexers/configure.zcml index a32374eabe..d95dcbc4cf 100644 --- a/bika/lims/catalog/indexers/configure.zcml +++ b/bika/lims/catalog/indexers/configure.zcml @@ -24,6 +24,9 @@ + + + diff --git a/bika/lims/content/analysisspec.py b/bika/lims/content/analysisspec.py index 114d74ba7d..d8824e372b 100644 --- a/bika/lims/content/analysisspec.py +++ b/bika/lims/content/analysisspec.py @@ -26,22 +26,19 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.content.clientawaremixin import ClientAwareMixin +from bika.lims.content.sampletype import SampleTypeAwareMixin from bika.lims.interfaces import IAnalysisSpec, IDeactivable from Products.Archetypes import atapi from Products.Archetypes.public import BaseFolder -from Products.Archetypes.public import ComputedField -from Products.Archetypes.public import ComputedWidget from Products.Archetypes.public import ReferenceWidget from Products.Archetypes.public import Schema from Products.Archetypes.utils import DisplayList from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin from Products.ATExtensions.field.records import RecordsField -from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import safe_unicode from zope.i18n import translate from zope.interface import implements -from bika.lims.interfaces import IClient schema = Schema(( @@ -55,21 +52,6 @@ ), ), - ComputedField( - 'SampleTypeTitle', - expression="context.getSampleType().Title() if context.getSampleType() else ''", - widget=ComputedWidget( - visible=False, - ), - ), - - ComputedField( - 'SampleTypeUID', - expression="context.getSampleType().UID() if context.getSampleType() else ''", - widget=ComputedWidget( - visible=False, - ), - ), )) + BikaSchema.copy() + Schema(( RecordsField( @@ -119,21 +101,14 @@ "in lists and results reports instead of the real result.") ), ), - - ComputedField( - 'ClientUID', - expression="context.aq_parent.UID()", - widget=ComputedWidget( - visible=False, - ), - ), )) schema['description'].widget.visible = True schema['title'].required = True -class AnalysisSpec(BaseFolder, HistoryAwareMixin, ClientAwareMixin): +class AnalysisSpec(BaseFolder, HistoryAwareMixin, ClientAwareMixin, + SampleTypeAwareMixin): """Analysis Specification """ implements(IAnalysisSpec, IDeactivable) @@ -155,9 +130,7 @@ def Title(self): if self.title: title = self.title else: - sampletype = self.getSampleType() - if sampletype: - title = sampletype.Title() + title = self.getSampleTypeTitle() or "" return safe_unicode(title).encode('utf-8') def contextual_title(self): diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 4e61f9b88a..1c24a8bc31 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -31,6 +31,7 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.content.clientawaremixin import ClientAwareMixin +from bika.lims.content.sampletype import SampleTypeAwareMixin from bika.lims.interfaces import IARTemplate, IDeactivable from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField @@ -96,13 +97,6 @@ showOn=True, ), ), - ComputedField( - "SampleTypeUID", - expression="context.Schema()['SampleType'].get(context) and context.Schema()['SampleType'].get(context).UID() or ''", - widget=ComputedWidget( - visible=False, - ), - ), BooleanField( "Composite", default=False, @@ -300,7 +294,7 @@ schema["title"]._validationLayer() -class ARTemplate(BaseContent, ClientAwareMixin): +class ARTemplate(BaseContent, ClientAwareMixin, SampleTypeAwareMixin): security = ClassSecurityInfo() schema = schema displayContentsTab = False diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index 9e88666e76..6efff397af 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -34,6 +34,10 @@ 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 zope.interface import implements + +from bika.lims import api from bika.lims import bikaMessageFactory as _ from bika.lims import deprecated from bika.lims.browser.fields import CoordinateField @@ -45,9 +49,8 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.content.clientawaremixin import ClientAwareMixin +from bika.lims.content.sampletype import SampleTypeAwareMixin from bika.lims.interfaces import IDeactivable -from plone.app.blob.field import FileField as BlobFileField -from zope.interface import implements schema = BikaSchema.copy() + Schema(( CoordinateField( @@ -98,6 +101,11 @@ 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."), + catalog_name='bika_setup_catalog', + base_query={"is_active": True, + "sort_on": "sortable_title", + "sort_order": "ascending"}, + showOn=True, ), ), @@ -126,7 +134,8 @@ schema['description'].schemata = 'default' -class SamplePoint(BaseContent, HistoryAwareMixin, ClientAwareMixin): +class SamplePoint(BaseContent, HistoryAwareMixin, ClientAwareMixin, + SampleTypeAwareMixin): implements(IDeactivable) security = ClassSecurityInfo() displayContentsTab = False @@ -141,19 +150,6 @@ def _renameAfterCreation(self, check_auto_id=False): def Title(self): return safe_unicode(self.getField('title').get(self)).encode('utf-8') - def getSampleTypeTitle(self): - """Returns a comma separated list of sample type titles - """ - sample_types = self.getSampleTypes() - sample_type_titles = map(lambda obj: obj.Title(), sample_types) - if not sample_type_titles: - return "" - return ",".join(sample_type_titles) - - def SampleTypesVocabulary(self): - from bika.lims.content.sampletype import SampleTypes - return SampleTypes(self, allow_blank=False) - def setSampleTypes(self, value, **kw): """ For the moment, we're manually trimming the sampletype<>samplepoint relation to be equal on both sides, here. @@ -188,31 +184,5 @@ def setSampleTypes(self, value, **kw): return ret - def getSampleTypes(self, **kw): - return self.Schema()['SampleTypes'].get(self) - 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', - 'is_active': True, - 'sort_on': 'sortable_title'} - if lab_only: - lab_path = instance.bika_setup.bika_samplepoints.getPhysicalPath() - contentFilter['path'] = {"query": "/".join(lab_path), "level": 0} - for sp in bsc(contentFilter): - sp = sp.getObject() - if sp.aq_parent.portal_type == 'Client': - sp_title = "{}: {}".format(sp.aq_parent.Title(), sp.Title()) - else: - sp_title = sp.Title() - items.append((sp.UID(), sp_title)) - items = allow_blank and [['', '']] + list(items) or list(items) - return DisplayList(items) diff --git a/bika/lims/content/sampletype.py b/bika/lims/content/sampletype.py index 01269926f0..e196ca9f36 100644 --- a/bika/lims/content/sampletype.py +++ b/bika/lims/content/sampletype.py @@ -34,9 +34,11 @@ from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.interfaces import ISampleType, IDeactivable +from bika.lims.interfaces import ISampleTypeAwareMixin from bika.lims.vocabularies import getStickerTemplates from magnitude import mg from zope.interface import implements +from bika.lims import api SMALL_DEFAULT_STICKER = 'small_default' LARGE_DEFAULT_STICKER = 'large_default' @@ -56,6 +58,56 @@ def sticker_templates(): return voc +class SampleTypeAwareMixin(BaseObject): + implements(ISampleTypeAwareMixin) + + def getSampleType(self): + """Returns the sample type(s) assigned to this object, if any + """ + if ISampleType.providedBy(self): + return self + + field = self._get_field() + if not field: + return None + + sample_type = field.get(self) + return sample_type or None + + def getSampleTypeUID(self): + """Returns the UID(s) of the Sample Type(s) assigned to this object + """ + sample_type = self.getSampleType() + if isinstance(sample_type, (list, tuple)): + return map(api.get_uid, sample_type) + elif sample_type: + return api.get_uid(sample_type) + return None + + def getSampleTypeTitle(self): + """Returns the title or a comma separated list of sample type titles + """ + sample_type = self.getSampleType() + if isinstance(sample_type, (list, tuple)): + title = map(api.get_title, sample_type) + return ",".join(title) + elif sample_type: + return api.get_title(sample_type) + return None + + def _get_field(self): + """Returns the field that stores the SampleType object, if any + """ + field = self.getField("SampleType", None) + if not field: + field = self.getField("SampleTypes", None) + + if not field: + return None + + return field + + schema = BikaSchema.copy() + Schema(( DurationField('RetentionPeriod', required = 1, @@ -119,13 +171,17 @@ def sticker_templates(): required = 0, multiValued = 1, allowed_types = ('SamplePoint',), - vocabulary = 'SamplePointsVocabulary', relationship = 'SampleTypeSamplePoint', widget = brw( label=_("Sample Points"), description =_("The list of sample points from which this sample " "type can be collected. If no sample points are " "selected, then all sample points are available."), + catalog_name='bika_setup_catalog', + base_query={"is_active": True, + "sort_on": "sortable_title", + "sort_order": "ascending"}, + showOn=True, ), ), ComputedField( @@ -181,7 +237,8 @@ def sticker_templates(): schema['description'].schemata = 'default' schema['description'].widget.visible = True -class SampleType(BaseContent, HistoryAwareMixin): + +class SampleType(BaseContent, HistoryAwareMixin, SampleTypeAwareMixin): implements(ISampleType, IDeactivable) security = ClassSecurityInfo() @@ -221,10 +278,6 @@ def getDefaultLifetime(self): settings = getToolByName(self, 'bika_setup') return settings.getDefaultSampleLifetime() - def SamplePointsVocabulary(self): - from bika.lims.content.samplepoint import SamplePoints - return SamplePoints(self, allow_blank=False, lab_only=False) - def setSamplePoints(self, value, **kw): """ For the moment, we're manually trimming the sampletype<>samplepoint relation to be equal on both sides, here. @@ -257,9 +310,6 @@ def setSamplePoints(self, value, **kw): return ret - def getSamplePoints(self, **kw): - return self.Schema()['SamplePoints'].get(self) - def SampleMatricesVocabulary(self): from bika.lims.content.samplematrix import SampleMatrices return SampleMatrices(self, allow_blank=True) @@ -367,14 +417,3 @@ def _set_sticker_subfield(self, subfield, value): registerType(SampleType, PROJECTNAME) - -def SampleTypes(self, instance=None, allow_blank=False): - instance = instance or self - bsc = getToolByName(instance, 'bika_setup_catalog') - items = [] - for st in bsc(portal_type='SampleType', - is_active=True, - sort_on = 'sortable_title'): - items.append((st.UID, st.Title)) - items = allow_blank and [['','']] + list(items) or list(items) - return DisplayList(items) diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 3116794db7..cd64656bff 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -999,3 +999,17 @@ def getClient(self): def getClientUID(self): """Returns the client UID this object is bound to, if any """ + + +class ISampleTypeAwareMixin(Interface): + """Marker interface for objects that can be assigned to one, or multiple + SampleType objects through a ReferenceField + """ + + def getSampleType(self): + """Returns the sample type(s) assigned to this object, if any + """ + + def getSampleTypeUID(self): + """Returns the UID(s) of the Sample Type(s) assigned to this object + """ diff --git a/bika/lims/setuphandlers.py b/bika/lims/setuphandlers.py index b333331fc5..736402e279 100644 --- a/bika/lims/setuphandlers.py +++ b/bika/lims/setuphandlers.py @@ -203,7 +203,7 @@ # Sorting of listings: Sample Points, Specifications ("bika_setup_catalog", "getSampleTypeTitle", "", "FieldIndex"), # Filter in Add2: Sample Points, Specifications, Templates - ("bika_setup_catalog", "getSampleTypeUID", "", "FieldIndex"), + ("bika_setup_catalog", "getSampleTypeUID", "", "KeywordIndex"), ("bika_setup_catalog", "getServiceUID", "", "FieldIndex"), ("bika_setup_catalog", "getServiceUIDs", "", "KeywordIndex"), ("bika_setup_catalog", "getTotalPrice", "", "FieldIndex"), diff --git a/bika/lims/upgrade/v01_03_003.py b/bika/lims/upgrade/v01_03_003.py index ed97847ae6..3072d537f9 100644 --- a/bika/lims/upgrade/v01_03_003.py +++ b/bika/lims/upgrade/v01_03_003.py @@ -30,6 +30,12 @@ version = "1.3.3" # Remember version number in metadata.xml and setup.py profile = "profile-{0}:default".format(product) +INDEXES_TO_ADD = [ + # We changed the type of this index from FieldIndex to KeywordIndex + # https://github.com/senaite/senaite.core/pull/1481 + ("bika_setup_catalog", "getSampleTypeUID", "KeywordIndex"), +] + INDEXES_TO_REMOVE = [ # Only used in add2 to filter Sample Points by Sample Type when a Sample # Type was selected. Now, getSampleTypeUID is used instead because of @@ -44,7 +50,11 @@ # Not used neither for searches nor filtering of any of the content types # stored in bika_catalog (Batch, BatchFolder and ReferenceSample) - ("bika_catalog", "getSampleTypeUID") + ("bika_catalog", "getSampleTypeUID"), + + # We remove this index because we changed it's type to KeywordIndex + # https://github.com/senaite/senaite.core/pull/1481 + ("bika_setup_catalog", "getSampleTypeUID") ] METADATA_TO_REMOVE = [ @@ -88,6 +98,10 @@ def upgrade(tool): remove_stale_indexes(portal) remove_stale_metadata(portal) + # Add new indexes + # https://github.com/senaite/senaite.core/pull/1481 + add_new_indexes(portal) + # Reindex client's related fields (getClientUID, getClientTitle, etc.) # https://github.com/senaite/senaite.core/pull/1477 reindex_client_fields(portal) @@ -166,3 +180,22 @@ def del_metadata(catalog_id, column): .format(column, catalog_id)) return catalog.delColumn(column) + + +def add_new_indexes(portal): + logger.info("Adding new indexes ...") + for catalog_id, index_name, index_metatype in INDEXES_TO_ADD: + add_index(catalog_id, index_name, index_metatype) + logger.info("Adding new indexes ... [DONE]") + + +def add_index(catalog_id, index_name, index_metatype): + logger.info("Adding '{}' index to '{}' ...".format(index_name, catalog_id)) + catalog = api.get_tool(catalog_id) + if index_name in catalog.indexes(): + logger.info("Index '{}' already in catalog '{}' [SKIP]" + .format(index_name, catalog_id)) + return + catalog.addIndex(index_name, index_metatype) + logger.info("Indexing new index '{}' ...".format(index_name)) + catalog.manage_reindexIndex(index_name) From 64b2486ed581e4814ff2b58c0e8e9a40d56d9bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 09:24:41 +0100 Subject: [PATCH 23/30] Remove SampleTypeVocabulary (no longer used) --- bika/lims/content/samplepoint.py | 1 - bika/lims/vocabularies/__init__.py | 10 ---------- bika/lims/vocabularies/configure.zcml | 5 ----- 3 files changed, 16 deletions(-) diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index 6efff397af..e2ba5cfeef 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -94,7 +94,6 @@ required=0, multiValued=1, allowed_types=('SampleType',), - vocabulary='SampleTypesVocabulary', relationship='SamplePointSampleType', widget=BikaReferenceWidget( label=_("Sample Types"), diff --git a/bika/lims/vocabularies/__init__.py b/bika/lims/vocabularies/__init__.py index 8346228e23..3ffcc0eeac 100644 --- a/bika/lims/vocabularies/__init__.py +++ b/bika/lims/vocabularies/__init__.py @@ -214,16 +214,6 @@ def __init__(self): SamplePointVocabularyFactory = SamplePointVocabulary() -class SampleTypeVocabulary(BikaContentVocabulary): - def __init__(self): - BikaContentVocabulary.__init__(self, - ['bika_setup/bika_sampletypes', ], - ['SampleType', ]) - - -SampleTypeVocabularyFactory = SampleTypeVocabulary() - - class AnalysisServiceVocabulary(BikaContentVocabulary): def __init__(self): BikaContentVocabulary.__init__(self, diff --git a/bika/lims/vocabularies/configure.zcml b/bika/lims/vocabularies/configure.zcml index 0c7b10385a..57ccb9bf27 100644 --- a/bika/lims/vocabularies/configure.zcml +++ b/bika/lims/vocabularies/configure.zcml @@ -50,11 +50,6 @@ name="bika.lims.vocabularies.StorageLocations" /> - - Date: Thu, 28 Nov 2019 09:44:45 +0100 Subject: [PATCH 24/30] Index: getSampleTypeUID --> sampletype_uids --- bika/lims/browser/analysisrequest/add2.py | 6 +++--- bika/lims/catalog/indexers/bikasetup.py | 2 +- bika/lims/catalog/indexers/configure.zcml | 6 +++++- bika/lims/content/analysisrequest.py | 2 +- bika/lims/setuphandlers.py | 2 +- bika/lims/upgrade/v01_03_003.py | 2 +- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index 0bef224842..5760dd976b 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -1018,18 +1018,18 @@ def get_sampletype_info(self, obj): # Display Sample Points that have this sample type assigned plus # those that do not have a sample type assigned "SamplePoint": { - "getSampleTypeUID": [sample_type_uid, None], + "sampletype_uids": [sample_type_uid, None], "getClientUID": [client_uid, ""], }, # Display Specifications that have this sample type assigned only "Specification": { - "getSampleTypeUID": sample_type_uid, + "sampletype_uids": sample_type_uid, "getClientUID": [client_uid, ""], }, # Display AR Templates that have this sample type assigned plus # those that do not have a sample type assigned "Template": { - "getSampleTypeUID": [sample_type_uid, None], + "sampletype_uids": [sample_type_uid, None], "getClientUID": [client_uid, ""], } } diff --git a/bika/lims/catalog/indexers/bikasetup.py b/bika/lims/catalog/indexers/bikasetup.py index dfb2958256..6b7a0e3159 100644 --- a/bika/lims/catalog/indexers/bikasetup.py +++ b/bika/lims/catalog/indexers/bikasetup.py @@ -6,7 +6,7 @@ @indexer(ISampleTypeAwareMixin, IBikaSetupCatalog) -def getSampleTypeUID(instance): +def sampletype_uids(instance): """Returns the list of SampleType UIDs the instance is assigned to This is a KeywordIndex, so it will be indexed as a list, even if only one diff --git a/bika/lims/catalog/indexers/configure.zcml b/bika/lims/catalog/indexers/configure.zcml index d95dcbc4cf..58fe0514a8 100644 --- a/bika/lims/catalog/indexers/configure.zcml +++ b/bika/lims/catalog/indexers/configure.zcml @@ -25,7 +25,11 @@ - + + + diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 2982dff1e0..227d7ead22 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -690,7 +690,7 @@ 'width': '30', 'label': _('Title'), 'align': 'left'}, - {'columnName': 'SampleTypeTitle', + {'columnName': 'getSampleTypeTitle', 'width': '70', 'label': _('SampleType'), 'align': 'left'}, diff --git a/bika/lims/setuphandlers.py b/bika/lims/setuphandlers.py index 736402e279..489dae7fc2 100644 --- a/bika/lims/setuphandlers.py +++ b/bika/lims/setuphandlers.py @@ -203,7 +203,7 @@ # Sorting of listings: Sample Points, Specifications ("bika_setup_catalog", "getSampleTypeTitle", "", "FieldIndex"), # Filter in Add2: Sample Points, Specifications, Templates - ("bika_setup_catalog", "getSampleTypeUID", "", "KeywordIndex"), + ("bika_setup_catalog", "sampletype_uids", "", "KeywordIndex"), ("bika_setup_catalog", "getServiceUID", "", "FieldIndex"), ("bika_setup_catalog", "getServiceUIDs", "", "KeywordIndex"), ("bika_setup_catalog", "getTotalPrice", "", "FieldIndex"), diff --git a/bika/lims/upgrade/v01_03_003.py b/bika/lims/upgrade/v01_03_003.py index 3072d537f9..07a0ec8d6b 100644 --- a/bika/lims/upgrade/v01_03_003.py +++ b/bika/lims/upgrade/v01_03_003.py @@ -33,7 +33,7 @@ INDEXES_TO_ADD = [ # We changed the type of this index from FieldIndex to KeywordIndex # https://github.com/senaite/senaite.core/pull/1481 - ("bika_setup_catalog", "getSampleTypeUID", "KeywordIndex"), + ("bika_setup_catalog", "sampletype_uids", "KeywordIndex"), ] INDEXES_TO_REMOVE = [ From c0838ab65a02d5383da7de90fa71c1dcf213089f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:35:12 +0100 Subject: [PATCH 25/30] Added code header --- bika/lims/catalog/indexers/bikasetup.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/bika/lims/catalog/indexers/bikasetup.py b/bika/lims/catalog/indexers/bikasetup.py index 6b7a0e3159..e9ee811f1c 100644 --- a/bika/lims/catalog/indexers/bikasetup.py +++ b/bika/lims/catalog/indexers/bikasetup.py @@ -1,4 +1,23 @@ -from Products.Archetypes.interfaces import IBaseObject +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE. +# +# SENAITE.CORE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, version 2. +# +# This program 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2018-2019 by it's authors. +# Some rights reserved, see README and LICENSE. + from plone.indexer import indexer from bika.lims.interfaces import IBikaSetupCatalog From 244dc3199534324e6b1652237f209b9ce11996fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:35:29 +0100 Subject: [PATCH 26/30] Added some comments --- bika/lims/catalog/indexers/configure.zcml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bika/lims/catalog/indexers/configure.zcml b/bika/lims/catalog/indexers/configure.zcml index 58fe0514a8..55e6990556 100644 --- a/bika/lims/catalog/indexers/configure.zcml +++ b/bika/lims/catalog/indexers/configure.zcml @@ -24,13 +24,14 @@ + - + + From 49cbaa5472b663d8af83461cf6dfc48b41309aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:35:48 +0100 Subject: [PATCH 27/30] security.public and titles separated with ", " --- bika/lims/content/sampletype.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bika/lims/content/sampletype.py b/bika/lims/content/sampletype.py index e196ca9f36..da2ea2c533 100644 --- a/bika/lims/content/sampletype.py +++ b/bika/lims/content/sampletype.py @@ -61,6 +61,9 @@ def sticker_templates(): class SampleTypeAwareMixin(BaseObject): implements(ISampleTypeAwareMixin) + security = ClassSecurityInfo() + + @security.public def getSampleType(self): """Returns the sample type(s) assigned to this object, if any """ @@ -74,6 +77,7 @@ def getSampleType(self): sample_type = field.get(self) return sample_type or None + @security.public def getSampleTypeUID(self): """Returns the UID(s) of the Sample Type(s) assigned to this object """ @@ -84,13 +88,14 @@ def getSampleTypeUID(self): return api.get_uid(sample_type) return None + @security.public def getSampleTypeTitle(self): """Returns the title or a comma separated list of sample type titles """ sample_type = self.getSampleType() if isinstance(sample_type, (list, tuple)): title = map(api.get_title, sample_type) - return ",".join(title) + return ", ".join(title) elif sample_type: return api.get_title(sample_type) return None From 0006e314ce252868be48f9fc30bf6f42afa1d49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:37:55 +0100 Subject: [PATCH 28/30] Organize imports --- bika/lims/content/analysisspec.py | 23 ++++++++++++----------- bika/lims/content/artemplate.py | 28 +++++++++++++++------------- bika/lims/content/samplepoint.py | 3 --- bika/lims/content/sampletype.py | 10 ++++++---- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/bika/lims/content/analysisspec.py b/bika/lims/content/analysisspec.py index d8824e372b..4a3fccc4f1 100644 --- a/bika/lims/content/analysisspec.py +++ b/bika/lims/content/analysisspec.py @@ -19,26 +19,27 @@ # Some rights reserved, see README and LICENSE. from AccessControl import ClassSecurityInfo -from bika.lims import api -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.fields import UIDReferenceField -from bika.lims.browser.widgets import AnalysisSpecificationWidget -from bika.lims.config import PROJECTNAME -from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientawaremixin import ClientAwareMixin -from bika.lims.content.sampletype import SampleTypeAwareMixin -from bika.lims.interfaces import IAnalysisSpec, IDeactivable +from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin +from Products.ATExtensions.field.records import RecordsField from Products.Archetypes import atapi from Products.Archetypes.public import BaseFolder from Products.Archetypes.public import ReferenceWidget from Products.Archetypes.public import Schema from Products.Archetypes.utils import DisplayList -from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin -from Products.ATExtensions.field.records import RecordsField from Products.CMFPlone.utils import safe_unicode from zope.i18n import translate from zope.interface import implements +from bika.lims import api +from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields import UIDReferenceField +from bika.lims.browser.widgets import AnalysisSpecificationWidget +from bika.lims.config import PROJECTNAME +from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientawaremixin import ClientAwareMixin +from bika.lims.content.sampletype import SampleTypeAwareMixin +from bika.lims.interfaces import IAnalysisSpec +from bika.lims.interfaces import IDeactivable schema = Schema(( diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 1c24a8bc31..72f8b260ca 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -21,18 +21,7 @@ import sys from AccessControl import ClassSecurityInfo -from bika.lims import api -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import ARTemplateAnalysesWidget -from bika.lims.browser.widgets import ARTemplatePartitionsWidget -from bika.lims.browser.widgets import ReferenceWidget -from bika.lims.browser.widgets import RemarksWidget -from bika.lims.config import PROJECTNAME -from bika.lims.content.bikaschema import BikaSchema -from bika.lims.content.clientawaremixin import ClientAwareMixin -from bika.lims.content.sampletype import SampleTypeAwareMixin -from bika.lims.interfaces import IARTemplate, IDeactivable +from Products.ATExtensions.field.records import RecordsField from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField from Products.Archetypes.public import BooleanWidget @@ -43,10 +32,23 @@ from Products.Archetypes.public import Schema from Products.Archetypes.public import registerType from Products.Archetypes.references import HoldingReference -from Products.ATExtensions.field.records import RecordsField from Products.CMFCore.utils import getToolByName from zope.interface import implements +from bika.lims import api +from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import ARTemplateAnalysesWidget +from bika.lims.browser.widgets import ARTemplatePartitionsWidget +from bika.lims.browser.widgets import ReferenceWidget +from bika.lims.browser.widgets import RemarksWidget +from bika.lims.config import PROJECTNAME +from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.clientawaremixin import ClientAwareMixin +from bika.lims.content.sampletype import SampleTypeAwareMixin +from bika.lims.interfaces import IARTemplate +from bika.lims.interfaces import IDeactivable + schema = BikaSchema.copy() + Schema(( ReferenceField( "SamplePoint", diff --git a/bika/lims/content/samplepoint.py b/bika/lims/content/samplepoint.py index e2ba5cfeef..e2bdd30f81 100644 --- a/bika/lims/content/samplepoint.py +++ b/bika/lims/content/samplepoint.py @@ -25,7 +25,6 @@ from Products.Archetypes.public import BaseContent from Products.Archetypes.public import BooleanField from Products.Archetypes.public import BooleanWidget -from Products.Archetypes.public import DisplayList from Products.Archetypes.public import FileWidget from Products.Archetypes.public import ReferenceField from Products.Archetypes.public import Schema @@ -37,9 +36,7 @@ from plone.app.blob.field import FileField as BlobFileField from zope.interface import implements -from bika.lims import api 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 diff --git a/bika/lims/content/sampletype.py b/bika/lims/content/sampletype.py index da2ea2c533..d77207379f 100644 --- a/bika/lims/content/sampletype.py +++ b/bika/lims/content/sampletype.py @@ -25,6 +25,10 @@ from Products.Archetypes.references import HoldingReference from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import safe_unicode +from magnitude import mg +from zope.interface import implements + +from bika.lims import api from bika.lims import bikaMessageFactory as _ from bika.lims import logger from bika.lims.browser.fields import DurationField @@ -33,12 +37,10 @@ from bika.lims.browser.widgets.referencewidget import ReferenceWidget as brw from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.interfaces import ISampleType, IDeactivable +from bika.lims.interfaces import IDeactivable +from bika.lims.interfaces import ISampleType from bika.lims.interfaces import ISampleTypeAwareMixin from bika.lims.vocabularies import getStickerTemplates -from magnitude import mg -from zope.interface import implements -from bika.lims import api SMALL_DEFAULT_STICKER = 'small_default' LARGE_DEFAULT_STICKER = 'large_default' From bdfe66a7c110d9c0eb78e40636e74a162db629a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:43:16 +0100 Subject: [PATCH 29/30] Add getSampleTypesTitle in ISampleTypeAwareMixin interface --- bika/lims/interfaces/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index cd64656bff..b74fc273d7 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -1013,3 +1013,7 @@ def getSampleType(self): def getSampleTypeUID(self): """Returns the UID(s) of the Sample Type(s) assigned to this object """ + + def getSampleTypeTitle(self): + """Returns the title or a comma separated list of sample type titles + """ From da45ff6b2a106b3680e603f2ee4d3bca69972730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Thu, 28 Nov 2019 10:47:33 +0100 Subject: [PATCH 30/30] Remove stupid conditional --- bika/lims/content/sampletype.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bika/lims/content/sampletype.py b/bika/lims/content/sampletype.py index d77207379f..532769a9e1 100644 --- a/bika/lims/content/sampletype.py +++ b/bika/lims/content/sampletype.py @@ -109,9 +109,6 @@ def _get_field(self): if not field: field = self.getField("SampleTypes", None) - if not field: - return None - return field