def test_create_post_same_reference_and_revision(self): """ Tests that when auto is True and we intent to create an object with the same type, reference and revision: * a new and available reference is given to the new object * the object is created. """ data = self.DATA.copy() ref = self.controller.reference rev = self.controller.revision data.update({ "type": self.TYPE, "reference": ref, "auto": True, "revision": rev, "name": "A valid object", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data) obj = m.PLMObject.objects.get(type=self.TYPE, revision=rev, name="A valid object") self.assertNotEqual(ref, obj.reference) self.assertEqual(self.group.id, obj.group_id) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator)
def test_create_post_error_same_reference(self): """ Tests that the creation of an object with the same type and reference but a different revision is forbidden. """ data = self.DATA.copy() ref = self.controller.reference rev = "a new revision" data.update({ "type": self.TYPE, "reference": ref, "auto": False, "revision": rev, "name": "An invalid object", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data) qset = m.PLMObject.objects.filter(type=self.TYPE, reference=ref, revision=rev) self.assertFalse(response.context["creation_form"].is_valid()) self.assertFalse(qset.exists())
def test_create_post_same_reference_and_revision(self): """ Tests that when auto is True and we intent to create an object with the same type, reference and revision: * a new and available reference is given to the new object * the object is created. """ data = self.DATA.copy() ref = self.controller.reference rev = self.controller.revision data.update({ "type" : self.TYPE, "reference" : ref, "auto" : True, "revision" : rev, "name" : "A valid object", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data) obj = m.PLMObject.objects.get(type=self.TYPE, revision=rev, name="A valid object") self.assertNotEqual(ref, obj.reference) self.assertEqual(self.group.id, obj.group_id) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator)
def test_create_from_template_post(self): data = self.DATA.copy() data["lifecycle"] = m.Lifecycle.objects.get(name="Template") template = self.CONTROLLER.create('TPL_1', 'Document', 'a', self.user, data, True, True) template.add_file(self.get_file("data.test", "plop")) template.promote(checked=True) data = self.DATA.copy() data.update({ "type" : self.TYPE, "reference" : "doc2", "auto" : False, "revision" : "a", "name" : "Docc", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, "template": str(template.id), }) response = self.post("/object/create/", data) obj = m.Document.objects.get(type=self.TYPE, reference="doc2", revision="a") self.assertEqual(obj, response.context["obj"].object) self.assertEqual("Docc", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) df, = obj.files self.assertEqual("data.test", df.filename) self.assertEqual("plop", open(df.file.path).read()) self.assertNotEqual(template.files[0].id, df.id) self.assertEqual(template.object, obj.template)
def test_create_and_attach_post(self): doc = self.attach_to_official_document() data = self.DATA.copy() data.update({ "__next__": "/home/", "related_doc": doc.id, "type": self.TYPE, "reference": "mapart", "auto": False, "revision": "a", "name": "MaPart", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data, follow=False, status_code=302) self.assertRedirects(response, "/home/") obj = m.PLMObject.objects.get(type=self.TYPE, reference="mapart", revision="a") self.assertEqual("MaPart", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) link = m.DocumentPartLink.current_objects.get(document=doc.object, part=obj)
def test_create_redirect_post(self): data = self.DATA.copy() data.update({ "__next__": "/home/", "type": self.TYPE, "reference": "mapart", "auto": False, "revision": "a", "name": "MaPart", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] page = "files" if issubclass(model_cls, m.Document) else "attributes" response = self.post("/object/create/", data, follow=False, status_code=302) self.assertRedirects(response, "/home/") obj = m.PLMObject.objects.get(type=self.TYPE, reference="mapart", revision="a") self.assertEqual("MaPart", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator)
def test_create_and_attach_post(self): part = self.get_part("part1") data = self.DATA.copy() data.update({ "__next__": "/home/", "related_part": part.id, "type": self.TYPE, "reference": "doc2", "auto": False, "revision": "a", "name": "Docc", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) response = self.post("/object/create/", data, follow=False, status_code=302) self.assertRedirects(response, "/home/") obj = m.PLMObject.objects.get(type=self.TYPE, reference="doc2", revision="a") self.assertEqual("Docc", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) link = m.DocumentPartLink.current_objects.get(document=obj, part=part.id)
def revise(self, new_revision, group=None): u""" Makes a new revision: duplicates :attr:`object`. The duplicated object's revision is *new_revision*. Returns a controller of the new object. """ self.check_readable() if not new_revision or new_revision == self.revision: raise RevisionError("Bad value for new_revision") try: validate_revision(new_revision) except ValueError as e: raise RevisionError(unicode(e)) if self.is_cancelled or self.is_deprecated: raise RevisionError("Object is deprecated or cancelled.") if models.RevisionLink.objects.now().filter(old=self.object.pk).exists(): raise RevisionError("A revision already exists for %s" % self.object) if group is None: group = self.object.group if not group.user_set.filter(id=self._user.id).exists(): raise ValueError("Invalid group") data = {} fields = self.get_modification_fields() + self.get_creation_fields() for attr in fields: if attr not in ("reference", "type", "revision"): data[attr] = getattr(self.object, attr) data["group"] = group data["state"] = models.get_default_state(self.lifecycle) new_controller = self.create(self.reference, self.type, new_revision, self._user, data) details = "old : %s, new : %s" % (self.object, new_controller.object) self._save_histo(models.RevisionLink.ACTION_NAME, details) models.RevisionLink.objects.create(old=self.object, new=new_controller.object) return new_controller
def test_create_and_attach_post_error(self): part = self.get_part("part1") # cancels the part so that it can not be attached part.cancel() data = self.DATA.copy() data.update({ "__next__": "/home/", "related_part": part.id, "type": self.TYPE, "reference": "doc2", "auto": False, "revision": "a", "name": "Docc", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, }) response = self.post("/object/create/", data, follow=True, page="parts") msgs = list(response.context["messages"]) self.assertEqual(2, len(msgs)) self.assertEqual(messages.INFO, msgs[0].level) self.assertEqual(messages.ERROR, msgs[1].level) obj = m.PLMObject.objects.get(type=self.TYPE, reference="doc2", revision="a") self.assertEqual("Docc", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) self.assertFalse( m.DocumentPartLink.current_objects.filter(document=obj, part=part.id).exists())
def revise(self, new_revision): u""" Makes a new revision: duplicates :attr:`object`. The duplicated object's revision is *new_revision*. Returns a controller of the new object. """ self.check_readable() if not new_revision or new_revision == self.revision or \ rx_bad_ref.search(new_revision): raise RevisionError("Bad value for new_revision") if models.RevisionLink.objects.filter(old=self.object.pk): raise RevisionError("a revision already exists for %s" % self.object) data = {} fields = self.get_modification_fields() + self.get_creation_fields() for attr in fields: if attr not in ("reference", "type", "revision"): data[attr] = getattr(self.object, attr) data["state"] = models.get_default_state(self.lifecycle) new_controller = self.create(self.reference, self.type, new_revision, self._user, data) details = "old : %s, new : %s" % (self.object, new_controller.object) self._save_histo(models.RevisionLink.ACTION_NAME, details) models.RevisionLink.objects.create(old=self.object, new=new_controller.object) return new_controller
def test_create_and_attach_post_error(self): doc = self.attach_to_official_document() # cancels the doc so that it can not be attached doc.cancel() data = self.DATA.copy() data.update({ "__next__" : "/home/", "related_doc" : doc.id, "type" : self.TYPE, "reference" : "mapart", "auto" : False, "revision" : "a", "name" : "MaPart", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) response = self.post("/object/create/", data, follow=True, page="doc-cad") msgs = list(response.context["messages"]) self.assertEqual(2, len(msgs)) self.assertEqual(messages.INFO, msgs[0].level) self.assertEqual(messages.ERROR, msgs[1].level) obj = m.PLMObject.objects.get(type=self.TYPE, reference="mapart", revision="a") self.assertEqual("MaPart", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) self.assertFalse(m.DocumentPartLink.current_objects.filter( document=doc.object, part=obj).exists())
def test_create_from_template_post(self): data = self.DATA.copy() data["lifecycle"] = m.Lifecycle.objects.get(name="Template") template = self.CONTROLLER.create('TPL_1', 'Document', 'a', self.user, data, True, True) template.add_file(self.get_file("data.test", "plop")) template.promote(checked=True) data = self.DATA.copy() data.update({ "type": self.TYPE, "reference": "doc2", "auto": False, "revision": "a", "name": "Docc", "group": str(self.group.id), "lifecycle": m.get_default_lifecycle().pk, "state": m.get_default_state().pk, "template": str(template.id), }) response = self.post("/object/create/", data) obj = m.Document.objects.get(type=self.TYPE, reference="doc2", revision="a") self.assertEqual(obj, response.context["obj"].object) self.assertEqual("Docc", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) df, = obj.files self.assertEqual("data.test", df.filename) self.assertEqual("plop", open(df.file.path).read()) self.assertNotEqual(template.files[0].id, df.id) self.assertEqual(template.object, obj.template)
def test_create_post(self): data = self.DATA.copy() data.update({ "type" : self.TYPE, "reference" : "mapart", "auto" : False, "revision" : "a", "name" : "MaPart", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] page = "files" if issubclass(model_cls, m.Document) else "attributes" response = self.post("/object/create/", data, page=page) obj = m.PLMObject.objects.get(type=self.TYPE, reference="mapart", revision="a") self.assertEqual(obj.id, response.context["obj"].id) self.assertEqual("MaPart", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator)
def revise(self, new_revision, group=None): u""" Makes a new revision: duplicates :attr:`object`. The duplicated object's revision is *new_revision*. Returns a controller of the new object. """ self.check_readable() if not new_revision or new_revision == self.revision: raise RevisionError("Bad value for new_revision") try: validate_revision(new_revision) except ValueError as e: raise RevisionError(unicode(e)) if self.is_cancelled or self.is_deprecated: raise RevisionError("Object is deprecated or cancelled.") if models.RevisionLink.objects.now().filter( old=self.object.pk).exists(): raise RevisionError("A revision already exists for %s" % self.object) if group is None: group = self.object.group if not group.user_set.filter(id=self._user.id).exists(): raise ValueError("Invalid group") data = {} fields = self.get_modification_fields() + self.get_creation_fields() for attr in fields: if attr not in ("reference", "type", "revision"): data[attr] = getattr(self.object, attr) data["group"] = group data["state"] = models.get_default_state(self.lifecycle) new_controller = self.create(self.reference, self.type, new_revision, self._user, data) details = "old : %s, new : %s" % (self.object, new_controller.object) self._save_histo(models.RevisionLink.ACTION_NAME, details) models.RevisionLink.objects.create(old=self.object, new=new_controller.object) return new_controller
def test_create_and_attach_post(self): part = self.get_part("part1") data = self.DATA.copy() data.update({ "__next__" : "/home/", "related_part" : part.id, "type" : self.TYPE, "reference" : "doc2", "auto" : False, "revision" : "a", "name" : "Docc", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) response = self.post("/object/create/", data, follow=False, status_code=302) self.assertRedirects(response, "/home/") obj = m.PLMObject.objects.get(type=self.TYPE, reference="doc2", revision="a") self.assertEqual("Docc", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) link = m.DocumentPartLink.current_objects.get(document=obj, part=part.id)
def test_create_post_error_same_reference_and_revision(self): """ Tests that the creation of an object with the same type , reference and revision is forbidden when auto is not True. """ data = self.DATA.copy() ref = self.controller.reference rev = self.controller.revision data.update({ "type" : self.TYPE, "reference" : ref, "auto" : False, "revision" : rev, "name" : "An invalid object", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data) qset = m.PLMObject.objects.filter(type=self.TYPE, reference=ref, revision=rev) self.assertFalse(response.context["creation_form"].is_valid())
def test_create_and_attach_post(self): doc = self.attach_to_official_document() data = self.DATA.copy() data.update({ "__next__" : "/home/", "related_doc" : doc.id, "type" : self.TYPE, "reference" : "mapart", "auto" : False, "revision" : "a", "name" : "MaPart", "group" : str(self.group.id), "lifecycle" : m.get_default_lifecycle().pk, "state" : m.get_default_state().pk, }) model_cls = m.get_all_plmobjects()[self.TYPE] response = self.post("/object/create/", data, follow=False, status_code=302) self.assertRedirects(response, "/home/") obj = m.PLMObject.objects.get(type=self.TYPE, reference="mapart", revision="a") self.assertEqual("MaPart", obj.name) self.assertEqual(self.user, obj.owner) self.assertEqual(self.user, obj.creator) link = m.DocumentPartLink.current_objects.get(document=doc.object, part=obj)
def create(cls, reference, user, data={}, block_mails=False, no_index=False): u""" This method builds a new :class:`.ECR` of type *class_* and return a :class:`ECRController` associated to the created object. :param reference: reference of the objet :param user: user who creates/owns the object :param data: a dict<key, value> with informations to add to the ECR :rtype: :class:`ECRController` """ if not user.is_active: raise PermissionError(u"%s's account is inactive" % user) # even a restricted account can create an ECR if not reference: raise ValueError("Empty value not permitted for reference") validate_reference(reference) # create an object try: reference_number = int(re.search(r"^ECR_(\d+)$", reference).group(1)) if reference_number > 2**31 - 1: reference_number = 0 except: reference_number = 0 obj = ECR(reference=reference, owner=user, creator=user, reference_number=reference_number) if no_index: obj.no_index = True if data: for key, value in data.iteritems(): if key != "reference": setattr(obj, key, value) obj.state = models.get_default_state(obj.lifecycle) obj.save() res = cls(obj, user) if block_mails: res.block_mails() # record creation in history infos = {"reference" : reference} infos.update(data) details = u",".join(u"%s : %s" % (k, v) for k, v in infos.items()) res._save_histo("Create", details) # add links ECRUserLink.objects.create(ecr=obj, user=user, role="owner") try: l = models.DelegationLink.current_objects.get(delegatee=user, role=models.ROLE_SPONSOR) sponsor = l.delegator if sponsor.username == settings.COMPANY: sponsor = user except models.DelegationLink.DoesNotExist: sponsor = user # the user can promote to the next state ECRUserLink.objects.create(ecr=obj, user=user, role=level_to_sign_str(0)) # from the next state, only the sponsor can promote this object for i in range(1, obj.lifecycle.nb_states - 1): ECRUserLink.objects.create(ecr=obj, user=sponsor, role=level_to_sign_str(i)) #res._update_state_history() return res
def create(cls, reference, type, revision, user, data={}, block_mails=False, no_index=False): u""" This method builds a new :class:`.PLMObject` of type *class_* and return a :class:`PLMObjectController` associated to the created object. Raises :exc:`ValueError` if *reference*, *type* or *revision* are empty. Raises :exc:`ValueError` if *type* is not valid. :param reference: reference of the objet :param type: type of the object :param revision: revision of the object :param user: user who creates/owns the object :param data: a dict<key, value> with informations to add to the plmobject :rtype: :class:`PLMObjectController` """ profile = user.get_profile() if not (profile.is_contributor or profile.is_administrator): raise PermissionError("%s is not a contributor" % user) if not reference or not type or not revision: raise ValueError("Empty value not permitted for reference/type/revision") if rx_bad_ref.search(reference) or rx_bad_ref.search(revision): raise ValueError("Reference or revision contains a '/' or a '..'") try: class_ = models.get_all_plmobjects()[type] except KeyError: raise ValueError("Incorrect type") # create an object obj = class_(reference=reference, type=type, revision=revision, owner=user, creator=user) if no_index: obj.no_index = True if data: for key, value in data.iteritems(): if key not in ["reference", "type", "revision"]: setattr(obj, key, value) obj.state = models.get_default_state(obj.lifecycle) obj.save() res = cls(obj, user) if block_mails: res.block_mails() # record creation in history infos = {"type" : type, "reference" : reference, "revision" : revision} infos.update(data) details = u",".join(u"%s : %s" % (k, v) for k, v in infos.items()) res._save_histo("Create", details) # add links models.PLMObjectUserLink.objects.create(plmobject=obj, user=user, role="owner") try: l = models.DelegationLink.objects.get(delegatee=user, role=models.ROLE_SPONSOR) sponsor = l.delegator if sponsor.username == settings.COMPANY: sponsor = user except models.DelegationLink.DoesNotExist: sponsor = user # the user can promote to the next state models.PLMObjectUserLink.objects.create(plmobject=obj, user=user, role=level_to_sign_str(0)) # from the next state, only the sponsor can promote this object for i in range(1, obj.lifecycle.nb_states - 1): models.PLMObjectUserLink.objects.create(plmobject=obj, user=sponsor, role=level_to_sign_str(i)) return res
def test_get_default_state(self): state = get_default_state() self.assertEqual(state.name, "draft")
def create(cls, reference, type, revision, user, data={}, block_mails=False, no_index=False): u""" This method builds a new :class:`.PLMObject` of type *class_* and return a :class:`PLMObjectController` associated to the created object. Raises :exc:`ValueError` if *reference*, *type* or *revision* are empty. Raises :exc:`ValueError` if *type* is not valid. :param reference: reference of the objet :param type: type of the object :param revision: revision of the object :param user: user who creates/owns the object :param data: a dict<key, value> with informations to add to the plmobject :rtype: :class:`PLMObjectController` """ profile = user.profile if not (profile.is_contributor or profile.is_administrator): raise PermissionError("%s is not a contributor" % user) if not user.is_active: raise PermissionError(u"%s's account is inactive" % user) if profile.restricted: raise PermissionError("Restricted account can not create a part or document.") if not reference or not type or not revision: raise ValueError("Empty value not permitted for reference/type/revision") validate_reference(reference) validate_revision(revision) try: class_ = models.get_all_plmobjects()[type] except KeyError: raise ValueError("Incorrect type") # create an object reference_number = parse_reference_number(reference, class_) obj = class_(reference=reference, type=type, revision=revision, owner=user, creator=user, reference_number=reference_number) if no_index: obj.no_index = True if data: for key, value in data.iteritems(): if key not in ["reference", "type", "revision", "auto", "pfiles"]: setattr(obj, key, value) obj.state = models.get_default_state(obj.lifecycle) obj.save() res = cls(obj, user) if block_mails: res.block_mails() # record creation in history infos = {"type" : type, "reference" : reference, "revision" : revision} infos.update(data) details = u" / ".join(u"%s : %s" % (k, v) for k, v in infos.items() if k not in ("auto", "pfiles", "type", "reference", "revision", "name")) res._save_histo("created", details) # add links (bulk create) ctime = obj.ctime links = [models.PLMObjectUserLink(plmobject=obj, user=user, role="owner", ctime=ctime)] try: l = models.DelegationLink.current_objects.select_related("delegator").get(delegatee=user, role=models.ROLE_SPONSOR) sponsor = l.delegator if sponsor.username == settings.COMPANY: sponsor = user elif not res.check_in_group(sponsor, False): sponsor = user except models.DelegationLink.DoesNotExist: sponsor = user # the user can promote to the next state links.append(models.PLMObjectUserLink(plmobject=obj, user=user, role=level_to_sign_str(0), ctime=ctime)) # from the next state, only the sponsor can promote this object for i in range(1, obj.lifecycle.nb_states - 1): links.append(models.PLMObjectUserLink(plmobject=obj, user=sponsor, role=level_to_sign_str(i), ctime=ctime)) models.PLMObjectUserLink.objects.bulk_create(links) res._update_state_history() return res
def create(cls, reference, type, revision, user, data={}, block_mails=False, no_index=False): u""" This method builds a new :class:`.PLMObject` of type *class_* and return a :class:`PLMObjectController` associated to the created object. Raises :exc:`ValueError` if *reference*, *type* or *revision* are empty. Raises :exc:`ValueError` if *type* is not valid. :param reference: reference of the objet :param type: type of the object :param revision: revision of the object :param user: user who creates/owns the object :param data: a dict<key, value> with informations to add to the plmobject :rtype: :class:`PLMObjectController` """ profile = user.profile if not (profile.is_contributor or profile.is_administrator): raise PermissionError("%s is not a contributor" % user) if not user.is_active: raise PermissionError(u"%s's account is inactive" % user) if profile.restricted: raise PermissionError( "Restricted account can not create a part or document.") if not reference or not type or not revision: raise ValueError( "Empty value not permitted for reference/type/revision") validate_reference(reference) validate_revision(revision) try: class_ = models.get_all_plmobjects()[type] except KeyError: raise ValueError("Incorrect type") # create an object reference_number = parse_reference_number(reference, class_) obj = class_(reference=reference, type=type, revision=revision, owner=user, creator=user, reference_number=reference_number) if no_index: obj.no_index = True if data: for key, value in data.iteritems(): if key not in [ "reference", "type", "revision", "auto", "pfiles" ]: setattr(obj, key, value) obj.state = models.get_default_state(obj.lifecycle) obj.save() res = cls(obj, user) if block_mails: res.block_mails() # record creation in history infos = {"type": type, "reference": reference, "revision": revision} infos.update(data) details = u" / ".join(u"%s : %s" % (k, v) for k, v in infos.items() if k not in ("auto", "pfiles", "type", "reference", "revision", "name")) res._save_histo("created", details) # add links (bulk create) ctime = obj.ctime links = [ models.PLMObjectUserLink(plmobject=obj, user=user, role="owner", ctime=ctime) ] try: l = models.DelegationLink.current_objects.select_related( "delegator").get(delegatee=user, role=models.ROLE_SPONSOR) sponsor = l.delegator if sponsor.username == settings.COMPANY: sponsor = user elif not res.check_in_group(sponsor, False): sponsor = user except models.DelegationLink.DoesNotExist: sponsor = user # the user can promote to the next state links.append( models.PLMObjectUserLink(plmobject=obj, user=user, role=level_to_sign_str(0), ctime=ctime)) # from the next state, only the sponsor can promote this object for i in range(1, obj.lifecycle.nb_states - 1): links.append( models.PLMObjectUserLink(plmobject=obj, user=sponsor, role=level_to_sign_str(i), ctime=ctime)) models.PLMObjectUserLink.objects.bulk_create(links) res._update_state_history() return res