def __call__(self, result=None, specification=None, **kwargs): searchTerm = _c(self.request.get('searchTerm', '')).lower() force_all = self.request.get('force_all', 'true') searchFields = 'search_fields' in self.request \ and json.loads(_u(self.request.get('search_fields', '[]'))) \ or ('Title',) # lookup objects from ZODB catalog_name = _c(self.request.get('catalog_name', 'portal_catalog')) catalog = getToolByName(self.context, catalog_name) base_query = json.loads(_c(self.request['base_query'])) search_query = json.loads(_c(self.request.get('search_query', "{}"))) # first with all queries contentFilter = dict( (k, self.to_utf8(v)) for k, v in base_query.items()) contentFilter.update( dict((k, self.to_utf8(v)) for k, v in search_query.items())) try: brains = catalog(contentFilter) except: from bika.lims import logger logger.info(contentFilter) raise if brains and searchTerm: _brains = [] if len(searchFields) == 0 \ or (len(searchFields) == 1 and searchFields[0] == 'Title'): _brains = [ p for p in brains if p.Title.lower().find(searchTerm) > -1 ] else: for p in brains: for fieldname in searchFields: value = getattr(p, fieldname, None) if not value: instance = p.getObject() schema = instance.Schema() if fieldname in schema: value = schema[fieldname].get(instance) if callable(value): value = value() if value and value.lower().find(searchTerm) > -1: _brains.append(p) break brains = _brains # Then just base_query alone ("show all if no match") if not brains and force_all.lower() == 'true': if search_query: brains = catalog(base_query) if brains and searchTerm: _brains = [ p for p in brains if p.Title.lower().find(searchTerm) > -1 ] if _brains: brains = _brains return brains
def search_fields(self): """Returns the object field names to search against """ search_fields = self.request.get("search_fields", None) if not search_fields: return [] search_fields = json.loads(_u(search_fields)) return search_fields
def __call__(self, result=None, specification=None, **kwargs): searchTerm = _c(self.request.get('searchTerm', '')).lower() force_all = self.request.get('force_all', 'true') searchFields = 'search_fields' in self.request \ and json.loads(_u(self.request.get('search_fields', '[]'))) \ or ('Title',) # lookup objects from ZODB catalog_name = _c(self.request.get('catalog_name', 'portal_catalog')) catalog = getToolByName(self.context, catalog_name) base_query = json.loads(_c(self.request['base_query'])) search_query = json.loads(_c(self.request.get('search_query', "{}"))) # first with all queries contentFilter = dict((k, self.to_utf8(v)) for k, v in base_query.items()) contentFilter.update(dict((k, self.to_utf8(v)) for k, v in search_query.items())) try: brains = catalog(contentFilter) except: from bika.lims import logger logger.info(contentFilter) raise if brains and searchTerm: _brains = [] if len(searchFields) == 0 \ or (len(searchFields) == 1 and searchFields[0] == 'Title'): _brains = [p for p in brains if p.Title.lower().find(searchTerm) > -1] else: for p in brains: for fieldname in searchFields: value = getattr(p, fieldname, None) if not value: instance = p.getObject() schema = instance.Schema() if fieldname in schema: value = schema[fieldname].get(instance) if callable(value): value = value() if value and value.lower().find(searchTerm) > -1: _brains.append(p) break brains = _brains # Then just base_query alone ("show all if no match") if not brains and force_all.lower() == 'true': if search_query: brains = catalog(base_query) if brains and searchTerm: _brains = [p for p in brains if p.Title.lower().find(searchTerm) > -1] if _brains: brains = _brains return brains
def make_title(o): # the javascript uses these strings to decide if it should # check the blank or hazardous checkboxes when a reference # definition is selected (./js/referencesample.js) if not o: return '' title = _u(o.Title()) if o.getBlank(): title += " %s" % t(_('(Blank)')) if o.getHazardous(): title += " %s" % t(_('(Hazardous)')) return title
def get_field_names(self): """Return the field names to get values for """ col_model = self.request.get("colModel", None) if not col_model: return [ "UID", ] names = [] col_model = json.loads(_u(col_model)) if isinstance(col_model, (list, tuple)): names = map(lambda c: c.get("columnName", "").strip(), col_model) # UID is used by reference widget to know the object that the user # selected from the popup list if "UID" not in names: names.append("UID") return filter(None, names)
def __call__(self): plone.protect.CheckAuthenticator(self.request) page = self.request['page'] nr_rows = self.request['rows'] sord = self.request['sord'] sidx = self.request['sidx'] colModel = json.loads(_u(self.request.get('colModel', '[]'))) discard_empty = json.loads(_c(self.request.get('discard_empty', "[]"))) rows = [] brains = [] for name, adapter in getAdapters((self.context, self.request), IReferenceWidgetVocabulary): brains.extend(adapter()) for p in brains: row = {'UID': getattr(p, 'UID'), 'Title': getattr(p, 'Title')} other_fields = [x for x in colModel if x['columnName'] not in row.keys()] instance = schema = None discard = False # This will be faster if the columnNames are catalog indexes for field in other_fields: fieldname = field['columnName'] # Prioritize method retrieval over field retrieval from schema obj = p.getObject() value = getattr(obj, fieldname, None) if not value or hasattr(value, 'im_self'): value = getattr(p, fieldname, None) if not value: if instance is None: instance = p.getObject() schema = instance.Schema() if fieldname in schema: value = schema[fieldname].get(instance) elif hasattr(instance, fieldname): value = getattr(instance, fieldname) if callable(value): value = value() if fieldname in discard_empty and not value: discard = True break # ' ' instead of '' because empty div fields don't render # correctly in combo results table row[fieldname] = value and value or ' ' if discard is False: rows.append(row) rows = sorted(rows, cmp=lambda x, y: cmp( str(x).lower(), str(y).lower()), key=itemgetter(sidx and sidx or 'Title')) if sord == 'desc': rows.reverse() pages = len(rows) / int(nr_rows) pages += divmod(len(rows), int(nr_rows))[1] and 1 or 0 start = (int(page) - 1) * int(nr_rows) end = int(page) * int(nr_rows) ret = {'page': page, 'total': pages, 'records': len(rows), 'rows': rows[start:end]} return json.dumps(ret)
def __call__(self): """Create and render selected report """ # if there's an error, we return productivity.pt which requires these. self.selection_macros = SelectionMacrosView(self.context, self.request) report_id = self.request.get('report_id', '') if not report_id: message = "No report specified in request" self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() self.date = DateTime() username = self.context.portal_membership.getAuthenticatedMember( ).getUserName() self.reporter = self.user_fullname(username) self.reporter_email = self.user_email(username) # signature image self.reporter_signature = "" c = [ x for x in self.bika_setup_catalog(portal_type='LabContact') if x.getObject().getUsername() == username ] if c: sf = c[0].getObject().getSignature() if sf: self.reporter_signature = sf.absolute_url() + "/Signature" lab = self.context.bika_setup.laboratory self.laboratory = lab self.lab_title = lab.getName() self.lab_address = lab.getPrintAddress() self.lab_email = lab.getEmailAddress() self.lab_url = lab.getLabURL() client = logged_in_client(self.context) if client: clientuid = client.UID() self.client_title = client.Title() self.client_address = client.getPrintAddress() else: clientuid = None self.client_title = None self.client_address = None ## Render form output # the report can add file names to this list; they will be deleted # once the PDF has been generated. temporary plot image files, etc. self.request['to_remove'] = [] try: exec("from bika.lims.browser.reports.%s import Report" % report_id) except ImportError: message = "Report %s not found (shouldn't happen)" % report_id self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() # Report must return dict with: # - report_title - title string for pdf/history listing # - report_data - rendered report output = Report(self.context, self.request)() if type(output) in (str, unicode, bytes): # remove temporary files for f in self.request['to_remove']: os.remove(f) return output ## The report output gets pulled through report_frame.pt self.reportout = output['report_data'] framed_output = self.frame_template() # this is the good part pisa.showLogging() ramdisk = StringIO() pdf = pisa.CreatePDF(framed_output, ramdisk) result = ramdisk.getvalue() ramdisk.close() ## Create new report object reportid = self.aq_parent.generateUniqueId('Report') self.aq_parent.invokeFactory(id=reportid, type_name="Report") report = self.aq_parent._getOb(reportid) report.edit(Client=clientuid) report.processForm() ## write pdf to report object report.edit(title=output['report_title'], ReportFile=result) report.reindexObject() fn = "%s - %s" % (self.date.strftime( self.date_format_short), _u(output['report_title'])) # remove temporary files for f in self.request['to_remove']: os.remove(f) if not pdf.err: setheader = self.request.RESPONSE.setHeader setheader('Content-Type', 'application/pdf') setheader("Content-Disposition", "attachment;filename=\"%s\"" % _c(fn)) self.request.RESPONSE.write(result) return
def __call__(self, result=None, specification=None, **kwargs): searchTerm = _c(self.request.get('searchTerm', '')).lower() force_all = self.request.get('force_all', 'false') searchFields = 'search_fields' in self.request \ and json.loads(_u(self.request.get('search_fields', '[]'))) \ or ('Title',) # lookup objects from ZODB catalog_name = _c(self.request.get('catalog_name', 'portal_catalog')) catalog = getToolByName(self.context, catalog_name) # json.loads does unicode conversion, which will fail in the catalog # search for some cases. So we need to convert the strings to utf8 # see: https://github.com/senaite/bika.lims/issues/443 base_query = json.loads(self.request['base_query']) search_query = json.loads(self.request.get('search_query', "{}")) base_query = self.to_utf8(base_query) search_query = self.to_utf8(search_query) # first with all queries contentFilter = dict((k, v) for k, v in base_query.items()) contentFilter.update(search_query) # Sorted by? (by default, Title) sort_on = self.request.get('sidx', 'Title') if sort_on == 'Title': sort_on = 'sortable_title' if sort_on: # Check if is an index and if is sortable. Otherwise, assume the # sorting must be done manually index = catalog.Indexes.get(sort_on, None) if index and index.meta_type in ['FieldIndex', 'DateIndex']: contentFilter['sort_on'] = sort_on # Sort order? sort_order = self.request.get('sord', 'asc') if (sort_order in ['desc', 'reverse', 'rev', 'descending']): contentFilter['sort_order'] = 'descending' else: contentFilter['sort_order'] = 'ascending' # Can do a search for indexes? criterias = [] fields_wo_index = [] if searchTerm: for field_name in searchFields: index = catalog.Indexes.get(field_name, None) if not index: fields_wo_index.append(field_name) continue if index.meta_type in ('ZCTextIndex'): if searchTerm.isspace(): # earchTerm != ' ' added because of # https://github.com/plone/Products.CMFPlone/issues # /1537 searchTerm = '' continue else: temp_st = searchTerm + '*' criterias.append(MatchRegexp(field_name, temp_st)) elif index.meta_type in ('FieldIndex'): criterias.append(MatchRegexp(field_name, searchTerm)) elif index.meta_type == 'DateIndex': msg = "Unhandled DateIndex search on '%s'" % field_name from bika.lims import logger logger.warn(msg) else: criterias.append(Generic(field_name, searchTerm)) if criterias: # Advanced search advanced_query = catalog.makeAdvancedQuery(contentFilter) aq_or = Or() for criteria in criterias: aq_or.addSubquery(criteria) advanced_query &= aq_or brains = catalog.evalAdvancedQuery(advanced_query) else: brains = catalog(contentFilter) if brains and searchTerm and fields_wo_index: _brains = [] for brain in brains: for field_name in fields_wo_index: value = getattr(brain, field_name, None) if not value: instance = brain.getObject() schema = instance.Schema() if field_name in schema: value = schema[field_name].get(instance) if callable(value): value = value() if value and value.lower().find(searchTerm) > -1: _brains.append(brain) break brains = _brains # Then just base_query alone ("show all if no match") if not brains and force_all.lower() == 'true': if search_query: brains = catalog(base_query) if brains and searchTerm: _brains = [ p for p in brains if p.Title.lower().find(searchTerm) > -1 ] if _brains: brains = _brains return brains
def __call__(self): """Create and render selected report """ # if there's an error, we return productivity.pt which requires these. self.selection_macros = SelectionMacrosView(self.context, self.request) self.additional_reports = [] adapters = getAdapters((self.context, ), IProductivityReport) for name, adapter in adapters: report_dict = adapter(self.context, self.request) report_dict['id'] = name self.additional_reports.append(report_dict) report_id = self.request.get('report_id', '') if not report_id: message = _("No report specified in request") self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() self.date = DateTime() username = self.context.portal_membership.getAuthenticatedMember( ).getUserName() self.reporter = self.user_fullname(username) self.reporter_email = self.user_email(username) # signature image self.reporter_signature = "" c = [ x for x in self.bika_setup_catalog(portal_type='LabContact') if x.getObject().getUsername() == username ] if c: sf = c[0].getObject().getSignature() if sf: self.reporter_signature = sf.absolute_url() + "/Signature" lab = self.context.bika_setup.laboratory self.laboratory = lab self.lab_title = lab.getName() self.lab_address = lab.getPrintAddress() self.lab_email = lab.getEmailAddress() self.lab_url = lab.getLabURL() client = logged_in_client(self.context) if client: clientuid = client.UID() self.client_title = client.Title() self.client_address = client.getPrintAddress() else: clientuid = None self.client_title = None self.client_address = None # Render form output # the report can add file names to this list; they will be deleted # once the PDF has been generated. temporary plot image files, etc. self.request['to_remove'] = [] if "report_module" in self.request: module = self.request["report_module"] else: module = "bika.lims.browser.reports.%s" % report_id try: exec("from %s import Report" % module) # required during error redirect: the report must have a copy of # additional_reports, because it is used as a surrogate view. Report.additional_reports = self.additional_reports except ImportError: message = "Report %s.Report not found (shouldn't happen)" % module self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() # Report must return dict with: # - report_title - title string for pdf/history listing # - report_data - rendered report output = Report(self.context, self.request)() # if CSV output is chosen, report returns None if not output: return if type(output) in (str, unicode, bytes): # remove temporary files for f in self.request['to_remove']: os.remove(f) return output # The report output gets pulled through report_frame.pt self.reportout = output['report_data'] framed_output = self.frame_template() # this is the good part result = createPdf(framed_output, False) # remove temporary files for f in self.request['to_remove']: os.remove(f) if result: # Create new report object reportid = self.aq_parent.generateUniqueId('Report') report = _createObjectByType("Report", self.aq_parent, reportid) report.edit(Client=clientuid) report.processForm() # write pdf to report object report.edit(title=output['report_title'], ReportFile=result) report.reindexObject() fn = "%s - %s" % (self.date.strftime( self.date_format_short), _u(output['report_title'])) setheader = self.request.RESPONSE.setHeader setheader('Content-Type', 'application/pdf') setheader("Content-Disposition", "attachment;filename=\"%s\"" % _c(fn)) self.request.RESPONSE.write(result) return
def __call__(self): """Create and render selected report """ # if there's an error, we return productivity.pt which requires these. self.selection_macros = SelectionMacrosView(self.context, self.request) self.additional_reports = [] adapters = getAdapters((self.context, ), IProductivityReport) for name, adapter in adapters: report_dict = adapter(self.context, self.request) report_dict['id'] = name self.additional_reports.append(report_dict) report_id = self.request.get('report_id', '') if not report_id: message = "No report specified in request" self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() self.date = DateTime() username = self.context.portal_membership.getAuthenticatedMember().getUserName() self.reporter = self.user_fullname(username) self.reporter_email = self.user_email(username) # signature image self.reporter_signature = "" c = [x for x in self.bika_setup_catalog(portal_type='LabContact') if x.getObject().getUsername() == username] if c: sf = c[0].getObject().getSignature() if sf: self.reporter_signature = sf.absolute_url() + "/Signature" lab = self.context.bika_setup.laboratory self.laboratory = lab self.lab_title = lab.getName() self.lab_address = lab.getPrintAddress() self.lab_email = lab.getEmailAddress() self.lab_url = lab.getLabURL() client = logged_in_client(self.context) if client: clientuid = client.UID() self.client_title = client.Title() self.client_address = client.getPrintAddress() else: clientuid = None self.client_title = None self.client_address = None # Render form output # the report can add file names to this list; they will be deleted # once the PDF has been generated. temporary plot image files, etc. self.request['to_remove'] = [] if "report_module" in self.request: module = self.request["report_module"] else: module = "bika.lims.browser.reports.%s" % report_id try: exec("from %s import Report" % module) # required during error redirect: the report must have a copy of # additional_reports, because it is used as a surrogate view. Report.additional_reports = self.additional_reports except ImportError: message = "Report %s.Report not found (shouldn't happen)" % module self.logger.error(message) self.context.plone_utils.addPortalMessage(message, 'error') return self.template() # Report must return dict with: # - report_title - title string for pdf/history listing # - report_data - rendered report output = Report(self.context, self.request)() # if CSV output is chosen, report returns None if not output: return if type(output) in (str, unicode, bytes): # remove temporary files for f in self.request['to_remove']: os.remove(f) return output # The report output gets pulled through report_frame.pt self.reportout = output['report_data'] framed_output = self.frame_template() # this is the good part result = createPdf(framed_output) # remove temporary files for f in self.request['to_remove']: os.remove(f) if result: # Create new report object reportid = self.aq_parent.generateUniqueId('Report') self.aq_parent.invokeFactory(id=reportid, type_name="Report") report = self.aq_parent._getOb(reportid) report.edit(Client=clientuid) report.processForm() # write pdf to report object report.edit(title=output['report_title'], ReportFile=result) report.reindexObject() fn = "%s - %s" % (self.date.strftime(self.date_format_short), _u(output['report_title'])) setheader = self.request.RESPONSE.setHeader setheader('Content-Type', 'application/pdf') setheader("Content-Disposition", "attachment;filename=\"%s\"" % _c(fn)) self.request.RESPONSE.write(result) return
def getContainers(instance, minvol=None, allow_blank=True, show_container_types=True, show_containers=True): """ Containers vocabulary This is a separate class so that it can be called from ajax to filter the container list, as well as being used as the AT field vocabulary. Returns a tuple of tuples: ((object_uid, object_title), ()) If the partition is flagged 'Separate', only containers are displayed. If the Separate flag is false, displays container types. XXX bsc = self.portal.bika_setup_catalog XXX obj = bsc(getKeyword='Moist')[0].getObject() XXX u'Container Type: Canvas bag' in obj.getContainers().values() XXX True """ bsc = getToolByName(instance, 'bika_setup_catalog') items = allow_blank and [['', _('Any')]] or [] containers = [] for container in bsc(portal_type='Container', sort_on='sortable_title'): container = container.getObject() # verify container capacity is large enough for required sample volume. if minvol is not None: capacity = container.getCapacity() try: capacity = capacity.split(' ', 1) capacity = mg(float(capacity[0]), capacity[1]) if capacity < minvol: continue except: # if there's a unit conversion error, allow the container # to be displayed. pass containers.append(container) if show_containers: # containers with no containertype first for container in containers: if not container.getContainerType(): items.append((container.UID(), container.Title())) ts = getToolByName(instance, 'translation_service').translate cat_str = ts(_('Container Type')) containertypes = [c.getContainerType() for c in containers] containertypes = dict([(ct.UID(), ct.Title()) for ct in containertypes if ct]) for ctype_uid, ctype_title in containertypes.items(): ctype_title = _u(ctype_title) if show_container_types: items.append((ctype_uid, "%s: %s"%(cat_str, ctype_title))) if show_containers: for container in containers: ctype = container.getContainerType() if ctype and ctype.UID() == ctype_uid: items.append((container.UID(), container.Title())) items = tuple(items) return items
def __call__(self, result=None, specification=None, **kwargs): searchTerm = _c(self.request.get('searchTerm', '')).lower() force_all = self.request.get('force_all', 'false') searchFields = 'search_fields' in self.request \ and json.loads(_u(self.request.get('search_fields', '[]'))) \ or ('Title',) # lookup objects from ZODB catalog_name = _c(self.request.get('catalog_name', 'portal_catalog')) catalog = getToolByName(self.context, catalog_name) # N.B. We don't use json.loads to avoid unicode conversion, which will # fail in the catalog search for some cases # see: https://github.com/senaite/bika.lims/issues/443 base_query = ast.literal_eval(self.request['base_query']) search_query = ast.literal_eval(self.request.get('search_query', "{}")) # first with all queries contentFilter = dict((k, v) for k, v in base_query.items()) contentFilter.update(search_query) # Sorted by? (by default, Title) sort_on = self.request.get('sidx', 'Title') if sort_on == 'Title': sort_on = 'sortable_title' if sort_on: # Check if is an index and if is sortable. Otherwise, assume the # sorting must be done manually index = catalog.Indexes.get(sort_on, None) if index and index.meta_type in ['FieldIndex', 'DateIndex']: contentFilter['sort_on'] = sort_on # Sort order? sort_order = self.request.get('sord', 'asc') if (sort_order in ['desc', 'reverse', 'rev', 'descending']): contentFilter['sort_order'] = 'descending' else: contentFilter['sort_order'] = 'ascending' # Can do a search for indexes? criterias = [] fields_wo_index = [] if searchTerm: for field_name in searchFields: index = catalog.Indexes.get(field_name, None) if not index: fields_wo_index.append(field_name) continue if index.meta_type in ('ZCTextIndex'): if searchTerm.isspace(): # earchTerm != ' ' added because of # https://github.com/plone/Products.CMFPlone/issues # /1537 searchTerm = '' continue else: temp_st = searchTerm + '*' criterias.append(MatchRegexp(field_name, temp_st)) elif index.meta_type in ('FieldIndex'): criterias.append(MatchRegexp(field_name, searchTerm)) elif index.meta_type == 'DateIndex': msg = "Unhandled DateIndex search on '%s'" % field_name from bika.lims import logger logger.warn(msg) else: criterias.append(Generic(field_name, searchTerm)) if criterias: # Advanced search advanced_query = catalog.makeAdvancedQuery(contentFilter) aq_or = Or() for criteria in criterias: aq_or.addSubquery(criteria) advanced_query &= aq_or brains = catalog.evalAdvancedQuery(advanced_query) else: brains = catalog(contentFilter) if brains and searchTerm and fields_wo_index: _brains = [] for brain in brains: for field_name in fields_wo_index: value = getattr(brain, field_name, None) if not value: instance = brain.getObject() schema = instance.Schema() if field_name in schema: value = schema[field_name].get(instance) if callable(value): value = value() if value and value.lower().find(searchTerm) > -1: _brains.append(brain) break brains = _brains # Then just base_query alone ("show all if no match") if not brains and force_all.lower() == 'true': if search_query: brains = catalog(base_query) if brains and searchTerm: _brains = [p for p in brains if p.Title.lower().find(searchTerm) > -1] if _brains: brains = _brains return brains
def getContainers(instance, minvol=None, allow_blank=True, show_container_types=True, show_containers=True): """ Containers vocabulary This is a separate class so that it can be called from ajax to filter the container list, as well as being used as the AT field vocabulary. Returns a tuple of tuples: ((object_uid, object_title), ()) If the partition is flagged 'Separate', only containers are displayed. If the Separate flag is false, displays container types. XXX bsc = self.portal.bika_setup_catalog XXX obj = bsc(getKeyword='Moist')[0].getObject() XXX u'Container Type: Canvas bag' in obj.getContainers().values() XXX True """ bsc = getToolByName(instance, 'bika_setup_catalog') items = allow_blank and [['', _('Any')]] or [] containers = [] for container in bsc(portal_type='Container', sort_on='sortable_title'): container = container.getObject() # verify container capacity is large enough for required sample volume. if minvol is not None: capacity = container.getCapacity() try: capacity = capacity.split(' ', 1) capacity = mg(float(capacity[0]), capacity[1]) if capacity < minvol: continue except: # if there's a unit conversion error, allow the container # to be displayed. pass containers.append(container) if show_containers: # containers with no containertype first for container in containers: if not container.getContainerType(): items.append((container.UID(), container.Title())) ts = getToolByName(instance, 'translation_service').translate cat_str = ts(_('Container Type')) containertypes = [c.getContainerType() for c in containers] containertypes = dict([(ct.UID(), ct.Title()) for ct in containertypes if ct]) for ctype_uid, ctype_title in containertypes.items(): ctype_title = _u(ctype_title) if show_container_types: items.append((ctype_uid, "%s: %s" % (cat_str, ctype_title))) if show_containers: for container in containers: ctype = container.getContainerType() if ctype and ctype.UID() == ctype_uid: items.append((container.UID(), container.Title())) items = tuple(items) return items