def test_02(self): """Was written for :ticket:`488` (Kann Person nicht mehr von Organisation entfernen (delete mti child with siblings)). """ Person = rt.models.contacts.Person Company = rt.models.contacts.Company Partner = rt.models.contacts.Partner # 1 : does delete_child work in normal situation? john = create(Person, first_name="John", last_name="Doe") as_partner = Partner.objects.get(pk=john.pk) delete_child(as_partner, Person) self.assertEqual(Person.objects.count(), 0) self.assertEqual(Partner.objects.count(), 1) as_partner.delete() # 2 : delete_child with an mti sibling john = create(Person, first_name="John", last_name="Doe") as_partner = Partner.objects.get(pk=john.pk) insert_child(as_partner, Company, full_clean=True) # this is the sitation: self.assertEqual(Person.objects.count(), 1) self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Company.objects.count(), 1) # the following failed before #488 was fixed: delete_child(as_partner, Company)
def test_02(self): """Was written for :ticket:`488` (Kann Person nicht mehr von Organisation entfernen (delete mti child with siblings)). """ Person = rt.modules.contacts.Person Company = rt.modules.contacts.Company Partner = rt.modules.contacts.Partner # 1 : does delete_child work in normal situation? john = create(Person, first_name="John", last_name="Doe") as_partner = Partner.objects.get(pk=john.pk) delete_child(as_partner, Person) self.assertEqual(Person.objects.count(), 0) self.assertEqual(Partner.objects.count(), 1) as_partner.delete() # 2 : delete_child with an mti sibling john = create(Person, first_name="John", last_name="Doe") as_partner = Partner.objects.get(pk=john.pk) insert_child(as_partner, Company, full_clean=True) # this is the sitation: self.assertEqual(Person.objects.count(), 1) self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Company.objects.count(), 1) # the following failed before #488 was fixed: delete_child(as_partner, Company)
def run_on_row(self, obj, ar): if ar is not None: pre_remove_child.send(sender=obj, request=ar.request, child=self.child_model) mti.delete_child(obj, self.child_model, ar) ar.set_response(refresh=True)
def run_on_row(self, obj, ar): if ar is not None: pre_remove_child.send( sender=obj, request=ar.request, child=self.child_model) mti.delete_child(obj, self.child_model, ar) ar.set_response(refresh=True)
def swapclass(self, watcher, new_class, data): """ Convert the watched object to a new_class instance and apply data accordingly. Caution: Here be dragons! See also :mod:`lino_welfare.tests.watchtim_tests`. """ obj = watcher.watched old_class = obj.__class__ assert old_class is not new_class newobj = None # ~ print 20130222, old_class, new_class if old_class is Partner: partner = obj else: partner = obj.partner_ptr if old_class is Client: # convert Client to Person, then continue as if old_class had been # Person dd.pre_remove_child.send(sender=obj, request=REQUEST, child=old_class) mti.delete_child(obj, old_class) newobj = obj = obj.person_ptr old_class = Person if old_class is new_class: return # ~ if new_class is not old_class and not issubclass(new_class,old_class): if not issubclass(new_class, old_class): dd.pre_remove_child.send(sender=obj, request=REQUEST, child=old_class) mti.delete_child(obj, old_class) newobj = obj = partner if new_class is Client: # create the Person if necessary: # ~ partner = obj.partner_ptr try: person = Person.objects.get(pk=partner.id) except Person.DoesNotExist: dd.pre_add_child.send(sender=partner, request=REQUEST, child=Person) person = mti.insert_child(partner, Person) self.applydata(person, data) self.validate_and_save(person) # create the Client newobj = obj = person if new_class is not Partner: dd.pre_add_child.send(sender=obj, request=REQUEST, child=new_class) newobj = mti.insert_child(obj, new_class) if newobj is not None: self.applydata(newobj, data) self.validate_and_save(newobj)
def test01(self): from lino.modlib.users.choicelists import UserTypes User = rt.models.users.User Partner = rt.models.contacts.Partner Person = rt.models.contacts.Person Company = rt.models.contacts.Company Role = rt.models.contacts.Role Account = rt.models.sepa.Account Invoice = rt.models.vat.VatAccountInvoice Journal = rt.models.ledger.Journal VoucherTypes = rt.models.ledger.VoucherTypes JournalGroups = rt.models.ledger.JournalGroups u = User(username='******', user_type=UserTypes.admin, language="en") u.save() def createit(): obj = Person(first_name="John", last_name="Doe") obj.full_clean() obj.save() pk = obj.pk return (obj, Partner.objects.get(pk=pk)) # # If there are no vetos, user can ask to delete from any MTI form # pe, pa = createit() pe.delete() pe, pa = createit() pa.delete() # # Cascade-related objects (e.g. addresses) are deleted # independently of the polymorphic form which initiated # deletion. # def check_cascade(model): pe, pa = createit() obj = model.objects.get(pk=pa.pk) rel = Account(partner=pa, iban="AL32293653370340154130927280") rel.full_clean() rel.save() obj.delete() self.assertEqual(Account.objects.count(), 0) check_cascade(Partner) check_cascade(Person) # # Vetos of one form are deteced by all other forms. # def check_veto(obj, expected): try: obj.delete() self.fail("Failed to raise Warning({0})".format(expected)) except Warning as e: self.assertEqual(str(e), expected) VKR = create_row( Journal, ref="VKR", name="VKR", voucher_type=VoucherTypes.get_for_model(Invoice), journal_group=JournalGroups.sales) pe, pa = createit() def check_vetos(obj, msg): m = obj.__class__ obj.full_clean() obj.save() check_veto(pa, msg) check_veto(pe, msg) self.assertEqual(m.objects.count(), 1) obj.delete() self.assertEqual(m.objects.count(), 0) msg = "Cannot delete Partner Doe John because 1 Invoices refer to it." msg = "Kann Partner Doe John nicht löschen weil 1 Rechnungen darauf verweisen." check_vetos(Invoice(partner=pa, journal=VKR), msg) # # Having an invoice does not prevent from removing the Person # child of the partner. # invoice = create_row(Invoice, partner=pa, journal=VKR) self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Person.objects.count(), 1) self.assertEqual(Invoice.objects.count(), 1) delete_child(pa, Person) # tidy up: self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Person.objects.count(), 0) invoice.delete() pa.delete() self.assertEqual(Partner.objects.count(), 0) # But Lino refuses to remove the Person child if it has vetos. # For example if the person form is being used as a contact person. pe, pa = createit() co = create_row(Company, name="Test") create_row(Role, company=co, person=pe) msg = "[u'Cannot delete Partner Doe John because 1 Contact Persons refer to it.']" if six.PY2: msg = "[u'Kann Partner Doe John nicht l\\xf6schen weil 1 Kontaktpersonen darauf verweisen.']" else: msg = "['Kann Partner Doe John nicht löschen weil 1 Kontaktpersonen darauf verweisen.']" try: delete_child(pa, Person) self.fail("Expected ValidationError") except ValidationError as e: self.assertEqual(msg, str(e))
def test_create_entry(self): ses = rt.login() Person = rt.models.app.Person Restaurant = rt.models.app.Restaurant Place = rt.models.app.Place Visit = rt.models.app.Visit Meal = rt.models.app.Meal # Create some initial data: Person(name="Alfred").save() Person(name="Bert").save() Person(name="Claude").save() Person(name="Dirk").save() r = Restaurant(id=1, name="First") r.save() for i in 1, 2: r.owners.add(Person.objects.get(pk=i)) for i in 3, 4: r.cooks.add(Person.objects.get(pk=i)) # Here is our data: lst = list(Person.objects.all()) self.assertEqual(str(lst), "[<Person: Alfred>, <Person: Bert>, <Person: Claude>, <Person: Dirk>]") lst = list(Restaurant.objects.all()) self.assertEqual( str(lst), "[Restaurant #1 ('First (owners=Alfred, Bert, cooks=Claude, Dirk)')]") x = list(Place.objects.all()) self.assertEqual( str(x), "[Place #1 ('First (owners=Alfred, Bert)')]") """ The :func:`delete_child` function --------------------------------- Imagine that a user of our application discovers that Restaurant #1 isn't actually a `Restaurant`, it's just a `Place`. They would like to "remove it's Restaurant data" from the database, but keep the `Place` data. Especially the primary key (#1) and the related objects (the owners) should remain unchanged. But the cooks must be deleted since they exist only for restaurants. It seems that this is not trivial in Django (`How do you delete child class object without deleting parent class object? <http://stackoverflow.com/questions/9439730>`__). That's why we wrote the :func:`delete_child` function. Here is how to "reduce" a Restaurant to a `Place` by calling the :func:`delete_child` function: """ from lino.utils.mti import delete_child p = Place.objects.get(id=1) delete_child(p, Restaurant) # The Place still exists, but no longer as a Restaurant: x = Place.objects.get(pk=1) self.assertEqual( str(x), "First (owners=Alfred, Bert)") try: list(Restaurant.objects.get(pk=1)) self.fail("Expected DoesNotExist") except Restaurant.DoesNotExist: pass # Traceback (most recent call last): # ... # DoesNotExist: Restaurant matching query does not exist. """ The :func:`insert_child` function ---------------------------------- The opposite operation, "promoting a simple Place to a Restaurant", is done using :func:`insert_child`. from lino.utils.mti import insert_child Let's first create a simple Place #2 with a single owner. """ obj = Place(id=2, name="Second") obj.save() obj.owners.add(Person.objects.get(pk=2)) obj.save() self.assertEqual( str(obj), "Second (owners=Bert)") # Now this Place becomes a Restaurant and hires 2 cooks: obj = insert_child(obj, Restaurant) for i in 3, 4: obj.cooks.add(Person.objects.get(pk=i)) self.assertEqual( str(obj), "Second (owners=Bert, cooks=Claude, Dirk)") # If you try to promote a Person to a Restaurant, you'll get an exception: person = Person.objects.get(pk=2) try: insert_child(person, Restaurant).save() self.fail("Expected ValidationError") except ValidationError as e: self.assertEqual( str(e), "['A Person cannot be parent for a Restaurant']") """ The :class:`EnableChild` virtual field -------------------------------------- This section shows how the :class:`EnableChild` virtual field is being used by Lino, and thus is Lino-specific. After the above examples our database looks like this: """ x = list(Person.objects.all()) self.assertEqual( str(x), "[<Person: Alfred>, <Person: Bert>, <Person: Claude>, <Person: Dirk>]") x = list(Place.objects.all()) self.assertEqual( str(x), "[Place #1 ('First (owners=Alfred, Bert)'), Place #2 ('Second (owners=Bert)')]") x = list(Restaurant.objects.all()) self.assertEqual( str(x), "[Restaurant #2 ('Second (owners=Bert, cooks=Claude, Dirk)')]") # Let's take Place #1 and look at it. obj = Place.objects.get(pk=1) self.assertEqual( str(obj), "First (owners=Alfred, Bert)") # How to see whether a given Place is a Restaurant? x = "" for i in Place.objects.all(): x += "{0} -> {1}\n".format(i, i.get_mti_child('restaurant')) self.assertEqual( x, """\ First (owners=Alfred, Bert) -> None Second (owners=Bert) -> Second (owners=Bert, cooks=Claude, Dirk) """) # Let's promote First (currently a simple Place) to a Restaurant: x = insert_child(obj, Restaurant) # Restaurant #1 ('#1 (name=First, owners=Alfred, Bert, cooks=)') # And Second stops being a Restaurant: second = Place.objects.get(pk=2) delete_child(second, Restaurant) # This operation has removed the related Restaurant instance: try: Restaurant.objects.get(pk=2) self.fail("Expected DoesNotExist") except Restaurant.DoesNotExist: pass # And finally, rather to explain why Restaurants sometimes # close and later reopen: bert = Person.objects.get(pk=2) second = Place.objects.get(pk=2) insert_child(second, Restaurant) # Restaurant #2 ('#2 (name=Second, owners=Bert, cooks=)') # Now we can see this place again as a Restaurant second = Restaurant.objects.get(pk=2) # And engage for example a new cook: second.cooks.add(bert) # second # Restaurant #2 ('#2 (name=Second, owners=Bert, cooks=Bert)') # Related objects # --------------- # Now let's have a more detailed look at what happens to the related # objects (Person, Visit and Meal). # Bert, the owner of Restaurant #2 does two visits: second = Restaurant.objects.get(pk=2) Visit(purpose="Say hello", person=bert, place=second).save() Visit(purpose="Hang around", person=bert, place=second).save() x = list(second.visit_set.all()) self.assertEqual( str(x), "[<Visit: Say hello visit by Bert at Second>, <Visit: Hang around visit by Bert at Second>]") # Claude and Dirk, now workless, still go to eat in restaurants: Meal(what="Fish",person=Person.objects.get(pk=3),restaurant=second).save() Meal(what="Meat",person=Person.objects.get(pk=4),restaurant=second).save() x = list(second.meal_set.all()) self.assertEqual( str(x), "[<Meal: Claude eats Fish at Second>, <Meal: Dirk eats Meat at Second>]") # Now we reduce Second to a Place: second = Place.objects.get(pk=2) delete_child(second, Restaurant) # Restaurant #2 no longer exists: try: Restaurant.objects.get(pk=2) self.fail("Expected DoesNotExist") except Restaurant.DoesNotExist: pass # Note that `Meal` has :attr:`allow_cascaded_delete # <lino.core.model.Model.allow_cascaded_delete>` set to # `['restaurant']`, otherwise the above code would have raised a # ValidationError :message:`Cannot delete #2 # (name=Second,owners=Bert,cooks=Bert) because 2 meals refer to it.` But # the meals have been deleted: self.assertEqual(Meal.objects.count(), 0) # Of course, #2 remains as a Place # The owner and visits have been taken over: second = Place.objects.get(pk=2) x = list(second.visit_set.all()) self.assertEqual( str(x), "[<Visit: Say hello visit by Bert at Second>, <Visit: Hang around visit by Bert at Second>]") # The :func:`create_mti_child` function # ------------------------------------- # This function is for rather internal use. :ref:`Python dumps <dpy>` # generated by :class:`lino.utils.dpy.Serializer` use this function for # creating MTI children instances without having to lookup their parent. # .. currentmodule:: lino.utils.dpy # In a Python dump we are in a special situation: All Place instances # are being generated first, and in another step we are going to create # all the Restaurant instances. So how can we create a Restaurant whose # Place already exists *without first having to do a lookup of the Place # record*? That's why :func:`create_mti_child` was written for. obj = Place(id=3, name="Third") obj.save() obj.owners.add(Person.objects.get(pk=2)) obj.save() self.assertEqual( str(obj), "Third (owners=Bert)") from lino.utils.dpy import create_mti_child obj = create_mti_child(Place, 3, Restaurant) # The return value is technically a normal model instance, # but whose `save` and `full_clean` methods have been # patched: `full_clean` is overridden to do nothing, # and `save` will call a "raw" save to avoid the # need of a proper Place instance for that Restaurant. # The only thing you can do with it is to save it: obj.save() # The `save` and `full_clean` methods are the only methods that # will be called by # :class:`lino.utils.dpy.Deserializer`. # To test whether :func:`create_mti_child` did her job, # we must re-read an instance: obj = Restaurant.objects.get(pk=3) self.assertEqual( str(obj), "Third (owners=Bert, cooks=)") # Note that :func:`create_mti_child` supports changing the # `name` although that field is defined in the Place model, # not in Restaurant. This feature was added 20170626 (#1926, # #1923). Before that date Lino raised an exception when you # specified a field of the parent model. And before *that* # (until 20120930) this case was silently ignored for # backwards compatibility (`/blog/2011/1210`). obj = Place(id=4, name="Fourth") obj.save() ow = create_mti_child(Place, 4, Restaurant, name="A new name") ow.full_clean() ow.save() obj = Restaurant.objects.get(id=4) self.assertEqual(obj.name, "A new name")
def test01(self): from lino.modlib.users.choicelists import UserTypes User = rt.modules.users.User Partner = rt.modules.contacts.Partner Person = rt.modules.contacts.Person Company = rt.modules.contacts.Company Role = rt.modules.contacts.Role Account = rt.modules.sepa.Account Invoice = rt.modules.vat.VatAccountInvoice Journal = rt.modules.ledger.Journal VoucherTypes = rt.modules.ledger.VoucherTypes JournalGroups = rt.modules.ledger.JournalGroups u = User(username='******', profile=UserTypes.admin, language="en") u.save() def createit(): obj = Person(first_name="John", last_name="Doe") obj.full_clean() obj.save() pk = obj.pk return (obj, Partner.objects.get(pk=pk)) # # If there are no vetos, user can ask to delete from any MTI form # pe, pa = createit() pe.delete() pe, pa = createit() pa.delete() # # Cascade-related objects (e.g. addresses) are deleted # independently of the polymorphic form which initiated # deletion. # def check_cascade(model): pe, pa = createit() obj = model.objects.get(pk=pa.pk) rel = Account(partner=pa, iban="AL32293653370340154130927280") rel.full_clean() rel.save() obj.delete() self.assertEqual(Account.objects.count(), 0) check_cascade(Partner) check_cascade(Person) # # Vetos of one form are deteced by all other forms. # def check_veto(obj, expected): try: obj.delete() self.fail("Failed to raise Warning({0})".format(expected)) except Warning as e: self.assertEqual(str(e), expected) VKR = create( Journal, ref="VKR", name="VKR", voucher_type=VoucherTypes.get_for_model(Invoice), journal_group=JournalGroups.sales) pe, pa = createit() def check_vetos(obj, msg): m = obj.__class__ obj.full_clean() obj.save() check_veto(pa, msg) check_veto(pe, msg) self.assertEqual(m.objects.count(), 1) obj.delete() self.assertEqual(m.objects.count(), 0) msg = "Cannot delete Partner Doe John because 1 Invoices refer to it." check_vetos(Invoice(partner=pa, journal=VKR), msg) # # Having an invoice does not prevent from removing the Person # child of the partner. # invoice = create(Invoice, partner=pa, journal=VKR) self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Person.objects.count(), 1) self.assertEqual(Invoice.objects.count(), 1) delete_child(pa, Person) # tidy up: self.assertEqual(Partner.objects.count(), 1) self.assertEqual(Person.objects.count(), 0) invoice.delete() pa.delete() self.assertEqual(Partner.objects.count(), 0) # But Lino refuses to remove the Person child if it has vetos. # For example if the person form is being used as a contact person. pe, pa = createit() co = create(Company, name="Test") create(Role, company=co, person=pe) msg = "[u'Cannot delete Partner Doe John because " \ "1 Contact Persons refer to it.']" try: delete_child(pa, Person) self.fail("Expected ValidationError") except ValidationError as e: self.assertEqual(str(e), msg)