def add(self, task): """Adds a task to the queue. It pushes the task directly to the queue server via POST and stores the task in the local pool as well :param task: the QueueTask to add :return: the added QueueTask object :rtype: queue.QueueTask """ # Only QueueTask type is supported if not is_task(task): raise ValueError("{} is not supported".format(repr(task))) # Don't add to the queue if the task is already in there if task in self._tasks: logger.warn("Task {} ({}) in the queue already".format( task.name, task.task_short_uid)) return None # Do not add the task if unique and task for same context and name if task.get("unique", False): if self.get_tasks_for(task.context_uid, name=task.name): logger.debug("Task {} for {} in the queue already".format( task.name, task.context_path)) return None # Add the task to the queue server err = None try: self._post("add", payload=task) except (ConnectionError, Timeout, TooManyRedirects) as e: err = "{}: {}".format(type(e).__name__, str(e)) except HTTPError as e: status = e.response.status_code or 500 if status < 500 or status >= 600: raise e message = e.response.json() or {} err = "{}: {}".format(status, message.get("message", str(e))) except APIError as e: if e.status < 500 or e.status >= 600: raise e err = "{}: {}".format(e.status, e.message) if err: # Not able to add the task to the queue server. Keep it locally # so it can be synchronized as soon as we have connectivity again logger.warn(err) capi.get_request().response.setStatus(200) task.update({"offline": "add"}) # Add the task to our local pool task.update({"status": "queued"}) if task not in self._tasks: self._tasks.append(task) # Sort by priority + created self._tasks.sort(key=lambda t: (t.created + (300 * t.priority))) return task
def _sync_push(self): """Pushes the tasks modified locally to the queue server """ for task in filter(lambda t: t.get("offline"), self._tasks): action = task.get("offline") action_func = getattr(self, action) try: task.pop("offline") action_func(task) except Exception as e: # push is not critical to operate, dismiss err = "{}: {}".format(type(e).__name__, str(e)) logger.error(err) capi.get_request().response.setStatus(200)
def bika_url_fetcher(url): """Basically the same as the default_url_fetcher from WeasyPrint, but injects the __ac cookie to make an authenticated request to the resource. """ from weasyprint import VERSION_STRING from weasyprint.compat import Request from weasyprint.compat import urlopen_contenttype request = api.get_request() __ac = request.cookies.get("__ac", "") if request.get_header("HOST") in url: result, mime_type, charset = urlopen_contenttype( Request(url, headers={ 'Cookie': "__ac={}".format(__ac), 'User-Agent': VERSION_STRING, 'Authorization': request._auth, })) return dict(file_obj=result, redirected_url=result.geturl(), mime_type=mime_type, encoding=charset) return default_url_fetcher(url)
def create_sample(**kwargs): """Creates a new sample """ values = kwargs and kwargs or {} request = _api.get_request() date_sampled = DateTime().strftime("%Y-%m-%d") values.update({ "DateSampled": values.get("DateSampled") or date_sampled, }) to_update = ["Client", "Contact", "SampleType"] for portal_type in to_update: field_value = values.get(portal_type) if not field_value: field_value = _api.get_uid(get_object(portal_type)) values[portal_type] = field_value services = None if "services" in values: services = values.pop("services") if not services: services = map(_api.get_uid, get_objects("AnalysisService")) client = _api.get_object_by_uid(values.get("Client")) sample = create_analysisrequest(client, request, values, services) return sample
def guard(self, action): """Returns False if the sample is queued or contains queued analyses """ # Check if this current request life-cycle is handled by a consumer request = capi.get_request() queue_task_uid = request.get("queue_tuid", "") if capi.is_uid(queue_task_uid): ctx_id = capi.get_id(self.context) logger.info("Skip guard for {}: {}".format(ctx_id, action)) return True # Don't do anything if senaite.queue is not enabled if not api.is_queue_enabled(): return True # Check if the sample is queued if api.is_queued(self.context, status=["queued"]): return False # Check whether the sample contains queued analyses for brain in self.context.getAnalyses(): if api.is_queued(brain, status=["queued"]): return False return True
def _post(self, endpoint, resource=None, payload=None, timeout=10): """Sends a POST request to SENAITE's Queue Server Raises an exception if the response status is not HTTP 2xx or timeout :param endpoint: the endpoint to POST against :param resource: (Optional) resource from the endpoint to POST against :param payload: (Optional) hashable payload for the POST """ server_url = api.get_server_url() parts = "/".join(filter(None, [endpoint, resource])) url = "{}/@@API/senaite/v1/queue_server/{}".format(server_url, parts) logger.info("** POST: {}".format(url)) # HTTP Queue Authentication to be added in the request auth = QueueAuth(capi.get_current_user().id) # Additional information to the payload request = capi.get_request() if payload is None: payload = {} payload.update({"__zeo": request.get("SERVER_URL")}) # This might rise exceptions (e.g. TimeoutException) response = self._req.post(url, json=payload, auth=auth, timeout=timeout) # Check the request is successful. Raise exception otherwise response.raise_for_status() # Return the result return response.json()
def new_task(name, context, **kw): """Creates a QueueTask :param name: the name of the task :param context: the context the task is bound or relates to :param min_seconds: (optional) int, minimum seconds to book for the task :param max_seconds: (optional) int, maximum seconds to wait for the task :param retries: (optional) int, maximum number of retries on failure :param username: (optional) str, the name of the user assigned to the task :param priority: (optional) int, the priority value for this task :param unique: (optional) bool, if True, the task will only be added if there is no other task with same name and for same context :param chunk_size: (optional) the number of items to process asynchronously at once from this task (if it contains multiple elements) :return: :class:`QueueTask <QueueTask>` :rtype: senaite.queue.queue.QueueTask """ # Skip attrs that are assigned when the QueueTask is instantiated exclude = ["task_uid", "name", "request", "context_uid", "context_path"] out_keys = filter(lambda k: k not in exclude, kw.keys()) kwargs = dict(map(lambda k: (k, kw[k]), out_keys)) # Create the Queue Task task = QueueTask(name, api.get_request(), context, **kwargs) # Set the username (if provided in kw) task.username = kw.get("username", task.username) return task
def get_request_data(request=None): """Get request header/form data A typical request behind NGINX looks like this: { 'CONNECTION_TYPE': 'close', 'CONTENT_LENGTH': '52', 'CONTENT_TYPE': 'application/x-www-form-urlencoded; charset=UTF-8', 'GATEWAY_INTERFACE': 'CGI/1.1', 'HTTP_ACCEPT': 'application/json, text/javascript, */*; q=0.01', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', 'HTTP_COOKIE': '_ga=GA1.2.1058345096.1522506452; ...', 'HTTP_HOST': 'senaite.ridingbytes.com', 'HTTP_ORIGIN': 'https://senaite.ridingbytes.com', 'HTTP_REFERER': 'https://senaite.ridingbytes.com/clients/client-1/H2O-0054', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36', 'HTTP_X_FORWARDED_FOR': '93.238.47.95', 'HTTP_X_REAL_IP': '93.238.47.95', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', 'PATH_INFO': '/VirtualHostBase/https/senaite.ridingbytes.com/senaite/VirtualHostRoot//@@API/update', 'PATH_TRANSLATED': '/VirtualHostBase/https/senaite.ridingbytes.com/senaite/VirtualHostRoot/@@API/update', 'QUERY_STRING': '', 'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'POST', 'SCRIPT_NAME': '', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '8081', 'SERVER_PROTOCOL': 'HTTP/1.0', 'SERVER_SOFTWARE': 'Zope/(2.13.28, python 2.7.12, linux2) ZServer/1.1', 'channel.creation_time': 1556086048 } :param request: Request object :returns: Dictionary of extracted request header/form data """ # noqa if request is None: # get the request request = api.get_request() # Happens in the test runner if not request: return {} # Try to obtain the real IP address of the client forwarded_for = request.get_header("X_FORWARDED_FOR") real_ip = request.get_header("X_REAL_IP") remote_address = request.get_header("REMOTE_ADDR") return { "comments": request.form.get("comments", ""), "remote_address": forwarded_for or real_ip or remote_address, "user_agent": request.get_header("HTTP_USER_AGENT"), "referer": request.get_header("HTTP_REFERER"), }
def done(self, task): """Notifies the queue that the task has been processed successfully. Sends a POST to the queue server and removes the task from local pool :param task: task's unique id (task_uid) or QueueTask object """ # Tell the queue server the task is done task_uid = get_task_uid(task) payload = {"task_uid": task_uid} err = None try: self._post("done", payload=payload) except (ConnectionError, Timeout, TooManyRedirects) as e: err = "{}: {}".format(type(e).__name__, str(e)) except HTTPError as e: status = e.response.status_code or 500 if status < 500 or status >= 600: raise e message = e.response.json() or {} err = "{}: {}".format(status, message.get("message", str(e))) except APIError as e: if e.status < 500 or e.status >= 600: raise e err = "{}: {}".format(e.status, e.message) if err: # Not able to tell the queue server. Keep it locally so it can be # synchronized as soon as we have connectivity again logger.warn(err) capi.get_request().response.setStatus(200) task_uid = get_task_uid(task_uid) tasks = filter(lambda t: t.task_uid == task_uid, self._tasks) if tasks: task = tasks[0] else: self._tasks.append(task) task.update({"offline": "done"}) return # Remove from local pool self._tasks = filter(lambda t: t.task_uid != task_uid, self._tasks)
def handle_action(context, items_or_uids, action): """Simulates the handling of an action when multiple items from a list are selected and the action button is pressed """ if not isinstance(items_or_uids, (list, tuple)): items_or_uids = [items_or_uids] items_or_uids = map(_api.get_uid, items_or_uids) request = _api.get_request() request.set("workflow_action", action) request.set("uids", items_or_uids) WorkflowActionHandler(context, request)()
def translate_i18n(i18n_msg): """Safely translate and convert to UTF8, any zope i18n msgid returned from senaite health's message factory """ text = to_unicode(i18n_msg) try: request = api.get_request() domain = getattr(i18n_msg, "domain", "senaite.health") text = translate(text, domain=domain, context=request) except UnicodeDecodeError: logger.warn("{} couldn't be translated".format(text)) return to_utf8(text)
def on_object_edited(instance, event): """Event handler when a sample was edited """ # XXX "save" from Sample's header view does not call widget's process_form request = api.get_request() field_name = "DateOfBirth" if field_name in request.form: dob_field = instance.getField(field_name) dob = dob_field.widget.process_form(instance, dob_field, request.form) if dob is not None: dob_field.set(instance, dob[0]) update_patient(instance)
def t(i18n_msg): """Safely translate and convert to UTF8, any zope i18n msgid returned from a bikaMessageFactory _ """ text = to_unicode(i18n_msg) try: request = api.get_request() domain = getattr(i18n_msg, "domain", "senaite.core") text = translate(text, domain=domain, context=request) except UnicodeDecodeError: # TODO: This is only a quick fix logger.warn("{} couldn't be translated".format(text)) return to_utf8(text)
def process(context, request, task_uid=None): # noqa """Processes the task passed-in """ # disable CSRF req.disable_csrf_protection() # Maybe the task uid has been sent via POST task_uid = task_uid or req.get_json().get("task_uid") # Get the task task = get_task(task_uid) if task.username != capi.get_current_user().id: # 403 Authenticated, but user does not have access to the resource _fail(403) # Process t0 = time.time() task_context = task.get_context() if not task_context: _fail(500, "Task's context is not available") # Get the adapter able to process this specific type of task adapter = queryAdapter(task_context, IQueuedTaskAdapter, name=task.name) if not adapter: _fail(501, "No adapter found for {}".format(task.name)) logger.info("Processing task {}: '{}' for '{}' ({}) ...".format( task.task_short_uid, task.name, capi.get_id(task_context), task.context_uid)) # Inject the queue_consumer marker to the request so guards skip checks # against the queue request = capi.get_request() request.set("queue_tuid", task_uid) # If the task refers to a worksheet, inject (ws_id) in params to make # sure guards (assign, un-assign) return True if IWorksheet.providedBy(task_context): request.set("ws_uid", capi.get_uid(task_context)) # Process the task adapter.process(task) # Sleep a bit for minimum effect against userland threads # Better to have a transaction conflict here than in userland min_seconds = task.get("min_seconds", 3) while time.time() - t0 < min_seconds: time.sleep(0.5) msg = "Processed: {}".format(task.task_short_uid) return get_message_summary(msg, "consumer.process")
def get_rejection_pdf(sample): """Generates a pdf with sample rejection reasons """ # Avoid circular dependencies from bika.lims.browser.analysisrequest.reject import \ AnalysisRequestRejectPdfView # Render the html's rejection document tpl = AnalysisRequestRejectPdfView(sample, api.get_request()) html = tpl.template() html = safe_unicode(html).encode("utf-8") # Generate the pdf return createPdf(htmlreport=html)
def __call__(self, context): # XXX Workaround for missing context in nested choice widget vocabulary if context is None: # fetch context from the request request = api.get_request() if request and request["PARENTS"]: context = request["PARENTS"][0] items = [] adapted = IDataBoxBehavior(context, None) if adapted is None: return SimpleVocabulary.fromValues([]) for field in adapted.get_fields(): items.append(SimpleTerm(field, token=field, title=field)) return SimpleVocabulary(items)
def to_localized_time(self, date, **kw): """Converts the given date to a localized time string """ if date is None: return "" # default options options = { "long_format": True, "time_only": False, "context": api.get_portal(), "request": api.get_request(), "domain": "senaite.core", } options.update(kw) return ulocalized_time(date, **options)
def create_sample(services, client, contact, sample_type, receive=True): """Creates a new sample with the specified services """ request = _api.get_request() values = { 'Client': client.UID(), 'Contact': contact.UID(), 'DateSampled': DateTime().strftime("%Y-%m-%d"), 'SampleType': sample_type.UID() } service_uids = map(_api.get_uid, services) sample = create_analysisrequest(client, request, values, service_uids) if receive: do_action_for(sample, "receive") transaction.commit() return sample
def is_worksheet_context(): """Returns whether the current context from the request is a Worksheet """ request = api.get_request() parents = request.get("PARENTS", []) portal_types_names = map(lambda p: getattr(p, "portal_type", None), parents) if "Worksheet" in portal_types_names: return True # Check if the worksheet is declared in request explicitly ws_uid = request.get("ws_uid", "") obj = api.get_object_by_uid(ws_uid, None) if IWorksheet.providedBy(obj): return True return False
def process(browser, task_uid): """Simulates the processing of the task """ request = _api.get_request() site_url = _api.get_url(_api.get_portal()) url = "{}/@@API/senaite/v1/queue_consumer/process".format(site_url) payload = {"task_uid": task_uid} browser.post(url, parse.urlencode(payload, doseq=True)) # We loose the globalrequest each time we do a post with browser globalrequest.setRequest(request) # Mark the task as done api.get_queue().done(task_uid) transaction.commit() return browser.contents
def __call__(self, context): # XXX Workaround for missing context in nested choice widget vocabulary if context is None: # fetch context from the request request = api.get_request() if request and request["PARENTS"]: context = request["PARENTS"][0] items = [] adapted = IDataBoxBehavior(context, None) if adapted is None: return SimpleVocabulary.fromValues([]) catalog = adapted.get_catalog_tool() indexes = catalog.getIndexObjects() for index in indexes: name = index.getId() items.append(SimpleTerm(name, token=name, title=name)) return SimpleVocabulary(items)
def clear_department_cookies(event): """ Logout event handler. When user explicitly logs out from the Logout menu, clean department filtering related cookies. """ if not is_bika_installed(): logger.warn( "Package 'bika.lims' is not installed, skipping event handler " "for IUserLoggedOutEvent.") return request = api.get_request() response = request.RESPONSE # Voiding our special cookie on logout response.setCookie( 'filter_by_department_info', None, path='/', max_age=0) response.setCookie( 'dep_filter_disabled', None, path='/', max_age=0)
def to_task(task_dict): """Converts a dict representation of a task to a QueueTask object :param task_dict: dict that represents a task :return: the QueueTask object the passed-in task_dict represents :rtype: QueueTask """ name = task_dict.get("name") context_uid = task_dict.get("context_uid") context_path = task_dict.get("context_path") if not all([name, context_uid, context_path]): return None # Skip attrs that are assigned when the QueueTask is instantiated exclude = ["name", "request"] out_keys = filter(lambda k: k not in exclude, task_dict.keys()) kwargs = dict(map(lambda k: (k, task_dict[k]), out_keys)) # Create the Queue Task return QueueTask(name, api.get_request(), context_uid, **kwargs)
def url_fetcher(self, url): """Fetches internal URLs by path and not via an external request. N.B. Multiple calls to this method might exhaust the available threads of the server, which causes a hanging instance. """ if url.startswith("data"): logger.info("Data URL, delegate to default URL fetcher...") return default_url_fetcher(url) logger.info("Fetching URL '{}' for WeasyPrint".format(url)) # get the pyhsical path from the URL request = api.get_request() host = request.get_header("HOST") path = "/".join(request.physicalPathFromURL(url)) # fetch the object by sub-request portal = api.get_portal() context = portal.restrictedTraverse(path, None) if context is None or host not in url: logger.info("External URL, delegate to default URL fetcher...") return default_url_fetcher(url) logger.info("Local URL, fetching data by path '{}'".format(path)) # get the data via an authenticated subrequest response = subrequest(path) # Prepare the return data as required by WeasyPrint string = response.getBody() filename = url.split("/")[-1] mime_type = mimetypes.guess_type(url)[0] redirected_url = url return { "string": string, "filename": filename, "mime_type": mime_type, "redirected_url": redirected_url, }
def process(self, task): """Process the task from the queue """ # If there are too many objects to process, split them in chunks to # prevent the task to take too much time to complete chunks = get_chunks(task["uids"], 50) # Process the first chunk map(self.reindex_security, chunks[0]) # Add remaining objects to the queue if chunks[1]: request = _api.get_request() context = task.get_context() kwargs = { "uids": chunks[1], "priority": task.priority, } new_task = QueueTask(task.name, request, context, **kwargs) api.get_queue().add(new_task)
def set_department_cookies(event): """ Login event handler. When user logs in, departments must be selected if filtering by department is enabled in Bika Setup. - For (Lab)Managers and Client Contacts, all the departments from the system must be selected. - For regular Lab Contacts, default Department must be selected. If the Contact doesn't have any default department assigned, then first department in alphabetical order will be selected. """ if not is_bika_installed(): logger.warn( "Package 'bika.lims' is not installed, skipping event handler " "for IUserLoggedInEvent.") return # get the bika_setup object portal = api.get_portal() bika_setup = portal.get("bika_setup") # just to be sure... # This should go into the api.py module once it is in place if bika_setup is None: raise RuntimeError( "bika_setup not found in this Bika LIMS installation") # Getting request, response and username request = api.get_request() response = request.RESPONSE user = api.get_current_user() username = user and user.getUserName() or None is_manager = user and (user.has_role('Manager') or user.has_role('LabManager')) portal_catalog = api.get_tool("portal_catalog") # If department filtering is disabled, disable the cookies if not bika_setup.getAllowDepartmentFiltering(): response.setCookie( 'filter_by_department_info', None, path='/', max_age=0) response.setCookie( 'dep_filter_disabled', None, path='/', max_age=0) return selected_deps = [] # Select all Departments for Lab Managers if is_manager: selected_deps = portal_catalog( portal_type='Department', sort_on='sortable_title', sort_order='ascending', inactive_state='active') response.setCookie( 'dep_filter_disabled', 'true', path='/', max_age=24 * 3600) else: brain = portal_catalog(getUsername=username) # It is possible that current user is created by Plone ZMI. # Just log it as a warning and go on if not brain: logger.warn( "No lab Contact found... Plone user or Client " "Contact logged in. " + username) response.setCookie( 'filter_by_department_info', None, path='/', max_age=0) response.setCookie( 'dep_filter_disabled', None, path='/', max_age=0) return # If it is a Client Contact, select all departments no need to filter. elif brain[0].portal_type == 'Contact': selected_deps = portal_catalog( portal_type='Department', sort_on='sortable_title', sort_order='ascending', inactive_state='active') response.setCookie( 'dep_filter_disabled', None, path='/', max_age=24 * 3600) # It is a LabContact, select only one department. It must be Default # Department of the Lab Contact if possible elif brain[0].portal_type == 'LabContact': lab_con = brain[0].getObject() if lab_con.getDefaultDepartment(): selected_deps = [lab_con.getDefaultDepartment()] else: departments = lab_con.getSortedDepartments() selected_deps = [departments[0]] if departments else [] response.setCookie( 'dep_filter_disabled', None, path='/', max_age=0) selected_dep_uids = ','.join([api.get_uid(dep) for dep in selected_deps]) response.setCookie( 'filter_by_department_info', selected_dep_uids, path='/', max_age=24 * 3600) return
def request(self): request = api.get_request() if not isinstance(request, HTTPRequest): return None return request
def wrapper(*args, **kw): # set the content type header request = api.get_request() request.response.setHeader("Content-Type", "application/json") return func(*args, **kw)
def is_installed(): """Returns whether the product is installed or not """ request = get_request() return ISenaitePatientLayer.providedBy(request)
def senaite_url_fetcher(url): """Uses plone.subrequest to fetch an internal image resource. If the URL points to an external resource, the URL is handed to weasyprint.default_url_fetcher. Please see these links for details: - https://github.com/plone/plone.subrequest - https://pypi.python.org/pypi/plone.subrequest - https://github.com/senaite/senaite.core/issues/538 :returns: A dict with the following keys: * One of ``string`` (a byte string) or ``file_obj`` (a file-like object) * Optionally: ``mime_type``, a MIME type extracted e.g. from a *Content-Type* header. If not provided, the type is guessed from the file extension in the URL. * Optionally: ``encoding``, a character encoding extracted e.g. from a *charset* parameter in a *Content-Type* header * Optionally: ``redirected_url``, the actual URL of the resource if there were e.g. HTTP redirects. * Optionally: ``filename``, the filename of the resource. Usually derived from the *filename* parameter in a *Content-Disposition* header If a ``file_obj`` key is given, it is the caller’s responsibility to call ``file_obj.close()``. """ logger.info("Fetching URL '{}' for WeasyPrint".format(url)) # get the pyhsical path from the URL request = api.get_request() host = request.get_header("HOST") path = "/".join(request.physicalPathFromURL(url)) # fetch the object by sub-request portal = api.get_portal() context = portal.restrictedTraverse(path, None) # We double check here to avoid an edge case, where we have the same path # as well in our local site, e.g. we have `/senaite/img/systems/senaite.png`, # but the user requested http://www.ridingbytes.com/img/systems/senaite.png: # # "/".join(request.physicalPathFromURL("http://www.ridingbytes.com/img/systems/senaite.png")) # '/senaite/img/systems/senaite.png' if context is None or host not in url: logger.info( "URL is external, passing over to the default URL fetcher...") return default_url_fetcher(url) logger.info( "URL is local, fetching data by path '{}' via subrequest".format(path)) # get the data via an authenticated subrequest response = subrequest(path) # Prepare the return data as required by WeasyPrint string = response.getBody() filename = url.split("/")[-1] mime_type = mimetypes.guess_type(url)[0] redirected_url = url return { "string": string, "filename": filename, "mime_type": mime_type, "redirected_url": redirected_url, }