summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Zamotaev <nzamotaev@luxoft.com>2019-02-12 18:14:39 +0300
committerEgor Nemtsev <enemtsev@luxoft.com>2019-04-25 14:32:44 +0000
commit75a0b2cfd442c5499bb520eee6131263763e8687 (patch)
tree341bb097da89070863dd97cb6071db014e8e7b79
parent962bbcbcdab445169952c0a98c911d08ec564529 (diff)
Category icon upload implementation
Task-number: AUTOSUITE-759 Change-Id: I1030d4b127b41cccfee545e1d4412e61e67f8fc1 Reviewed-by: Egor Nemtsev <enemtsev@luxoft.com>
-rw-r--r--appstore/settings.py8
-rw-r--r--requirements.txt2
-rw-r--r--store/admin.py94
-rw-r--r--store/api.py27
-rw-r--r--store/migrations/0001_initial.py7
-rw-r--r--store/models.py85
-rw-r--r--store/static/img/category_All.pngbin0 -> 620 bytes
7 files changed, 101 insertions, 122 deletions
diff --git a/appstore/settings.py b/appstore/settings.py
index e3edabc..8e67698 100644
--- a/appstore/settings.py
+++ b/appstore/settings.py
@@ -96,6 +96,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'ordered_model',
'store',
)
@@ -153,3 +154,10 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''
+
+# Icon size (icons are resized to this size on upload)
+ICON_SIZE_X = 36
+ICON_SIZE_Y = 32
+# If the icon should be transformed to monochrome, with alpha channel, when uploaded or not
+ICON_DECOLOR = True
+
diff --git a/requirements.txt b/requirements.txt
index 79be93a..41de88c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@ pkg-resources==0.0.0
PyYAML
django==1.11
django-common
+django-ordered-model==2.1
pyOpenSSL
M2Crypto
Enum34
@@ -9,4 +10,5 @@ ipaddress
cffi
paramiko
cryptography
+pillow
python-magic==0.4.15
diff --git a/store/admin.py b/store/admin.py
index f2adb23..61d5ff0 100644
--- a/store/admin.py
+++ b/store/admin.py
@@ -33,34 +33,51 @@
import os
from django import forms
-from django.conf import settings
-from django.conf.urls import include, url
from django.contrib import admin
-from django.core.exceptions import PermissionDenied
-from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
+from ordered_model.admin import OrderedModelAdmin
+from django.core.files.uploadedfile import InMemoryUploadedFile
from store.models import *
from utilities import parseAndValidatePackageMetadata, writeTempIcon, makeTagList
+import StringIO
class CategoryAdminForm(forms.ModelForm):
class Meta:
exclude = ["id", "rank"]
def save(self, commit=False):
- m = super(CategoryAdminForm, self).save(commit)
- try:
- test = Category.objects.all().order_by('-rank')[:1].values('rank')[0]['rank'] + 1
- except:
- test = 0
- m.rank = test
+ m = super(CategoryAdminForm, self).save(commit=False)
return m
-class CategoryAdmin(admin.ModelAdmin):
+ def clean(self):
+ cleaned_data = super(CategoryAdminForm, self).clean()
+ #Icon fixing (resize, turn monochrome, add alpha channel)
+ if cleaned_data['icon']:
+ if settings.ICON_DECOLOR:
+ # make image monochrome + alpha channel, this is done to compensate for
+ # how icons are treated in neptune3-ui
+ im = Image.open(cleaned_data['icon'])
+ grey, alpha = im.convert('LA').split()
+ grey = ImageChops.invert(grey)
+ im.putalpha(grey)
+ im = im.convert('LA')
+ else:
+ # No conversion, icons are uploaded as-is, only scaling is used.
+ im = Image.open(cleared_data['icon'])
+ size = (settings.ICON_SIZE_X,settings.ICON_SIZE_Y,)
+ im.thumbnail(size, Image.ANTIALIAS)
+ imagefile = StringIO.StringIO()
+ im.save(imagefile, format='png')
+ imagefile.seek(0)
+ cleaned_data['icon'] = InMemoryUploadedFile(imagefile, 'icon', "icon.png", 'image/png', imagefile.len, None)
+ return cleaned_data
+
+class CategoryAdmin(OrderedModelAdmin):
form = CategoryAdminForm
- list_display = ('name', 'move')
- ordering = ('rank',)
+ list_display = ('name', 'icon_image', 'move_up_down_links')
+ ordering = ('order',)
def save_model(self, request, obj, form, change):
obj.save()
@@ -70,53 +87,14 @@ class CategoryAdmin(admin.ModelAdmin):
return obj.name
name.short_description = ugettext_lazy('Item caption')
- def move(self, obj):
- """
- Returns html with links to move_up and move_down views.
- """
- button = u'<a href="%s"><img src="%simg/admin/arrow-%s.gif" /> %s</a>'
- prefix = settings.STATIC_URL
-
- link = '%d/move_up/' % obj.pk
- html = button % (link, prefix, 'up', _('up')) + " | "
- link = '%d/move_down/' % obj.pk
- html += button % (link, prefix, 'down', _('down'))
+ def icon_image(self, obj):
+ prefix = settings.URL_PREFIX
+ image_request = prefix + "/category/icon?id=%s" % (obj.id)
+ html = u'<img width=%s height=%s src="%s" />' % (settings.ICON_SIZE_X, settings.ICON_SIZE_Y, image_request)
return html
- move.allow_tags = True
- move.short_description = ugettext_lazy('Move')
-
- def get_urls(self):
- admin_view = self.admin_site.admin_view
- urls = [
- url(r'^(?P<item_pk>\d+)/move_up/$', admin_view(self.move_up)),
- url(r'^(?P<item_pk>\d+)/move_down/$', admin_view(self.move_down)),
- ]
- return urls + super(CategoryAdmin, self).get_urls()
-
- def move_up(self, request, item_pk):
- """
- Decrease rank (change ordering) of the menu item with
- id=``item_pk``.
- """
- if self.has_change_permission(request):
- item = get_object_or_404(Category, pk=item_pk)
- item.decrease_rank()
- else:
- raise PermissionDenied
- return redirect('admin:store_category_changelist')
-
- def move_down(self, request, item_pk):
- """
- Increase rank (change ordering) of the menu item with
- id=``item_pk``.
- """
- if self.has_change_permission(request):
- item = get_object_or_404(Category, pk=item_pk)
- item.increase_rank()
- else:
- raise PermissionDenied
- return redirect('admin:store_category_changelist')
+ icon_image.allow_tags = True
+ icon_image.short_description = ugettext_lazy('Category icon')
class AppAdminForm(forms.ModelForm):
diff --git a/store/api.py b/store/api.py
index 63c3121..1c26944 100644
--- a/store/api.py
+++ b/store/api.py
@@ -50,7 +50,6 @@ from tags import SoftwareTagList
def hello(request):
status = 'ok'
-
if settings.APPSTORE_MAINTENANCE:
status = 'maintenance'
elif getRequestDictionary(request).get("platform", "") != str(settings.APPSTORE_PLATFORM_ID):
@@ -308,7 +307,7 @@ def appDownload(request, path):
def categoryList(request):
# this is not valid JSON, since we are returning a list!
allmeta = [{'id': -1, 'name': 'All'}, ] #All metacategory
- categoryobject = Category.objects.all().order_by('rank').values('id', 'name')
+ categoryobject = Category.objects.all().order_by('order').values('id', 'name')
categoryobject=allmeta + list(categoryobject)
return JsonResponse(categoryobject, safe = False)
@@ -316,14 +315,26 @@ def categoryList(request):
def categoryIcon(request):
response = HttpResponse(content_type = 'image/png')
categoryId = getRequestDictionary(request)['id']
-
- # there are no category icons (yet), so we just return the icon of the first app in this category
try:
- app = App.objects.filter(category__exact = categoryId).order_by('-dateModified')[0] #FIXME - the category icon is unimplemented
- with open(iconPath(app.appid,app.architecture), 'rb') as iconPng:
- response.write(iconPng.read())
+ if categoryId != '-1':
+ category = Category.objects.filter(id__exact = categoryId)[0]
+ filename = iconPath() + "category_" + str(category.id) + ".png"
+ else:
+ from django.contrib.staticfiles import finders
+ filename = finders.find('img/category_All.png')
+ with open(filename, 'rb') as icon:
+ response.write(icon.read())
+ response['Content-Length'] = icon.tell()
+
except:
- emptyPng = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x03PLTE\x00\x00\x00\xa7z=\xda\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00\nIDAT\x08\xd7c`\x00\x00\x00\x02\x00\x01\xe2!\xbc3\x00\x00\x00\x00IEND\xaeB`\x82'
+ # In case there was error in searching for category,
+ # return this image:
+ # +-----+
+ # | |
+ # |Error|
+ # | |
+ # +-----+
+ emptyPng = "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x01\x03\x00\x00\x00I\xb4\xe8\xb7\x00\x00\x00\x06PLTE\x00\x00\x00\x00\x00\x00\xa5\x67\xb9\xcf\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00\x33IDAT\x08\xd7\x63\xf8\x0f\x04\x0c\x0d\x0c\x0c\x8c\x44\x13\x7f\x40\xc4\x01\x10\x71\xb0\xf4\x5c\x2c\xc3\xcf\x36\xc1\x44\x86\x83\x2c\x82\x8e\x48\xc4\x5f\x16\x3e\x47\xd2\x0c\xc5\x46\x80\x9c\x06\x00\xa4\xe5\x1d\xb4\x8e\xae\xe8\x43\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82"
response.write(emptyPng)
return response
diff --git a/store/migrations/0001_initial.py b/store/migrations/0001_initial.py
index bde513e..dca1599 100644
--- a/store/migrations/0001_initial.py
+++ b/store/migrations/0001_initial.py
@@ -69,9 +69,14 @@ class Migration(migrations.Migration):
name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.PositiveIntegerField(db_index=True, editable=False)),
('name', models.CharField(max_length=200)),
- ('rank', models.SmallIntegerField(db_index=True, unique=True)),
+ ('icon', models.ImageField(storage=store.models.OverwriteStorage(), upload_to=store.models.category_file_name)),
],
+ options={
+ 'ordering': ('order',),
+ 'abstract': False,
+ },
),
migrations.CreateModel(
name='Vendor',
diff --git a/store/models.py b/store/models.py
index eb3ede9..08bebc3 100644
--- a/store/models.py
+++ b/store/models.py
@@ -31,69 +31,50 @@
#############################################################################
import os
+from PIL import Image, ImageChops
from django.db import models
+from ordered_model.models import OrderedModel
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
+from django.db.models.fields.files import ImageFieldFile
from utilities import packagePath, writeTempIcon, makeTagList
+def category_file_name(instance, filename):
+ # filename parameter is unused. See django documentation for details:
+ # https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.FileField.upload_to
+ return settings.MEDIA_ROOT + "icons/category_" + str(instance.id) + ".png"
-class Category(models.Model):
+class OverwriteStorage(FileSystemStorage):
+ def get_available_name(self, name, max_length=None):
+ if self.exists(name):
+ os.remove(os.path.join(settings.MEDIA_ROOT, name))
+ return name
+
+class Category(OrderedModel):
name = models.CharField(max_length = 200)
- rank = models.SmallIntegerField(unique = True, db_index = True)
+ icon = models.ImageField(upload_to = category_file_name, storage = OverwriteStorage())
+
+ class Meta(OrderedModel.Meta):
+ ordering = ('order',)
def __unicode__(self):
return self.name
- def is_first(self):
- """
- Returns ``True`` if item is the first one in the menu.
- """
- return Category.objects.filter(rank__lt = self.rank).count() == 0
-
- def is_last(self):
- """
- Returns ``True`` if item is the last one in the menu.
- """
- return Category.objects.filter(rank__gt = self.rank).count() == 0
-
- def increase_rank(self):
- """
- Changes position of this item with the next item in the
- menu. Does nothing if this item is the last one.
- """
- try:
- next_item = Category.objects.filter(rank__gt = self.rank)[0]
- except IndexError:
- pass
- else:
- self.swap_ranks(next_item)
-
- def decrease_rank(self):
- """
- Changes position of this item with the previous item in the
- menu. Does nothing if this item is the first one.
- """
- try:
- list = Category.objects.filter(rank__lt = self.rank).reverse()
- prev_item = list[len(list) - 1]
- except IndexError:
- pass
- else:
- self.swap_ranks(prev_item)
-
- def swap_ranks(self, other):
- """
- Swap positions with ``other`` menu item.
- """
- maxrank = 5000
- prev_rank, self.rank = self.rank, maxrank
- self.save()
- self.rank, other.rank = other.rank, prev_rank
- other.save()
- self.save()
+ def save(self, *args, **kwargs):
+ if self.id is None:
+ # This is a django hack. When category icon is saved and then later accessed,
+ # category_id is used as a unique icon identifier. When category is first created,
+ # but not saved yet, category_id is None. So this hack first saves category without icon
+ # and then saves the icon separately. This is done to prevent creation of category_None.png
+ # file, when the icon is saved.
+ saved_icon = self.icon
+ self.icon = None
+ super(Category, self).save(*args, **kwargs)
+ self.icon = saved_icon
+ super(Category, self).save(*args, **kwargs)
class Vendor(models.Model):
user = models.ForeignKey(User, primary_key = True)
@@ -104,12 +85,6 @@ class Vendor(models.Model):
return self.name
-class OverwriteStorage(FileSystemStorage):
- def get_available_name(self, name):
- if self.exists(name):
- os.remove(os.path.join(settings.MEDIA_ROOT, name))
- return name
-
def content_file_name(instance, filename):
return packagePath(instance.appid, instance.architecture)
diff --git a/store/static/img/category_All.png b/store/static/img/category_All.png
new file mode 100644
index 0000000..2f44f41
--- /dev/null
+++ b/store/static/img/category_All.png
Binary files differ