Skip to content

Commit 61e672d

Browse files
committed
Merge pull request django-cms#2066 from yakky/feature/edit_model_frontend
Frontend-editable models
2 parents 27a2ce6 + 159ab7d commit 61e672d

File tree

19 files changed

+727
-12
lines changed

19 files changed

+727
-12
lines changed

cms/admin/placeholderadmin.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import sys
3+
from django.contrib.admin.helpers import AdminForm
34
from django.utils.decorators import method_decorator
45
from django.db import transaction
56
from django.utils import simplejson
@@ -34,6 +35,82 @@
3435
from cms.utils.i18n import get_language_list
3536

3637

38+
class FrontendEditableAdmin(object):
39+
frontend_editable_fields = []
40+
41+
def get_urls(self):
42+
"""
43+
Register the url for the single field edit view
44+
"""
45+
from django.conf.urls import patterns, url
46+
47+
info = "%s_%s" % (self.model._meta.app_label, self.model._meta.module_name)
48+
pat = lambda regex, fn: url(regex, self.admin_site.admin_view(fn), name='%s_%s' % (info, fn.__name__))
49+
50+
url_patterns = patterns(
51+
'',
52+
pat(r'edit-field/([0-9]+)/([a-z\-]+)/$', self.edit_field),
53+
)
54+
return url_patterns + super(FrontendEditableAdmin, self).get_urls()
55+
56+
def _get_object_for_single_field(self, object_id, language):
57+
# Quick and dirty way to retrieve objects for django-hvad
58+
# Cleaner implementation will extend this method in a child mixin
59+
try:
60+
return self.model.objects.language(language).get(pk=object_id)
61+
except AttributeError:
62+
return self.model.objects.get(pk=object_id)
63+
64+
def edit_field(self, request, object_id, language):
65+
obj = self._get_object_for_single_field(object_id, language)
66+
opts = obj.__class__._meta
67+
saved_successfully = False
68+
cancel_clicked = request.POST.get("_cancel", False)
69+
raw_fields = request.GET.get("edit_fields")
70+
fields = [field for field in raw_fields.split(",") if field in self.frontend_editable_fields]
71+
if not fields:
72+
return HttpResponseBadRequest(_("Fields %s not editabled in the frontend") % raw_fields)
73+
if not request.user.has_perm("%s_change" % self.model._meta.module_name):
74+
return HttpResponseForbidden(_("You do not have permission to edit this item"))
75+
# Dinamically creates the form class with only `field_name` field
76+
# enabled
77+
form_class = self.get_form(request, obj, fields=fields)
78+
if not cancel_clicked and request.method == 'POST':
79+
form = form_class(instance=obj, data=request.POST)
80+
if form.is_valid():
81+
form.save()
82+
saved_successfully = True
83+
else:
84+
form = form_class(instance=obj)
85+
admin_form = AdminForm(form, fieldsets=[(None, {'fields': fields})], prepopulated_fields={},
86+
model_admin=self)
87+
media = self.media + admin_form.media
88+
context = {
89+
'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'),
90+
'title': opts.verbose_name,
91+
'plugin': None,
92+
'plugin_id': None,
93+
'adminform': admin_form,
94+
'add': False,
95+
'is_popup': True,
96+
'media': media,
97+
'opts': opts,
98+
'change': True,
99+
'save_as': False,
100+
'has_add_permission': False,
101+
'window_close_timeout': 10,
102+
}
103+
if cancel_clicked:
104+
# cancel button was clicked
105+
context.update({
106+
'cancel': True,
107+
})
108+
return render_to_response('admin/cms/page/plugin/confirm_form.html', context, RequestContext(request))
109+
if not cancel_clicked and request.method == 'POST' and saved_successfully:
110+
return render_to_response('admin/cms/page/plugin/confirm_form.html', context, RequestContext(request))
111+
return render_to_response('admin/cms/page/plugin/change_form.html', context, RequestContext(request))
112+
113+
37114
class PlaceholderAdmin(ModelAdmin):
38115
def get_urls(self):
39116
"""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% load i18n l10n sekizai_tags cms_tags %}{% load url from future %}{% spaceless %}
2+
{% addtoblock "js" %}
3+
<script type="text/javascript">
4+
(function($) {
5+
// CMS.$ will be passed for $
6+
$(document).ready(function () {
7+
new CMS.PlaceholderItem('#cms_placeholder-model-{{ opts.app_label }}-{{ instance.pk|unlocalize }}-{{ attribute_name|slugify }}', {
8+
'type': 'generic',
9+
'plugin_name': 'Edit {{ opts.verbose_name }}',
10+
'urls': {
11+
'edit_plugin': '{{ admin_url }}'
12+
}
13+
});
14+
});
15+
})(CMS.$);
16+
</script>
17+
{% endaddtoblock %}
18+
<span id="cms_placeholder-model-{{ opts.app_label }}-{{ instance.pk|unlocalize }}-{{ attribute_name|slugify }}" class="cms_placeholder-generic">{{ item }}</span>{% endspaceless %}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ item }}

cms/templatetags/placeholder_tags.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from classytags.core import Tag, Options
44
from django import template
55
from django.template.defaultfilters import safe
6+
from django.utils.http import urlencode
7+
from classytags.helpers import InclusionTag
8+
from django.core.urlresolvers import reverse
9+
from cms.utils import get_language_from_request
610

711
register = template.Library()
812

@@ -23,4 +27,74 @@ def render_tag(self, context, placeholder, width, language=None):
2327
if not placeholder:
2428
return ''
2529
return safe(placeholder.render(context, width, lang=language))
26-
register.tag(RenderPlaceholder)
30+
register.tag(RenderPlaceholder)
31+
32+
33+
class CMSEditableObject(InclusionTag):
34+
template = 'cms/toolbar/model_attribute_noedit.html'
35+
edit_template = 'cms/toolbar/model_attribute_edit.html'
36+
name = 'show_editable_model'
37+
options = Options(
38+
Argument('instance'),
39+
Argument('attribute'),
40+
Argument('edit_fields', default=None, required=False),
41+
Argument('language', default=None, required=False),
42+
Argument('view_url', default=None, required=False),
43+
Argument('view_method', default=None, required=False),
44+
)
45+
46+
def _is_editable(self, request):
47+
return (request and hasattr(request, 'toolbar') and
48+
request.toolbar.edit_mode)
49+
50+
def get_template(self, context, **kwargs):
51+
if self._is_editable(context.get('request', None)):
52+
return self.edit_template
53+
return self.template
54+
55+
def get_context(self, context, instance, attribute, edit_fields, language,
56+
view_url, view_method):
57+
if not language:
58+
language = get_language_from_request(context['request'])
59+
# This allow the requested item to be a method, a property or an
60+
# attribute
61+
context['attribute_name'] = attribute
62+
querystring = {'language': language}
63+
if edit_fields:
64+
context['edit_fields'] = edit_fields.split(",")
65+
context['item'] = getattr(instance, attribute, '')
66+
if callable(context['item']):
67+
context['item'] = context['item'](context['request'])
68+
# If the toolbar is not enabled the following part is just skipped: it
69+
# would cause a perfomance hit for no reason
70+
if self._is_editable(context.get('request', None)):
71+
context['instance'] = instance
72+
context['opts'] = instance._meta
73+
# view_method has the precedence and we retrieve the corresponding
74+
# attribute in the instance class.
75+
# If view_method refers to a method it will be called passing the
76+
# request; if it's an attribute, it's stored for later use
77+
if view_method:
78+
method = getattr(instance, view_method)
79+
if callable(method):
80+
url_base = method(context['request'])
81+
else:
82+
url_base = method
83+
else:
84+
# The default view_url is the default admin changeform for the
85+
# current instance
86+
if not edit_fields:
87+
view_url = 'admin:%s_%s_change' % (
88+
instance._meta.app_label, instance._meta.module_name)
89+
url_base = reverse(view_url, args=(instance.pk,))
90+
else:
91+
if not view_url:
92+
view_url = 'admin:%s_%s_edit_field' % (
93+
instance._meta.app_label, instance._meta.module_name)
94+
url_base = reverse(view_url, args=(instance.pk, language))
95+
querystring['edit_fields'] = ",".join(context['edit_fields'])
96+
context['admin_url'] = "%s?%s" % (url_base, urlencode(querystring))
97+
return context
98+
99+
100+
register.tag(CMSEditableObject)

cms/test_utils/project/placeholderapp/admin.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
from cms.admin.placeholderadmin import PlaceholderAdmin
1+
from cms.admin.placeholderadmin import PlaceholderAdmin, FrontendEditableAdmin
22
from cms.test_utils.project.placeholderapp.models import (Example1, MultilingualExample1, TwoPlaceholderExample)
33
from django.contrib import admin
44
from hvad.admin import TranslatableAdmin
55

66

7-
class ExampleAdmin(PlaceholderAdmin):
8-
pass
7+
class ExampleAdmin(FrontendEditableAdmin, PlaceholderAdmin):
8+
frontend_editable_fields = ("char_1", "char_2")
99

1010

1111
class TwoPlaceholderExampleAdmin(PlaceholderAdmin):
1212
pass
1313

1414

15-
class MultilingualAdmin(TranslatableAdmin, PlaceholderAdmin):
16-
pass
15+
class MultilingualAdmin(FrontendEditableAdmin, TranslatableAdmin,
16+
PlaceholderAdmin):
17+
frontend_editable_fields = ("char_1", "char_2")
1718

1819

1920
admin.site.register(Example1, ExampleAdmin)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from cms.apphook_pool import apphook_pool
2+
from cms.app_base import CMSApp
3+
from django.utils.translation import ugettext_lazy as _
4+
5+
6+
class Example1App(CMSApp):
7+
name = _("Example1 App")
8+
urls = ["cms.test_utils.project.placeholderapp.urls"]
9+
10+
apphook_pool.register(Example1App)
11+
12+
class MultilingualExample1App(CMSApp):
13+
name = _("MultilingualExample1 App")
14+
urls = ["cms.test_utils.project.placeholderapp.urls_multi"]
15+
16+
apphook_pool.register(MultilingualExample1App)

cms/test_utils/project/placeholderapp/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.core.urlresolvers import reverse
12
from cms.utils.compat.dj import python_2_unicode_compatible
23
from django.db import models
34
from cms.models.fields import PlaceholderField
@@ -19,6 +20,15 @@ class Example1(models.Model):
1920
char_4 = models.CharField(u'char_4', max_length=255)
2021
placeholder = PlaceholderField('placeholder')
2122

23+
def callable_item(self, request):
24+
return self.char_1
25+
26+
def __str__(self):
27+
return self.char_1
28+
29+
def get_absolute_url(self):
30+
return reverse("detail", args=(self.pk,))
31+
2232

2333
class TwoPlaceholderExample(models.Model):
2434
char_1 = models.CharField(u'char_1', max_length=255)
@@ -45,4 +55,7 @@ class MultilingualExample1(TranslatableModel):
4555
placeholder_1 = PlaceholderField('placeholder_1')
4656

4757
def __str__(self):
48-
return self.char_1
58+
return self.char_1
59+
60+
def get_absolute_url(self):
61+
return reverse("detail_multi", args=(self.pk,))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
{% load placeholder_tags %}
3+
4+
{% block content %}
5+
<h1>{% show_editable_model instance "char_1" %}</h1>
6+
{% show_editable_model instance "char_2" "char_1,char_2" %}
7+
{% render_placeholder instance.placeholder %}
8+
{% endblock content %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
{% load placeholder_tags %}
3+
4+
{% block content %}
5+
<h1>{% show_editable_model instance "char_1" "char_1" %}</h1>
6+
{% show_editable_model instance "char_2" "char_1,char_2" %}
7+
{% render_placeholder instance.placeholder_1 %}
8+
{% endblock content %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
{% load placeholder_tags %}
3+
4+
{% block content %}
5+
{% for example in examples %}
6+
<h2><a href="{{example.get_absolute_url}}">!!{{example}}</a></h2>
7+
{% endfor %}
8+
{% endblock content %}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.conf.urls import *
2+
3+
urlpatterns = patterns('cms.test_utils.project.placeholderapp.views',
4+
url(r'^detail/(?P<id>[0-9]+)/$', 'detail_view', name="detail"),
5+
url(r'^$', 'list_view', name="list"),
6+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.conf.urls import *
2+
try:
3+
from django.conf.urls.i18n import i18n_patterns
4+
except ImportError:
5+
from i18nurls.i18n import i18n_patterns
6+
7+
urlpatterns = patterns('cms.test_utils.project.placeholderapp.views',
8+
url(r'^detail/(?P<id>[0-9]+)/$', 'detail_view_multi', name="detail_multi"),
9+
url(r'^$', 'list_view_multi', name="list_multi"),
10+
)
Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
1+
from django.http import HttpResponse
12
from django.shortcuts import render_to_response
3+
from django.template.base import Template
24
from django.template.context import RequestContext
3-
from cms.test_utils.project.placeholderapp.models import Example1
5+
from cms.test_utils.project.placeholderapp.models import (Example1,
6+
MultilingualExample1)
7+
from cms.utils import get_language_from_request
48

59

610
def example_view(request):
711
context = RequestContext(request)
812
context['examples'] = Example1.objects.all()
913
return render_to_response('placeholderapp.html', context)
14+
15+
16+
def _base_detail(request, instance, template_name='detail.html', item_name="char_1",
17+
template_string='',):
18+
context = RequestContext(request)
19+
context['instance'] = instance
20+
context['item_name'] = item_name
21+
if template_string:
22+
template = Template(template_string)
23+
return HttpResponse(template.render(context))
24+
else:
25+
return render_to_response(template_name, context)
26+
27+
28+
def list_view_multi(request):
29+
context = RequestContext(request)
30+
context['examples'] = MultilingualExample1.objects.language(
31+
get_language_from_request(request)).all()
32+
return render_to_response('list.html', context)
33+
34+
35+
def detail_view_multi(request, id, template_name='detail_multi.html', item_name="char_1",
36+
template_string='',):
37+
instance = MultilingualExample1.objects.language(
38+
get_language_from_request(request)).get(pk=id)
39+
return _base_detail(request, instance, template_name, item_name,
40+
template_string)
41+
42+
43+
def list_view(request):
44+
context = RequestContext(request)
45+
context['examples'] = Example1.objects.all()
46+
return render_to_response('list.html', context)
47+
48+
49+
def detail_view(request, id, template_name='detail.html', item_name="char_1",
50+
template_string='',):
51+
instance = Example1.objects.get(pk=id)
52+
return _base_detail(request, instance, template_name, item_name,
53+
template_string)

0 commit comments

Comments
 (0)