Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.0.0 (Unreleased)
------------------

- #35 Rejection inbound samples
- #32 Fix build test after adding default contact
- #31 Add default contact for ExternalLaboratory and InboundSampleShipment content
- First version
11 changes: 3 additions & 8 deletions src/senaite/referral/browser/inbound/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="senaite.referral">

<!-- Package includes -->
<include package=".samples"/>

<!-- Inbound Sample Shipment view -->
<browser:page
name="view"
Expand All @@ -12,12 +15,4 @@
permission="zope2.View"
layer="senaite.referral.interfaces.ISenaiteReferralLayer" />

<!-- Inbound Sample Shipment's samples listing -->
<browser:page
name="samples_listing"
for="senaite.referral.interfaces.IInboundSampleShipment"
class=".samples.SamplesListingView"
permission="zope2.View"
layer="senaite.referral.interfaces.ISenaiteReferralLayer" />

</configure>
19 changes: 19 additions & 0 deletions src/senaite/referral/browser/inbound/samples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.REFERRAL.
#
# SENAITE.REFERRAL 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 2021-2025 by it's authors.
# Some rights reserved, see README and LICENSE.
17 changes: 17 additions & 0 deletions src/senaite/referral/browser/inbound/samples/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="senaite.referral">

<!-- Package includes -->
<include package=".rejection"/>

<!-- Inbound Sample Shipment's samples listing -->
<browser:page
name="samples_listing"
for="senaite.referral.interfaces.IInboundSampleShipment"
class=".view.SamplesListingView"
permission="zope2.View"
layer="senaite.referral.interfaces.ISenaiteReferralLayer" />

</configure>
19 changes: 19 additions & 0 deletions src/senaite/referral/browser/inbound/samples/rejection/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.REFERRAL.
#
# SENAITE.REFERRAL 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 2021-2025 by it's authors.
# Some rights reserved, see README and LICENSE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="senaite.referral">

<!-- Reject Inbound Samples View -->
<browser:page
for="*"
name="reject_inbound_samples"
class=".reject_inbound_samples.RejectInboundSamplesView"
permission="senaite.core.permissions.ManageBika"
layer="senaite.referral.interfaces.ISenaiteReferralLayer" />

</configure>
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.REFERRAL.
#
# SENAITE.REFERRAL 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 2021-2025 by it's authors.
# Some rights reserved, see README and LICENSE.

import six
from bika.lims import api
from bika.lims import senaiteMessageFactory as _
from plone.memoize import view
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from senaite.core import logger
from senaite.core.api.dtime import to_localized_time
from senaite.referral.catalog import INBOUND_SAMPLE_CATALOG


class RejectInboundSamplesView(BrowserView):
"""View that renders the Inbound Samples rejection view
"""
template = ViewPageTemplateFile("templates/reject_inbound_samples.pt")

def __init__(self, context, request):
super(RejectInboundSamplesView, self).__init__(context, request)
self.context = context
self.request = request
self.back_url = api.get_url(self.context.getInboundShipment())

def __call__(self):
form = self.request.form

# Form submit toggle
form_submitted = form.get("submitted", False)

# Buttons
form_continue = form.get("button_continue", False)
form_cancel = form.get("button_cancel", False)

# Get the objects from request
inbound_samples = self.get_inbound_samples_from_request()

# No Inbound Samples selected
if not inbound_samples:
return self.redirect(message=_("No items selected"),
level="warning")

# Handle rejection
if form_submitted and form_continue:
logger.info("*** REJECT INBOUND SAMPLES ***")
processed = []
for inbound_sample in form.get("inbound_samples", []):
inbound_sample_uid = inbound_sample.get("uid", "")
reasons = inbound_sample.get("reasons", [])
other = inbound_sample.get("other_reasons", "")
if not inbound_sample_uid:
continue

# Omit if no rejection reason specified
if not any([reasons, other]):
continue

obj = api.get_object_by_uid(inbound_sample_uid)

obj.setSelectedRejectionReasons(reasons)
obj.setOtherRejectionReasons(other)

# Reject the sample
processed.append(obj)

if not processed:
return self.redirect(message=_("No samples were rejected"))

message = _("Rejected {} samples: {}").format(
len(processed), ", ".join(map(api.get_id, processed)))
return self.redirect(message=message)

# Handle cancel
if form_submitted and form_cancel:
logger.info("*** CANCEL REJECTION ***")
return self.redirect(message=_("Rejection cancelled"))

return self.template()

@view.memoize
def get_inbound_samples_from_request(self):
"""Returns a list of objects coming from the "uids" request parameter
"""
uids = self.request.form.get("uids", "")
if isinstance(uids, six.string_types):
uids = uids.split(",")

uids = list(set(uids))
if not uids:
return []

inbound_samples = []
query = dict(portal_type="InboundSample", UID=uids)
for brain in api.search(query, INBOUND_SAMPLE_CATALOG):
inbound_sample = api.get_object(brain)
inbound_samples.append(inbound_sample)
return inbound_samples

@view.memoize
def get_rejection_reasons(self):
"""Returns the list of available rejection reasons
"""
return api.get_setup().getRejectionReasonsItems()

def get_inbound_samples_data(self):
"""Returns a list of Inbound Samples data (dictionary)
"""
for obj in self.get_inbound_samples_from_request():
yield {
"obj": obj,
"id": api.get_id(obj),
"uid": api.get_uid(obj),
"title": obj.Title(),
"sample_type": obj.getSampleType(),
"analyses": obj.getAnalyses(),
"date": to_localized_time(
obj.getDateSampled(), long_format=True
),
}

def redirect(self, redirect_url=None, message=None, level="info"):
"""Redirect with a message
"""
if redirect_url is None:
redirect_url = self.back_url
if message is not None:
self.add_status_message(message, level)
return self.request.response.redirect(redirect_url)

def add_status_message(self, message, level="info"):
"""Set a portal status message
"""
return self.context.plone_utils.addPortalMessage(message, level)
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="here/main_template/macros/master"
i18n:domain="senaite.referral">

<head>
<metal:block fill-slot="senaite_legacy_resources"
tal:define="portal context/@@plone_portal_state/portal;">
</metal:block>
</head>

<body>

<!-- Title -->
<metal:title fill-slot="content-title">
<h2 i18n:translate="" class="d-inline align-middle">
Reject Inbound Samples
</h2>
</metal:title>

<!-- Description -->
<metal:description fill-slot="content-description">
</metal:description>

<!-- Content -->
<metal:core fill-slot="content-core">

<div class="d-flex mb-3">
<a href="#" class="btn btn-outline-secondary"
tal:attributes="href python:context.absolute_url()"
i18n:translate="">
&larr; Go back</a>
</div>

<div id="reject-samples-view"
tal:define="portal context/@@plone_portal_state/portal;">

<form class="form"
id="reject_samples_form"
name="reject_samples_form"
method="POST">

<!-- Hidden Fields -->
<input type="hidden" name="submitted" value="1"/>
<input tal:replace="structure context/@@authenticator/authenticator"/>

<tal:samples repeat="inbound_sample view/get_inbound_samples_data">

<!-- Remember the initial UIDs coming in -->
<input type="hidden" name="uids:list"
tal:attributes="value inbound_sample/uid"/>

<div class="card mb-3">

<div class="card-header">
<!-- Origin Sample ID -->
<div tal:content="inbound_sample/title"></div>

<!-- Sample Type -->
<div class="d-inline-flex">
<div tal:content="inbound_sample/sample_type"></div>
</div>

<!-- Analyses -->
<div class="d-inline-flex">
<div tal:content="inbound_sample/analyses"></div>
</div>
</div>

<div class="card-body"
tal:define="reasons python: view.get_rejection_reasons()">

<input type="hidden" name="inbound_samples.uid:records"
tal:attributes="value inbound_sample/uid"/>

<!-- Pre-defined rejection reasons -->
<div class="form-group field">
<label class="font-weight-bold" i18n:translate="">
Rejection reasons
</label>
<div tal:condition="reasons" tal:repeat="reason reasons">
<input type="checkbox"
tal:attributes="name string:inbound_samples.reasons:records:list;
value reason"/>
<span tal:content="reason"></span>
</div>
<div tal:condition="python: not reasons" i18n:translate="">
There are no pre-defined conditions set
</div>
</div>

<!-- Other rejection reasons -->
<div class="form-group">
<label class="font-weight-bold"
tal:condition="reasons" i18n:translate="">
Other reasons
</label>
<label class="font-weight-bold"
tal:condition="python: not reasons"
i18n:translate="">
Rejection reasons
</label>
<textarea rows="5" class="form-control"
tal:attributes="name string:inbound_samples.other_reasons:records"></textarea>
</div>

</div>
</div>
</tal:samples>

<!-- Form Controls -->
<div>
<!-- Cancel -->
<input class="btn btn-secondary btn-sm"
type="submit"
name="button_cancel"
i18n:attributes="value"
value="Cancel"/>
<!-- Continue -->
<input class="btn btn-danger btn-sm"
type="submit"
name="button_continue"
i18n:attributes="value"
value="Reject"/>
</div>
</form>
</div>
</metal:core>
</body>
</html>
Loading