def migrate_versionId_storage(obj): """Migrate storage of versionId """ old_storage = obj.__annotations__.get("versionId") if not old_storage: msg = "no versionId stored for %s, but preset in catalog" % obj.absolute_url() logger.warning(msg) if isinstance(old_storage, basestring): msg = "Skipping migration of versionId for %s, " "already migrated" % obj return versionId = obj.__annotations__["versionId"]["versionId"].strip() obj.__annotations__["versionId"] = versionId # has versionId set but does not provide IVersionEnhanced # all versioned objects should provide IVersionEnhanced if versionId and not IVersionEnhanced.providedBy(obj): logger.info("versionId assigned without IVersionEnhanced " "provided %s", obj.absolute_url()) alsoProvides(obj, IVersionEnhanced) # doesn't have a good versionId (could be empty string), # but provides IVersionEnhanced. Will supply object with new versionId if not versionId and IVersionEnhanced.providedBy(obj): obj.__annotations__["versionId"] = _random_id(obj) msg = "Migrated versionId storage for %s (%s)" % (obj.absolute_url(), versionId) logger.info(msg)
def migrate_versionId_storage(obj): """Migrate storage of versionId """ old_storage = obj.__annotations__.get('versionId') if not old_storage: msg = ("no versionId stored for %s, but preset in catalog" % obj.absolute_url()) logger.warning(msg) if isinstance(old_storage, basestring): msg = ("Skipping migration of versionId for %s, " "already migrated" % obj) return versionId = obj.__annotations__['versionId']['versionId'].strip() obj.__annotations__['versionId'] = versionId #has versionId set but does not provide IVersionEnhanced #all versioned objects should provide IVersionEnhanced if versionId and not IVersionEnhanced.providedBy(obj): logger.info( "versionId assigned without IVersionEnhanced " "provided %s", obj.absolute_url()) alsoProvides(obj, IVersionEnhanced) #doesn't have a good versionId (could be empty string), #but provides IVersionEnhanced. Will supply object with new versionId if not versionId and IVersionEnhanced.providedBy(obj): obj.__annotations__['versionId'] = _random_id(obj) msg = "Migrated versionId storage for %s (%s)" % \ (obj.absolute_url(), versionId) logger.info(msg)
def __call__(self): catalog = self.context.portal_catalog index = self.request.form.get('index') portal_type = self.request.form.get('portal_type') fix = self.request.form.get("fix") results = self.not_indexed_results(catalog, index) out = StringIO() results = [ z for z in results if (z.portal_type == portal_type) and self.get_real_versionid(z) ] print >> out, "Got %s results" % len(results) for brain in results: print >> out, brain.portal_type, brain.getURL() if fix: obj = brain.getObject() if not IVersionEnhanced.providedBy(obj): alsoProvides(obj, IVersionEnhanced) obj.reindexObject() if fix: print >> out, "Fixed, try calling again this page "\ "to see if different" out.seek(0) return out.read()
def __call__(self): catalog = self.context.portal_catalog index = self.request.form.get('index') portal_type = self.request.form.get('portal_type') fix = self.request.form.get("fix") results = self.not_indexed_results(catalog, index) out = StringIO() results = [z for z in results if (z.portal_type == portal_type) and self.get_real_versionid(z)] print >> out, "Got %s results" % len(results) for brain in results: print >> out, brain.portal_type, brain.getURL() if fix: obj = brain.getObject() if not IVersionEnhanced.providedBy(obj): alsoProvides(obj, IVersionEnhanced) obj.reindexObject() if fix: print >> out, "Fixed, try calling again this page "\ "to see if different" out.seek(0) return out.read()
def evolve(context): """ Migrate versionIds for objects that don't have them set """ cat = context.portal_catalog brains = cat.searchResults(missing=True, Language="all") i = 0 for brain in brains: obj = brain.getObject() if not IVersionEnhanced.providedBy(obj): continue # first, check the brain's versionId brain_version = brain.getVersionId if isinstance(brain_version, basestring) and brain_version: # everything fine continue if brain.portal_type == "Discussion Item": continue # skipping Discussion Items, they can't be reindexed versionId = IGetVersions(obj).versionId if isinstance(brain_version, basestring) and not brain_version.strip(): # an empty string, assigning new versionId IAnnotations(obj)[VERSION_ID] = _random_id(obj) #obj.reindexObject() msg = "Migrated versionId storage (empty string) for %s (%s)" % \ (obj.absolute_url(), versionId) logger.info(msg) if (i % 500) == 0: transaction.commit() i += 1 continue if isinstance(versionId, basestring) and not versionId.strip(): # an empty string, assigning new versionId IAnnotations(obj)[VERSION_ID] = _random_id(obj) #obj.reindexObject() msg = "Migrated versionId storage (empty string) for %s (%s)" % \ (obj.absolute_url(), versionId) logger.info(msg) if (i % 500) == 0: transaction.commit() i += 1 continue if not brain.getVersionId: IAnnotations(obj)[VERSION_ID] = _random_id(obj) #obj.reindexObject() msg = "Migrated versionId storage (empty storage) for %s (%s)" % \ (obj.absolute_url(), versionId) logger.info(msg) if (i % 500) == 0: transaction.commit() i += 1 continue migrate_versionId_storage(obj) #this is an old storage:
def assign_version(context, new_version): """ Assign a specific version id to an object """ # Verify if there are more objects under this version cat = getToolByName(context, 'portal_catalog') brains = cat.searchResults({'getversionid': new_version, 'show_inactive': True}) if brains and not IVersionEnhanced.providedBy(context): alsoProvides(context, IVersionEnhanced) if len(brains) == 1: target_ob = brains[0].getObject() if not IVersionEnhanced.providedBy(target_ob): alsoProvides(target_ob, IVersionEnhanced) # Set new version ID verparent = IVersionControl(context) verparent.setVersionId(new_version) context.reindexObject()
def __init__(self, context, request): state = getMultiAdapter((context, request), name='plone_context_state') # #91514 fix for folders with a default view set, when creating a # version, we need the folder, not the page parent = state.canonical_object() if IVersionEnhanced.providedBy(parent): self.context = parent else: self.context = context self.url = self.context.absolute_url() self.request = request
def assign_version(context, new_version): """ Assign a specific version id to an object """ # Verify if there are more objects under this version cat = getToolByName(context, 'portal_catalog') brains = cat.searchResults({ 'getVersionId': new_version, 'show_inactive': True }) if brains and not IVersionEnhanced.providedBy(context): alsoProvides(context, IVersionEnhanced) if len(brains) == 1: target_ob = brains[0].getObject() if not IVersionEnhanced.providedBy(target_ob): alsoProvides(target_ob, IVersionEnhanced) # Set new version ID verparent = IVersionControl(context) verparent.setVersionId(new_version) context.reindexObject(idxs=['getVersionId'])
def migrate_versionId_storage(obj): """Migrate storage of versionId """ raw_versionId = obj.__annotations__["versionId"]["versionId"] logger.info("versionID: %s", raw_versionId) versionId = raw_versionId.strip() # doesn't have a good versionId (could be empty string), if not versionId and IVersionEnhanced.providedBy(obj): obj.__annotations__["versionId"] = _random_id(obj) else: obj.__annotations__["versionId"] = versionId msg = "Migrated versionId storage (old version) for %s (%s)" % (obj.absolute_url(), versionId) logger.info(msg)
def migrate_versionId_storage(obj): """Migrate storage of versionId """ raw_versionId = obj.__annotations__['versionId']['versionId'] logger.info('versionID: %s', raw_versionId) versionId = raw_versionId.strip() #doesn't have a good versionId (could be empty string), if not versionId and IVersionEnhanced.providedBy(obj): obj.__annotations__['versionId'] = _random_id(obj) else: obj.__annotations__['versionId'] = versionId msg = "Migrated versionId storage (old version) for %s (%s)" % \ (obj.absolute_url(), versionId) logger.info(msg)
def __init__(self, context): """ Constructor """ request = getattr(context, 'REQUEST', None) state = getMultiAdapter((context, request), name='plone_context_state') # #91514 fix for folders with a default view set, when creating a # version, we need the folder, not the page self.context = context if state.is_default_page(): parent = aq_parent(context) if IVersionEnhanced.providedBy(parent): self.context = parent self.versionId = IVersionControl(self.context).versionId failsafe = lambda obj: "Unknown" self.state_title_getter = queryMultiAdapter( (self.context, request), name=u'getWorkflowStateTitle') or failsafe
def __call__(self): event = self.event service_to_ping = self.element.service_to_ping obj = self.event.object container = obj.getParentNode() noasync_msg = 'No instance for async operations was defined.' def pingCRSDS(service_to_ping, obj_url, create): """ Ping the CR/SDS service """ if async_service is None: logger.warn("Can't pingCRSDS, plone.app.async not installed!") return options = {} options['service_to_ping'] = service_to_ping options['obj_url'] = self.sanitize_url(obj_url) options['create'] = create queue = async_service.getQueues()[''] try: async_service.queueJobInQueue(queue, ('rdf', ), ping_CRSDS, self.context, options) except ComponentLookupError: logger.info(noasync_msg) def pingCRSDS_backrel(service_to_ping, obj, create): """ Ping backward relations """ back_relations = obj.getBRefs('relatesTo') for rel in back_relations: if rel is not None: obj_url = "%s/@@rdf" % rel.absolute_url() pingCRSDS(service_to_ping, obj_url, create) def pingCRSDS_children(service_to_ping, obj, create): """ Ping all sub-objects """ if obj.portal_type == "Discussion Item": # 22047 skip object if it's of type Discussion Item return for child in obj.objectIds(): child_obj = obj.get(child) if not child_obj: logger.info("Couldn't retrieve child id %s for %s", child, obj.absolute_url()) continue obj_url = "%s/@@rdf" % child_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) pingCRSDS_children(service_to_ping, child_obj, create) # When no request the task is called from a async task, see #19830 request = getattr(obj, 'REQUEST', None) # Detect special object used to force acquisition, see #18904 if isinstance(request, str): request = None create = IObjectAddedEvent.providedBy(event) if service_to_ping == "": return if hasVersionsInstalled and IVersionEnhanced.providedBy(obj) \ and request: obj_versions = IGetVersions(obj).versions() else: obj_versions = [obj] async_service = queryUtility(IAsyncService) # If object has translations if hasLinguaPloneInstalled and ITranslatable.providedBy(obj): if obj.isCanonical(): # Ping all translations for trans in obj.getTranslations().items(): if trans[0] != 'en': trans_obj = trans[1][0] obj_url = trans_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) else: # Ping only canonical can_obj = obj.getCanonical() obj_url = can_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) # If object was deleted if IObjectRemovedEvent.providedBy(event): # Ping backward relations pingCRSDS_backrel(service_to_ping, obj, create) # Ping all sub-objects pingCRSDS_children(service_to_ping, obj, create) # If object was moved/renamed first ping with the old object's URL if IObjectMovedOrRenamedEvent.providedBy(event): obj_url = "%s/%s/@@rdf" % (event.oldParent.absolute_url(), event.oldName) pingCRSDS(service_to_ping, obj_url, False) # then ping with the container of the old object obj_url = "%s/@@rdf" % event.oldParent.absolute_url() pingCRSDS(service_to_ping, obj_url, False) # Ping backward relations pingCRSDS_backrel(service_to_ping, obj, create) # Ping all sub-objects pingCRSDS_children(service_to_ping, obj, create) # Ping each version for obj in obj_versions: obj_url = "%s/@@rdf" % obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) # If no Aquisition there is no container, see #18904 if container: obj_url = "%s/@@rdf" % container.absolute_url() pingCRSDS(service_to_ping, obj_url, False) return True
def versions(self): """ Return a list of sorted version objects """ # Avoid making a catalog call if versionId is empty if not self.versionId: return [self.context] if not isinstance(self.versionId, basestring): return [self.context] # this is an old, unmigrated storage cat = getToolByName(self.context, 'portal_catalog', None) if not cat: return [] query = {'getVersionId': self.versionId} mtool = getToolByName(self.context, 'portal_membership') if mtool.isAnonymousUser(): query['review_state'] = 'published' brains = cat.unrestrictedSearchResults(**query) else: brains = cat(**query) objects = [] for b in brains: try: obj = b.getObject() except Exception as err: # Because of the unrestricted search done above, this # might happen, and we don't need a crash. Also do not # crash if the object is missing/not re-indexed yet. logger.warn(err) continue else: # #93975: before #91514 versions were sometimes incorrectly # created in that pages set as default view were assigned as # versions for folders. For such cases we need to replace the # page with the 'canonical_object' state = getMultiAdapter((obj, self.context.REQUEST), name='plone_context_state') canonical_obj = state.canonical_object() if IVersionEnhanced.providedBy( canonical_obj) and canonical_obj != obj: canonical_obj_version = IAnnotations(canonical_obj)[ VERSION_ID] if canonical_obj_version != self.versionId: query['getVersionId'] = canonical_obj_version o_brains = cat.unrestrictedSearchResults(**query) if len(o_brains) > 1: logger.warn( 'DefaultView: Object %s has different ' 'version id than its default view' % canonical_obj.absolute_url()) else: assign_version(canonical_obj, self.versionId) RevokeVersion(obj, self.context.REQUEST).__call__() logger.warn( 'DefaultView: Version id moved from default ' 'view to canonical object for %s' % canonical_obj.absolute_url()) else: # the default view had the same version id as # the canonical object, thre is no need for that RevokeVersion(obj, self.context.REQUEST).__call__() if canonical_obj not in objects: objects.append(canonical_obj) else: if obj not in objects: objects.append(obj) # Some objects don't have EffectiveDate so we have to sort # them using CreationDate. This has the side effect that # in certain conditions the list of versions is reordered # For the anonymous users this is never a problem because # they only see published (and with effective_date) objects # during creation self.context has not been indexed if not self.context.UID() in [o.UID() for o in objects if o]: objects.append(self.context) # Store versions as ordered list, with the oldest item first # #20827 check if creation_date isn't bigger than the effective # date of the object as there are situation where the effective_date # is smaller such as for object without an workflow like FigureFile _versions = sorted( objects, key=lambda ob: ob.effective_date if ob.effective_date else ob.creation_date) return _versions
def __call__(self): event = self.event service_to_ping = self.element.service_to_ping obj = self.event.object container = obj.getParentNode() noasync_msg = 'No instance for async operations was defined.' def pingCRSDS(service_to_ping, obj_url, create): """ Ping the CR/SDS service """ options = {} options['service_to_ping'] = service_to_ping options['obj_url'] = self.sanitize_url(obj_url) options['create'] = create # Use RabbitMQ if available if self.rabbit_config: options['create'] = 'create' if options.get( 'create', False) else 'update' return ping_RabbitMQ(options) # Use zc.async if available if async_service is None: logger.warn("Can't pingCRSDS, plone.app.async not installed!") return queue = async_service.getQueues()[''] try: async_service.queueJobInQueue( queue, ('rdf',), ping_CRSDS, self.context, options ) except ComponentLookupError: logger.info(noasync_msg) def pingCRSDS_backrel(service_to_ping, obj, create): """ Ping backward relations """ if hasattr(obj, 'getBRefs'): back_relations = obj.getBRefs('relatesTo') else: back_relations = [o.to_object for o in getattr(obj, 'relatedItems') ] for rel in back_relations: if rel is not None: obj_url = "%s/@@rdf" % rel.absolute_url() pingCRSDS(service_to_ping, obj_url, create) def pingCRSDS_children(service_to_ping, obj, create): """ Ping all sub-objects """ if obj.portal_type == "Discussion Item": # 22047 skip object if it's of type Discussion Item return for child in obj.objectIds(): child_obj = obj.get(child) if not child_obj: logger.info( "Couldn't retrieve child id %s for %s", child, obj.absolute_url()) continue obj_url = "%s/@@rdf" % child_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) pingCRSDS_children(service_to_ping, child_obj, create) # When no request the task is called from a async task, see #19830 request = getattr(obj, 'REQUEST', None) # Detect special object used to force acquisition, see #18904 if isinstance(request, str): request = None create = IObjectAddedEvent.providedBy(event) if service_to_ping == "": return if hasVersionsInstalled and IVersionEnhanced.providedBy(obj) \ and request: obj_versions = IGetVersions(obj).versions() else: obj_versions = [obj] async_service = queryUtility(IAsyncService) # If object has translations if hasLinguaPloneInstalled and ITranslatable.providedBy(obj): if obj.isCanonical(): # Ping all translations for trans in obj.getTranslations().items(): if trans[0] != 'en': trans_obj = trans[1][0] obj_url = trans_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) else: # Ping only canonical can_obj = obj.getCanonical() obj_url = can_obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) # If object was deleted if IObjectRemovedEvent.providedBy(event): # Ping backward relations pingCRSDS_backrel(service_to_ping, obj, create) # Ping all sub-objects pingCRSDS_children(service_to_ping, obj, create) # If object was moved/renamed first ping with the old object's URL if IObjectMovedOrRenamedEvent.providedBy(event): obj_url = "%s/%s/@@rdf" % (event.oldParent.absolute_url(), event.oldName) pingCRSDS(service_to_ping, obj_url, False) # then ping with the container of the old object obj_url = "%s/@@rdf" % event.oldParent.absolute_url() pingCRSDS(service_to_ping, obj_url, False) # Ping backward relations pingCRSDS_backrel(service_to_ping, obj, create) # Ping all sub-objects pingCRSDS_children(service_to_ping, obj, create) # Ping each version for obj in obj_versions: obj_url = "%s/@@rdf" % obj.absolute_url() pingCRSDS(service_to_ping, obj_url, create) # If no Aquisition there is no container, see #18904 if container: obj_url = "%s/@@rdf" % container.absolute_url() pingCRSDS(service_to_ping, obj_url, False) return True
def __call__(self): if not IVersionEnhanced.providedBy(self.context): return False return IVersionControl(self.context).can_version()
def create_version(context, reindex=True): """ Create a new version of an object This is done by copy&pasting the object, then assigning, as versionId, the one from the original object. Additionally, we rename the object using a number based scheme and then clean it up to avoid various problems. """ logger.info("Started creating version of %s", context.absolute_url()) obj_id = context.getId() parent = utils.parent(context) # Adapt version parent (if case) if not IVersionEnhanced.providedBy(context): alsoProvides(context, IVersionEnhanced) # _ = IVersionControl(context).getVersionId() # Create version object # 1. copy object clipb = parent.manage_copyObjects(ids=[obj_id]) # 2. pregenerate new id for the copy new_id = generateNewId(parent, obj_id) # 3. alter the clipboard data and inject the desired new id clipb_decoded = _cb_decode(clipb) clipb = _cb_encode((clipb_decoded[0], clipb_decoded[1], [new_id])) # 4. call paste operation manage_pasteObjects_Version(parent, clipb) # 5. get the version object - no need for a rename anymore ver = parent[new_id] # #31440 apply related items from original object to the new version ver.setRelatedItems(context.getRelatedItems()) # Set effective date today ver.setCreationDate(DateTime()) ver.setEffectiveDate(None) ver.setExpirationDate(None) mtool = getToolByName(context, 'portal_membership') auth_user = mtool.getAuthenticatedMember() auth_username = auth_user.getUserName() auth_username_list = [auth_username] current_creators = ver.Creators() auth_username_list.extend(current_creators) username_list = [] for name in auth_username_list: if name == auth_username and name in username_list: continue else: username_list.append(name) new_creators = tuple(username_list) ver.setCreators(new_creators) # Remove comments if hasNewDiscussion: conversation = IConversation(ver) while conversation.keys(): conversation.__delitem__(conversation.keys()[0]) else: if hasattr(aq_base(ver), 'talkback'): tb = ver.talkback if tb is not None: for obj in tb.objectValues(): obj.__of__(tb).unindexObject() tb._container = PersistentMapping() notify(VersionCreatedEvent(ver, context)) if reindex: ver.reindexObject() # some catalogued values of the context may depend on versions _reindex(context) logger.info("Created version at %s", ver.absolute_url()) return ver
def create_version(context, reindex=True): """ Create a new version of an object This is done by copy&pasting the object, then assigning, as versionId, the one from the original object. Additionally, we rename the object using a number based scheme and then clean it up to avoid various problems. """ logger.info("Started creating version of %s", context.absolute_url()) obj_id = context.getId() parent = utils.parent(context) # Adapt version parent (if case) if not IVersionEnhanced.providedBy(context): alsoProvides(context, IVersionEnhanced) # _ = IVersionControl(context).getVersionId() # Create version object clipb = parent.manage_copyObjects(ids=[obj_id]) res = parent.manage_pasteObjects(clipb) new_id = res[0]['new_id'] ver = getattr(parent, new_id) # Fixes the generated id: remove copy_of from ID # ZZZ: add -vX sufix to the ids vid = ver.getId() new_id = vid.replace('copy_of_', '') new_id = generateNewId(parent, new_id) parent.manage_renameObject(id=vid, new_id=new_id) ver = parent[new_id] # Set effective date today ver.setCreationDate(DateTime()) ver.setEffectiveDate(None) ver.setExpirationDate(None) mtool = getToolByName(context, 'portal_membership') auth_user = mtool.getAuthenticatedMember() auth_username = auth_user.getUserName() auth_username_list = [auth_username] current_creators = ver.Creators() auth_username_list.extend(current_creators) username_list = [] for name in auth_username_list: if name == auth_username and name in username_list: continue else: username_list.append(name) new_creators = tuple(username_list) ver.setCreators(new_creators) # Remove comments if hasNewDiscussion: conversation = IConversation(ver) while conversation.keys(): conversation.__delitem__(conversation.keys()[0]) else: if hasattr(aq_base(ver), 'talkback'): tb = ver.talkback if tb is not None: for obj in tb.objectValues(): obj.__of__(tb).unindexObject() tb._container = PersistentMapping() notify(VersionCreatedEvent(ver, context)) if reindex: ver.reindexObject() # some catalogued values of the context may depend on versions _reindex(context) logger.info("Created version at %s", ver.absolute_url()) return ver
def isVersionEnhanced(context): """ Returns bool if context implements IVersionEnhanced """ return bool(IVersionEnhanced.providedBy(context))
def versions(self): """ Return a list of sorted version objects """ # Avoid making a catalog call if versionId is empty if not self.versionId: return [self.context] if not isinstance(self.versionId, basestring): return [self.context] # this is an old, unmigrated storage cat = getToolByName(self.context, 'portal_catalog', None) if not cat: return [] query = {'getVersionId': self.versionId} mtool = getToolByName(self.context, 'portal_membership') if mtool.isAnonymousUser(): query['review_state'] = 'published' brains = cat.unrestrictedSearchResults(**query) else: brains = cat(**query) objects = [] for b in brains: try: obj = b.getObject() except Exception as err: # Because of the unrestricted search done above, this # might happen, and we don't need a crash. Also do not # crash if the object is missing/not re-indexed yet. logger.warn(err) continue else: # #93975: before #91514 versions were sometimes incorrectly # created in that pages set as default view were assigned as # versions for folders. For such cases we need to replace the # page with the 'canonical_object' state = getMultiAdapter((obj, self.context.REQUEST), name='plone_context_state') canonical_obj = state.canonical_object() if IVersionEnhanced.providedBy( canonical_obj) and canonical_obj != obj: canonical_obj_version = IAnnotations( canonical_obj)[VERSION_ID] if canonical_obj_version != self.versionId: query['getVersionId'] = canonical_obj_version o_brains = cat.unrestrictedSearchResults(**query) if len(o_brains) > 1: logger.warn('DefaultView: Object %s has different ' 'version id than its default view' % canonical_obj.absolute_url()) else: assign_version(canonical_obj, self.versionId) RevokeVersion(obj, self.context.REQUEST).__call__() logger.warn( 'DefaultView: Version id moved from default ' 'view to canonical object for %s' % canonical_obj.absolute_url()) else: # the default view had the same version id as # the canonical object, thre is no need for that RevokeVersion(obj, self.context.REQUEST).__call__() if canonical_obj not in objects: objects.append(canonical_obj) else: if obj not in objects: objects.append(obj) # Some objects don't have EffectiveDate so we have to sort # them using CreationDate. This has the side effect that # in certain conditions the list of versions is reordered # For the anonymous users this is never a problem because # they only see published (and with effective_date) objects # during creation self.context has not been indexed if not self.context.UID() in [o.UID() for o in objects if o]: objects.append(self.context) # Store versions as ordered list, with the oldest item first # #20827 check if creation_date isn't bigger than the effective # date of the object as there are situation where the effective_date # is smaller such as for object without an workflow like FigureFile _versions = sorted(objects, key=lambda ob: ob.effective_date if ob.effective_date else ob.creation_date) return _versions