Code

Ticket #8261: view_on_site.diff

File view_on_site.diff, 16.9 KB (added by kratorius, 6 years ago)
Line 
1diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
2index aaa2e30..334db29 100644
3--- a/django/contrib/admin/helpers.py
4+++ b/django/contrib/admin/helpers.py
5@@ -103,7 +103,8 @@ class InlineAdminFormSet(object):
6 
7     def __iter__(self):
8         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
9-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
10+            view_url = self.opts.get_view_on_site_url(original)
11+            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original, view_url)
12         for form in self.formset.extra_forms:
13             yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
14 
15@@ -125,12 +126,13 @@ class InlineAdminForm(AdminForm):
16     """
17     A wrapper around an inline form for use in the admin system.
18     """
19-    def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
20+    def __init__(self, formset, form, fieldsets, prepopulated_fields, original, view_on_site_url=None):
21         self.formset = formset
22         self.original = original
23         if original is not None:
24             self.original.content_type_id = ContentType.objects.get_for_model(original).pk
25-        self.show_url = original and hasattr(original, 'get_absolute_url')
26+        self.show_url = original and view_on_site_url is not None
27+        self.absolute_url = view_on_site_url
28         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
29     
30     def __iter__(self):
31diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
32index 3d60b9d..360504a 100644
33--- a/django/contrib/admin/options.py
34+++ b/django/contrib/admin/options.py
35@@ -38,6 +38,7 @@ class BaseModelAdmin(object):
36     filter_horizontal = ()
37     radio_fields = {}
38     prepopulated_fields = {}
39+    view_on_site = False
40 
41     def formfield_for_dbfield(self, db_field, **kwargs):
42         """
43@@ -151,6 +152,26 @@ class BaseModelAdmin(object):
44         return None
45     declared_fieldsets = property(_declared_fieldsets)
46 
47+    def get_view_on_site_url(self, obj=None):
48+        if obj is None or (hasattr(self, 'view_on_site') and not self.view_on_site):
49+            return None
50+
51+        if callable(self.view_on_site):
52+            return self.view_on_site(obj)
53+        elif type(self.view_on_site) == bool and self.view_on_site:
54+            # in case we have overriden the default view_on_site with a
55+            # boolean flag, revert to the ContentType lookup
56+            return self._view_on_site(obj)
57+
58+        return None
59+
60+    def _view_on_site(self, obj):
61+        content_type_id, object_id = ContentType.objects.get_for_model(obj).pk, obj.pk
62+
63+        return "../../../r/%(content_type_id)s/%(object_id)s/" % ({
64+            'content_type_id': content_type_id,
65+            'object_id': object_id})
66+
67 class ModelAdmin(BaseModelAdmin):
68     "Encapsulates all admin options and functionality for a given model."
69     __metaclass__ = forms.MediaDefiningClass
70@@ -392,7 +413,8 @@ class ModelAdmin(BaseModelAdmin):
71             'has_change_permission': self.has_change_permission(request, obj),
72             'has_delete_permission': self.has_delete_permission(request, obj),
73             'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
74-            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
75+            'has_absolute_url': self.get_view_on_site_url(obj) is not None,
76+            'absolute_url': self.get_view_on_site_url(obj),
77             'ordered_objects': ordered_objects,
78             'form_url': mark_safe(form_url),
79             'opts': opts,
80diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
81index 2fb17bb..052d75e 100644
82--- a/django/contrib/admin/templates/admin/change_form.html
83+++ b/django/contrib/admin/templates/admin/change_form.html
84@@ -25,7 +25,7 @@
85 {% block object-tools %}
86 {% if change %}{% if not is_popup %}
87   <ul class="object-tools"><li><a href="history/" class="historylink">{% trans "History" %}</a></li>
88-  {% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
89+  {% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
90   </ul>
91 {% endif %}{% endif %}
92 {% endblock %}
93diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
94index 9d9f598..f43e7e0 100644
95--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
96+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
97@@ -10,7 +10,7 @@
98     {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
99   </h3>
100   {% if inline_admin_form.show_url %}
101-  <p><a href="../../../r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a></p>
102+  <p><a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a></p>
103   {% endif %}
104   {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
105 
106diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
107index 820928a..7d6cc2f 100644
108--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
109+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
110@@ -24,7 +24,7 @@
111         <td class="original">
112           {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
113           {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
114-          {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
115+          {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
116             </p>{% endif %}
117           {{ inline_admin_form.pk_field.field }} {{ inline_admin_form.fk_field.field }}
118           {% spaceless %}
119diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
120index ccade8a..faf70c7 100644
121--- a/django/contrib/admin/validation.py
122+++ b/django/contrib/admin/validation.py
123@@ -232,6 +232,11 @@ def validate_base(cls, model):
124             for idx, f in enumerate(val):
125                 get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f)
126 
127+    # view_on_site
128+    if hasattr(cls, 'view_on_site'):
129+        if not callable(cls.view_on_site) and not isinstance(cls.view_on_site, bool):
130+            raise ImproperlyConfigured("%s.view_on_site is not a callable or a boolean value." % cls.__name__)
131+
132 def check_isseq(cls, label, obj):
133     if not isinstance(obj, (list, tuple)):
134         raise ImproperlyConfigured("'%s.%s' must be a list or tuple." % (cls.__name__, label))
135diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
136index f24dc46..3373f9c 100644
137--- a/docs/ref/contrib/admin.txt
138+++ b/docs/ref/contrib/admin.txt
139@@ -597,6 +597,28 @@ with an operator:
140     Performs a full-text match. This is like the default search method but uses
141     an index. Currently this is only available for MySQL.
142 
143+``view_on_site``
144+~~~~~~~~~~~~~~~~
145+
146+.. versionadded:: 1.1
147+
148+Set ``view_on_site`` to control whether to display or not the "View on site" link.
149+This link should bring you to a URL where you can display the saved object.
150+
151+Example::
152+
153+    class PersonAdmin(admin.ModelAdmin):
154+        view_on_site = True
155+
156+This value can be either a boolean flag or a callable. Default is ``False``.
157+
158+In case it is a callable, it accepts one parameter for the model instance.
159+For example::
160+
161+    class PersonAdmin(admin.ModelAdmin):
162+        def view_on_site(self, obj):
163+            return '/show/%s/%s/' % (obj.first_name, obj.last_name)
164+
165 ``ModelAdmin`` methods
166 ----------------------
167 
168diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml b/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml
169new file mode 100644
170index 0000000..fcb2efb
171--- /dev/null
172+++ b/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml
173@@ -0,0 +1,51 @@
174+<?xml version="1.0" encoding="utf-8"?>
175+<django-objects version="1.0">
176+    <object pk="1" model="admin_views.city">
177+        <field type="CharField" name="name">New York</field>
178+    </object>
179+    <object pk="2" model="admin_views.city">
180+        <field type="CharField" name="name">Chicago</field>
181+    </object>
182+    <object pk="3" model="admin_views.city">
183+        <field type="CharField" name="name">San Francisco</field>
184+    </object>
185+    <object pk="1" model="admin_views.restaurant">
186+        <field to="admin_views.city" name="city" rel="ManyToOneRel">1</field>
187+        <field type="CharField" name="name">Italian Pizza</field>
188+    </object>
189+    <object pk="2" model="admin_views.restaurant">
190+        <field to="admin_views.city" name="city" rel="ManyToOneRel">1</field>
191+        <field type="CharField" name="name">Boulevard</field>
192+    </object>
193+    <object pk="3" model="admin_views.restaurant">
194+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
195+        <field type="CharField" name="name">Chinese Dinner</field>
196+    </object>
197+    <object pk="4" model="admin_views.restaurant">
198+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
199+        <field type="CharField" name="name">Angels</field>
200+    </object>
201+    <object pk="5" model="admin_views.restaurant">
202+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
203+        <field type="CharField" name="name">Take Away</field>
204+    </object>
205+    <object pk="6" model="admin_views.restaurant">
206+        <field to="admin_views.city" name="city" rel="ManyToOneRel">3</field>
207+        <field type="CharField" name="name">The Unknown Restaurant</field>
208+    </object>
209+    <object pk="1" model="admin_views.worker">
210+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
211+        <field type="CharField" name="name">Mario</field>
212+        <field type="CharField" name="surname">Rossi</field>
213+    </object>
214+    <object pk="2" model="admin_views.worker">
215+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
216+        <field type="CharField" name="name">Antonio</field>
217+        <field type="CharField" name="surname">Bianchi</field>
218+    </object>
219+    <object pk="3" model="admin_views.worker">
220+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
221+        <field type="CharField" name="name">John</field>
222+        <field type="CharField" name="surname">Doe</field>
223+    </object>
224+</django-objects>
225diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
226index 050823f..8b001d8 100644
227--- a/tests/regressiontests/admin_views/models.py
228+++ b/tests/regressiontests/admin_views/models.py
229@@ -20,7 +20,7 @@ class Article(models.Model):
230 
231     def __unicode__(self):
232         return self.title
233-   
234+
235     def model_year(self):
236         return self.date.year
237     model_year.admin_order_field = 'date'
238@@ -79,6 +79,7 @@ class ChapterInline(admin.TabularInline):
239 class ArticleAdmin(admin.ModelAdmin):
240     list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year')
241     list_filter = ('date',)
242+    view_on_site = False
243 
244     def changelist_view(self, request):
245         "Test that extra_context works"
246@@ -134,12 +135,48 @@ class Thing(models.Model):
247 class ThingAdmin(admin.ModelAdmin):
248     list_filter = ('color',)
249 
250+class City(models.Model):
251+    name = models.CharField(max_length=100)
252+
253+class Restaurant(models.Model):
254+    city = models.ForeignKey(City)
255+    name = models.CharField(max_length=100)
256+
257+class Worker(models.Model):
258+    work_at = models.ForeignKey(Restaurant)
259+    name = models.CharField(max_length=50)
260+    surname = models.CharField(max_length=50)
261+
262+class RestaurantInlineAdmin(admin.TabularInline):
263+    model = Restaurant
264+    view_on_site = True
265+
266+class CityAdmin(admin.ModelAdmin):
267+    inlines = [ RestaurantInlineAdmin ]
268+    view_on_site = True
269+
270+class WorkerAdmin(admin.ModelAdmin):
271+    def view_on_site(self, obj):
272+        return '/worker/%s/%s/' % (obj.surname, obj.name)
273+
274+class WorkerInlineAdmin(admin.TabularInline):
275+    model = Worker
276+    def view_on_site(self, obj):
277+        return '/worker_inline/%s/%s/' % (obj.surname, obj.name)
278+
279+class RestaurantAdmin(admin.ModelAdmin):
280+    inlines = [ WorkerInlineAdmin ]
281+    view_on_site = False
282+
283 admin.site.register(Article, ArticleAdmin)
284 admin.site.register(CustomArticle, CustomArticleAdmin)
285 admin.site.register(Section, inlines=[ArticleInline])
286 admin.site.register(ModelWithStringPrimaryKey)
287 admin.site.register(Color)
288 admin.site.register(Thing, ThingAdmin)
289+admin.site.register(City, CityAdmin)
290+admin.site.register(Restaurant, RestaurantAdmin)
291+admin.site.register(Worker, WorkerAdmin)
292 
293 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
294 # That way we cover all four cases:
295diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
296index 391d1ff..1663c34 100644
297--- a/tests/regressiontests/admin_views/tests.py
298+++ b/tests/regressiontests/admin_views/tests.py
299@@ -9,7 +9,7 @@ from django.contrib.admin.util import quote
300 from django.utils.html import escape
301 
302 # local test models
303-from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
304+from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, City, Restaurant, Worker
305 
306 class AdminViewBasicTest(TestCase):
307     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
308@@ -735,3 +735,63 @@ class AdminViewUnicodeTest(TestCase):
309         self.failUnlessEqual(response.status_code, 200)
310         response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
311         self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
312+
313+class AdminViewOnSiteTest(TestCase):
314+    fixtures = ['admin-views-users.xml', 'admin-views-restaurants.xml']
315+   
316+    def setUp(self):
317+        self.client.login(username='super', password='secret')
318+   
319+    def tearDown(self):
320+        self.client.logout()
321+
322+    def test_false(self):
323+        "Ensure that the 'View on site' button is not displayed if view_on_site is False"
324+        response = self.client.get('/test_admin/admin/admin_views/restaurant/1/')
325+        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
326+        self.failIf(
327+            '"../../../r/%s/1/"' % content_type_pk in response.content,
328+            '"View on site" button displayed even if it has been disabled.')
329+
330+    def test_true(self):
331+        "Ensure that the default behaviour is followed if view_on_site is True"
332+        response = self.client.get('/test_admin/admin/admin_views/city/1/')
333+        content_type_pk = ContentType.objects.get_for_model(City).pk
334+        self.failUnless(
335+            '"../../../r/%s/1/"' % content_type_pk in response.content,
336+            '"View on site" is enabled but has not been displayed.')
337+
338+    def test_callable(self):
339+        "Ensure that the right link is displayed if view_on_site is a callable"
340+        response = self.client.get('/test_admin/admin/admin_views/worker/1/')
341+        worker = Worker.objects.get(pk=1)
342+        self.failUnless(
343+            '"/worker/%s/%s/"' % (worker.surname, worker.name) in response.content,
344+            '"View on site" is defined but has not been displayed.')
345+
346+class InlineAdminViewOnSiteTest(TestCase):
347+    fixtures = ['admin-views-users.xml', 'admin-views-restaurants.xml']
348+   
349+    def setUp(self):
350+        self.client.login(username='super', password='secret')
351+   
352+    def tearDown(self):
353+        self.client.logout()
354+
355+    def test_true(self):
356+        "Ensure that the 'View on site' button is displayed if view_on_site is True"
357+        response = self.client.get('/test_admin/admin/admin_views/city/1/')
358+        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
359+        self.failUnless(
360+            '../../../r/%s/1/' % content_type_pk in response.content,
361+            '"View on site" is enabled but has not been displayed.')
362+
363+    def test_callable(self):
364+        "Ensure that the default behaviour is followed if view_on_site is True"
365+        response = self.client.get('/test_admin/admin/admin_views/restaurant/1/')
366+        content_type_pk = ContentType.objects.get_for_model(Worker).pk
367+        worker = Worker.objects.get(pk=1)
368+        self.failUnless(
369+            '"/worker_inline/%s/%s/"' % (worker.surname, worker.name) in response.content,
370+            '"View on site" is defined but has not been displayed.')
371+