| Total Complexity | 54 |
| Total Lines | 871 |
| Duplicated Lines | 17.45 % |
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like bika.lims.browser.instrument often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # -*- coding: utf-8 -*- |
||
| 2 | # |
||
| 3 | # This file is part of SENAITE.CORE |
||
| 4 | # |
||
| 5 | # Copyright 2018 by it's authors. |
||
| 6 | # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. |
||
| 7 | |||
| 8 | import json |
||
| 9 | |||
| 10 | import plone |
||
| 11 | from bika.lims import api |
||
| 12 | from bika.lims import bikaMessageFactory as _ |
||
| 13 | from bika.lims.browser import BrowserView |
||
| 14 | from bika.lims.browser.analyses import AnalysesView |
||
| 15 | from bika.lims.browser.bika_listing import BikaListingView |
||
| 16 | from bika.lims.browser.chart.analyses import EvolutionChart |
||
| 17 | from bika.lims.browser.resultsimport.autoimportlogs import AutoImportLogsView |
||
| 18 | from bika.lims.browser.viewlets import InstrumentQCFailuresViewlet # noqa |
||
| 19 | from bika.lims.catalog.analysis_catalog import CATALOG_ANALYSIS_LISTING |
||
| 20 | from bika.lims.content.instrumentmaintenancetask import \ |
||
| 21 | InstrumentMaintenanceTaskStatuses as mstatus |
||
| 22 | from bika.lims.utils import get_image, get_link, t |
||
| 23 | from plone.app.layout.globals.interfaces import IViewView |
||
| 24 | from Products.CMFCore.utils import getToolByName |
||
| 25 | from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
||
| 26 | from zExceptions import Forbidden |
||
| 27 | from ZODB.POSException import POSKeyError |
||
| 28 | from zope.interface import implements |
||
| 29 | |||
| 30 | |||
| 31 | class InstrumentMaintenanceView(BikaListingView): |
||
| 32 | """Listing view for instrument maintenance tasks |
||
| 33 | """ |
||
| 34 | |||
| 35 | View Code Duplication | def __init__(self, context, request): |
|
|
|
|||
| 36 | super(InstrumentMaintenanceView, self).__init__(context, request) |
||
| 37 | self.catalog = "portal_catalog" |
||
| 38 | self.contentFilter = { |
||
| 39 | "portal_type": "InstrumentMaintenanceTask", |
||
| 40 | "path": { |
||
| 41 | "query": api.get_path(context), |
||
| 42 | "depth": 1 # searching just inside the specified folder |
||
| 43 | }, |
||
| 44 | "sort_on": "created", |
||
| 45 | "sort_order": "descending", |
||
| 46 | } |
||
| 47 | |||
| 48 | self.form_id = "instrumentmaintenance" |
||
| 49 | self.title = self.context.translate(_("Instrument Maintenance")) |
||
| 50 | |||
| 51 | self.icon = "{}/{}".format( |
||
| 52 | self.portal_url, |
||
| 53 | "++resource++bika.lims.images/instrumentmaintenance_big.png" |
||
| 54 | ) |
||
| 55 | self.context_actions = { |
||
| 56 | _("Add"): { |
||
| 57 | "url": "createObject?type_name=InstrumentMaintenanceTask", |
||
| 58 | "icon": "++resource++bika.lims.images/add.png"} |
||
| 59 | } |
||
| 60 | |||
| 61 | self.allow_edit = False |
||
| 62 | self.show_select_column = False |
||
| 63 | self.show_workflow_action_buttons = True |
||
| 64 | self.pagesize = 30 |
||
| 65 | |||
| 66 | self.columns = { |
||
| 67 | 'getCurrentState': {'title': ''}, |
||
| 68 | 'Title': {'title': _('Task'), |
||
| 69 | 'index': 'sortable_title'}, |
||
| 70 | 'getType': {'title': _('Task type', 'Type'), 'sortable': True}, |
||
| 71 | 'getDownFrom': {'title': _('Down from'), 'sortable': True}, |
||
| 72 | 'getDownTo': {'title': _('Down to'), 'sortable': True}, |
||
| 73 | 'getMaintainer': {'title': _('Maintainer'), 'sortable': True}, |
||
| 74 | } |
||
| 75 | |||
| 76 | self.review_states = [ |
||
| 77 | { |
||
| 78 | "id": "default", |
||
| 79 | "title": _("Open"), |
||
| 80 | "contentFilter": {"cancellation_state": "active"}, |
||
| 81 | "columns": [ |
||
| 82 | "getCurrentState", |
||
| 83 | "Title", |
||
| 84 | "getType", |
||
| 85 | "getDownFrom", |
||
| 86 | "getDownTo", |
||
| 87 | "getMaintainer", |
||
| 88 | ] |
||
| 89 | }, { |
||
| 90 | "id": "cancelled", |
||
| 91 | "title": _("Cancelled"), |
||
| 92 | "contentFilter": {"cancellation_state": "cancelled"}, |
||
| 93 | "columns": [ |
||
| 94 | "getCurrentState", |
||
| 95 | "Title", |
||
| 96 | "getType", |
||
| 97 | "getDownFrom", |
||
| 98 | "getDownTo", |
||
| 99 | "getMaintainer", |
||
| 100 | ] |
||
| 101 | }, { |
||
| 102 | "id": "all", |
||
| 103 | "title": _("All"), |
||
| 104 | "contentFilter": {}, |
||
| 105 | "columns": [ |
||
| 106 | "getCurrentState", |
||
| 107 | "Title", |
||
| 108 | "getType", |
||
| 109 | "getDownFrom", |
||
| 110 | "getDownTo", |
||
| 111 | "getMaintainer" |
||
| 112 | ] |
||
| 113 | } |
||
| 114 | ] |
||
| 115 | |||
| 116 | def localize_date(self, date): |
||
| 117 | """Return the localized date |
||
| 118 | """ |
||
| 119 | return self.ulocalized_time(date, long_format=1) |
||
| 120 | |||
| 121 | def folderitem(self, obj, item, index): |
||
| 122 | """Augment folder listing item |
||
| 123 | """ |
||
| 124 | url = item.get("url") |
||
| 125 | title = item.get("Title") |
||
| 126 | |||
| 127 | item["replace"]["Title"] = get_link(url, value=title) |
||
| 128 | item["getType"] = _(obj.getType()[0]) |
||
| 129 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
| 130 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
| 131 | item["getMaintainer"] = obj.getMaintainer() |
||
| 132 | |||
| 133 | status = obj.getCurrentState() |
||
| 134 | statustext = obj.getCurrentStateI18n() |
||
| 135 | statusimg = "" |
||
| 136 | |||
| 137 | if status == mstatus.CLOSED: |
||
| 138 | statusimg = "instrumentmaintenance_closed.png" |
||
| 139 | item["state_class"] = "state-inactive" |
||
| 140 | elif status == mstatus.CANCELLED: |
||
| 141 | statusimg = "instrumentmaintenance_cancelled.png" |
||
| 142 | item["state_class"] = "state-cancelled" |
||
| 143 | elif status == mstatus.INQUEUE: |
||
| 144 | statusimg = "instrumentmaintenance_inqueue.png" |
||
| 145 | item["state_class"] = "state-open" |
||
| 146 | elif status == mstatus.OVERDUE: |
||
| 147 | statusimg = "instrumentmaintenance_overdue.png" |
||
| 148 | item["state_class"] = "state-open" |
||
| 149 | elif status == mstatus.PENDING: |
||
| 150 | statusimg = "instrumentmaintenance_pending.png" |
||
| 151 | item["state_class"] = "state-pending" |
||
| 152 | |||
| 153 | item["replace"]["getCurrentState"] = get_image( |
||
| 154 | statusimg, title=statustext) |
||
| 155 | return item |
||
| 156 | |||
| 157 | |||
| 158 | class InstrumentCalibrationsView(BikaListingView): |
||
| 159 | """Listing view for instrument calibrations |
||
| 160 | """ |
||
| 161 | |||
| 162 | def __init__(self, context, request): |
||
| 163 | super(InstrumentCalibrationsView, self).__init__(context, request) |
||
| 164 | self.catalog = "portal_catalog" |
||
| 165 | self.contentFilter = { |
||
| 166 | "portal_type": "InstrumentCalibration", |
||
| 167 | "path": { |
||
| 168 | "query": api.get_path(context), |
||
| 169 | "depth": 1 # searching just inside the specified folder |
||
| 170 | }, |
||
| 171 | "sort_on": "created", |
||
| 172 | "sort_order": "descending", |
||
| 173 | } |
||
| 174 | |||
| 175 | self.form_id = "instrumentcalibrations" |
||
| 176 | self.title = self.context.translate(_("Instrument Calibrations")) |
||
| 177 | self.icon = "{}/{}".format( |
||
| 178 | self.portal_url, |
||
| 179 | "++resource++bika.lims.images/instrumentcalibration_big.png" |
||
| 180 | ) |
||
| 181 | self.context_actions = { |
||
| 182 | _("Add"): { |
||
| 183 | "url": "createObject?type_name=InstrumentCalibration", |
||
| 184 | "icon": "++resource++bika.lims.images/add.png"} |
||
| 185 | } |
||
| 186 | |||
| 187 | self.allow_edit = False |
||
| 188 | self.show_select_column = False |
||
| 189 | self.show_workflow_action_buttons = True |
||
| 190 | self.pagesize = 30 |
||
| 191 | |||
| 192 | # instrument calibrations |
||
| 193 | calibrations = self.context.getCalibrations() |
||
| 194 | # current running calibrations |
||
| 195 | self.active_calibrations = filter( |
||
| 196 | lambda c: c.isCalibrationInProgress(), calibrations) |
||
| 197 | self.latest_calibration = self.context.getLatestValidCalibration() |
||
| 198 | |||
| 199 | self.columns = { |
||
| 200 | "Title": {"title": _("Task"), |
||
| 201 | "index": "sortable_title"}, |
||
| 202 | "getDownFrom": {"title": _("Down from")}, |
||
| 203 | "getDownTo": {"title": _("Down to")}, |
||
| 204 | "getCalibrator": {"title": _("Calibrator")}, |
||
| 205 | } |
||
| 206 | self.review_states = [ |
||
| 207 | { |
||
| 208 | "id": "default", |
||
| 209 | "title": _("All"), |
||
| 210 | "contentFilter": {}, |
||
| 211 | "columns": [ |
||
| 212 | "Title", |
||
| 213 | "getDownFrom", |
||
| 214 | "getDownTo", |
||
| 215 | "getCalibrator", |
||
| 216 | ] |
||
| 217 | } |
||
| 218 | ] |
||
| 219 | |||
| 220 | def localize_date(self, date): |
||
| 221 | """Return the localized date |
||
| 222 | """ |
||
| 223 | return self.ulocalized_time(date, long_format=1) |
||
| 224 | |||
| 225 | def folderitem(self, obj, item, index): |
||
| 226 | """Augment folder listing item |
||
| 227 | """ |
||
| 228 | url = item.get("url") |
||
| 229 | title = item.get("Title") |
||
| 230 | calibrator = obj.getCalibrator() |
||
| 231 | |||
| 232 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
| 233 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
| 234 | item["getCalibrator"] = "" |
||
| 235 | if calibrator: |
||
| 236 | props = api.get_user_properties(calibrator) |
||
| 237 | name = props.get("fullname", calibrator) |
||
| 238 | item["getCalibrator"] = name |
||
| 239 | item["replace"]["Title"] = get_link(url, value=title) |
||
| 240 | |||
| 241 | # calibration with the most remaining days |
||
| 242 | if obj == self.latest_calibration: |
||
| 243 | item["state_class"] = "state-published" |
||
| 244 | # running calibrations |
||
| 245 | elif obj in self.active_calibrations: |
||
| 246 | item["state_class"] = "state-active" |
||
| 247 | # inactive calibrations |
||
| 248 | else: |
||
| 249 | item["state_class"] = "state-inactive" |
||
| 250 | |||
| 251 | return item |
||
| 252 | |||
| 253 | |||
| 254 | class InstrumentValidationsView(BikaListingView): |
||
| 255 | """Listing view for instrument validations |
||
| 256 | """ |
||
| 257 | |||
| 258 | def __init__(self, context, request): |
||
| 259 | super(InstrumentValidationsView, self).__init__(context, request) |
||
| 260 | self.catalog = "portal_catalog" |
||
| 261 | self.contentFilter = { |
||
| 262 | "portal_type": "InstrumentValidation", |
||
| 263 | "path": { |
||
| 264 | "query": api.get_path(context), |
||
| 265 | "depth": 1 # searching just inside the specified folder |
||
| 266 | }, |
||
| 267 | "sort_on": "created", |
||
| 268 | "sort_order": "descending", |
||
| 269 | } |
||
| 270 | |||
| 271 | self.form_id = "instrumentvalidations" |
||
| 272 | self.title = self.context.translate(_("Instrument Validations")) |
||
| 273 | self.icon = "{}/{}".format( |
||
| 274 | self.portal_url, |
||
| 275 | "++resource++bika.lims.images/instrumentvalidation_big.png" |
||
| 276 | ) |
||
| 277 | self.context_actions = { |
||
| 278 | _("Add"): { |
||
| 279 | "url": "createObject?type_name=InstrumentValidation", |
||
| 280 | "icon": "++resource++bika.lims.images/add.png"} |
||
| 281 | } |
||
| 282 | |||
| 283 | self.allow_edit = False |
||
| 284 | self.show_select_column = False |
||
| 285 | self.show_workflow_action_buttons = True |
||
| 286 | self.pagesize = 30 |
||
| 287 | |||
| 288 | # instrument validations |
||
| 289 | validations = self.context.getValidations() |
||
| 290 | # current running validations |
||
| 291 | self.active_validations = filter( |
||
| 292 | lambda v: v.isValidationInProgress(), validations) |
||
| 293 | self.latest_validation = self.context.getLatestValidValidation() |
||
| 294 | |||
| 295 | self.columns = { |
||
| 296 | "Title": {"title": _("Task"), |
||
| 297 | "index": "sortable_title"}, |
||
| 298 | "getDownFrom": {"title": _("Down from")}, |
||
| 299 | "getDownTo": {"title": _("Down to")}, |
||
| 300 | "getValidator": {"title": _("Validator")}, |
||
| 301 | } |
||
| 302 | self.review_states = [ |
||
| 303 | { |
||
| 304 | "id": "default", |
||
| 305 | "title": _("All"), |
||
| 306 | "contentFilter": {}, |
||
| 307 | "columns": [ |
||
| 308 | "Title", |
||
| 309 | "getDownFrom", |
||
| 310 | "getDownTo", |
||
| 311 | "getValidator", |
||
| 312 | ] |
||
| 313 | } |
||
| 314 | ] |
||
| 315 | |||
| 316 | def localize_date(self, date): |
||
| 317 | """Return the localized date |
||
| 318 | """ |
||
| 319 | return self.ulocalized_time(date, long_format=1) |
||
| 320 | |||
| 321 | def folderitem(self, obj, item, index): |
||
| 322 | """Augment folder listing item |
||
| 323 | """ |
||
| 324 | url = item.get("url") |
||
| 325 | title = item.get("Title") |
||
| 326 | |||
| 327 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
| 328 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
| 329 | item["getValidator"] = obj.getValidator() |
||
| 330 | item["replace"]["Title"] = get_link(url, value=title) |
||
| 331 | |||
| 332 | # validation with the most remaining days |
||
| 333 | if obj == self.latest_validation: |
||
| 334 | item["state_class"] = "state-published" |
||
| 335 | # running validations |
||
| 336 | elif obj in self.active_validations: |
||
| 337 | item["state_class"] = "state-active" |
||
| 338 | # inactive validations |
||
| 339 | else: |
||
| 340 | item["state_class"] = "state-inactive" |
||
| 341 | |||
| 342 | return item |
||
| 343 | |||
| 344 | |||
| 345 | class InstrumentScheduleView(BikaListingView): |
||
| 346 | """Listing view for instrument scheduled tasks |
||
| 347 | """ |
||
| 348 | |||
| 349 | View Code Duplication | def __init__(self, context, request): |
|
| 350 | super(InstrumentScheduleView, self).__init__(context, request) |
||
| 351 | self.catalog = "portal_catalog" |
||
| 352 | self.contentFilter = { |
||
| 353 | "portal_type": "InstrumentScheduledTask", |
||
| 354 | "path": { |
||
| 355 | "query": api.get_path(context), |
||
| 356 | "depth": 1 # searching just inside the specified folder |
||
| 357 | }, |
||
| 358 | "sort_on": "created", |
||
| 359 | "sort_order": "descending", |
||
| 360 | } |
||
| 361 | |||
| 362 | self.form_id = "instrumentschedule" |
||
| 363 | self.title = self.context.translate(_("Instrument Scheduled Tasks")) |
||
| 364 | |||
| 365 | self.icon = "{}/{}".format( |
||
| 366 | self.portal_url, |
||
| 367 | "++resource++bika.lims.images/instrumentschedule_big.png" |
||
| 368 | ) |
||
| 369 | self.context_actions = { |
||
| 370 | _("Add"): { |
||
| 371 | "url": "createObject?type_name=InstrumentScheduledTask", |
||
| 372 | "icon": "++resource++bika.lims.images/add.png"} |
||
| 373 | } |
||
| 374 | |||
| 375 | self.allow_edit = False |
||
| 376 | self.show_select_column = False |
||
| 377 | self.show_workflow_action_buttons = True |
||
| 378 | self.pagesize = 30 |
||
| 379 | |||
| 380 | self.columns = { |
||
| 381 | "Title": {"title": _("Scheduled task"), |
||
| 382 | "index": "sortable_title"}, |
||
| 383 | "getType": {"title": _("Task type", "Type")}, |
||
| 384 | "getCriteria": {"title": _("Criteria")}, |
||
| 385 | "creator": {"title": _("Created by")}, |
||
| 386 | "created": {"title": _("Created")}, |
||
| 387 | } |
||
| 388 | |||
| 389 | self.review_states = [ |
||
| 390 | { |
||
| 391 | "id": "default", |
||
| 392 | "title": _("Active"), |
||
| 393 | "contentFilter": {"inactive_state": "active"}, |
||
| 394 | "transitions": [{"id": "deactivate"}, ], |
||
| 395 | "columns": [ |
||
| 396 | "Title", |
||
| 397 | "getType", |
||
| 398 | "getCriteria", |
||
| 399 | "creator", |
||
| 400 | "created", |
||
| 401 | ] |
||
| 402 | }, { |
||
| 403 | "id": "inactive", |
||
| 404 | "title": _("Dormant"), |
||
| 405 | "contentFilter": {"inactive_state": "inactive"}, |
||
| 406 | "transitions": [{"id": "activate"}, ], |
||
| 407 | "columns": [ |
||
| 408 | "Title", |
||
| 409 | "getType", |
||
| 410 | "getCriteria", |
||
| 411 | "creator", |
||
| 412 | "created" |
||
| 413 | ] |
||
| 414 | }, { |
||
| 415 | "id": "all", |
||
| 416 | "title": _("All"), |
||
| 417 | "contentFilter": {}, |
||
| 418 | "columns": [ |
||
| 419 | "Title", |
||
| 420 | "getType", |
||
| 421 | "getCriteria", |
||
| 422 | "creator", |
||
| 423 | "created", |
||
| 424 | ] |
||
| 425 | } |
||
| 426 | ] |
||
| 427 | |||
| 428 | def localize_date(self, date): |
||
| 429 | """Return the localized date |
||
| 430 | """ |
||
| 431 | return self.ulocalized_time(date, long_format=1) |
||
| 432 | |||
| 433 | def folderitem(self, obj, item, index): |
||
| 434 | """Augment folder listing item |
||
| 435 | """ |
||
| 436 | url = item.get("url") |
||
| 437 | title = item.get("Title") |
||
| 438 | creator = obj.Creator() |
||
| 439 | |||
| 440 | item["replace"]["Title"] = get_link(url, value=title) |
||
| 441 | item["created"] = self.localize_date(obj.created()) |
||
| 442 | item["getType"] = _(obj.getType()[0]) |
||
| 443 | item["creator"] = "" |
||
| 444 | if creator: |
||
| 445 | props = api.get_user_properties(creator) |
||
| 446 | name = props.get("fullname", creator) |
||
| 447 | item["creator"] = name |
||
| 448 | |||
| 449 | return item |
||
| 450 | |||
| 451 | |||
| 452 | class InstrumentReferenceAnalysesViewView(BrowserView): |
||
| 453 | """View of Reference Analyses linked to the Instrument. |
||
| 454 | |||
| 455 | Only shows the Reference Analyses (Control and Blanks), the rest of regular |
||
| 456 | and duplicate analyses linked to this instrument are not displayed. |
||
| 457 | |||
| 458 | The Reference Analyses from an Instrument can be from Worksheets (QC |
||
| 459 | analysis performed regularly for any Analysis Request) or attached directly |
||
| 460 | to the instrument, without being linked to any Worksheet). |
||
| 461 | |||
| 462 | In this case, the Reference Analyses are created automatically by the |
||
| 463 | instrument import tool. |
||
| 464 | """ |
||
| 465 | |||
| 466 | implements(IViewView) |
||
| 467 | template = ViewPageTemplateFile( |
||
| 468 | "templates/instrument_referenceanalyses.pt") |
||
| 469 | |||
| 470 | def __init__(self, context, request): |
||
| 471 | super(InstrumentReferenceAnalysesViewView, self).__init__( |
||
| 472 | context, request) |
||
| 473 | |||
| 474 | self.title = self.context.translate(_("Internal Calibration Tests")) |
||
| 475 | self.icon = "{}/{}".format( |
||
| 476 | self.portal_url, |
||
| 477 | "++resource++bika.lims.images/referencesample_big.png" |
||
| 478 | ) |
||
| 479 | self._analysesview = None |
||
| 480 | |||
| 481 | def __call__(self): |
||
| 482 | return self.template() |
||
| 483 | |||
| 484 | def get_analyses_table_view(self): |
||
| 485 | view_name = "table_instrument_referenceanalyses" |
||
| 486 | view = api.get_view( |
||
| 487 | view_name, context=self.context, request=self.request) |
||
| 488 | # Call listing hooks |
||
| 489 | view.update() |
||
| 490 | view.before_render() |
||
| 491 | |||
| 492 | # TODO Refactor QC Charts as React Components |
||
| 493 | # The current QC Chart is rendered by looking at the value from a hidden |
||
| 494 | # input with id "graphdata", that is rendered below the contents listing |
||
| 495 | # (see instrument_referenceanalyses.pt). |
||
| 496 | # The value is a json, is built by folderitem function and is returned |
||
| 497 | # by self.chart.get_json(). This function is called directly by the |
||
| 498 | # template on render, but the template itself does not directly render |
||
| 499 | # the contents listing, but is done asyncronously. |
||
| 500 | # Hence the function at this point returns an empty dictionary because |
||
| 501 | # folderitems hasn't been called yet. As a result, the chart appears |
||
| 502 | # empty. Here, we force folderitems function to be called in order to |
||
| 503 | # ensure the graphdata is filled before the template is rendered. |
||
| 504 | view.get_folderitems() |
||
| 505 | return view |
||
| 506 | |||
| 507 | |||
| 508 | class InstrumentReferenceAnalysesView(AnalysesView): |
||
| 509 | """View for the table of Reference Analyses linked to the Instrument. |
||
| 510 | |||
| 511 | Only shows the Reference Analyses (Control and Blanks), the rest of regular |
||
| 512 | and duplicate analyses linked to this instrument are not displayed. |
||
| 513 | """ |
||
| 514 | |||
| 515 | def __init__(self, context, request, **kwargs): |
||
| 516 | AnalysesView.__init__(self, context, request, **kwargs) |
||
| 517 | |||
| 518 | self.form_id = "{}_qcanalyses".format(api.get_uid(context)) |
||
| 519 | self.allow_edit = False |
||
| 520 | self.show_select_column = False |
||
| 521 | self.show_search = False |
||
| 522 | self.omit_form = True |
||
| 523 | |||
| 524 | self.catalog = CATALOG_ANALYSIS_LISTING |
||
| 525 | |||
| 526 | self.contentFilter = { |
||
| 527 | "portal_type": "ReferenceAnalysis", |
||
| 528 | "getInstrumentUID": api.get_uid(self.context), |
||
| 529 | "sort_on": "getResultCaptureDate", |
||
| 530 | "sort_order": "reverse" |
||
| 531 | } |
||
| 532 | self.columns["getReferenceAnalysesGroupID"] = { |
||
| 533 | "title": _("QC Sample ID"), |
||
| 534 | "sortable": False |
||
| 535 | } |
||
| 536 | self.columns["Partition"] = { |
||
| 537 | "title": _("Reference Sample"), |
||
| 538 | "sortable": False |
||
| 539 | } |
||
| 540 | self.columns["Retractions"] = { |
||
| 541 | "title": "", |
||
| 542 | "sortable": False |
||
| 543 | } |
||
| 544 | |||
| 545 | self.review_states[0]["columns"] = [ |
||
| 546 | "Service", |
||
| 547 | "getReferenceAnalysesGroupID", |
||
| 548 | "Partition", |
||
| 549 | "Result", |
||
| 550 | "Uncertainty", |
||
| 551 | "CaptureDate", |
||
| 552 | "Retractions" |
||
| 553 | ] |
||
| 554 | self.review_states[0]["transitions"] = [{}] |
||
| 555 | self.chart = EvolutionChart() |
||
| 556 | |||
| 557 | def isItemAllowed(self, obj): |
||
| 558 | allowed = super(InstrumentReferenceAnalysesView, |
||
| 559 | self).isItemAllowed(obj) |
||
| 560 | return allowed or obj.getResult != "" |
||
| 561 | |||
| 562 | def folderitem(self, obj, item, index): |
||
| 563 | item = super(InstrumentReferenceAnalysesView, |
||
| 564 | self).folderitem(obj, item, index) |
||
| 565 | analysis = api.get_object(obj) |
||
| 566 | |||
| 567 | # Partition is used to group/toggle QC Analyses |
||
| 568 | sample = analysis.getSample() |
||
| 569 | item["replace"]["Partition"] = get_link(api.get_url(sample), |
||
| 570 | api.get_id(sample)) |
||
| 571 | |||
| 572 | # Get retractions field |
||
| 573 | item["Retractions"] = "" |
||
| 574 | report = analysis.getRetractedAnalysesPdfReport() |
||
| 575 | if report: |
||
| 576 | url = api.get_url(analysis) |
||
| 577 | href = "{}/at_download/RetractedAnalysesPdfReport".format(url) |
||
| 578 | attrs = {"class": "pdf", "target": "_blank"} |
||
| 579 | title = _("Retractions") |
||
| 580 | link = get_link(href, title, **attrs) |
||
| 581 | item["Retractions"] = title |
||
| 582 | item["replace"]["Retractions"] = link |
||
| 583 | |||
| 584 | # Add the analysis to the QC Chart |
||
| 585 | self.chart.add_analysis(analysis) |
||
| 586 | |||
| 587 | return item |
||
| 588 | |||
| 589 | |||
| 590 | class InstrumentCertificationsView(BikaListingView): |
||
| 591 | """Listing view for instrument certifications |
||
| 592 | """ |
||
| 593 | |||
| 594 | def __init__(self, context, request, **kwargs): |
||
| 595 | BikaListingView.__init__(self, context, request, **kwargs) |
||
| 596 | self.catalog = "portal_catalog" |
||
| 597 | self.contentFilter = { |
||
| 598 | "portal_type": "InstrumentCertification", |
||
| 599 | "path": { |
||
| 600 | "query": api.get_path(context), |
||
| 601 | "depth": 1 # searching just inside the specified folder |
||
| 602 | }, |
||
| 603 | "sort_on": "created", |
||
| 604 | "sort_order": "descending", |
||
| 605 | } |
||
| 606 | |||
| 607 | self.form_id = "instrumentcertifications" |
||
| 608 | self.title = self.context.translate(_("Calibration Certificates")) |
||
| 609 | self.icon = "{}/{}".format( |
||
| 610 | self.portal_url, |
||
| 611 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
| 612 | ) |
||
| 613 | self.context_actions = { |
||
| 614 | _("Add"): { |
||
| 615 | "url": "createObject?type_name=InstrumentCertification", |
||
| 616 | "icon": "++resource++bika.lims.images/add.png" |
||
| 617 | } |
||
| 618 | } |
||
| 619 | |||
| 620 | self.allow_edit = False |
||
| 621 | self.show_select_column = False |
||
| 622 | self.show_workflow_action_buttons = True |
||
| 623 | self.pagesize = 30 |
||
| 624 | |||
| 625 | # latest valid certificate UIDs |
||
| 626 | self.valid_certificates = self.context.getValidCertifications() |
||
| 627 | self.latest_certificate = self.context.getLatestValidCertification() |
||
| 628 | |||
| 629 | self.columns = { |
||
| 630 | "Title": {"title": _("Cert. Num"), "index": "sortable_title"}, |
||
| 631 | "getAgency": {"title": _("Agency"), "sortable": False}, |
||
| 632 | "getDate": {"title": _("Date"), "sortable": False}, |
||
| 633 | "getValidFrom": {"title": _("Valid from"), "sortable": False}, |
||
| 634 | "getValidTo": {"title": _("Valid to"), "sortable": False}, |
||
| 635 | "getDocument": {"title": _("Document"), "sortable": False}, |
||
| 636 | } |
||
| 637 | |||
| 638 | self.review_states = [ |
||
| 639 | { |
||
| 640 | "id": "default", |
||
| 641 | "title": _("All"), |
||
| 642 | "contentFilter": {}, |
||
| 643 | "columns": [ |
||
| 644 | "Title", |
||
| 645 | "getAgency", |
||
| 646 | "getDate", |
||
| 647 | "getValidFrom", |
||
| 648 | "getValidTo", |
||
| 649 | "getDocument", |
||
| 650 | ], |
||
| 651 | "transitions": [] |
||
| 652 | } |
||
| 653 | ] |
||
| 654 | |||
| 655 | def get_document(self, certificate): |
||
| 656 | """Return the document of the given document |
||
| 657 | """ |
||
| 658 | try: |
||
| 659 | return certificate.getDocument() |
||
| 660 | except POSKeyError: # POSKeyError: "No blob file" |
||
| 661 | # XXX When does this happen? |
||
| 662 | return None |
||
| 663 | |||
| 664 | def localize_date(self, date): |
||
| 665 | """Return the localized date |
||
| 666 | """ |
||
| 667 | return self.ulocalized_time(date, long_format=0) |
||
| 668 | |||
| 669 | def folderitem(self, obj, item, index): |
||
| 670 | """Augment folder listing item with additional data |
||
| 671 | """ |
||
| 672 | url = item.get("url") |
||
| 673 | title = item.get("Title") |
||
| 674 | |||
| 675 | item["replace"]["Title"] = get_link(url, value=title) |
||
| 676 | item["getDate"] = self.localize_date(obj.getDate()) |
||
| 677 | item["getValidFrom"] = self.localize_date(obj.getValidFrom()) |
||
| 678 | item["getValidTo"] = self.localize_date(obj.getValidTo()) |
||
| 679 | |||
| 680 | if obj.getInternal() is True: |
||
| 681 | item["replace"]["getAgency"] = "" |
||
| 682 | item["state_class"] = "%s %s" % \ |
||
| 683 | (item["state_class"], "internalcertificate") |
||
| 684 | |||
| 685 | item["getDocument"] = "" |
||
| 686 | item["replace"]["getDocument"] = "" |
||
| 687 | doc = self.get_document(obj) |
||
| 688 | if doc and doc.get_size() > 0: |
||
| 689 | filename = doc.filename |
||
| 690 | download_url = "{}/at_download/Document".format(url) |
||
| 691 | anchor = get_link(download_url, filename) |
||
| 692 | item["getDocument"] = filename |
||
| 693 | item["replace"]["getDocument"] = anchor |
||
| 694 | |||
| 695 | # Latest valid certificate |
||
| 696 | if obj == self.latest_certificate: |
||
| 697 | item["state_class"] = "state-published" |
||
| 698 | # Valid certificate |
||
| 699 | elif obj in self.valid_certificates: |
||
| 700 | item["state_class"] = "state-valid state-published" |
||
| 701 | # Invalid certificates |
||
| 702 | else: |
||
| 703 | img = get_image("exclamation.png", title=t(_("Out of date"))) |
||
| 704 | item["replace"]["getValidTo"] = "%s %s" % (item["getValidTo"], img) |
||
| 705 | item["state_class"] = "state-invalid" |
||
| 706 | |||
| 707 | return item |
||
| 708 | |||
| 709 | |||
| 710 | class InstrumentAutoImportLogsView(AutoImportLogsView): |
||
| 711 | """Logs of Auto-Imports of this instrument. |
||
| 712 | """ |
||
| 713 | |||
| 714 | def __init__(self, context, request, **kwargs): |
||
| 715 | AutoImportLogsView.__init__(self, context, request, **kwargs) |
||
| 716 | del self.columns["Instrument"] |
||
| 717 | self.review_states[0]["columns"].remove("Instrument") |
||
| 718 | self.contentFilter = { |
||
| 719 | "portal_type": "AutoImportLog", |
||
| 720 | "path": { |
||
| 721 | "query": api.get_path(context), |
||
| 722 | "depth": 1 # searching just inside the specified folder |
||
| 723 | }, |
||
| 724 | "sort_on": "created", |
||
| 725 | "sort_order": "descending", |
||
| 726 | } |
||
| 727 | |||
| 728 | self.title = self.context.translate( |
||
| 729 | _("Auto Import Logs of %s" % self.context.Title())) |
||
| 730 | self.icon = "{}/{}".format( |
||
| 731 | self.portal_url, |
||
| 732 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
| 733 | ) |
||
| 734 | self.context_actions = {} |
||
| 735 | |||
| 736 | self.allow_edit = False |
||
| 737 | self.show_select_column = False |
||
| 738 | self.show_workflow_action_buttons = True |
||
| 739 | self.pagesize = 30 |
||
| 740 | |||
| 741 | |||
| 742 | class InstrumentMultifileView(BikaListingView): |
||
| 743 | """Listing view for instrument multi files |
||
| 744 | """ |
||
| 745 | |||
| 746 | def __init__(self, context, request): |
||
| 747 | super(InstrumentMultifileView, self).__init__(context, request) |
||
| 748 | |||
| 749 | self.catalog = "bika_setup_catalog" |
||
| 750 | self.contentFilter = { |
||
| 751 | "portal_type": "Multifile", |
||
| 752 | "path": { |
||
| 753 | "query": api.get_path(context), |
||
| 754 | "depth": 1 # searching just inside the specified folder |
||
| 755 | }, |
||
| 756 | "sort_on": "created", |
||
| 757 | "sort_order": "descending", |
||
| 758 | } |
||
| 759 | |||
| 760 | self.form_id = "instrumentfiles" |
||
| 761 | self.title = self.context.translate(_("Instrument Files")) |
||
| 762 | self.icon = "{}/{}".format( |
||
| 763 | self.portal_url, |
||
| 764 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
| 765 | ) |
||
| 766 | self.context_actions = { |
||
| 767 | _("Add"): { |
||
| 768 | "url": "createObject?type_name=Multifile", |
||
| 769 | "icon": "++resource++bika.lims.images/add.png" |
||
| 770 | } |
||
| 771 | } |
||
| 772 | |||
| 773 | self.allow_edit = False |
||
| 774 | self.show_select_column = False |
||
| 775 | self.show_workflow_action_buttons = True |
||
| 776 | self.pagesize = 30 |
||
| 777 | |||
| 778 | self.columns = { |
||
| 779 | "DocumentID": {"title": _("Document ID"), |
||
| 780 | "index": "sortable_title"}, |
||
| 781 | "DocumentVersion": {"title": _("Document Version"), |
||
| 782 | "index": "sortable_title"}, |
||
| 783 | "DocumentLocation": {"title": _("Document Location"), |
||
| 784 | "index": "sortable_title"}, |
||
| 785 | "DocumentType": {"title": _("Document Type"), |
||
| 786 | "index": "sortable_title"}, |
||
| 787 | "FileDownload": {"title": _("File")} |
||
| 788 | } |
||
| 789 | |||
| 790 | self.review_states = [ |
||
| 791 | { |
||
| 792 | "id": "default", |
||
| 793 | "title": _("All"), |
||
| 794 | "contentFilter": {}, |
||
| 795 | "columns": [ |
||
| 796 | "DocumentID", |
||
| 797 | "DocumentVersion", |
||
| 798 | "DocumentLocation", |
||
| 799 | "DocumentType", |
||
| 800 | "FileDownload" |
||
| 801 | ] |
||
| 802 | }, |
||
| 803 | ] |
||
| 804 | |||
| 805 | def get_file(self, obj): |
||
| 806 | """Return the file of the given object |
||
| 807 | """ |
||
| 808 | try: |
||
| 809 | return obj.getFile() |
||
| 810 | except POSKeyError: # POSKeyError: "No blob file" |
||
| 811 | # XXX When does this happen? |
||
| 812 | return None |
||
| 813 | |||
| 814 | def folderitem(self, obj, item, index): |
||
| 815 | """Augment folder listing item with additional data |
||
| 816 | """ |
||
| 817 | url = item.get("url") |
||
| 818 | title = item.get("DocumentID") |
||
| 819 | |||
| 820 | item["replace"]["DocumentID"] = get_link(url, title) |
||
| 821 | |||
| 822 | item["FileDownload"] = "" |
||
| 823 | item["replace"]["FileDownload"] = "" |
||
| 824 | file = self.get_file(obj) |
||
| 825 | if file and file.get_size() > 0: |
||
| 826 | filename = file.filename |
||
| 827 | download_url = "{}/at_download/File".format(url) |
||
| 828 | anchor = get_link(download_url, filename) |
||
| 829 | item["FileDownload"] = filename |
||
| 830 | item["replace"]["FileDownload"] = anchor |
||
| 831 | |||
| 832 | item["DocumentVersion"] = obj.getDocumentVersion() |
||
| 833 | item["DocumentLocation"] = obj.getDocumentLocation() |
||
| 834 | item["DocumentType"] = obj.getDocumentType() |
||
| 835 | |||
| 836 | return item |
||
| 837 | |||
| 838 | |||
| 839 | class ajaxGetInstrumentMethods(BrowserView): |
||
| 840 | """ Returns the method assigned to the defined instrument. |
||
| 841 | uid: unique identifier of the instrument |
||
| 842 | """ |
||
| 843 | # Modified to return multiple methods after enabling multiple method |
||
| 844 | # for intruments. |
||
| 845 | def __call__(self): |
||
| 846 | out = { |
||
| 847 | "title": None, |
||
| 848 | "instrument": None, |
||
| 849 | "methods": [], |
||
| 850 | } |
||
| 851 | try: |
||
| 852 | plone.protect.CheckAuthenticator(self.request) |
||
| 853 | except Forbidden: |
||
| 854 | return json.dumps(out) |
||
| 855 | bsc = getToolByName(self, "bika_setup_catalog") |
||
| 856 | results = bsc(portal_type="Instrument", |
||
| 857 | UID=self.request.get("uid", "0")) |
||
| 858 | instrument = results[0] if results and len(results) == 1 else None |
||
| 859 | if instrument: |
||
| 860 | instrument_obj = instrument.getObject() |
||
| 861 | out["title"] = instrument_obj.Title() |
||
| 862 | out["instrument"] = instrument.UID |
||
| 863 | # Handle multiple Methods per instrument |
||
| 864 | methods = instrument_obj.getMethods() |
||
| 865 | for method in methods: |
||
| 866 | out["methods"].append({ |
||
| 867 | "uid": method.UID(), |
||
| 868 | "title": method.Title(), |
||
| 869 | }) |
||
| 870 | return json.dumps(out) |
||
| 871 |