Repository: wagtail/wagtail

Issue:
ManyToManyFields display incorrectly in ModelAdmin inspect view
### Issue Summary

If a model with a ManyToManyField is registered with ModelAdmin with the Inspect view enabled, the ManyToManyField is included in the inspect output and always displays a value of `some_app.SomeTargetModel.None`. This happens because InspectView.get_field_display_value returns `getattr(instance, field_name)`, which in the case of a ManyToManyField is the manager object for the relation; for some reason the string representation of a manager is `some_app.SomeTargetModel.None`.

### Steps to Reproduce

On a fresh Wagtail project, add `wagtail.contrib.modeladmin` to INSTALLED_APPS and edit `home/models.py` to:

    from __future__ import absolute_import, unicode_literals

    from django.db import models

    from wagtail.wagtailadmin.edit_handlers import FieldPanel
    from wagtail.wagtailcore.models import Page


    class HomePage(Page):
        pass


    class Musician(models.Model):
        name = models.CharField(max_length=255)

        panels = [
            FieldPanel('name'),
        ]

        def __str__(self):
            return self.name


    class Band(models.Model):
        name = models.CharField(max_length=255)
        members = models.ManyToManyField(Musician)

        panels = [
            FieldPanel('name'),
            FieldPanel('members'),
        ]

        def __str__(self):
            return self.name

Create `home/wagtail_hooks.py`:

    from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register

    from home.models import Musician, Band

    class MusicianAdmin(ModelAdmin):
        model = Musician
        inspect_view_enabled = True

    modeladmin_register(MusicianAdmin)


    class BandAdmin(ModelAdmin):
        model = Band
        inspect_view_enabled = True

    modeladmin_register(BandAdmin)

From the Wagtail admin, create some musician / band objects then go to the inspect view for a band.

Expected behaviour: Either the 'members' field should be silently skipped (since that's what currently happens with one-to-many relations, and we might as well be consistent...), or it should output a list of band members

Actual behaviour: It displays the 'members' value as `home.Musician.None` (even if the band has some members).

### Technical details

* Python version: 3.4.3
* Django version: 1.10.4
* Wagtail version: master (1.9a0)
* Browser version: Chrome 55.0.2883.95



Discussion:
Hi @gasman,

I was under the (seemingly mistaken) impression that `get_inspect_view_fields()` on the `ModelAdmin` class filtered out `ManyToMany` fields by default, so that will need reviewing. I thought I'd followed [Django's model _meta API docs](https://docs.djangoproject.com/en/1.10/ref/models/meta/) properly there, but possibly not.

Either way, that default rendering treatment certainly isn't cool. Rendering a list (ul) of individual object strings seems like a good way to go. 

I've already refactored `InspectView.get_field_display_value()` quite a bit [in another fork](https://github.com/rkhleics/wagtail/blob/modeladmin/remove-app-dependecies/wagtail/contrib/modeladmin/views.py#L850) as part of a fix for some other issues, which I think would be better to precede work on this. However, I was waiting for #3288 and #3287 to be merged before submitting a pull for that, as I'm cautious about ending up in merge-conflict hell.


 
Have submitted the preceding changes mentioned above as #3349.
@gasman Would you be comfortable with something like the following to provide a fairer string representation of fields / other attributes that return a value of type `Manager`?

```
if isinstance(value, Manager):
    return value.get_queryset()
```

I know I mentioned before about outputting a `<ul>` list with an `<li>` for each item, but I'm cautious that might open up a few cans of worms, e.g.

- Should we also output a list if the value happens to be a `QuerySet`? 
- What about other iterables?
- What if each item is an `Image` or `Document` - should we render a list of thumbnails or specially formatted document links, like we do for individual instances of those?
- Do we need to think about having a representation for dictionaries / other types of value too?
I'd be happy with that suggested fix, yep.

Formatting as a `<ul>` would be nice, but not essential. I don't think there's any need to anticipate other types of value that might potentially be returned from a field - we'll cross those bridges when we come to them.

Re images and documents, it would be good to have as few special cases as possible in this code - ideally we'd have some sort of hook here so that wagtailimages / wagtaildocs can tell modeladmin "if you encounter a field of this type, here's what to do with it", rather than modeladmin itself having built-in knowledge of those types. (As far as possible, wagtailimages / wagtaildocs shouldn't have any special status with other apps - everything they do ought to be possible to replicate in an external third-party app.) But that's definitely out of scope here...

Resolving patch:
diff --git a/wagtail/contrib/modeladmin/views.py b/wagtail/contrib/modeladmin/views.py
--- a/wagtail/contrib/modeladmin/views.py
+++ b/wagtail/contrib/modeladmin/views.py
@@ -861,6 +861,14 @@ def get_field_display_value(self, field_name, field=None):
         # we can render something useful. raises AttributeError appropriately.
         val = getattr(self.instance, field_name)
 
+        if isinstance(val, models.Manager):
+            val = val.all()
+
+        if isinstance(val, models.QuerySet):
+            if val.exists():
+                return ', '.join(['%s' % obj for obj in val])
+            return self.model_admin.get_empty_value_display(field_name)
+
         # wagtail.wagtailimages might not be installed
         try:
             from wagtail.wagtailimages.models import AbstractImage
@@ -880,7 +888,9 @@ def get_field_display_value(self, field_name, field=None):
             pass
 
         # Resort to returning the real value or 'empty value'
-        return val or self.model_admin.get_empty_value_display(field_name)
+        if val or val is False:
+            return val
+        return self.model_admin.get_empty_value_display(field_name)
 
     def get_image_field_display(self, field_name, field):
         """ Render an image """


Test patch:
diff --git a/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py b/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py
--- a/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py
+++ b/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py
@@ -4,7 +4,7 @@
 from django.contrib.auth.models import Group, Permission
 from django.test import TestCase
 
-from wagtail.tests.testapp.models import BusinessIndex
+from wagtail.tests.testapp.models import BusinessIndex, EventCategory, EventPage
 from wagtail.tests.utils import WagtailTestUtils
 from wagtail.wagtailcore.models import GroupPagePermission, Page
 
@@ -125,6 +125,29 @@ def test_title_present(self):
         response = self.get(4)
         self.assertContains(response, 'Christmas', 2)
 
+    def test_manytomany_output(self):
+        """
+        Because ManyToMany fields are output InspectView by default, the
+        `categories` for the event should output as a comma separated list
+        once populated.
+        """
+        eventpage = EventPage.objects.get(pk=4)
+        free_category = EventCategory.objects.create(name='Free')
+        child_friendly_category = EventCategory.objects.create(name='Child-friendly')
+        eventpage.categories = (free_category, child_friendly_category)
+        eventpage.save()
+        response = self.get(4)
+        self.assertContains(response, '<dd>Free, Child-friendly</dd>', html=True)
+
+    def test_false_values_displayed(self):
+        """
+        Boolean fields with False values should display False, rather than the
+        value of `get_empty_value_display()`. For this page, those should be
+        `locked`, `expired` and `has_unpublished_changes`
+        """
+        response = self.get(4)
+        self.assertContains(response, '<dd>False</dd>', count=3, html=True)
+
     def test_location_present(self):
         """
         The location should appear once, in the field listing