← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands-website/wlimages_rework into lp:widelands-website

 

kaputtnik has proposed merging lp:~widelands-dev/widelands-website/wlimages_rework into lp:widelands-website.

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1523256 in Widelands Website: "Uploading images with uppercase letters cause server error"
  https://bugs.launchpad.net/widelands-website/+bug/1523256

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands-website/wlimages_rework/+merge/311985

Fixes the bug 1523256 (Uploading images with uppercase letters cause server error) with additional changes:

Admin pages:

wiki/articles:
 - Sort List by 'last_update' as default
 - Added search for article titles

wiki/changeset
 - Added search for article titles

wiki/article/<article_title>
 - Removed list of changesets -> We have revert functions in the wiki and a changeset list in admin
 - Instead show a list of images attached to this article
 - commented the 'Group'-section. We currently do not use this and Django grumble on save if this 
   is shown in the admin page

wlimages/images/image
 - content_type, object_id are 'read only' now, since it makes no sense to make this changeable
 - Show also the content_object (e.g. the name of article this image is attached to)
 - Add an admin action giving the possibility to remove the file from disk when deleting it 

Stripped the 'image_list' from wiki/edit.html to an own template in wlimages

Changes to wiki:
- Trying to edit a wikipage which has an image attached which couldn't be found on disk causes formerly an IOError and the wiki article couldn't be edited anymore. To prevent this Error a filter is added and a hint on the missing file is shown in the list of images.

- Using of the url property of Django's default FieldFile object instead of providing an own model 
field for that. The Django url() method returns automatically percent encoded strings (e.g. 'Br%C3%BCcke' instead of 'Brücke') See https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.fields.files.FieldFile.url

Formerly uploading an image may cause several errors (e.g.: 'File already exists...'). Those errors redirected to an ugly page with a form wich didn't work for uploading an image. This is changed as follows:

- If an error occurs the redirect still happens but the page shows only the error (no form anymore) and provide a button to go back to the previous page.

Filehandling in general:
Until all Form validation is passed (e.g. 'File name too long') the get_valid_name() method of 
Django's storage class is used to get a filename that is 'suitable for use on the target storage system'. See https://launchpadlibrarian.net/295298758/valid_name.jpg

More example image of changes:
- Redirect after error: https://launchpadlibrarian.net/295144720/upload_failure.jpg (top=old, bottom=new)
- Missing file and Url escaping: https://launchpadlibrarian.net/295145198/image_list.jpg

After merging './manage.py migrate' has to be run because i removed the url field in wlimages.Image

-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands-website/wlimages_rework into lp:widelands-website.
=== modified file 'templates/wiki/edit.html'
--- templates/wiki/edit.html	2016-03-02 21:02:38 +0000
+++ templates/wiki/edit.html	2016-11-28 21:46:58 +0000
@@ -20,7 +20,7 @@
 			$("#preview").html("<h3>Preview</h3>\n<hr>\n<div class=\"wiki_article\" id=\"content_preview\">Loading...</div>\n<hr>");
 			$("#content_preview").load( "{% url 'wiki_preview' %}", 
 				{"body": $("#id_content").val()});
-		})
+		});
 
 	{% ifequal new_article 0 %} 
 		// Insert diff  
@@ -29,7 +29,7 @@
 			$("#diff").html("<h3>Diff</h3>\n<hr>\n<div id=\"content_diff\">Loading...</div>\n<hr>");
 			$("#content_diff").load( "{% url 'wiki_preview_diff' %}", 
 				{"body": $("#id_content").val(), "article": {{ object_id }} });
-		})
+		});
 	{%endifequal%}
 	});
 </script>
@@ -95,55 +95,10 @@
 	{% else %}
 		{% if images.count %}
 		Images attached to this article:
-		
-		{% for img in images %}
-		<table width="100%" class="images">
-		<thead>
-			<tr>
-				<th class="first-column" style="text-align: right;">Name:</th>
-				<th>{{ img.name}}</th>
-			</tr>
-		</thead>
-		<tbody>
-			<tr>
-				<td>
-					<span>Properties:</span><br />
-					<span class="grey">Width:</span><br />
-					{{ img.image.width }} px<br />
-					<span class="grey">Height:</span><br />
-					{{ img.image.height }} px<br />
-					<span class="grey">Filesize:</span><br />
-					{{ img.image.size|filesizeformat }}
-				<td  style="vertical-align: middle; text-align: center;"><img src="{{ img.url }}"></td>
-			</tr>
-			<tr>
-				<td class="grey">Code to use in article:</td>
-				{# Show only one code snippet if the image is greater than 700px #}
-				{% if img.image.width > 700 %}
-				<td style="text-align: center;">[![{{img.name}}]({{ img.url }})]({{ img.url }})</td>
-			</tr>
-
-				{# otherwise provide all options #}
-				{% else %}
-					<td style="text-align: center;">![{{img.name}}]({{ img.url }})</td>
-			</tr>
-					
-				<tr>
-					<td class="grey">Right aligned:</td>
-					<td style="text-align: center;">![{{img.name}}]({{ img.url }}){: .right}</td>
-				</tr>
-				<tr>
-					<td class="grey">Left aligned:</td>
-					<td style="text-align: center;">![{{img.name}}]({{ img.url }}){: .left}</td>
-				</tr>
-				{% endif %}
-		</tbody>
-		</table>
-		{% endfor %}
 		{% endif %}
-
+		{% include 'wlimages/inlines/image_list.html' %}
 		{% get_upload_form as iform %}
-		<form enctype="multipart/form-data" action="/images/upload/{{content_type}}/{{object_id}}/{{request.path|iriencode}}" method="POST">
+		<form enctype="multipart/form-data" action="/images/upload/{{content_type}}/{{object_id}}/{{ request.path|iriencode }}" method="POST">
 			{{ iform.as_p }}
 			<div>
 				<input type="submit" value="{% trans "Upload Image" %}" />

=== added directory 'templates/wlimages/inlines'
=== added file 'templates/wlimages/inlines/image_list.html'
--- templates/wlimages/inlines/image_list.html	1970-01-01 00:00:00 +0000
+++ templates/wlimages/inlines/image_list.html	2016-11-28 21:46:58 +0000
@@ -0,0 +1,55 @@
+
+{% load wlimages_extras %}
+
+{% for img in images %}
+<table width="100%" class="images">
+	<thead>
+		<tr>
+			<th class="first-column" style="text-align: right;">Name:</th>
+			<th>{{ img.name}}</th>
+		</tr>
+	</thead>
+	<tbody>
+		{% if img|has_file %}
+		<tr>
+			<td>
+				<span>Properties:</span><br />
+				<span class="grey">Width:</span><br />
+				{{ img.image.width }} px<br />
+				<span class="grey">Height:</span><br />
+				{{ img.image.height }} px<br />
+				<span class="grey">Filesize:</span><br />
+				{{ img.image.size|filesizeformat }}
+			<td  style="vertical-align: middle; text-align: center;"><img src="{{ img.image.url }}"></td>
+		</tr>
+		<tr>
+			<td class="grey">Code to use in article:</td>
+			{# Show only one code snippet if the image is greater than 700px #}
+			{% if img.image.width > 700 %}
+				<td style="text-align: center;">[![{{img.name}}]({{ img.image.url }})]({{ img.image.url }})</td>
+		</tr>
+			{# provide all options #}
+			{% else %}
+				<td style="text-align: center;">![{{img.name}}]({{ img.image.url }})</td>
+		</tr>
+		<tr>
+			<td class="grey">Right aligned:</td>
+			<td style="text-align: center;">![{{img.name}}]({{ img.image.url }}){: .right}</td>
+		</tr>
+		<tr>
+			<td class="grey">Left aligned:</td>
+			<td style="text-align: center;">![{{img.name}}]({{ img.image.url }}){: .left}</td>
+		</tr>
+			{% endif %}
+		{% else %}
+		<tr>
+			<td colspan=2 align="center">
+				!! Haven't found a file to this image !!<br />
+				{{ img.image.name}}<br />
+				Please inform an admin about this issue
+			</td>
+		</tr>
+		{% endif %}
+	</tbody>
+</table>
+{% endfor %}

=== modified file 'templates/wlimages/upload.html'
--- templates/wlimages/upload.html	2011-07-04 01:14:23 +0000
+++ templates/wlimages/upload.html	2016-11-28 21:46:58 +0000
@@ -4,9 +4,12 @@
 {% endcomment %}
 
 {% block content %}
-<form  enctype="multipart/form-data" action="/images/upload/" method="POST">
-{{ upload_form.as_p }}
-<input type="submit" value="Submit" />
-{% csrf_token %}
-</form>
+	<div class="blogEntry"
+		<form  enctype="multipart/form-data" action="/images/upload/" method="POST">
+		{{ upload_form.imagename.errors }}
+		{% csrf_token %}
+		</form>
+		<div class="button"><a href="{% url 'wiki_edit' referer %}">Back</a></div>
+	</div>
+
 {% endblock %}

=== modified file 'wiki/admin.py'
--- wiki/admin.py	2009-02-20 12:25:18 +0000
+++ wiki/admin.py	2016-11-28 21:46:58 +0000
@@ -1,38 +1,43 @@
 # -*- coding: utf-8 -*-
 
 from django.contrib import admin
-
 from wiki.models import Article, ChangeSet
-
-
-class InlineChangeSet(admin.TabularInline):
-    model = ChangeSet
+from wlimages.models import Image
+from django.contrib.contenttypes.admin import GenericTabularInline
+
+
+class InlineImages(GenericTabularInline):
+    model = Image
     extra = 0
-    raw_id_fields = ('editor',)
+    fields = ('name', 'image', 'user', 'date_submitted')
+    raw_id_fields = ('user',)
+
 
 class ArticleAdmin(admin.ModelAdmin):
-    list_display = ('title', 'markup', 'created_at')
+    search_fields = ['title']
+    list_display = ('title', 'markup', 'created_at', 'last_update',)
     list_filter = ('title',)
-    ordering = ('last_update',)
+    ordering = ['-last_update']
     fieldsets = (
         (None, {'fields': ('title', 'content', 'markup')}),
         ('Creator', {'fields': ('creator', 'creator_ip'),
                      'classes': ('collapse', 'wide')}),
-        ('Group', {'fields': ('object_id', 'content_type'),
-                     'classes': ('collapse', 'wide')}),
+        # ('Group', {'fields': ('object_id', 'content_type'),
+        #              'classes': ('collapse', 'wide')}),
     )
     raw_id_fields = ('creator',)
-    inlines = [InlineChangeSet]
+    inlines = [InlineImages]
 
 admin.site.register(Article, ArticleAdmin)
 
 
 class ChangeSetAdmin(admin.ModelAdmin):
+    search_fields = ['article__title']
     list_display = ('article', 'revision', 'old_title', 'old_markup',
                     'editor', 'editor_ip', 'reverted', 'modified',
                     'comment')
     list_filter = ('old_title', 'content_diff')
-    ordering = ('modified',)
+    ordering = ('-modified',)
     fieldsets = (
         ('Article', {'fields': ('article',)}),
         ('Differences', {'fields': ('old_title', 'old_markup',

=== modified file 'wiki/models.py'
--- wiki/models.py	2016-06-18 18:51:06 +0000
+++ wiki/models.py	2016-11-28 21:46:58 +0000
@@ -84,7 +84,7 @@
     class Meta:
         verbose_name = _(u'Article')
         verbose_name_plural = _(u'Articles')
-	app_label = 'wiki'
+        app_label = 'wiki'
 
     def get_absolute_url(self):
         if self.group is None:
@@ -134,7 +134,6 @@
         changeset = self.changeset_set.get(revision=to_revision)
         return changeset.compare_to(from_revision)
 
-
     def __unicode__(self):
         return self.title
 

=== modified file 'wiki/views.py'
--- wiki/views.py	2016-10-12 20:42:21 +0000
+++ wiki/views.py	2016-11-28 21:46:58 +0000
@@ -277,7 +277,7 @@
             #     else:
             #         user_message = u"Your article was edited successfully."
             #     messages.success(request, user_message)
-                    
+
 
             if ((article is None) and (group_slug is not None)):
                 form.group = group
@@ -308,7 +308,6 @@
             initial['action'] = 'edit'
             form = ArticleFormClass(instance=article,
                                     initial=initial)
-
     if not article:
         template_params = {'form': form, "new_article": True }
     else:

=== modified file 'wl_utils.py'
--- wl_utils.py	2016-11-15 22:47:15 +0000
+++ wl_utils.py	2016-11-28 21:46:58 +0000
@@ -1,4 +1,5 @@
 
+# Get the real IP when the Django server runs behind a proxy
 def get_real_ip(request):
     """Returns the real user IP, even if behind a proxy."""
     for key in ('HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'):

=== modified file 'wlimages/admin.py'
--- wlimages/admin.py	2016-02-03 08:21:08 +0000
+++ wlimages/admin.py	2016-11-28 21:46:58 +0000
@@ -13,15 +13,25 @@
 from django.utils.translation import ugettext_lazy as _
 from models import Image
 
+
+def delete_with_file(modeladmin, request, queryset):
+    for obj in queryset:
+        storage = obj.image.storage
+        storage.delete(obj.image)
+        obj.delete()
+delete_with_file.short_description = 'Delete Image including File'
+
 class ImageAdmin(admin.ModelAdmin):
+    readonly_fields = ('content_object', 'content_type', 'object_id')
     list_display = ( '__unicode__', 'date_submitted', 'content_type', 'user')
     list_filter = ('date_submitted',)
     date_hierarchy = 'date_submitted'
     search_fields = ('image', 'user__username')
+    actions = [delete_with_file]
     fieldsets = (
-        (None, {'fields': ( ('image', 'name'), 'date_submitted', 'url','revision')}),
+        (None, {'fields': ( 'image', 'name', 'date_submitted','revision')}),
         (_('Upload data:'), { 'fields': ( 'user', 'editor_ip')}),
-        (_('Content object:'), { 'fields': ( 'content_type', 'object_id')}),
+        (_('Content object:'), { 'fields': ( ('content_type', 'content_object'), 'object_id')}),
     )
 
 admin.site.register(Image, ImageAdmin)

=== modified file 'wlimages/forms.py'
--- wlimages/forms.py	2013-06-14 19:23:53 +0000
+++ wlimages/forms.py	2016-11-28 21:46:58 +0000
@@ -4,12 +4,18 @@
 import os
 
 class UploadImageForm(forms.Form):
-    imagename = forms.ImageField()
+    # max_length = 90 because the length of 'upload_to =' has to be considered
+    imagename = forms.ImageField(max_length=90)
 
     def clean_imagename( self ):
-        name = self.cleaned_data["imagename"]
-
-        if Image.objects.has_image(name.name.lower()):
-            raise forms.ValidationError, "An Image with this name already exists. Please rename and upload again."
-
+        in_mem_img = self.cleaned_data["imagename"]
+        
+        if Image.objects.has_image(in_mem_img.name):
+            # Get the existing image to show it's properties
+            exist_img = Image.objects.get(name = in_mem_img.name)
+            raise forms.ValidationError(
+                ("An Image with name '%(name)s' is already attached to %(object)s '%(obj_name)s'. \
+                 Use the existing one or rename your file and upload gain."),
+                params={'name': exist_img.name, 'object': exist_img.content_type, 'obj_name': exist_img.content_object}
+                )
 

=== added file 'wlimages/migrations/0002_remove_image_url.py'
--- wlimages/migrations/0002_remove_image_url.py	1970-01-01 00:00:00 +0000
+++ wlimages/migrations/0002_remove_image_url.py	2016-11-28 21:46:58 +0000
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wlimages', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='image',
+            name='url',
+        ),
+    ]

=== modified file 'wlimages/models.py'
--- wlimages/models.py	2016-02-26 19:00:22 +0000
+++ wlimages/models.py	2016-11-28 21:46:58 +0000
@@ -6,8 +6,8 @@
 from django.utils.translation import ugettext_lazy as _
 from django.db import IntegrityError
 from datetime import datetime
-
 from settings import MEDIA_ROOT, MEDIA_URL
+from django.core.files.storage import FileSystemStorage
 
 class ImageManager(models.Manager):
     """
@@ -17,7 +17,6 @@
     """
     def has_image(self,image_name):
         return bool(self.filter(name=image_name).count())
-           
 
     def create(self,**keyw):
         """
@@ -28,25 +27,22 @@
 
         if self.filter(name=keyw["name"],revision=keyw["revision"]).count():
             raise Image.AlreadyExisting()
-        
+
         return super(ImageManager,self).create(**keyw)
 
-    def create_and_save_image(self,user,image, content_type, object_id, ip):
-        # if self.has_image(name):
-        #     raise RuntimeError,"Image with name %s already exists. This is likely an Error" % name
-        name = image.name.lower()
+    def create_and_save_image(self, user, image, content_type, object_id, ip):
+        # Use Django default's for a safe filename
+        storage = FileSystemStorage()
+        safe_filename = storage.get_valid_name(image.name)
         im = self.create(content_type=content_type, object_id=object_id, 
-                    user=user,revision=1,name=name, editor_ip = ip)
-
-        path = "%swlimages/%s" % (MEDIA_ROOT,image.name)
-        url = "%swlimages/%s" % (MEDIA_URL,image.name)
+                    user=user, revision=1, name=image.name, editor_ip = ip)
+        path = "%swlimages/%s" % (MEDIA_ROOT,safe_filename)
 
         destination = open(path,"wb")
         for chunk in image.chunks():
             destination.write(chunk)
 
-        im.image = "wlimages/%s" % (name)
-        im.url = url
+        im.image = "wlimages/%s" % (safe_filename)
 
         im.save()
 
@@ -74,7 +70,6 @@
     # Date Fields
     date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
     image = models.ImageField(upload_to="wlimages/")
-    url = models.CharField(max_length=250)
 
 
     objects = ImageManager()

=== modified file 'wlimages/templatetags/wlimages_extras.py'
--- wlimages/templatetags/wlimages_extras.py	2016-03-07 20:38:39 +0000
+++ wlimages/templatetags/wlimages_extras.py	2016-11-28 21:46:58 +0000
@@ -8,6 +8,7 @@
 # import re
 from django import template
 from wlimages.forms import UploadImageForm
+import os.path
 
 register = template.Library()
 
@@ -33,6 +34,12 @@
         form = UploadImageForm()
         context[self.context_name] = form
         return ''
-
 register.tag('get_upload_form', do_get_upload_image_form)
 
+def has_file(obj):
+    try:
+        os.path.isfile(obj.image.file.name)
+        return True
+    except IOError:
+        return False
+register.filter('has_file', has_file)

=== modified file 'wlimages/views.py'
--- wlimages/views.py	2016-10-12 20:42:21 +0000
+++ wlimages/views.py	2016-11-28 21:46:58 +0000
@@ -30,12 +30,17 @@
         if form.is_valid(): # All validation rules pass
             Image.objects.create_and_save_image(user=request.user,image=request.FILES["imagename"],
                         content_type=ContentType.objects.get(pk=content_type),object_id=object_id, ip=get_real_ip(request))
-
             return HttpResponseRedirect(next) # Redirect after POST
     else:
         form = UploadImageForm() # An unbound form
+    
+    # Get the App (model) to which this image belongs to:
+    app = ContentType.objects.get(id=content_type)
+    # Get the current objects name (provided by __unicode__()) from this model
+    name = app.get_object_for_this_type(id=object_id)
 
     return render_to_response('wlimages/upload.html', {
         'upload_form': form,
+        'referer': name,
     }, context_instance=RequestContext(request))
 


Follow ups