class File(Document, CMFFile): """ A File can contain raw data which can be uploaded and downloaded. It is the root class of Image, OOoDocument (ERP5OOo product), etc. The main purpose of the File class is to handle efficiently large files. It uses Pdata from OFS.File for this purpose. File inherits from XMLObject and can be synchronized accross multiple sites. Subcontent: File can only contain role information. TODO: * make sure ZODB BLOBS are supported to prevent feeding the ZODB cache with unnecessary large data """ meta_type = 'ERP5 File' portal_type = 'File' add_permission = Permissions.AddPortalContent # Declarative security security = ClassSecurityInfo() security.declareObjectProtected(Permissions.AccessContentsInformation) # Default global values content_type = '' # Required for WebDAV support (default value) data = '' # A hack required to use OFS.Image.index_html without calling OFS.Image.__init__ # Default Properties property_sheets = (PropertySheet.Base, PropertySheet.XMLObject, PropertySheet.CategoryCore, PropertySheet.DublinCore, PropertySheet.Version, PropertySheet.Reference, PropertySheet.Document, PropertySheet.Data, PropertySheet.ExternalDocument, PropertySheet.Url, PropertySheet.Periodicity) # OFS.File has an overloaded __str__ that returns the file content __str__ = object.__str__ ### Special edit method security.declarePrivate('_edit') def _edit(self, **kw): """ This is used to edit files """ if 'file' in kw: file_object = kw.pop('file') precondition = kw.get('precondition') filename = getattr(file_object, 'filename', None) # if file field is empty(no file is uploaded), # filename is empty string. if not filename: # settings the filename before calling # _setFile is required to setup the content_type # property filename = kw.get('filename') if filename: self._setFilename(filename) if self._isNotEmpty(file_object): self._setFile(file_object, precondition=precondition) Base._edit(self, **kw) security.declareProtected(Permissions.ModifyPortalContent, 'edit') edit = WorkflowMethod(_edit) def get_size(self): """ has to be overwritten here, otherwise WebDAV fails """ return self.getSize() getcontentlength = get_size def _get_content_type(*args, **kw): """Override original implementation from OFS/Image.py to disable content_type discovery because id of object its used to read the filename value. In ERP5, an interaction document_conversion_interaction_workflow/Document_file, update the content_type by reading filename property """ return None def _setFile(self, data, precondition=None): if data is not None and self.hasData() and \ str(data.read()) == str(self.getData()): # Same data as previous, no need to change it's content return CMFFile._edit(self, precondition=precondition, file=data) security.declareProtected(Permissions.ModifyPortalContent, 'setFile') def setFile(self, data, precondition=None): self._setFile(data, precondition=precondition) self.reindexObject() security.declareProtected(Permissions.AccessContentsInformation, 'hasFile') def hasFile(self): """ Checks whether a file was uploaded into the document. """ return self.hasData() security.declareProtected(Permissions.AccessContentsInformation, 'guessMimeType') @deprecated def guessMimeType(self, fname=None): """ Deprecated """ return self.getPortalObject().portal_contributions.\ guessMimeTypeFromFilename(fname) security.declareProtected(Permissions.ModifyPortalContent, '_setData') def _setData(self, data): """ """ size = None # update_data use len(data) when size is None, which breaks this method. # define size = 0 will prevent len be use and keep the consistency of # getData() and setData() if data is None: size = 0 if not isinstance(data, Pdata) and data is not None: file = cStringIO.StringIO(data) data, size = self._read_data(file) if getattr(self, 'update_data', None) is not None: # We call this method to make sure size is set and caches reset self.update_data(data, size=size) else: self._baseSetData( data) # XXX - It would be better to always use this accessor self._setSize(size) self.ZCacheable_invalidate() self.ZCacheable_set(None) self.http__refreshEtag() security.declareProtected(Permissions.AccessContentsInformation, 'getData') def getData(self, default=None): """return Data as str.""" self._checkConversionFormatPermission(None) data = self._baseGetData() if data is None: return None else: return str(data) security.declareProtected(Permissions.ModifyPortalContent, 'PUT') def PUT(self, REQUEST, RESPONSE): CMFFile.PUT(self, REQUEST, RESPONSE) # DAV Support PUT = CMFFile.PUT security.declareProtected(Permissions.FTPAccess, 'manage_FTPstat', 'manage_FTPlist') manage_FTPlist = CMFFile.manage_FTPlist manage_FTPstat = CMFFile.manage_FTPstat security.declareProtected(Permissions.AccessContentsInformation, 'getMimeTypeAndContent') def getMimeTypeAndContent(self): """This method returns a tuple which contains mimetype and content.""" from Products.ERP5.Document.EmailDocument import MimeTypeException # return a tuple (mime_type, data) content = None mime_type = self.getContentType() if mime_type is None: raise ValueError('Cannot find mimetype of the document.') try: mime_type, content = self.convert(None) except ConversionError: mime_type = self.getBaseContentType() content = self.getBaseData() except (NotImplementedError, MimeTypeException): pass if content is None: if getattr(self, 'getTextContent', None) is not None: content = self.getTextContent() elif getattr(self, 'getData', None) is not None: content = self.getData() elif getattr(self, 'getBaseData', None) is not None: content = self.getBaseData() if content and not isinstance(content, str): content = str(content) return (mime_type, content) def _convert(self, format, **kw): """File is only convertable if it is an image. Only Image conversion, original format and text formats are allowed. However this document can migrate to another portal_type which support others conversions. """ content_type = self.getContentType() or '' if (format in VALID_IMAGE_FORMAT_LIST + (None, "")) and \ content_type.startswith("image/"): # The file should behave like it is an Image for convert # the content to target format. from Products.ERP5Type.Document import newTempImage return newTempImage(self, self.getId(), data=self.getData(), content_type=content_type, filename=self.getFilename())._convert( format, **kw) elif format in (None, ""): # No conversion is asked, # we can return safely the file content itself return content_type, self.getData() elif format in VALID_TEXT_FORMAT_LIST: # This is acceptable to return empty string # for a File we can not convert return 'text/plain', '' raise NotImplementedError # backward compatibility security.declareProtected(Permissions.AccessContentsInformation, 'getFilename') def getFilename(self, default=_MARKER): """Fallback on getSourceReference as it was used before to store filename property """ if self.hasFilename(): if default is _MARKER: return self._baseGetFilename() else: return self._baseGetFilename(default) else: if default is _MARKER: return self._baseGetSourceReference() else: return self._baseGetSourceReference(default)
def _migrateSimulationTree(self, get_matching_key, get_original_property_dict, root_rule=None): """Migrate an entire simulation tree in order to use new rules This must be called on a root applied rule, with interaction workflows disabled. It is required that: - All related simulation trees are properly indexed (due to use of isSimulated). Unfortunately, this method temporarily unindexes everything, so you have to be careful when migrating several trees at once. - All simulation trees it may depend on are already migrated. It is adviced to first migrate all root applied rule for the first phase (usually order) and to continue respecting the order of phases. def get_matching_key(simulation_movement): # Return arbitrary value to match old and new simulation movements, # or null if the old simulation movement is dropped. def get_original_property_dict(tester, old_sm, sm, movement): # Return values to override on the new simulation movement. # In most cases, it would return the result of: # tester.getUpdatablePropertyDict(old_sm, movement) root_rule # If not null and tree is about to be regenerated, 'specialise' # is changed on self to this relative url. """ assert WorkflowMethod.disabled(), \ "Interaction workflows must be disabled using WorkflowMethod.disable" simulation_tool = self.getParentValue() assert simulation_tool.getPortalType() == 'Simulation Tool' portal = simulation_tool.getPortalObject() delivery = self.getCausalityValue() # Check the whole history to not drop simulation in case of redraft draft_state_list = portal.getPortalDraftOrderStateList() workflow, = [wf for wf in portal.portal_workflow.getWorkflowsFor(delivery) if wf.isInfoSupported(delivery, 'simulation_state')] for history_item in workflow.getInfoFor(delivery, 'history', ()): if history_item['simulation_state'] in draft_state_list: continue # Delivery is/was not is draft state resolveCategory = portal.portal_categories.resolveCategory order_dict = {} old_dict = {} # Caller may want to drop duplicate SM, like a unbuilt SM if there's # already a built one, or one with no quantity. So first call # 'get_matching_key' on SM that would be kept. 'get_matching_key' would # remember them and returns None for duplicates. sort_sm = lambda x: (not x.getDelivery(), not x.getQuantity(), x.getId()) for sm in sorted(self.objectValues(), key=sort_sm): line = sm.getOrder() or sm.getDelivery() # Check SM is not orphan, which happened with old buggy trees. if resolveCategory(line) is not None: sm_dict = old_dict.setdefault(line, {}) recurse_list = deque(({get_matching_key(sm): (sm,)},)) while recurse_list: for k, x in recurse_list.popleft().iteritems(): if not k: continue if len(x) > 1: x = [x for x in x if x.getDelivery() or x.getQuantity()] if len(x) > 1: x.sort(key=sort_sm) sm_dict.setdefault(k, []).extend(x) for x in x: r = {} for x in x.objectValues(): sm_list = x.getMovementList() if sm_list: r.setdefault(x.getSpecialise(), []).append(sm_list) for x in r.values(): if len(x) > 1: x = [y for y in x if any(z.getDelivery() for z in y)] or x[:1] x, = x r = {} for x in x: r.setdefault(get_matching_key(x), []).append(x) recurse_list.append(r) self._delObject(sm.getId()) # Here Delivery.isSimulated works because Movement.isSimulated # does not see the simulated movements we've just deleted. if delivery.isSimulated(): break # XXX: delivery.isSimulated may wrongly return False when a duplicate RAR # was migrated but has not been reindexed yet. Delay migration of # this one. rar_list = delivery.getCausalityRelatedValueList( portal_type='Applied Rule') rar_list.remove(self) if rar_list and portal.portal_activities.countMessage( path=[x.getPath() for x in rar_list], method_id=('immediateReindexObject', 'recursiveImmediateReindexObject', 'recursiveImmediateReindexSimulationMovement')): raise ConflictError # Do not try to keep simulation tree for draft delivery # if it was already out of sync. if delivery.getSimulationState() in draft_state_list and \ any(x.getRelativeUrl() not in old_dict for x in delivery.getMovementList()): break if root_rule: self._setSpecialise(root_rule) delivery_set = {delivery} def updateMovementCollection(rule, context, *args, **kw): orig_updateMovementCollection(rule, context, *args, **kw) new_parent = context.getParentValue() for sm in context.getMovementList(): delivery = sm.getDelivery() if delivery: sm_dict = old_dict.pop(delivery) else: sm_dict = order_dict[new_parent] order_dict[sm] = sm_dict k = get_matching_key(sm) sm_list = sm_dict.pop(k, ()) if len(sm_list) > 1: # Heuristic to find matching old simulation movements for the # currently expanded applied rule. We first try to preserve same # tree structure (new & old parent SM match), then we look for an # old possible parent that is in the same branch. try: old_parent = old_dict[new_parent] except KeyError: old_parent = simulation_tool best_dict = {} for old_sm in sm_list: parent = old_sm.getParentValue().getParentValue() if parent is old_parent: parent = None elif not (parent.aq_inContextOf(old_parent) or old_parent.aq_inContextOf(parent)): continue best_dict.setdefault(parent, []).append(old_sm) try: best_sm_list = best_dict[None] except KeyError: best_sm_list, = best_dict.values() if len(best_sm_list) < len(sm_list): sm_dict[k] = list(set(sm_list).difference(best_sm_list)) sm_list = best_sm_list if len(sm_list) > 1: kw = sm.__dict__.copy() # We may have several old matching SM, e.g. in case of split. for old_sm in sm_list: movement = old_sm.getDeliveryValue() if sm is None: sm = context.newContent(portal_type=rule.movement_type) sm.__dict__ = dict(kw, **sm.__dict__) order_dict[sm] = sm_dict if delivery: assert movement.getRelativeUrl() == delivery elif movement is not None: sm._setDeliveryValue(movement) delivery_set.add(sm.getExplanationValue()) try: sm.delivery_ratio = old_sm.aq_base.delivery_ratio except AttributeError: pass recorded_property_dict = {} edit_kw = {} kw['quantity'] = 0 for tester in rule._getUpdatingTesterList(): old = get_original_property_dict(tester, old_sm, sm, movement) if old is not None: new = tester.getUpdatablePropertyDict(sm, movement) if old != new: edit_kw.update(old) if 'quantity' in new and old_sm is not sm_list[-1]: quantity = new.pop('quantity') kw['quantity'] = quantity - old.pop('quantity') if new != old or sm.quantity != quantity: raise NotImplementedError # quantity_unit/efficiency ? else: recorded_property_dict.update(new) if recorded_property_dict: sm._recorded_property_dict = PersistentMapping( recorded_property_dict) sm._edit(**edit_kw) old_dict[sm] = old_sm sm = None deleted = old_dict.items() for delivery, sm_dict in deleted: if not sm_dict: del old_dict[delivery] from Products.ERP5.Document.SimulationMovement import SimulationMovement from Products.ERP5.mixin.movement_collection_updater import \ MovementCollectionUpdaterMixin as mixin # Patch is already protected by WorkflowMethod.disable lock. orig_updateMovementCollection = mixin.__dict__['updateMovementCollection'] try: AppliedRule.isIndexable = SimulationMovement.isIndexable = \ ConstantGetter('isIndexable', value=False) mixin.updateMovementCollection = updateMovementCollection self.expand("immediate") finally: mixin.updateMovementCollection = orig_updateMovementCollection del AppliedRule.isIndexable, SimulationMovement.isIndexable self.recursiveReindexObject() assert str not in map(type, old_dict), old_dict return {k: sum(v.values(), []) for k, v in deleted}, delivery_set simulation_tool._delObject(self.getId())