Zero to Django in 4 months - What I've learned - Part 3

  • Wed 28 November 2012

  • These two projects, primarily the second project presented many challenges. This was good and bad. I work best when challenged, but the stress level often went to 11. The challenges were compounded by the fact that I was 1) learning Django, 2) learning Python 3) know no Ajax, 4) know enough jquery to hide a div. I knew I needed to use these technologies to do what I envisioned for the user experience.

    The second project was created for artists, a few requirements needed to be met. 1) A user must be able to upload/manage their own images. 2) An administrator must be able to manage the user's uploads. 3) I needed the easiest/quickest way possible to upload images across all browsers (well, except for that one browser). 4) Images uploaded must also create thumbnails. To ensure consistency across the site, thumbnail dimensions were hardcoded. Ideally something to crop the thumbnail image with the right dimensions regardless of the layout/dimensions of the original image would have been preferred, and may be looked at later.

    I'm not sure how, but I had stumbled upon a project on Github that did exactly what I wanted! Yay me! jQuery File Upload project on Github was exactly what I wanted! View a demo to see the behavior of the uploader. Someone had actually put forth the effort to port it to Django. There were a few things I needed/did and my fork exists here.

    I kind of want to tear into some of the code that posed the greatest challenges for me.

    Before I throw some code, perhaps I should give a little back story. This particular app is for community events. Community events, have images associated with them. I want to delete the images that are specific to an event, and once finished deleting, I want to be redirected to the event's images that I was managing.

    urls.py

    ### View Community Event Images
    url(r'^event/(?P<pk>\d+)/images/$',login_required(MySpecialListView.as_view(
        template_name="account/community_images.html")),
        name="admin-community-images"),
    
    ### Add Images to a community event
    url(r'^event/(?P<pk>\d+)/images/add/$',
        login_required(CommunityPictureCreateView.as_view(),
            {}),
            name="add-community-images"),
    ### Delete community event images
    url(r'^event/(?P<id>\d+)/images/image/(?P<pk>\d+)/delete/$',
        login_required(CommunityPictureDeleteView.as_view()),
            name="community-upload-delete"),
    

    As mentioned above, tld/event/event_id/images/ will display the images associated with a specific event. If I want to add images to an event, I would browse to the URL tld/event/event_id/images/add/. If I wanted to delete an image associated with an event, I would browse to tld/event/event_id/images/image/image_id/delete/. After adding/deleting, I would be redirected to tld/event/event_id/images/ so that I could continue managing images associated with that event.

    Models.py

    class CommunityPicture(models.Model):
        event = models.ForeignKey(CommunityEvent)
        file = ImageWithThumbsField(upload_to="community", sizes=((125, 125), (250, 250),
                (640, 427),))
        slug = models.SlugField(max_length=50, blank=True)
    
        def __unicode__(self):
            return '%s' '%s' % (self.event, self.file)
    
        @models.permalink
        def get_absolute_url(self):
            return('admin-community-images', {
                'pk': self.event.id
                })
    
        def save(self, *args, **kwargs):
            self.slug = self.file.name
            super(CommunityPicture, self).save(*args, **kwargs)
    
        def delete(self, *args, **kwargs):
        self.file.delete(False)
            super(CommunityPicture, self).delete(*args, **kwargs)
    

    To get the above mention redirect to work, I needed this in the models.py

    @models.permalink
    def get_absolute_url(self):
        return('admin-community-images', {
           'pk': self.event.id
           })
    

    The above block associated the event id to pk in the URL allowing me to pass that in the HTTP request (I think this is how it works -- I really hope someone ELI5 in the comments) so that I could get a successful redirect to where I had come from.

    Views.py

    def response_mimetype(request):
        if "application/json" in request.META['HTTP_ACCEPT']:
            return "application/json"
        else:
            return "text/plain"
    
    class CommunityPictureCreateView(CreateView):
        model = CommunityPicture
        template_name = "account/upload.html"
        form_class = CommunityPictureForm
    
        def form_valid(self, form, **kwargs):
            obj = form.save(commit=False)
            obj.event_id = self.kwargs['pk']
            obj.save()
    
            data = [
                {
                'name': obj.file.name,
                'url': obj.file.url,
                'thumbnail_url': obj.file.url,
                'delete_url': reverse('community-upload-delete',
                kwargs={'id':self.kwargs['pk'], 'pk': obj.id}),
                'delete_type': "DELETE"
                }
            ]
    
            response = JSONResponse(data, {}, response_mimetype(self.request))
            response['Content-Disposition'] = 'inline; filename=files.json'
            return response
    
    class CommunityPictureDeleteView(DeleteView):
        model = CommunityPicture
    
        def get_context_data(self, **kwargs):
            context = super(CommunityPictureDeleteView, self).get_context_data(**kwargs)
            context['pk'] = self.kwargs['pk']
            return context
    
        def delete(self, request, *args, **kwargs):
            self.object = self.get_object()
            self.object.delete()
            if request.is_ajax():
                response = JSONResponse(True, {}, response_mimetype(self.request))
                response['Content-Disposition'] = 'inline; filename=files.json'
                return response
            else:
                return redirect('admin-community-images', pk=kwargs['id'])
    
    class JSONResponse(HttpResponse):
        """JSON response class."""
        def __init__(self.obj='', json_opts={},mimetype="application/json",
        *args,**kwargs):
            content = simplejson.dumps(obj,**json_opts)
            super(JSONResponse,self).__init__(content,mimetype,*args,**kwargs)
    

    Pieces from views.py that challeneged me the most. This first block:

    def form_valid(self, form, **kwargs):
        obj = form.save(commit=False)
        obj.event_id = self.kwargs['pk']
        obj.save()
    

    I needed to get the event_id passed in the URL, into the model, BEFORE the model was saved. So the form.save(commit=False) did such. Once I had the event_id, I could then save the form with obj.save(). Anytime you have a model with a foreign key constraint, you need to ensure you have all your data before you can save anything.

    Another block of code to pose a problem was the following:

    'delete_url': reverse('community-upload-delete',
                kwargs={'id':self.kwargs['pk'], 'pk': obj.id}),
    

    Django Debug Toolbar proved invaluable troubleshooting this issue. I needed to pass the event_id as well as the image_id in the HTTP request. I'm not sure who developed the Django Debug Toolbar, but I'd Gittip the shit out of them!

    From the CommunityPictureDeleteView I needed to override(?) the get_context_data to allow me to include the event_id.

    def get_context_data(self, **kwargs):
        context = super(CommunityPictureDeleteView,self).get_context_data(**kwargs)
        context['pk'] = self.kwargs['pk']
        return context
    

    All the above pieces allowed me to do as I described above. Add/Delete images associated with an event. Upon completion of managing images, I would be redirected to the event images that I was managing.

    I reused the above blocks of code, edited only slightly for the apps/models/views/urls to allow me to manage images for all other apps. This provided a consistent user experience for managing all images across the site.

    Comments !

    social