diff --git a/CHANGES.rst b/CHANGES.rst index 781ab4986d..1b2a4467c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,7 @@ Changelog **Changed** +- #1495 Better Remarks handling and display - #1502 Improved DateTime Widget - #1490 Support Dexterity Behavior Fields in API - #1488 Support Dexterity Contents in Catalog Indexers diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt index 8df7a0b733..b71bf6a148 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt @@ -8,21 +8,6 @@ - - @@ -87,7 +72,7 @@ https://github.com/senaite/senaite.core/pull/920 -->
Remarks -
- -
+
diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt index afd27104af..6ef8edea80 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt @@ -8,21 +8,6 @@ - - @@ -72,7 +57,7 @@ https://github.com/senaite/senaite.core/pull/920 -->
Remarks -
- -
+
diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 4a590eb0ca..3f30876073 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -18,18 +18,102 @@ # Copyright 2018-2019 by it's authors. # Some rights reserved, see README and LICENSE. +import re + +import six + from AccessControl import ClassSecurityInfo -from AccessControl import getSecurityManager +from bika.lims import api from bika.lims.browser.widgets import RemarksWidget +from bika.lims.events import RemarksAddedEvent from bika.lims.interfaces import IRemarksField +from bika.lims.utils import tmpID from DateTime import DateTime from Products.Archetypes.event import ObjectEditedEvent from Products.Archetypes.Field import ObjectField from Products.Archetypes.Registry import registerField +from Products.CMFPlone.i18nl10n import ulocalized_time from zope import event from zope.interface import implements +class RemarksHistory(list): + """A list containing a remarks history, but __str__ returns the legacy + format from instances prior v1.3.3 + """ + + def html(self): + return api.text_to_html(str(self)) + + def __str__(self): + """Returns the remarks in legacy format + """ + remarks = map(lambda rec: str(rec), self) + remarks = filter(None, remarks) + return "\n".join(remarks) + + def __eq__(self, y): + if isinstance(y, six.string_types): + return str(self) == y + return super(RemarksHistory, self).__eq__(y) + + +class RemarksHistoryRecord(dict): + """A dict implementation that represents a record/entry of Remarks History + """ + + def __init__(self, *arg, **kw): + super(RemarksHistoryRecord, self).__init__(*arg, **kw) + self["id"] = self.id or tmpID() + self["user_id"] = self.user_id + self["user_name"] = self.user_name + self["created"] = self.created or DateTime().ISO() + self["content"] = self.content + + @property + def id(self): + return self.get("id", "") + + @property + def user_id(self): + return self.get("user_id", "") + + @property + def user_name(self): + return self.get("user_name", "") + + @property + def created(self): + return self.get("created", "") + + @property + def created_ulocalized(self): + return ulocalized_time(self.created, + long_format=True, + context=api.get_portal(), + request=api.get_request(), + domain="senaite.core") + + @property + def content(self): + return self.get("content", "") + + @property + def html_content(self): + return api.text_to_html(self.content) + + def __str__(self): + """Returns a legacy string format of the Remarks record + """ + if not self.content: + return "" + if self.created and self.user_id: + # Build the legacy format + return "=== {} ({})\n{}".format(self.created, self.user_id, + self.content) + return self.content + + class RemarksField(ObjectField): """A field that stores remarks. The value submitted to the setter will always be appended to the actual value of the field. @@ -44,40 +128,65 @@ class RemarksField(ObjectField): }) implements(IRemarksField) - security = ClassSecurityInfo() - security.declarePrivate('set') + @property + def searchable(self): + """Returns False, preventing this field to be searchable by AT's + SearcheableText + """ + return False + @security.private def set(self, instance, value, **kwargs): """Adds the value to the existing text stored in the field, along with a small divider showing username and date of this entry. """ + if not value: return - value = value.strip() - date = DateTime().rfc822() - user = getSecurityManager().getUser() - username = user.getUserName() - divider = "=== {} ({})".format(date, username) - existing_remarks = instance.getRawRemarks() - remarks = '\n'.join([divider, value, existing_remarks]) - ObjectField.set(self, instance, remarks) - # reindex the object after save to update all catalog metadata + + if isinstance(value, RemarksHistory): + # Override the whole history here + history = value + + elif isinstance(value, (list, tuple)): + # This is a list, convert to RemarksHistory + remarks = map(lambda item: RemarksHistoryRecord(item), value) + history = RemarksHistory(remarks) + + elif isinstance(value, RemarksHistoryRecord): + # This is a record, append to the history + history = self.get_history(instance) + history.insert(0, value) + + elif isinstance(value, six.string_types): + # Create a new history record + record = self.to_history_record(value) + + # Append the new record to the history + history = self.get_history(instance) + history.insert(0, record) + + else: + raise ValueError("Type not supported: {}".format(type(value))) + + # Store the data + ObjectField.set(self, instance, history) + + # N.B. ensure updated catalog metadata for the snapshot instance.reindexObject() + # notify object edited event event.notify(ObjectEditedEvent(instance)) - def get_cooked_remarks(self, instance): - text = self.get(instance) - if not text: - return "" - return text.replace('\n', '
') + # notify new remarks for e.g. later email notification etc. + event.notify(RemarksAddedEvent(instance, history)) def get(self, instance, **kwargs): - """Returns raw field value. + """Returns a RemarksHistory object """ - return self.getRaw(instance, **kwargs) + return self.get_history(instance) def getRaw(self, instance, **kwargs): """Returns raw field value (possible wrapped in BaseUnit) @@ -88,5 +197,92 @@ def getRaw(self, instance, **kwargs): value = value() return value + def to_history_record(self, value): + """Transforms the value to an history record + """ + user = api.get_current_user() + contact = api.get_user_contact(user) + fullname = contact and contact.getFullname() or "" + if not contact: + # get the fullname from the user properties + props = api.get_user_properties(user) + fullname = props.get("fullname", "") + return RemarksHistoryRecord(user_id=user.id, + user_name=fullname, + content=value.strip()) + + def get_history(self, instance): + """Returns a RemarksHistory object with the remarks entries + """ + remarks = instance.getRawRemarks() + if not remarks: + return RemarksHistory() + + # Backwards compatibility with legacy from < v1.3.3 + if isinstance(remarks, six.string_types): + parsed_remarks = self._parse_legacy_remarks(remarks) + if parsed_remarks is None: + remark = RemarksHistoryRecord(content=remarks.strip()) + remarks = RemarksHistory([remark, ]) + else: + remarks = RemarksHistory( + map(lambda r: RemarksHistoryRecord(r), parsed_remarks)) + + return remarks + + def _parse_legacy_remarks(self, remarks): + """Parse legacy remarks + """ + records = [] + # split legacy remarks on the "===" delimiter into lines + lines = remarks.split("===") + for line in lines: + # skip empty lines + if line == "": + continue + + # strip leading and trailing whitespaces + line = line.strip() + + # split the line into date, user and content + groups = re.findall(r"(.*) \((.*)\)\n(.*)", line, re.DOTALL) + + # we should have one tuple in the list + if len(groups) != 1: + continue + + group = groups[0] + + # cancel the whole parsing + if len(group) != 3: + return None + + created, userid, content = group + + # try to get the full name of the user id + fullname = self._get_fullname_from_user_id(userid) + + # append the record + records.append({ + "created": created, + "user_id": userid, + "user_name": fullname, + "content": content, + }) + + return records + + def _get_fullname_from_user_id(self, userid, default=""): + """Try the fullname of the user + """ + fullname = default + user = api.get_user(userid) + if user: + props = api.get_user_properties(user) + fullname = props.get("fullname", fullname) + contact = api.get_user_contact(user) + fullname = contact and contact.getFullname() or fullname + return fullname + -registerField(RemarksField, title='Remarks', description='') +registerField(RemarksField, title="Remarks", description="") diff --git a/bika/lims/browser/js/bika.lims.worksheet.js b/bika/lims/browser/js/bika.lims.worksheet.js index fb4764eb66..64bcd16cba 100644 --- a/bika/lims/browser/js/bika.lims.worksheet.js +++ b/bika/lims/browser/js/bika.lims.worksheet.js @@ -881,13 +881,17 @@ el = event.currentTarget; $(el).prepOverlay({ subtype: "ajax", - filter: "h1,span.remarks_history", + filter: "h1,div.remarks-widget", config: { closeOnClick: true, closeOnEsc: true, onBeforeLoad: function(event) { var overlay; overlay = this.getOverlay(); + $("div.pb-ajax>div", overlay).addClass("container"); + $("h3", overlay).remove(); + $("textarea", overlay).remove(); + $("input", overlay).remove(); return overlay.draggable(); }, onLoad: function(event) { diff --git a/bika/lims/browser/js/coffee/bika.lims.worksheet.coffee b/bika/lims/browser/js/coffee/bika.lims.worksheet.coffee index 41890ed40e..88b13cf95d 100644 --- a/bika/lims/browser/js/coffee/bika.lims.worksheet.coffee +++ b/bika/lims/browser/js/coffee/bika.lims.worksheet.coffee @@ -824,12 +824,18 @@ class window.WorksheetManageResultsView # https://github.com/plone/plone.app.jquerytools/blob/master/plone/app/jquerytools/browser/overlayhelpers.js $(el).prepOverlay subtype: "ajax" - filter: "h1,span.remarks_history" + filter: "h1,div.remarks-widget" config: closeOnClick: yes closeOnEsc: yes onBeforeLoad: (event) -> overlay = this.getOverlay() + $("div.pb-ajax>div", overlay).addClass("container") + # Remove editable elements + $("h3", overlay).remove() + $("textarea", overlay).remove() + $("input", overlay).remove() + # make the overlay draggable overlay.draggable() onLoad: (event) -> $.mask.close() diff --git a/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee index 9e208916d1..10ccbaaf6e 100644 --- a/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee +++ b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee @@ -23,6 +23,7 @@ class window.RemarksWidgetView console.debug "RemarksWidgetView::bind_eventhandler" $("body").on "click", "input.saveRemarks", @on_remarks_submit + $("body").on "keyup", "textarea[name='Remarks']", @on_remarks_change # dev only window.rem = @ @@ -62,10 +63,21 @@ class window.RemarksWidgetView ### * Clear and update the widget's History with the provided value. ### + return if value.length < 1 widget = @get_remarks_widget(uid) return if widget is null el = widget.find('.remarks_history') - el.html(@format value) + val = value[0] + record_header = $("
") + record_header.append $(""+val["user_id"]+"") + record_header.append $(""+val["user_name"]+"") + record_header.append $(""+val["created"]+"") + record_content = $("
") + record_content.html(@format(val["content"])) + record = $("
") + record.append record_header + record.append record_content + el.prepend record clear_remarks_textarea: (uid) => ### @@ -154,9 +166,22 @@ class window.RemarksWidgetView ### EVENT HANDLERS ### + on_remarks_change: (event) => + ### + * Eventhandler for RemarksWidget's textarea changes + * + ### + console.debug "°°° RemarksWidgetView::on_remarks_change °°°" + el = event.target + return unless el.value + btn = el.parentElement.querySelector("input.saveRemarks") + # Enable the button + btn.disabled = false + + on_remarks_submit: (event) => ### - * Eventhandler for RemarksWidget"s "Save Remarks" button + * Eventhandler for RemarksWidget's "Save Remarks" button * ### console.debug "°°° RemarksWidgetView::on_remarks_submit °°°" diff --git a/bika/lims/browser/worksheet/templates/results.pt b/bika/lims/browser/worksheet/templates/results.pt index c78d9f75de..79a8710e1d 100644 --- a/bika/lims/browser/worksheet/templates/results.pt +++ b/bika/lims/browser/worksheet/templates/results.pt @@ -12,18 +12,6 @@ - - @@ -216,7 +204,7 @@ https://github.com/senaite/senaite.core/pull/920 -->
Remarks -
- -
+
diff --git a/bika/lims/content/analysisprofile.py b/bika/lims/content/analysisprofile.py index 42c4f2a8a6..b27050dcc8 100644 --- a/bika/lims/content/analysisprofile.py +++ b/bika/lims/content/analysisprofile.py @@ -24,26 +24,32 @@ """ from AccessControl import ClassSecurityInfo +from Products.ATExtensions.field import RecordsField +from Products.Archetypes.public import BaseContent +from Products.Archetypes.public import BooleanField +from Products.Archetypes.public import BooleanWidget +from Products.Archetypes.public import ComputedField +from Products.Archetypes.public import ComputedWidget +from Products.Archetypes.public import DecimalWidget +from Products.Archetypes.public import FixedPointField +from Products.Archetypes.public import ReferenceField +from Products.Archetypes.public import Schema +from Products.Archetypes.public import StringField +from Products.Archetypes.public import StringWidget +from Products.Archetypes.public import TextAreaWidget +from Products.Archetypes.public import TextField +from Products.Archetypes.public import registerType +from Products.CMFCore.utils import getToolByName +from zope.interface import implements + from bika.lims import api -from bika.lims import PMF, bikaMessageFactory as _ -from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims import bikaMessageFactory as _ from bika.lims.browser.widgets import AnalysisProfileAnalysesWidget -from bika.lims.browser.widgets import RemarksWidget -from bika.lims.browser.widgets import ServicesWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from Products.Archetypes.public import * - 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 -from Products.CMFCore.permissions import View, ModifyPortalContent -from Products.CMFCore.utils import getToolByName -from zope.interface import Interface, implements -import sys from bika.lims.interfaces import IAnalysisProfile -from bika.lims.interfaces import IClient +from bika.lims.interfaces import IDeactivable schema = BikaSchema.copy() + Schema(( StringField('ProfileKey', @@ -65,10 +71,11 @@ description = _("The analyses included in this profile, grouped per category"), ) ), - RemarksField('Remarks', - searchable=True, - widget=RemarksWidget( - label=_("Remarks") + TextField( + "Remarks", + allowable_content_types=("text/plain",), + widget=TextAreaWidget( + label=_("Remarks"), ) ), # Custom settings for the assigned analysis services diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 227d7ead22..53d5255680 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -1069,7 +1069,6 @@ RemarksField( 'Remarks', - searchable=True, read_permission=View, write_permission=FieldEditRemarks, widget=RemarksWidget( diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 72f8b260ca..1d5d425de4 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -30,6 +30,8 @@ from Products.Archetypes.public import DisplayList from Products.Archetypes.public import ReferenceField from Products.Archetypes.public import Schema +from Products.Archetypes.public import TextAreaWidget +from Products.Archetypes.public import TextField from Products.Archetypes.public import registerType from Products.Archetypes.references import HoldingReference from Products.CMFCore.utils import getToolByName @@ -37,11 +39,9 @@ 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 @@ -115,12 +115,12 @@ description=_("Enable sampling workflow for the created sample") ), ), - RemarksField( + TextField( "Remarks", - searchable=True, - widget=RemarksWidget( + allowable_content_types=("text/plain",), + widget=TextAreaWidget( label=_("Remarks"), - ), + ) ), RecordsField( "Partitions", diff --git a/bika/lims/content/batch.py b/bika/lims/content/batch.py index 1d9969af83..8bf8315409 100644 --- a/bika/lims/content/batch.py +++ b/bika/lims/content/batch.py @@ -119,7 +119,6 @@ def BatchDate(instance): RemarksField( 'Remarks', - searchable=True, widget=RemarksWidget( label=_('Remarks'), ) diff --git a/bika/lims/content/instrumentcertification.py b/bika/lims/content/instrumentcertification.py index 1e810f2a40..f1509c4bf8 100644 --- a/bika/lims/content/instrumentcertification.py +++ b/bika/lims/content/instrumentcertification.py @@ -194,12 +194,12 @@ ) ), - RemarksField( - 'Remarks', - searchable=True, - widget=RemarksWidget( + TextField( + "Remarks", + allowable_content_types=("text/plain",), + widget=TextAreaWidget( label=_("Remarks"), - ), + ) ), )) diff --git a/bika/lims/content/pricelist.py b/bika/lims/content/pricelist.py index 18a0bb83dc..fe1f30b39b 100644 --- a/bika/lims/content/pricelist.py +++ b/bika/lims/content/pricelist.py @@ -19,14 +19,6 @@ # Some rights reserved, see README and LICENSE. from AccessControl import ClassSecurityInfo -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import RemarksWidget -from bika.lims.config import PRICELIST_TYPES -from bika.lims.config import PROJECTNAME -from bika.lims.content.bikaschema import BikaSchema -from bika.lims.interfaces import IDeactivable -from bika.lims.interfaces import IPricelist from DateTime import DateTime from Products.Archetypes.public import BaseFolder from Products.Archetypes.public import BooleanField @@ -36,9 +28,17 @@ from Products.Archetypes.public import Schema from Products.Archetypes.public import SelectionWidget from Products.Archetypes.public import StringField +from Products.Archetypes.public import TextAreaWidget +from Products.Archetypes.public import TextField from Products.Archetypes.public import registerType from zope.interface import implements +from bika.lims import bikaMessageFactory as _ +from bika.lims.config import PRICELIST_TYPES +from bika.lims.config import PROJECTNAME +from bika.lims.content.bikaschema import BikaSchema +from bika.lims.interfaces import IDeactivable +from bika.lims.interfaces import IPricelist schema = BikaSchema.copy() + Schema(( @@ -77,14 +77,14 @@ ), ), - RemarksField( + TextField( "Remarks", - searchable=True, - widget=RemarksWidget( + allowable_content_types=("text/plain",), + widget=TextAreaWidget( label=_("Remarks"), - ), - )), -) + ) + ), +)) Field = schema["title"] Field.required = 1 diff --git a/bika/lims/content/referencesample.py b/bika/lims/content/referencesample.py index 78c4415947..93b120712b 100644 --- a/bika/lims/content/referencesample.py +++ b/bika/lims/content/referencesample.py @@ -100,13 +100,13 @@ label=_("Lot Number"), ), ), - RemarksField( - 'Remarks', - schemata='Description', - searchable=True, - widget=RemarksWidget( + TextField( + "Remarks", + allowable_content_types=("text/plain",), + schemata="Description", + widget=TextAreaWidget( label=_("Remarks"), - ), + ) ), DateTimeField('DateSampled', schemata = 'Dates', diff --git a/bika/lims/content/sample.py b/bika/lims/content/sample.py index 207a179991..6edb677d42 100644 --- a/bika/lims/content/sample.py +++ b/bika/lims/content/sample.py @@ -343,7 +343,6 @@ ), RemarksField( 'Remarks', - searchable=True, widget=RemarksWidget( label=_("Remarks"), ), diff --git a/bika/lims/content/supplier.py b/bika/lims/content/supplier.py index 5c6f6a8c07..b06a7b59ad 100644 --- a/bika/lims/content/supplier.py +++ b/bika/lims/content/supplier.py @@ -19,27 +19,28 @@ # Some rights reserved, see README and LICENSE. from AccessControl import ClassSecurityInfo -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import RemarksWidget -from bika.lims.config import PROJECTNAME -from bika.lims.content.organisation import Organisation -from bika.lims.interfaces import ISupplier, IDeactivable -from zope.interface import implements -from Products.Archetypes.public import registerType -from Products.Archetypes.public import StringField from Products.Archetypes.public import ManagedSchema +from Products.Archetypes.public import StringField from Products.Archetypes.public import StringWidget +from Products.Archetypes.public import TextAreaWidget +from Products.Archetypes.public import TextField +from Products.Archetypes.public import registerType +from zope.interface import implements +from bika.lims import bikaMessageFactory as _ +from bika.lims.config import PROJECTNAME +from bika.lims.content.organisation import Organisation +from bika.lims.interfaces import IDeactivable +from bika.lims.interfaces import ISupplier schema = Organisation.schema.copy() + ManagedSchema(( - RemarksField( + TextField( "Remarks", - searchable=True, - widget=RemarksWidget( + allowable_content_types=("text/plain",), + widget=TextAreaWidget( label=_("Remarks"), - ), + ) ), StringField( diff --git a/bika/lims/content/supplyorder.py b/bika/lims/content/supplyorder.py index 9440883d3f..21293bb120 100644 --- a/bika/lims/content/supplyorder.py +++ b/bika/lims/content/supplyorder.py @@ -19,30 +19,36 @@ # Some rights reserved, see README and LICENSE. import sys - -from Products.Archetypes.public import * +from decimal import Decimal from AccessControl import ClassSecurityInfo -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import DateTimeWidget -from bika.lims.browser.widgets import RemarksWidget -from bika.lims.browser.widgets import ReferenceWidget as BikaReferenceWidget -from bika.lims.config import PROJECTNAME -from bika.lims.content.bikaschema import BikaSchema -from bika.lims.interfaces import ISupplyOrder, ICancellable -from bika.lims.utils import t from DateTime import DateTime -from persistent.mapping import PersistentMapping -from decimal import Decimal 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 DateTimeField +from Products.Archetypes.public import ReferenceField +from Products.Archetypes.public import Schema +from Products.Archetypes.public import StringField +from Products.Archetypes.public import StringWidget +from Products.Archetypes.public import TextAreaWidget +from Products.Archetypes.public import TextField from Products.Archetypes.references import HoldingReference from Products.CMFCore.permissions import View from Products.CMFPlone.interfaces import IConstrainTypes from Products.CMFPlone.utils import safe_unicode +from persistent.mapping import PersistentMapping from zope.component import getAdapter from zope.interface import implements +from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.widgets import DateTimeWidget +from bika.lims.browser.widgets import ReferenceWidget as BikaReferenceWidget +from bika.lims.config import PROJECTNAME +from bika.lims.content.bikaschema import BikaSchema +from bika.lims.interfaces import ICancellable +from bika.lims.interfaces import ISupplyOrder schema = BikaSchema.copy() + Schema(( ReferenceField( @@ -96,12 +102,12 @@ label=_("Date Dispatched"), ), ), - RemarksField( - 'Remarks', - searchable=True, - widget=RemarksWidget( + TextField( + "Remarks", + allowable_content_types=("text/plain",), + widget=TextAreaWidget( label=_("Remarks"), - ), + ) ), ComputedField('ClientUID', expression = 'here.aq_parent.UID()', diff --git a/bika/lims/content/worksheet.py b/bika/lims/content/worksheet.py index b606cbb83e..cd4180befc 100644 --- a/bika/lims/content/worksheet.py +++ b/bika/lims/content/worksheet.py @@ -110,8 +110,8 @@ RemarksField( 'Remarks', - searchable=True, widget=RemarksWidget( + render_own_label=True, label=_("Remarks"), ), ), diff --git a/bika/lims/events/__init__.py b/bika/lims/events/__init__.py new file mode 100644 index 0000000000..1ae2978f09 --- /dev/null +++ b/bika/lims/events/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from remarks import IRemarksAddedEvent # noqa +from remarks import RemarksAddedEvent # noqa diff --git a/bika/lims/events/remarks.py b/bika/lims/events/remarks.py new file mode 100644 index 0000000000..1a0688d9bb --- /dev/null +++ b/bika/lims/events/remarks.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from zope.interface import Interface +from zope.interface import implements + + +class IRemarksAddedEvent(Interface): + """Remarks Added Event + """ + + +class RemarksAddedEvent(object): + implements(IRemarksAddedEvent) + + def __init__(self, context, history): + self.context = context + self.history = history diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.css b/bika/lims/skins/bika/bika_widgets/remarkswidget.css new file mode 100644 index 0000000000..0e7a8ab04f --- /dev/null +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.css @@ -0,0 +1,21 @@ +.remarks-widget { + margin: 20px 0 20px; } + +.remarks-widget input.saveRemarks { + margin: 10px 0 20px; } + +.remarks-widget .record { + margin: 20px 0 20px; } + +.remarks-widget .record .record-header { + font-size: 0.9em; + color: #666; + border-bottom: 1px solid #efefef; + margin: 0 0 5px; + padding: 0 0 3px; } + +.remarks-widget .record .record-header span { + padding-right:10px; } + +.remarks-widget .record .record-header .record-user { + font-weight:bold; } diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.js b/bika/lims/skins/bika/bika_widgets/remarkswidget.js index 4c1c2daafb..b300edb19a 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.js +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.js @@ -11,6 +11,7 @@ this.get_portal_url = bind(this.get_portal_url, this); this.ajax_submit = bind(this.ajax_submit, this); this.on_remarks_submit = bind(this.on_remarks_submit, this); + this.on_remarks_change = bind(this.on_remarks_change, this); this.post_remarks = bind(this.post_remarks, this); this.fetch_remarks = bind(this.fetch_remarks, this); this.set_remarks = bind(this.set_remarks, this); @@ -41,6 +42,7 @@ */ console.debug("RemarksWidgetView::bind_eventhandler"); $("body").on("click", "input.saveRemarks", this.on_remarks_submit); + $("body").on("keyup", "textarea[name='Remarks']", this.on_remarks_change); return window.rem = this; }; @@ -91,13 +93,26 @@ /* * Clear and update the widget's History with the provided value. */ - var el, widget; + var el, record, record_content, record_header, val, widget; + if (value.length < 1) { + return; + } widget = this.get_remarks_widget(uid); if (widget === null) { return; } el = widget.find('.remarks_history'); - return el.html(this.format(value)); + val = value[0]; + record_header = $("
"); + record_header.append($("" + val["user_id"] + "")); + record_header.append($("" + val["user_name"] + "")); + record_header.append($("" + val["created"] + "")); + record_content = $("
"); + record_content.html(this.format(val["content"])); + record = $("
"); + record.append(record_header); + record.append(record_content); + return el.prepend(record); }; RemarksWidgetView.prototype.clear_remarks_textarea = function(uid) { @@ -204,10 +219,26 @@ /* EVENT HANDLERS */ + RemarksWidgetView.prototype.on_remarks_change = function(event) { + + /* + * Eventhandler for RemarksWidget's textarea changes + * + */ + var btn, el; + console.debug("°°° RemarksWidgetView::on_remarks_change °°°"); + el = event.target; + if (!el.value) { + return; + } + btn = el.parentElement.querySelector("input.saveRemarks"); + return btn.disabled = false; + }; + RemarksWidgetView.prototype.on_remarks_submit = function(event) { /* - * Eventhandler for RemarksWidget"s "Save Remarks" button + * Eventhandler for RemarksWidget's "Save Remarks" button * */ var widget; diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index 4c88bd9051..8639208510 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -7,49 +7,71 @@ - textarea + + + +
+ +
+
+ + + +
+
+
+
+
- - - - Hide the widget's save button when rendering in portal_factory URLs: - XXX Need some abstracted way to instruct the widget to hide the save button - + + + + +
-
-
- - content - -
+
+ +
+
+ + + +
+
+
+
+
- + diff --git a/bika/lims/subscribers/configure.zcml b/bika/lims/subscribers/configure.zcml index e39cb3bc71..16bf26c8c4 100644 --- a/bika/lims/subscribers/configure.zcml +++ b/bika/lims/subscribers/configure.zcml @@ -3,6 +3,11 @@ xmlns:browser="http://namespaces.zope.org/browser" xmlns:i18n="http://namespaces.zope.org/i18n" i18n_domain="senaite.core"> + +