- {% if bvc_data.word_cloud_url %}
-
-
+ {% if bvc_data.word_cloud_medium_url %}
+
+
{% elif bvc_data.word_list %}
{{ bvc_data.word_list }}
diff --git a/teamtemp/tests/factories.py b/teamtemp/tests/factories.py
index 319a306f..f79de3cf 100644
--- a/teamtemp/tests/factories.py
+++ b/teamtemp/tests/factories.py
@@ -13,6 +13,7 @@
from teamtemp import utils
from teamtemp.responses.models import TeamResponseHistory, Teams, \
TeamTemperature, TemperatureResponse, User, WordCloudImage
+from teamtemp.views import DEFAULT_WORDCLOUD_HEIGHT, DEFAULT_WORDCLOUD_WIDTH
fake = Faker()
@@ -88,6 +89,8 @@ class Meta(object):
model = WordCloudImage
word_list = ' '.join(fake.words(nb=random.randint(1, 25)))
+ width = DEFAULT_WORDCLOUD_WIDTH
+ height = DEFAULT_WORDCLOUD_HEIGHT
image_url = "/%s/%s" % (fake.uri_path(), fake.file_name(category='image'))
creation_date = factory.LazyFunction(timezone.now)
diff --git a/teamtemp/tests/models/test_word_cloud_image.py b/teamtemp/tests/models/test_word_cloud_image.py
index 8fd9fc12..4d062abd 100644
--- a/teamtemp/tests/models/test_word_cloud_image.py
+++ b/teamtemp/tests/models/test_word_cloud_image.py
@@ -6,7 +6,7 @@
class WordCloudImageTestCases(TestCase):
def test_wordcloud(self):
- wordcloud = WordCloudImageFactory()
+ wordcloud = WordCloudImageFactory(width=666, height=333)
self.assertTrue(len(wordcloud.word_list) > 0)
self.assertTrue(len(wordcloud.word_hash) > 0)
self.assertTrue(len(wordcloud.image_url) > 0)
@@ -14,9 +14,11 @@ def test_wordcloud(self):
self.assertIsNotNone(wordcloud.modified_date)
self.assertEqual(
str(wordcloud),
- "%s: %s %s %s %s" %
+ "%s: %s %s %s %s %s %s" %
(wordcloud.id,
wordcloud.creation_date,
wordcloud.word_hash,
+ wordcloud.width,
+ wordcloud.height,
wordcloud.word_list,
wordcloud.image_url))
diff --git a/teamtemp/tests/view/test_wordcloud_view.py b/teamtemp/tests/view/test_wordcloud_view.py
index 731c3586..a99409be 100644
--- a/teamtemp/tests/view/test_wordcloud_view.py
+++ b/teamtemp/tests/view/test_wordcloud_view.py
@@ -1,3 +1,5 @@
+import sys
+
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
@@ -6,20 +8,11 @@
class WordcloudViewTestCases(TestCase):
- def assertWordCloudImage(
- self,
- response,
- expected_url='/media/blank.png',
- status_code=status.HTTP_302_FOUND):
- self.assertRedirects(
- response,
- expected_url=expected_url,
- status_code=status_code)
+ def assertWordCloudImage(self, response, expected_url='/media/blank.png', status_code=status.HTTP_302_FOUND):
+ self.assertRedirects(response, expected_url=expected_url, status_code=status_code)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response['Content-Type'], 'image/png')
- self.assertEqual(
- response['Cache-Control'],
- 'public, max-age=315360000')
+ self.assertEqual(response['Cache-Control'], 'public, max-age=315360000')
self.assertGreater(len(response.getvalue()), 0)
def test_wordcloud_view_blank(self):
@@ -52,3 +45,19 @@ def test_wordcloud_view_found(self):
response,
word_cloud_image.image_url,
status.HTTP_302_FOUND)
+
+ def test_wordcloud_view_found_with_size(self):
+ word_cloud_image = WordCloudImageFactory(image_url='/media/test.png', width=600, height=300)
+ response = self.client.get(
+ reverse(
+ 'wordcloud',
+ kwargs={
+ 'word_hash': word_cloud_image.word_hash,
+ 'width': 600,
+ 'height': 300
+ }),
+ follow=True)
+ self.assertWordCloudImage(
+ response,
+ word_cloud_image.image_url,
+ status.HTTP_302_FOUND)
diff --git a/teamtemp/urls.py b/teamtemp/urls.py
index 7dc090ba..d13fdfef 100644
--- a/teamtemp/urls.py
+++ b/teamtemp/urls.py
@@ -72,6 +72,8 @@
re_path(r'^team/(?P
[0-9a-zA-Z]{8})/(?P[-\w]{1,64})$', team_view, name='team'),
re_path(r'^team/(?P[0-9a-zA-Z]{8})/?$', team_view, name='team'),
re_path(r'^wordcloud/(?P[a-f0-9]{40})?$', wordcloud_view, name='wordcloud'),
+ re_path(r'^wordcloud/(?P[1-9][0-9]{2,3})x(?P[1-9][0-9]{2,3})/(?P[a-f0-9]{40})?$', wordcloud_view,
+ name='wordcloud'),
re_path(r'^static/(.*)$', serve_static, {'document_root': settings.STATIC_ROOT}, name='static'),
re_path(r'^media/(.*)$', media_view, {'document_root': settings.MEDIA_ROOT}, name='media'),
re_path(r'^healthcheck/?$', health_check_view, name='healthcheck'),
diff --git a/teamtemp/views.py b/teamtemp/views.py
index 96a69344..2148b90b 100644
--- a/teamtemp/views.py
+++ b/teamtemp/views.py
@@ -8,6 +8,8 @@
import errno
import sys
import time
+import string
+import random
import gviz_api
import os
@@ -20,7 +22,7 @@
from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.decorators import login_required
from django.db import transaction
-from django.http import Http404, HttpResponse
+from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.static import serve as serve_static
@@ -37,6 +39,11 @@
from urllib.parse import urlparse
+DEFAULT_WORDCLOUD_HEIGHT = 350
+DEFAULT_WORDCLOUD_WIDTH = 500
+MAX_WORDCLOUD_WIDTH = 1000
+MAX_WORDCLOUD_HEIGHT = 1000
+
class WordCloudImageViewSet(viewsets.ModelViewSet):
queryset = WordCloudImage.objects.all()
@@ -47,7 +54,7 @@ class WordCloudImageViewSet(viewsets.ModelViewSet):
filters.OrderingFilter,
)
filter_fields = ('creation_date', 'word_hash', 'image_url',)
- order_fields = ('id', 'creation_date', 'word_hash')
+ order_fields = ('id', 'creation_date', 'word_hash', 'width', 'height')
search_fields = ('word_list', 'word_hash', 'image_url')
@@ -536,15 +543,15 @@ def admin_view(request, survey_id, team_name=''):
})
-def generate_wordcloud(word_list, word_hash):
- print("Start Word Cloud Generation: [%s] %s" %
- (word_hash, word_list), file=sys.stderr)
+def generate_wordcloud(word_list, word_hash, width=DEFAULT_WORDCLOUD_WIDTH, height=DEFAULT_WORDCLOUD_HEIGHT):
+ print("Start Word Cloud Generation: [%s] %s x %s '%s'" %
+ (word_hash, width, height, word_list), file=sys.stderr)
wordcloud = WordCloud(
max_words=1000,
margin=20,
- width=settings.WORDCLOUD_WIDTH,
- height=settings.WORDCLOUD_HEIGHT,
+ width=width,
+ height=height,
background_color="white",
prefer_horizontal=0.7,
regexp=r"[^\s]+",
@@ -559,7 +566,7 @@ def generate_wordcloud(word_list, word_hash):
print("Finish Word Cloud Generation: [%s]" %
(word_hash), file=sys.stderr)
- return save_image(image, "%s_%d.png" % (word_hash, time.time()))
+ return save_image(image, "%s_%dx%d_%d.png" % (word_hash, width, height, time.time())), width, height
def require_dir(path):
@@ -594,20 +601,36 @@ def media_file(src, basename=None):
return filename
+def randomword(length):
+ letters = string.ascii_letters + string.digits
+ return ''.join(random.choice(letters) for i in range(length))
+
+
+def media_tempfile(src, basename=None):
+ image_name = media_filename(src, basename)
+ require_dir(settings.MEDIA_ROOT)
+ temp_image_name = ".%s.%s.tmp" % (image_name, randomword(8))
+ temp_filename = os.path.join(settings.MEDIA_ROOT, temp_image_name)
+ return temp_filename
+
+
def save_image(image, basename):
return_url = media_url(basename)
filename = media_file(basename)
+ temp_filename = media_tempfile(basename)
- print("Saving Word Cloud: %s as %s" % (basename, filename), file=sys.stderr)
+ print("Saving Word Cloud: %s as %s" % (basename, temp_filename), file=sys.stderr)
- if not os.path.exists(filename):
- try:
- image.save(filename, format='png', optimize=True)
- except IOError as exc:
- print("Failed Saving Word Cloud: IOError:%s %s as %s" %
- (str(exc), url, filename), file=sys.stderr)
- return None
+ try:
+ image.save(temp_filename, format='png', optimize=True)
+ print("Renaming Word Cloud: %s to %s" % (temp_filename, filename), file=sys.stderr)
+ os.rename(temp_filename, filename)
+ except IOError as exc:
+ print("Failed Saving Word Cloud: IOError:%s %s as %s" %
+ (str(exc), return_url, filename), file=sys.stderr)
+ return None
+ print("Returning Word Cloud: %s url %s" % (basename, return_url), file=sys.stderr)
return return_url
@@ -658,6 +681,7 @@ def prune_word_cloud_cache(_):
utc_timestamp(), file=sys.stderr)
rows_deleted = 0
+ rows_checked = 0
yesterday = timezone.now() + timedelta(days=-1)
@@ -665,6 +689,7 @@ def prune_word_cloud_cache(_):
modified_date__lte=yesterday)
for word_cloud_image in old_word_cloud_images:
+ rows_checked += 1
if word_cloud_image.image_url:
fname = media_file(word_cloud_image.image_url)
if os.path.isfile(fname):
@@ -677,14 +702,16 @@ def prune_word_cloud_cache(_):
rows_deleted += rows
for word_cloud_image in WordCloudImage.objects.all():
- if word_cloud_image.image_url and not os.path.isfile(
- media_file(word_cloud_image.image_url)):
- rows, _ = word_cloud_image.delete()
- rows_deleted += rows
+ rows_checked += 1
+ if word_cloud_image.image_url:
+ image_filename = media_file(word_cloud_image.image_url)
+ if not os.path.isfile(image_filename) or os.path.getsize(image_filename) == 0:
+ rows, _ = word_cloud_image.delete()
+ rows_deleted += rows
print(
- "prune_word_cloud_cache: %d rows deleted" %
- rows_deleted,
+ "prune_word_cloud_cache: %d rows deleted, %d rows checked" %
+ (rows_deleted, rows_checked),
file=sys.stderr)
print("prune_word_cloud_cache: Stop at %s" %
utc_timestamp(), file=sys.stderr)
@@ -1151,31 +1178,51 @@ def populate_bvc_data(
return bvc_data
-def cached_word_cloud(word_list=None, word_hash=None, generate=True):
+def cached_word_cloud(word_list=None, word_hash=None, generate=True, width=None, height=None):
+ if not width:
+ width = DEFAULT_WORDCLOUD_WIDTH
+ if not height:
+ height= DEFAULT_WORDCLOUD_HEIGHT
+
word_cloud_image = None
if word_list:
word_hash = hashlib.sha1(word_list.encode('utf-8')).hexdigest()
word_cloud_image, _ = WordCloudImage.objects.get_or_create(
- word_hash=word_hash, word_list=word_list)
+ word_hash=word_hash, width=width, height=height, word_list=word_list)
elif word_hash:
try:
- word_cloud_image = WordCloudImage.objects.get(word_hash=word_hash)
+ word_cloud_image = WordCloudImage.objects.get(word_hash=word_hash, width=width, height=height)
word_list = word_cloud_image.word_list
except WordCloudImage.DoesNotExist:
- return None
- else:
+ pass
+
+ if not word_cloud_image:
+ try:
+ word_cloud_image_wrong_size = WordCloudImage.objects.filter(word_hash=word_hash)[0]
+ word_list = word_cloud_image_wrong_size.word_list
+ except IndexError:
+ pass
+
+ if word_list:
+ word_cloud_image, _ = WordCloudImage.objects.get_or_create(
+ word_hash=word_hash, width=width, height=height, word_list=word_list)
+
+ if not word_cloud_image:
return None
if word_cloud_image.image_url:
filename = media_file(word_cloud_image.image_url)
- if os.path.isfile(filename):
+ if os.path.isfile(filename) and os.path.getsize(filename) > 0:
return word_cloud_image
else:
word_cloud_image.image_url = None
if generate and not word_cloud_image.image_url:
- word_cloud_image.image_url = generate_wordcloud(word_list, word_hash)
+ word_cloud_image.image_url, word_cloud_image.width, word_cloud_image.height = generate_wordcloud(word_list,
+ word_hash,
+ width=width,
+ height=height)
word_cloud_image.full_clean()
word_cloud_image.save()
@@ -1260,13 +1307,32 @@ def calc_multi_iteration_average(
return None
+def is_multiple_of_50(x):
+ if x == 0:
+ return False
+ return x % 50 == 0
+
+
@no_cache()
@csp_exempt
-def wordcloud_view(request, word_hash=''):
+def wordcloud_view(request, word_hash='', width=None, height=None):
+ if width:
+ width = int(width)
+ if not (0 < width <= MAX_WORDCLOUD_WIDTH and is_multiple_of_50(width)):
+ return HttpResponseBadRequest(reason="width out of range")
+ if height:
+ height = int(height)
+ if not (0 < height <= MAX_WORDCLOUD_HEIGHT and is_multiple_of_50(height)):
+ return HttpResponseBadRequest(reason="height out of range")
+
# Cached word cloud
if word_hash:
word_cloud_image = cached_word_cloud(
- word_hash=word_hash, generate=True)
+ word_hash=word_hash,
+ width=width,
+ height=height,
+ generate=True
+ )
if word_cloud_image and word_cloud_image.image_url:
return redirect(word_cloud_image.image_url)
@@ -1325,11 +1391,18 @@ def bvc_view(
# Cached word cloud
if bvc_data['word_list']:
word_cloud_image = cached_word_cloud(
- bvc_data['word_list'], generate=False)
- bvc_data['word_cloud_url'] = word_cloud_image.image_url or reverse(
- 'wordcloud', kwargs={'word_hash': word_cloud_image.word_hash})
- bvc_data['word_cloud_width'] = settings.WORDCLOUD_WIDTH
- bvc_data['word_cloud_height'] = settings.WORDCLOUD_HEIGHT
+ bvc_data['word_list'], width=DEFAULT_WORDCLOUD_WIDTH, height=DEFAULT_WORDCLOUD_HEIGHT, generate=False)
+
+ word_hash = word_cloud_image.word_hash
+
+ bvc_data['word_cloud_small_url'] = reverse('wordcloud',
+ kwargs={'word_hash': word_hash, 'width': 300, 'height': 350})
+ bvc_data['word_cloud_medium_url'] = word_cloud_image.image_url or reverse('wordcloud',
+ kwargs={'word_hash': word_hash,
+ 'width': DEFAULT_WORDCLOUD_WIDTH,
+ 'height': DEFAULT_WORDCLOUD_HEIGHT})
+ bvc_data['word_cloud_large_url'] = reverse('wordcloud',
+ kwargs={'word_hash': word_hash, 'width': 750, 'height': 500})
all_dept_names = set()
all_region_names = set()