Question

I'm putting a project together and attempting to utilize as much of open source libraries as possible to minimize the 'typing' involved in getting things done. I am using django, crispy-forms and bootstrap framework.

I've written code for handling one entity (add/edit/delete) and feeling that I must be doing something wrong because there seems to be too much code involved - there are over 20 different items that will be managed pretty much the same way and so I thought I'd ask the community for mistakes I can fix before I jump into getting the rest of code done.

So I have a model:

class Link(models.Model):
    name = models.CharField(max_length=255, blank=False, null=False, verbose_name=_(u'Название'))
    url = models.URLField(blank=False, null=False, verbose_name=_(u'Ссылка'))
    project = models.ForeignKey('Project')
    display_on_main_project_page = models.BooleanField(default=False, verbose_name=_(u'Показывать ссылку на главной странице проекта'))

    class Meta:
        app_label = 'core'
        verbose_name = _(u'Ссылка')
        verbose_name_plural = _(u'Ссылки')

    def __unicode__(self):
        return self.name

and a form with crispy bits added to it as per the manual:

class ProjectLinkForm(forms.Form):
    id = forms.IntegerField(required=False, widget=forms.HiddenInput())
    project = forms.ModelChoiceField(queryset=Project.objects.all(),required=True, widget=forms.HiddenInput())
    name = forms.CharField(required=True, label=_(u'Имя ссылки'))
    url = forms.URLField(required=True, label=_(u'URL ссылки'))
    display_on_main_project_page = forms.BooleanField(label=_(u'Показывать на главной странице проекта'), required=False)

    class Meta:
        model = Link
        fields = ('project','name','url','display_on_main_project_page')

    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_class = 'horizontal-form'
        self.helper.form_action = ''
        self.helper.form_id = 'link_form'
        self.helper.layout = Layout(
            Field('name',css_class='input-block-level'),
            Field('url', css_class='input-block-level')
        )
        super(ProjectLinkForm, self).__init__(*args, **kwargs)

and urls configured to serve the actions:

url(r'^forms/link/add/(?P<project_id>\d+)/$','core.views.forms.link.add'),
url(r'^forms/link/edit/(?P<link_id>\d+)/$','core.views.forms.link.edit'),
url(r'^forms/link/delete/(?P<link_id>\d+)/$','core.views.forms.link.delete'),

and the views (taking add as an example):

@login_required
def add(request, project_id):
    if request.method == 'GET':
        form = ProjectLinkForm(initial={'project':project_id})
        form.action = 'add'
        form.submit_url = request.path
        return render_to_response('core/forms/project_link.html',{'form':form,'links_form_title':_(u'Добавить ссылку')},context_instance=RequestContext(request))
    elif request.method == 'POST':
        form = ProjectLinkForm(request.POST)
        form.action = 'add'
        form.submit_url = request.path
        if form.is_valid():
            try:
                new_link = Link()
                new_link.name = form.cleaned_data['name']
                new_link.url = form.cleaned_data['url']
                new_link.project = form.cleaned_data['project']
                new_link.display_on_main_project_page = form.cleaned_data['display_on_main_project_page']
                new_link.save()
                return HttpResponse(status=http_statuses.SAVED, content="saved")
            except Exception as e:
                return HttpResponse(status=http_statuses.SERVER_ERROR, content=e.message)
        else:
            return render_to_response('core/forms/project_link.html',{'form':form,'links_form_title':_(u'Добавить ссылку')},context_instance=RequestContext(request))

    #not POST or GET
    else:
        return HttpResponse(status=http_statuses.METHOD_NOT_ALLOWED,content=_(u'Недопустимый тип запроса'))

there is one template used for all actions:

{% load crispy_forms_tags %}
<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
    <h3>{{ links_form_title|default:"Ссылки на проект" }}</h3>
</div>
<div class="modal-body">
    <p>
        {% if form.action == "delete" %}
            Уверены, что хотите удалить ссылку "{{ form.name.value }}"? Одно неосторожное движенье мышкой и все - не вернешь!
            <div class="hide">
                {% crispy form %}
            </div>
        {% else %}
            {% crispy form %}
        {% endif %}
    </p>
</div>
<div class="modal-footer">
    {% if form.action == "delete" %}
        <a href="#" class="btn btn-danger" id="" onclick='SubmitModalForm($("#link_form"),"{{ form.submit_url }}")'>Удалить</a>
    {% else %}
    <a href="#" class="btn btn-primary" id="" onclick='SubmitModalForm($("#link_form"),"{{ form.submit_url }}")'>Сохранить</a>
    {% endif %}
</div>

and a bit of JavaScript code to handle all the popups with forms:

function LoadModalForm(url){
        $("#dynamic_project_forms").html("").load(url).modal();
    }

    function SubmitModalForm(the_form,submit_url){
        $.ajax({
            type:'POST',
            url:submit_url,
            data: $(the_form).serialize(),
            complete: function(e, xhr, settings){
                if(e.status == 201){//HTTP.STATUS.SAVED
                    $("#dynamic_project_forms").modal('hide');
                    $(activeTab.hash).load('/api/get_project_tab/'+activeTab.hash.replace('#','')+'/{{ project.id }}/');
                }
                else{
                    $("#dynamic_project_forms").html(e.responseText);
                    alert('loaded fucking response');
                }
            }
        });
    }

it does work but there is so much typing - I guess I'm missing something or just not getting something right - please suggest on proper approach.

Was it helpful?

Solution

You can get rid of half of the code easily.

You missed modelforms and generic views.

You could just have such urls:

from django.views import generic


urlpatterns = patterns('test_app.views',
    url(r'link/create/$', generic.CreateView.as_view(model=Link),
        name='link_create'),
    url(r'link/(?P<pk>\d+)/update/$', generic.UpdateView.as_view(model=Link),
        name='link_update'),
    url(r'link/(?P<pk>\d+)/delete/$', generic.DeleteView.as_view(model=Link),
        name='link_delete'),

    # bonus:
    url(r'link/$', generic.ListView.as_view(model=Link), name='link_list'),
    url(r'link/(?P<pk>\d+)/$', generic.DetailView.as_view(model=Link),
        name='link_detail'),
)

But it doesn't support AJAX like you by default - however you could extend CreateView UpdateView and DeleteView and do the little overrides needed to support your javascript. For example, I have this in a project:

class AjaxDeleteView(generic.DeleteView):
    # now that I think of it, this could just be a DetailView ... oh well
    http_method_names = ['post']

    def post(self, *args, **kwargs):
        self.get_object().delete()
        return http.HttpResponse('', status=204)


class AjaxFormMixin(object):
    def form_valid(self, form):
        if form.instance.pk:
            status = 204
        else:
            status = 201

        self.object = form.save()
        return http.HttpResponse(self.object.pk, status=status)

Some other examples of extending Django generic views:

class PkUrlKwarg(SingleObjectMixin):
    """
    Take the pk from request.GET and sets it to kwargs, useful to avoid
    reversing urls from javascript
    """
    def get_object(self, queryset=None):
        self.kwargs[self.pk_url_kwarg] = self.request.REQUEST['pk']
        return super(PkUrlKwarg, self).get_object(queryset)


class WidgetFormMixin(object):
    def get_form(self, form_class):
        # [snip] ok there's quite a lot (11 SLOCs) going on here in my case, since Widget* views are
        # supposed to deal with any subclass of Widget
        return self.object.configuration_form_instance(self.request)

    def get_template_names(self):
        widget_name = self.object.__class__.__name__

        return [
            'form_designer/widget_forms/%s.html' % widget_name,
            'form_designer/widget_form.html',
        ]


class WidgetSecurity(object):
    """
    Return a queryset of Widget that have a tab in a form which author is
    request.user.  For security.
    """
    def get_queryset(self):
        return Widget.objects.filter(tab__form__author=self.request.user)

Finnaly, my CRUD views, have quite a lot non default and more or less complex logic, but still, with very few lines of code:

class WidgetCreateView(WidgetFormMixin, AjaxFormMixin, generic.CreateView):
    form_class = WidgetForm  # overridden by WidgetFormMixin.get_form, but make django happy


class WidgetUpdateView(PkUrlKwarg, WidgetSecurity, WidgetFormMixin, AjaxFormMixin, generic.UpdateView):
    form_class = WidgetForm  # overridden by WidgetFormMixin.get_form


class WidgetDeleteView(PkUrlKwarg, WidgetSecurity, AjaxDeleteView):
    pass

This may not be the best answer, but it should definitively put you on the right track, and inspire you.

Also note that you should also define get_absolute_url() in your model.

For the record, urls:

url(r'widget/create/$',
    login_required(WidgetCreateView.as_view()),
    name='form_designer_widget_create'),
# the following views accept 'pk' as URL/GET argument
# this avoids reversing urls from javascript at the cost of a 4-liner mixin
url(r'widget/update/$',
    login_required(WidgetUpdateView.as_view()),
    name='form_designer_widget_update'),
url(r'widget/delete/$',
    login_required(WidgetDeleteView.as_view()),
    name='form_designer_widget_delete'),
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top