diff --git a/AUTHORS b/AUTHORS index 1b2a0cfadb..8da6f9b5dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better: Marc Garcia Alex Gaynor Andy Gayton + Idan Gazit Baishampayan Ghose Dimitris Glezos glin@seznam.cz diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index ed41c7ff64..859229e430 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -522,10 +522,16 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = self.model() + prefixes = {} for FormSet in self.get_formsets(request): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1: + prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(data=request.POST, files=request.FILES, instance=new_object, - save_as_new=request.POST.has_key("_saveasnew")) + save_as_new=request.POST.has_key("_saveasnew"), + prefix=prefix) formsets.append(formset) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, change=False) @@ -547,8 +553,13 @@ class ModelAdmin(BaseModelAdmin): if isinstance(f, models.ManyToManyField): initial[k] = initial[k].split(",") form = ModelForm(initial=initial) + prefixes = {} for FormSet in self.get_formsets(request): - formset = FormSet(instance=self.model()) + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(instance=self.model(), prefix=prefix) formsets.append(formset) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) @@ -608,9 +619,14 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = obj + prefixes = {} for FormSet in self.get_formsets(request, new_object): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1: + prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(request.POST, request.FILES, - instance=new_object) + instance=new_object, prefix=prefix) formsets.append(formset) if all_valid(formsets) and form_validated: @@ -625,8 +641,13 @@ class ModelAdmin(BaseModelAdmin): else: form = ModelForm(instance=obj) + prefixes = {} for FormSet in self.get_formsets(request, obj): - formset = FormSet(instance=obj) + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(instance=obj, prefix=prefix) formsets.append(formset) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 13e7cd7a39..8dd735d15e 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -291,7 +291,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): ct_field_name = "content_type" ct_fk_field_name = "object_id" - def __init__(self, data=None, files=None, instance=None, save_as_new=None): + def __init__(self, data=None, files=None, instance=None, save_as_new=None, prefix=None): opts = self.model._meta self.instance = instance self.rel_name = '-'.join(( @@ -300,9 +300,17 @@ class BaseGenericInlineFormSet(BaseModelFormSet): )) super(BaseGenericInlineFormSet, self).__init__( queryset=self.get_queryset(), data=data, files=files, - prefix=self.rel_name + prefix=prefix ) + #@classmethod + def get_default_prefix(cls): + opts = cls.model._meta + return '-'.join((opts.app_label, opts.object_name.lower(), + cls.ct_field.name, cls.ct_fk_field.name, + )) + get_default_prefix = classmethod(get_default_prefix) + def get_queryset(self): # Avoid a circular import. from django.contrib.contenttypes.models import ContentType diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 887f130347..10306c7e6d 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -32,7 +32,7 @@ class BaseFormSet(StrAndUnicode): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList): self.is_bound = data is not None or files is not None - self.prefix = prefix or 'form' + self.prefix = prefix or self.get_default_prefix() self.auto_id = auto_id self.data = data self.files = files @@ -62,7 +62,7 @@ class BaseFormSet(StrAndUnicode): initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count} self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix) - + # construct the forms in the formset self._construct_forms() @@ -74,7 +74,7 @@ class BaseFormSet(StrAndUnicode): self.forms = [] for i in xrange(self._total_form_count): self.forms.append(self._construct_form(i)) - + def _construct_form(self, i, **kwargs): """ Instantiates and returns the i-th form instance in a formset. @@ -118,7 +118,7 @@ class BaseFormSet(StrAndUnicode): def _get_deleted_forms(self): """ - Returns a list of forms that have been marked for deletion. Raises an + Returns a list of forms that have been marked for deletion. Raises an AttributeError if deletion is not allowed. """ if not self.is_valid() or not self.can_delete: @@ -176,6 +176,11 @@ class BaseFormSet(StrAndUnicode): return [self.forms[i[0]] for i in self._ordering] ordered_forms = property(_get_ordered_forms) + #@classmethod + def get_default_prefix(cls): + return 'form' + get_default_prefix = classmethod(get_default_prefix) + def non_form_errors(self): """ Returns an ErrorList of errors that aren't associated with a particular diff --git a/django/forms/models.py b/django/forms/models.py index e6bbb98718..bfc0b35803 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -249,8 +249,8 @@ class BaseModelForm(BaseForm): # This is an extra field that's not on the ModelForm, ignore it continue if not isinstance(f, ModelField): - # This is an extra field that happens to have a name that matches, - # for example, a related object accessor for this model. So + # This is an extra field that happens to have a name that matches, + # for example, a related object accessor for this model. So # get_field_by_name found it, but it is not a Field so do not proceed # to use it as if it were. continue @@ -472,7 +472,7 @@ class BaseInlineFormSet(BaseModelFormSet): # is there a better way to get the object descriptor? self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() qs = self.model._default_manager.filter(**{self.fk.name: self.instance}) - super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name, + super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs) def _construct_forms(self): @@ -489,6 +489,12 @@ class BaseInlineFormSet(BaseModelFormSet): form.data[form.add_prefix(self._pk_field.name)] = None return form + #@classmethod + def get_default_prefix(cls): + from django.db.models.fields.related import RelatedObject + return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name() + get_default_prefix = classmethod(get_default_prefix) + def save_new(self, form, commit=True): fk_attname = self.fk.get_attname() kwargs = {fk_attname: self.instance.pk} diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index e6c1ee243e..1bc6577bf7 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -238,4 +238,9 @@ __test__ = {'API_TESTS':"""

+>>> formset = GenericFormSet(instance=lion, prefix='x') +>>> for form in formset.forms: +... print form.as_p() +

+

"""} diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 050823f6de..d849a7b9c1 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -20,7 +20,7 @@ class Article(models.Model): def __unicode__(self): return self.title - + def model_year(self): return self.date.year model_year.admin_order_field = 'date' @@ -54,14 +54,14 @@ class Chapter(models.Model): class ChapterXtra1(models.Model): chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') - xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') + xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') def __unicode__(self): return u'¿Xtra1: %s' % self.xtra class ChapterXtra2(models.Model): chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') - xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') + xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') def __unicode__(self): return u'¿Xtra2: %s' % self.xtra @@ -87,7 +87,7 @@ class ArticleAdmin(admin.ModelAdmin): 'extra_var': 'Hello!' } ) - + def modeladmin_year(self, obj): return obj.date.year modeladmin_year.admin_order_field = 'date' @@ -121,7 +121,7 @@ class ModelWithStringPrimaryKey(models.Model): class Color(models.Model): value = models.CharField(max_length=10) - warm = models.BooleanField() + warm = models.BooleanField() def __unicode__(self): return self.value @@ -134,12 +134,56 @@ class Thing(models.Model): class ThingAdmin(admin.ModelAdmin): list_filter = ('color',) +class Persona(models.Model): + """ + A simple persona associated with accounts, to test inlining of related + accounts which inherit from a common accounts class. + """ + name = models.CharField(blank=False, max_length=80) + def __unicode__(self): + return self.name + +class Account(models.Model): + """ + A simple, generic account encapsulating the information shared by all + types of accounts. + """ + username = models.CharField(blank=False, max_length=80) + persona = models.ForeignKey(Persona, related_name="accounts") + servicename = u'generic service' + + def __unicode__(self): + return "%s: %s" % (self.servicename, self.username) + +class FooAccount(Account): + """A service-specific account of type Foo.""" + servicename = u'foo' + +class BarAccount(Account): + """A service-specific account of type Bar.""" + servicename = u'bar' + +class FooAccountAdmin(admin.StackedInline): + model = FooAccount + extra = 1 + +class BarAccountAdmin(admin.StackedInline): + model = BarAccount + extra = 1 + +class PersonaAdmin(admin.ModelAdmin): + inlines = ( + FooAccountAdmin, + BarAccountAdmin + ) + admin.site.register(Article, ArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin) admin.site.register(Section, inlines=[ArticleInline]) admin.site.register(ModelWithStringPrimaryKey) admin.site.register(Color) admin.site.register(Thing, ThingAdmin) +admin.site.register(Persona, PersonaAdmin) # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # That way we cover all four cases: @@ -153,3 +197,5 @@ admin.site.register(Thing, ThingAdmin) admin.site.register(Book, inlines=[ChapterInline]) admin.site.register(Promo) admin.site.register(ChapterXtra1) + + diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 39daf116ab..bf198bc9a2 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1,5 +1,7 @@ # coding: utf-8 +import re + from django.test import TestCase from django.contrib.auth.models import User, Permission from django.contrib.contenttypes.models import ContentType @@ -9,22 +11,27 @@ from django.contrib.admin.util import quote from django.utils.html import escape # local test models -from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey +from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount + +try: + set +except NameError: + from sets import Set as set class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml'] - - # Store the bit of the URL where the admin is registered as a class + + # Store the bit of the URL where the admin is registered as a class # variable. That way we can test a second AdminSite just by subclassing # this test case and changing urlbit. urlbit = 'admin' - + def setUp(self): self.client.login(username='super', password='secret') - + def tearDown(self): self.client.logout() - + def testTrailingSlashRequired(self): """ If you leave off the trailing slash, app should redirect and add it. @@ -33,29 +40,29 @@ class AdminViewBasicTest(TestCase): self.assertRedirects(request, '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301 ) - + def testBasicAddGet(self): """ A smoke test to ensure GET on the add_view works. """ response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) - + def testAddWithGETArgs(self): response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'}) self.failUnlessEqual(response.status_code, 200) self.failUnless( - 'value="My Section"' in response.content, + 'value="My Section"' in response.content, "Couldn't find an input with the right value in the response." ) - + def testBasicEditGet(self): """ A smoke test to ensureGET on the change_view works. """ response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) - + def testBasicAddPost(self): """ A smoke test to ensure POST on add_view works. @@ -68,7 +75,7 @@ class AdminViewBasicTest(TestCase): } response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) self.failUnlessEqual(response.status_code, 302) # redirect somewhere - + def testBasicEditPost(self): """ A smoke test to ensure POST on edit_view works. @@ -116,7 +123,7 @@ class AdminViewBasicTest(TestCase): def testChangeListSortingCallable(self): """ - Ensure we can sort on a list_display field that is a callable + Ensure we can sort on a list_display field that is a callable (column 2 is callable_year in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2}) @@ -126,10 +133,10 @@ class AdminViewBasicTest(TestCase): response.content.index('Middle content') < response.content.index('Newest content'), "Results of sorting on callable are out of order." ) - + def testChangeListSortingModel(self): """ - Ensure we can sort on a list_display field that is a Model method + Ensure we can sort on a list_display field that is a Model method (colunn 3 is 'model_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3}) @@ -139,37 +146,37 @@ class AdminViewBasicTest(TestCase): response.content.index('Middle content') < response.content.index('Oldest content'), "Results of sorting on Model method are out of order." ) - + def testChangeListSortingModelAdmin(self): """ - Ensure we can sort on a list_display field that is a ModelAdmin method + Ensure we can sort on a list_display field that is a ModelAdmin method (colunn 4 is 'modeladmin_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4}) self.failUnlessEqual(response.status_code, 200) self.failUnless( - response.content.index('Oldest content') < response.content.index('Middle content') and + response.content.index('Oldest content') < response.content.index('Middle content') and response.content.index('Middle content') < response.content.index('Newest content'), "Results of sorting on ModelAdmin method are out of order." ) - + def testLimitedFilter(self): """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) self.failUnless( - '
' in response.content, + '
' in response.content, "Expected filter not found in changelist view." ) self.failIf( 'Blue' in response.content, "Changelist filter not correctly limited by limit_choices_to." ) - + def testIncorrectLookupParameters(self): """Ensure incorrect lookup parameters are handled gracefully.""" response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'}) - self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'}) self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) @@ -186,7 +193,7 @@ class CustomModelAdminTest(AdminViewBasicTest): request = self.client.get('/test_admin/admin2/') self.assertTemplateUsed(request, 'custom_admin/index.html') self.assert_('Hello from a custom index template *bar*' in request.content) - + def testCustomAdminSiteView(self): self.client.login(username='super', password='secret') response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) @@ -411,7 +418,7 @@ class AdminViewPermissionsTest(TestCase): post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) self.assertRedirects(post, '/test_admin/admin/admin_views/article/') self.failUnlessEqual(Article.objects.get(pk=1).content, '

edited article

') - + # one error in form should produce singular error message, multiple errors plural change_dict['title'] = '' post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) @@ -422,7 +429,7 @@ class AdminViewPermissionsTest(TestCase): post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) self.failUnlessEqual(request.status_code, 200) self.failUnless('Please correct the errors below.' in post.content, - 'Plural error message not found in response to post with multiple errors.') + 'Plural error message not found in response to post with multiple errors.') self.client.get('/test_admin/admin/logout/') def testCustomModelAdminTemplates(self): @@ -523,7 +530,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk)) should_contain = """%s""" % (quote(self.pk), escape(self.pk)) self.assertContains(response, should_contain) - + def test_url_conflicts_with_add(self): "A model with a primary key that ends with add should be visible" add_model = ModelWithStringPrimaryKey(id="i have something to add") @@ -531,7 +538,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(add_model.pk)) should_contain = """

Change model with string primary key

""" self.assertContains(response, should_contain) - + def test_url_conflicts_with_delete(self): "A model with a primary key that ends with delete should be visible" delete_model = ModelWithStringPrimaryKey(id="delete") @@ -539,7 +546,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(delete_model.pk)) should_contain = """

Change model with string primary key

""" self.assertContains(response, should_contain) - + def test_url_conflicts_with_history(self): "A model with a primary key that ends with history should be visible" history_model = ModelWithStringPrimaryKey(id="history") @@ -547,7 +554,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(history_model.pk)) should_contain = """

Change model with string primary key

""" self.assertContains(response, should_contain) - + class SecureViewTest(TestCase): fixtures = ['admin-views-users.xml'] @@ -582,28 +589,28 @@ class SecureViewTest(TestCase): LOGIN_FORM_KEY: 1, 'username': 'joepublic', 'password': 'secret'} - + def tearDown(self): self.client.logout() - + def test_secure_view_shows_login_if_not_logged_in(self): "Ensure that we see the login form" response = self.client.get('/test_admin/admin/secure-view/' ) self.assertTemplateUsed(response, 'admin/login.html') - + def test_secure_view_login_successfully_redirects_to_original_url(self): request = self.client.get('/test_admin/admin/secure-view/') self.failUnlessEqual(request.status_code, 200) query_string = "the-answer=42" login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string ) self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string) - + def test_staff_member_required_decorator_works_as_per_admin_login(self): """ Make sure only staff members can log in. Successful posts to the login page will redirect to the orignal url. - Unsuccessfull attempts will continue to render the login page with + Unsuccessfull attempts will continue to render the login page with a 200 status code. """ # Super User @@ -721,3 +728,80 @@ class AdminViewUnicodeTest(TestCase): self.failUnlessEqual(response.status_code, 200) response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict) self.assertRedirects(response, '/test_admin/admin/admin_views/book/') + +class AdminInheritedInlinesTest(TestCase): + fixtures = ['admin-views-users.xml',] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testInline(self): + "Ensure that inline models which inherit from a common parent are correctly handled by admin." + + foo_user = u"foo username" + bar_user = u"bar username" + + name_re = re.compile('name="(.*?)"') + + # test the add case + response = self.client.get('/test_admin/admin/admin_views/persona/add/') + names = name_re.findall(response.content) + # make sure we have no duplicate HTML names + self.failUnlessEqual(len(names), len(set(names))) + + # test the add case + post_data = { + "name": u"Test Name", + # inline data + "accounts-TOTAL_FORMS": u"1", + "accounts-INITIAL_FORMS": u"0", + "accounts-0-username": foo_user, + "accounts-2-TOTAL_FORMS": u"1", + "accounts-2-INITIAL_FORMS": u"0", + "accounts-2-0-username": bar_user, + } + + response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) + self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.failUnlessEqual(Persona.objects.count(), 1) + self.failUnlessEqual(FooAccount.objects.count(), 1) + self.failUnlessEqual(BarAccount.objects.count(), 1) + self.failUnlessEqual(FooAccount.objects.all()[0].username, foo_user) + self.failUnlessEqual(BarAccount.objects.all()[0].username, bar_user) + self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2) + + # test the edit case + + response = self.client.get('/test_admin/admin/admin_views/persona/1/') + names = name_re.findall(response.content) + # make sure we have no duplicate HTML names + self.failUnlessEqual(len(names), len(set(names))) + + post_data = { + "name": u"Test Name", + + "accounts-TOTAL_FORMS": "2", + "accounts-INITIAL_FORMS": u"1", + + "accounts-0-username": "%s-1" % foo_user, + "accounts-0-account_ptr": "1", + "accounts-0-persona": "1", + + "accounts-2-TOTAL_FORMS": u"2", + "accounts-2-INITIAL_FORMS": u"1", + + "accounts-2-0-username": "%s-1" % bar_user, + "accounts-2-0-account_ptr": "2", + "accounts-2-0-persona": "1", + } + response = self.client.post('/test_admin/admin/admin_views/persona/1/', post_data) + self.failUnlessEqual(response.status_code, 302) + self.failUnlessEqual(Persona.objects.count(), 1) + self.failUnlessEqual(FooAccount.objects.count(), 1) + self.failUnlessEqual(BarAccount.objects.count(), 1) + self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) + self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) + self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)