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 -->
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 -->
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 -->
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 @@
-
-
-
- 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
-
+
+
+
+
+
-
-
-
-
+
-
+
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">
+
+