From 54f60485d56ece6345b93b76539d40b8347f5d36 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 13:07:21 +0100 Subject: [PATCH 01/11] Spelling fix in comment Signed-off-by: Matthias Stevens --- geokey_sapelli/helper/project_mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geokey_sapelli/helper/project_mapper.py b/geokey_sapelli/helper/project_mapper.py index 4f5ee7f..011ee1b 100644 --- a/geokey_sapelli/helper/project_mapper.py +++ b/geokey_sapelli/helper/project_mapper.py @@ -50,7 +50,7 @@ def create_project(sapelli_project_info, user, sap_file_path=None): user ) - # If anyting below fails the geokey_project will be deleted: + # If anything below fails the geokey_project will be deleted: try: sapelli_project = SapelliProject.objects.create( geokey_project=geokey_project, From 86d87d46ed06db953565c98f07fb1b786cdea8e3 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 13:08:59 +0100 Subject: [PATCH 02/11] Bugfix: correctly set contribution permissions for new Sapelli projects Signed-off-by: Matthias Stevens --- geokey_sapelli/helper/project_mapper.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/geokey_sapelli/helper/project_mapper.py b/geokey_sapelli/helper/project_mapper.py index 011ee1b..e1373fb 100644 --- a/geokey_sapelli/helper/project_mapper.py +++ b/geokey_sapelli/helper/project_mapper.py @@ -4,6 +4,7 @@ from django.core.files import File from geokey.projects.models import Project +from geokey.projects.base import EVERYONE_CONTRIB from geokey.categories.models import Category, Field, LookupValue from ..models import ( @@ -41,13 +42,13 @@ def create_implicit_fields(category, stores_end_time=False): def create_project(sapelli_project_info, user, sap_file_path=None): geokey_project = Project.create( - sapelli_project_info.get('display_name'), - ('Sapelli project id: %s;\nSapelli project fingerprint: %s;' % ( + name = sapelli_project_info.get('display_name'), + description = ('Sapelli project id: %s;\nSapelli project fingerprint: %s;' % ( sapelli_project_info.get('sapelli_id'), sapelli_project_info.get('sapelli_fingerprint'))), - True, - False, - user + isprivate = True, + everyone_contributes = EVERYONE_CONTRIB.false, + creator = user ) # If anything below fails the geokey_project will be deleted: From a1ead20a278b65d3c4aab3dec04decaf0b0081fa Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 13:43:28 +0100 Subject: [PATCH 03/11] SapelliItem: removed image field and instead reply on symbol field in categories.LookupValue (new in GeoKey v0.8.5) Requires GeoKey v0.8.5 Signed-off-by: Matthias Stevens --- geokey_sapelli/helper/project_mapper.py | 14 ++--- .../0012_remove_sapelliitem_image.py | 27 +++++++++ geokey_sapelli/models.py | 1 - geokey_sapelli/tests/model_factories.py | 58 +++++++++---------- requirements.txt | 2 +- 5 files changed, 63 insertions(+), 39 deletions(-) create mode 100644 geokey_sapelli/migrations/0012_remove_sapelliitem_image.py diff --git a/geokey_sapelli/helper/project_mapper.py b/geokey_sapelli/helper/project_mapper.py index e1373fb..9d43f5d 100644 --- a/geokey_sapelli/helper/project_mapper.py +++ b/geokey_sapelli/helper/project_mapper.py @@ -120,11 +120,6 @@ def create_project(sapelli_project_info, user, sap_file_path=None): if field_type == 'LookupField': # Loop over items: for idx, item in enumerate(field.get('items')): - # Value: - value = LookupValue.objects.create( - name=item.get('value'), - field=geokey_field - ) # Image: img_relative_path = item.get('img') img_file = None @@ -138,12 +133,17 @@ def create_project(sapelli_project_info, user, sap_file_path=None): img_file.close() else: img_path = None + # Value: + value = LookupValue.objects.create( + name=item.get('value'), + field=geokey_field, + symbol=img_path #pass the path, not the file (otherwise it may be duplicated) + ) # Create SapelliItem: SapelliItem.objects.create( lookup_value=value, sapelli_field=sapelli_field, - number=idx, - image=img_path #pass the path, not the file (otherwise it may be duplicated) + number=idx ) except BaseException, e: try: # delete geokey_project: diff --git a/geokey_sapelli/migrations/0012_remove_sapelliitem_image.py b/geokey_sapelli/migrations/0012_remove_sapelliitem_image.py new file mode 100644 index 0000000..b90d042 --- /dev/null +++ b/geokey_sapelli/migrations/0012_remove_sapelliitem_image.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def copy_image_to_lookup_field(apps, schema_editor): + SapelliItem = apps.get_model('geokey_sapelli', 'SapelliItem') + for si in SapelliItem.objects.all(): + si.lookup_value.symbol = si.image + si.lookup_value.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('geokey_sapelli', '0011_dir_path_sap_path'), + ('categories', '0014_category_symbol_lookupvalue_symbol'), + ] + + operations = [ + migrations.RunPython(copy_image_to_lookup_field), + + migrations.RemoveField( + model_name='sapelliitem', + name='image', + ), + ] diff --git a/geokey_sapelli/models.py b/geokey_sapelli/models.py index 7f48b7e..8dc63d5 100755 --- a/geokey_sapelli/models.py +++ b/geokey_sapelli/models.py @@ -369,7 +369,6 @@ class SapelliItem(models.Model): primary_key=True, related_name='sapelli_item' ) - image = models.ImageField(upload_to=get_img_path, null=True, max_length=500) number = models.IntegerField() sapelli_field = models.ForeignKey( 'SapelliField', diff --git a/geokey_sapelli/tests/model_factories.py b/geokey_sapelli/tests/model_factories.py index 980f9b8..acd16b9 100644 --- a/geokey_sapelli/tests/model_factories.py +++ b/geokey_sapelli/tests/model_factories.py @@ -1,10 +1,7 @@ import factory -from StringIO import StringIO -from PIL import Image from os.path import dirname, normpath, abspath, join -from django.core.files.base import ContentFile from django.conf import settings from geokey.applications.tests.model_factories import ApplicationFactory @@ -26,18 +23,6 @@ ) -def get_image(file_name='test.png', width=200, height=200): - image_file = StringIO() - image = Image.new('RGBA', size=(width, height), color=(255, 0, 255)) - image.save(image_file, 'png') - image_file.seek(0) - - the_file = ContentFile(image_file.read(), file_name) - the_file.content_type = 'image/png' - - return the_file - - class GeoKeySapelliApplicationFactory(ApplicationFactory): client_id = settings.SAPELLI_CLIENT_ID authorization_grant_type = 'password' @@ -83,7 +68,6 @@ class Meta: model = SapelliItem lookup_value = factory.SubFactory(LookupValueFactory) - image = get_image() number = factory.Sequence(lambda n: n) sapelli_field = factory.SubFactory(SapelliFieldFactory) @@ -104,59 +88,73 @@ def create_horniman_sapelli_project(user): }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Red Flowers' + 'name': 'Red Flowers', + 'symbol': 'red flowers.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Blue Flowers' + 'name': 'Blue Flowers', + 'symbol': 'blue flowers.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Yellow Flowers' + 'name': 'Yellow Flowers', + 'symbol': 'yellow flowers.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Edible Plants' + 'name': 'Edible Plants', + 'symbol': 'BeenTold.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Medicinal Plants' + 'name': 'Medicinal Plants', + 'symbol': 'Medicine.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Two Legged Animal' + 'name': 'Two Legged Animal', + 'symbol': 'Chicken.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Four Legged Animal' + 'name': 'Four Legged Animal', + 'symbol': 'Sheep.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Old Bench With Memorial' + 'name': 'Old Bench With Memorial', + 'symbol': 'memorial.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Old Bench with No Memorial' + 'name': 'Old Bench with No Memorial', + 'symbol': 'no memorial' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'New Bench With Memorial' + 'name': 'New Bench With Memorial', + 'symbol': 'memorial.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'New Bench with No Memorial' + 'name': 'New Bench with No Memorial', + 'symbol': 'no memorial.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Covered Bin' + 'name': 'Covered Bin', + 'symbol': 'covered bin.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Uncovered Bin' + 'name': 'Uncovered Bin', + 'symbol': 'uncovered bin.png' }) LookupValueFactory.create(**{ 'field': select_field, - 'name': 'Dog Bin' + 'name': 'Dog Bin', + 'symbol': 'dog bin.png' }) sapelli_project = SapelliProjectFactory.create(**{ diff --git a/requirements.txt b/requirements.txt index 149d4f2..5109b83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -geokey==0.8.3 +geokey==0.8.5 qrcode==5.1 From c3ee9b6cc45705667c6a9cb6d0b63563be98ce05 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 13:59:35 +0100 Subject: [PATCH 04/11] models.py: removed get_img_path function (no longer used) Signed-off-by: Matthias Stevens --- geokey_sapelli/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/geokey_sapelli/models.py b/geokey_sapelli/models.py index 8dc63d5..9ed174e 100755 --- a/geokey_sapelli/models.py +++ b/geokey_sapelli/models.py @@ -352,12 +352,6 @@ class SapelliField(models.Model): sapelli_id = models.CharField(max_length=255) truefalse = models.BooleanField(default=False) - -def get_img_path(instance, filename): - if filename is None or instance.sapelli_field.sapelli_form.sapelli_project.dir_path is None: - return None - else: - return os.path.join(instance.sapelli_field.sapelli_form.sapelli_project.dir_path, 'img/', filename) class SapelliItem(models.Model): """ From 3261bb44f98a6105d9d8db549f549faa3378f299 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 15:05:44 +0100 Subject: [PATCH 05/11] LoginAPI: improved error handling Signed-off-by: Matthias Stevens --- geokey_sapelli/views.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/geokey_sapelli/views.py b/geokey_sapelli/views.py index a0045f7..6dae087 100755 --- a/geokey_sapelli/views.py +++ b/geokey_sapelli/views.py @@ -269,13 +269,18 @@ def post(self, request, *args, **kwargs): """ # ensure POST request is mutable: # (this isn't always the case, see http://stackoverflow.com/q/12611345) - if not (request.POST._mutable): - request.POST = request.POST.copy() - request.POST['client_id'] = settings.SAPELLI_CLIENT_ID - request.POST['grant_type'] = 'password' - - return super(LoginAPI, self).post(request, *args, **kwargs) - + try: + if not (request.POST._mutable): + request.POST = request.POST.copy() + try: + request.POST['client_id'] = settings.SAPELLI_CLIENT_ID + except AttributeError, e: + raise SapelliException('geokey-sapelli is not properly configured an application on the server: ' + str(e)) + request.POST['grant_type'] = 'password' + + return super(LoginAPI, self).post(request, *args, **kwargs) + except BaseException, e: + return Response({'error': str(e)}) class ProjectDescriptionAPI(APIView): """ From d4e6676e95bf7f9ae9fb313cd1fa7ce8dc7206b0 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 15:07:46 +0100 Subject: [PATCH 06/11] Added helper method to check if extension is correctly installed. The checks are run every time the project list is requested and any issues are reported as an error message. Signed-off-by: Matthias Stevens --- geokey_sapelli/helper/install_checks.py | 37 +++++++++++++++++++++++++ geokey_sapelli/views.py | 11 ++++++++ 2 files changed, 48 insertions(+) create mode 100644 geokey_sapelli/helper/install_checks.py diff --git a/geokey_sapelli/helper/install_checks.py b/geokey_sapelli/helper/install_checks.py new file mode 100644 index 0000000..2135135 --- /dev/null +++ b/geokey_sapelli/helper/install_checks.py @@ -0,0 +1,37 @@ +import commands +import re + +from django.conf import settings + +from geokey.applications.models import Application + +from .sapelli_exceptions import SapelliException +from .sapelli_loader import get_sapelli_dir_path, get_sapelli_jar_path + +MINIMAL_JAVA_VERSION = '1.7.0' + +def check_extension(): + # Check if SAPELLI_CLIENT_ID value is set in settings.py: + try: + client_id = settings.SAPELLI_CLIENT_ID + except AttributeError: + raise SapelliException('no SAPELLI_CLIENT_ID value set in geokey settings.py.') + # Check if an application is registered with this client_id: + try: + Application.objects.get(client_id=client_id) + except Application.DoesNotExist: + raise SapelliException('geokey_sapelli is not registered as an application on the server.') + # Check if java 1.7.0 or more recent is installed: + try: + status_output = commands.getstatusoutput('java -version') + if(status_output[0] != 0): + raise SapelliException('java not installed, please install JRE v7 or later.') + java_version = re.match(r'java version "(?P[0-9]+\.[0-9]+\.[0-9]+)_.*', status_output[1]).group('java_version') + if(java_version < MINIMAL_JAVA_VERSION): + raise SapelliException('installed version of java is too old (installed: %s, minimum required: %s).' % (java_version, MINIMAL_JAVA_VERSION)) + except BaseException, e: + raise SapelliException('could not run java command (%s).' % str(e)) + # Check if there is a sapelli working directory: + get_sapelli_dir_path() # raises SapelliException + # Check if we have the sapelli JAR: + get_sapelli_jar_path() # raises SapelliException diff --git a/geokey_sapelli/views.py b/geokey_sapelli/views.py index 6dae087..ef6f6aa 100755 --- a/geokey_sapelli/views.py +++ b/geokey_sapelli/views.py @@ -37,6 +37,7 @@ SapelliDuplicateException, SapelliCSVException ) +from .helper.install_checks import check_extension from helper.dynamic_menu import MenuEntry @@ -57,6 +58,15 @@ def get_menu_label(): def get_menu_url(): return None + def check(self): + """ + Checks if extension is correctly installed. + """ + try: + check_extension() + except SapelliException, se: + messages.error(self.request, 'The Sapelli extension is not properly installed: ' + str(se)) + def add_menu(self, context): menu_entries = [] for subclass in AbstractSapelliView.__subclasses__(): @@ -93,6 +103,7 @@ def get_context_data(self): dict """ context = {'sapelli_projects': SapelliProject.objects.get_list_for_contribution(self.request.user)} + self.check() return self.add_menu(context) From 12e9b4a87e3503d1d8982b2a8c458d22ca941a33 Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 15:08:58 +0100 Subject: [PATCH 07/11] Fixed regression from c3ee9b6: 0010_choice_root_path_image.py should work again Signed-off-by: Matthias Stevens --- geokey_sapelli/migrations/0010_choice_root_path_image.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/geokey_sapelli/migrations/0010_choice_root_path_image.py b/geokey_sapelli/migrations/0010_choice_root_path_image.py index 1c4e829..47b490e 100644 --- a/geokey_sapelli/migrations/0010_choice_root_path_image.py +++ b/geokey_sapelli/migrations/0010_choice_root_path_image.py @@ -5,6 +5,13 @@ import geokey_sapelli.models +def get_img_path(instance, filename): + if filename is None or instance.sapelli_field.sapelli_form.sapelli_project.dir_path is None: + return None + else: + return os.path.join(instance.sapelli_field.sapelli_form.sapelli_project.dir_path, 'img/', filename) + + class Migration(migrations.Migration): dependencies = [ @@ -25,6 +32,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sapelliitem', name='image', - field=models.ImageField(max_length=500, null=True, upload_to=geokey_sapelli.models.get_img_path), + field=models.ImageField(max_length=500, null=True, upload_to=get_img_path), ), ] From 7e60a30150ce415516e17d2cb034ddf18843441b Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Mon, 21 Dec 2015 15:42:04 +0100 Subject: [PATCH 08/11] Fixed tests Signed-off-by: Matthias Stevens --- geokey_sapelli/tests/test_views.py | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/geokey_sapelli/tests/test_views.py b/geokey_sapelli/tests/test_views.py index 55b3f43..5f5fd22 100644 --- a/geokey_sapelli/tests/test_views.py +++ b/geokey_sapelli/tests/test_views.py @@ -9,6 +9,7 @@ from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth.models import AnonymousUser from django.contrib.messages import get_messages +from django.contrib.messages.storage.fallback import FallbackStorage from django.conf import settings from django.test.client import RequestFactory @@ -24,8 +25,15 @@ from ..views import ProjectList, ProjectUpload, DataCSVUpload, LoginAPI, SAPDownloadAPI, SAPDownloadQRLinkAPI from ..helper.dynamic_menu import MenuEntry - class ProjectListTest(TestCase): + def setUp(self): + self.view = ProjectList.as_view() + self.request = HttpRequest() + self.request.method = 'GET' + + setattr(self.request, 'session', 'session') + setattr(self.request, '_messages', FallbackStorage(self.request)) + def test_url(self): self.assertEqual(reverse('geokey_sapelli:index'), '/admin/sapelli/') @@ -34,13 +42,9 @@ def test_url(self): def test_get_with_user(self): sapelli_project = SapelliProjectFactory.create() - view = ProjectList.as_view() - - request = HttpRequest() - request.method = 'GET' - request.user = sapelli_project.geokey_project.creator - - response = view(request).render() + self.request.user = sapelli_project.geokey_project.creator + + response = self.view(self.request).render() self.assertEqual(response.status_code, 200) rendered = render_to_string( @@ -48,7 +52,8 @@ def test_get_with_user(self): { 'sapelli_projects': [sapelli_project], 'user': sapelli_project.geokey_project.creator, - 'PLATFORM_NAME': get_current_site(request).name, + 'PLATFORM_NAME': get_current_site(self.request).name, + 'messages': get_messages(self.request), 'GEOKEY_VERSION': version.get_version(), 'GEOKEY_SAPELLI_VERSION': __version__, 'menu_entries': [ @@ -65,11 +70,8 @@ def test_get_with_user(self): self.assertEqual(unicode(response.content), rendered) def test_get_with_anonymous(self): - view = ProjectList.as_view() - request = HttpRequest() - request.method = 'GET' - request.user = AnonymousUser() - response = view(request) + self.request.user = AnonymousUser() + response = self.view(self.request) self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], '/admin/account/login/?next=') @@ -182,10 +184,8 @@ def setUp(self): self.request.method = 'GET' self.request.user = AnonymousUser() - from django.contrib.messages.storage.fallback import FallbackStorage setattr(self.request, 'session', 'session') - messages = FallbackStorage(self.request) - setattr(self.request, '_messages', messages) + setattr(self.request, '_messages', FallbackStorage(self.request)) def test_url(self): self.assertEqual( From 6362096c4e29ea7e782eb951fb1461011ec0e00a Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Tue, 22 Dec 2015 16:17:55 +0100 Subject: [PATCH 09/11] install_checks: improved error msgs Signed-off-by: Matthias Stevens --- geokey_sapelli/helper/install_checks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geokey_sapelli/helper/install_checks.py b/geokey_sapelli/helper/install_checks.py index 2135135..f2abb34 100644 --- a/geokey_sapelli/helper/install_checks.py +++ b/geokey_sapelli/helper/install_checks.py @@ -18,9 +18,9 @@ def check_extension(): raise SapelliException('no SAPELLI_CLIENT_ID value set in geokey settings.py.') # Check if an application is registered with this client_id: try: - Application.objects.get(client_id=client_id) + Application.objects.get(client_id=client_id, authorization_grant_type='password') except Application.DoesNotExist: - raise SapelliException('geokey_sapelli is not registered as an application on the server.') + raise SapelliException('geokey_sapelli is not registered as an application (with password authorisation) on the server.') # Check if java 1.7.0 or more recent is installed: try: status_output = commands.getstatusoutput('java -version') From 591de7bb7edc85f32678ff42f391b8f860be729b Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Tue, 22 Dec 2015 16:24:29 +0100 Subject: [PATCH 10/11] Massive improvements to QR link functionality: - QR links now remain valid for 1 day independent of whether the creating user logs out. - while the QR link is valid it will be shown again (rather than a newly generated one) if the same user asks for it (for the same project) - the remaining validity time is shown in the QR code dialog - the creating user can now revoke the QR link at any time before it expires automatically - all this also applies to hyperlink on the QR image (i.e. it can be distributed to others just like the QR image itself, and will remain valid and expire/be revoked together) Signed-off-by: Matthias Stevens --- .../migrations/0013_sapdownloadqrlink.py | 22 +++ geokey_sapelli/models.py | 35 +++++ .../templates/sapelli_project_list.html | 138 +++++++++++++++++- geokey_sapelli/tests/model_factories.py | 32 +++- geokey_sapelli/tests/test_models.py | 29 +++- geokey_sapelli/tests/test_views.py | 42 +++++- geokey_sapelli/views.py | 42 +++++- requirements.txt | 2 +- 8 files changed, 320 insertions(+), 22 deletions(-) create mode 100644 geokey_sapelli/migrations/0013_sapdownloadqrlink.py diff --git a/geokey_sapelli/migrations/0013_sapdownloadqrlink.py b/geokey_sapelli/migrations/0013_sapdownloadqrlink.py new file mode 100644 index 0000000..40c7b66 --- /dev/null +++ b/geokey_sapelli/migrations/0013_sapdownloadqrlink.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth2_provider', '0002_08_updates'), + ('geokey_sapelli', '0012_remove_sapelliitem_image'), + ] + + operations = [ + migrations.CreateModel( + name='SAPDownloadQRLink', + fields=[ + ('access_token', models.OneToOneField(primary_key=True, serialize=False, to='oauth2_provider.AccessToken')), + ('sapelli_project', models.ForeignKey(to='geokey_sapelli.SapelliProject')), + ], + ), + ] diff --git a/geokey_sapelli/models.py b/geokey_sapelli/models.py index 9ed174e..20ddcad 100755 --- a/geokey_sapelli/models.py +++ b/geokey_sapelli/models.py @@ -2,12 +2,19 @@ import re import os import shutil +from datetime import timedelta from django.db import models from django.dispatch import receiver +from django.conf import settings +from django.utils import timezone from geokey.projects.models import Project from geokey.contributions.models import Observation +from geokey.applications.models import Application + +from oauth2_provider.models import AccessToken +from oauthlib.common import generate_token from .manager import SapelliProjectManager @@ -368,3 +375,31 @@ class SapelliItem(models.Model): 'SapelliField', related_name='items' ) + + +class SAPDownloadQRLink(models.Model): + access_token = models.OneToOneField('oauth2_provider.AccessToken', primary_key=True) + sapelli_project = models.ForeignKey(SapelliProject) + + @classmethod + def create(cls, user, sapelli_project, days_valid=1): + a_t = AccessToken.objects.create( + user=user, + application=Application.objects.get(client_id=settings.SAPELLI_CLIENT_ID), + expires=timezone.now() + timedelta(days=days_valid), + token=generate_token(), + scope='read') + qr_link = cls(access_token = a_t, sapelli_project = sapelli_project) + qr_link.save() + return qr_link + + +@receiver(models.signals.pre_delete, sender=AccessToken) +def pre_delete_access_token(sender, instance, **kwargs): + """ + Receiver that is called after an AccessToken is deleted. Deletes related SAPDownloadQRLink. + """ + try: + SAPDownloadQRLink.objects.get(access_token=instance).delete() + except BaseException: + pass diff --git a/geokey_sapelli/templates/sapelli_project_list.html b/geokey_sapelli/templates/sapelli_project_list.html index 1182e2e..fd1bcf1 100644 --- a/geokey_sapelli/templates/sapelli_project_list.html +++ b/geokey_sapelli/templates/sapelli_project_list.html @@ -33,7 +33,7 @@

Upload data {% if sapelli_project.sap_path %} Download SAP -
SAP QR code
+ SAP QR code {% endif %} Manage project

@@ -42,16 +42,22 @@

@@ -71,4 +77,126 @@

+ + {% endblock %} diff --git a/geokey_sapelli/tests/model_factories.py b/geokey_sapelli/tests/model_factories.py index acd16b9..6e61a19 100644 --- a/geokey_sapelli/tests/model_factories.py +++ b/geokey_sapelli/tests/model_factories.py @@ -1,8 +1,11 @@ import factory +from datetime import timedelta + from os.path import dirname, normpath, abspath, join from django.conf import settings +from django.utils import timezone from geokey.applications.tests.model_factories import ApplicationFactory from geokey.projects.tests.model_factories import ProjectFactory @@ -13,13 +16,15 @@ LookupValueFactory, TextFieldFactory ) +from geokey.users.tests.model_factories import AccessTokenFactory from ..models import ( SapelliProject, SapelliForm, SapelliField, SapelliItem, - LocationField + LocationField, + SAPDownloadQRLink, ) @@ -236,3 +241,28 @@ def create_textunicode_sapelli_project(user): }) return sapelli_project + + +class SAPDownloadQRLinkFactory(factory.django.DjangoModelFactory): + class Meta: + model = SAPDownloadQRLink + + access_token = factory.SubFactory(AccessTokenFactory) + sapelli_project = factory.SubFactory(SapelliProjectFactory) + + +def create_qr_link(app, user, project): + access_token = AccessTokenFactory.create(**{ + 'user': user, + 'application': app, + 'expires': timezone.now() + timedelta(days=1), + 'token': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + 'scope': 'read' + }) + + qr_link = SAPDownloadQRLinkFactory.create(**{ + 'access_token': access_token, + 'sapelli_project': project + }) + + return qr_link diff --git a/geokey_sapelli/tests/test_models.py b/geokey_sapelli/tests/test_models.py index 5bbddda..62eaf94 100644 --- a/geokey_sapelli/tests/test_models.py +++ b/geokey_sapelli/tests/test_models.py @@ -7,8 +7,8 @@ from geokey.projects.models import Project from geokey.projects.tests.model_factories import ProjectFactory -from ..models import SapelliProject, post_save_project, pre_delete_project -from .model_factories import SapelliProjectFactory, create_horniman_sapelli_project, create_textunicode_sapelli_project +from ..models import SapelliProject, post_save_project, pre_delete_project, SAPDownloadQRLink +from .model_factories import SapelliProjectFactory, create_horniman_sapelli_project, create_textunicode_sapelli_project, GeoKeySapelliApplicationFactory from ..helper.sapelli_exceptions import SapelliCSVException @@ -133,3 +133,28 @@ def test_pre_delete_project(self): pre_delete_project(Project, instance=geokey_project) self.assertFalse(SapelliProject.objects.filter(pk=sapelli_project.pk).exists()) + + +class SAPDownloadQRLinkTest(TestCase): + def setUp(self): + self.app = GeoKeySapelliApplicationFactory.create() + self.user = UserFactory.create() + self.user.set_password('123456') + self.user.save() + + def tearDown(self): + try: + self.sap_download_qr_link.delete() + self.app.delete() + self.user.delete() + except BaseException: + pass + + def test_create_qr_link(self): + sapelli_project = create_horniman_sapelli_project(self.user) + self.sap_download_qr_link = SAPDownloadQRLink.create(user=self.user, sapelli_project=sapelli_project, days_valid=1) + + self.assertEqual(self.sap_download_qr_link.access_token.user, self.user) + self.assertEqual(self.sap_download_qr_link.access_token.application, self.app) + self.assertEqual(self.sap_download_qr_link.sapelli_project, sapelli_project) + diff --git a/geokey_sapelli/tests/test_views.py b/geokey_sapelli/tests/test_views.py index 5f5fd22..15e33a7 100644 --- a/geokey_sapelli/tests/test_views.py +++ b/geokey_sapelli/tests/test_views.py @@ -14,14 +14,18 @@ from django.test.client import RequestFactory +from oauth2_provider.models import AccessToken + +from rest_framework.test import force_authenticate + from geokey import version from geokey.applications.tests.model_factories import ApplicationFactory -from geokey.users.tests.model_factories import UserFactory, AccessTokenFactory +from geokey.users.tests.model_factories import UserFactory from geokey.projects.models import Project -from .model_factories import GeoKeySapelliApplicationFactory, SapelliProjectFactory, create_horniman_sapelli_project +from .model_factories import GeoKeySapelliApplicationFactory, SapelliProjectFactory, create_horniman_sapelli_project, create_qr_link from .. import __version__ -from ..models import SapelliProject +from ..models import SapelliProject, SAPDownloadQRLink from ..views import ProjectList, ProjectUpload, DataCSVUpload, LoginAPI, SAPDownloadAPI, SAPDownloadQRLinkAPI from ..helper.dynamic_menu import MenuEntry @@ -380,21 +384,26 @@ def test_get_with_user(self): class SAPDownloadQRLinkAPITest(TestCase): def setUp(self): self.app = GeoKeySapelliApplicationFactory.create() + self.user = UserFactory.create() self.user.set_password('123456') self.user.save() - AccessTokenFactory.create(user=self.user, application=self.app) - + self.view = SAPDownloadQRLinkAPI.as_view() + self.request = HttpRequest() self.request.method = 'GET' - self.request.user = AnonymousUser() # necessary for request.build_absolute_uri() to work: self.request.META['SERVER_NAME'] = 'test-server' self.request.META['SERVER_PORT'] = '80' def tearDown(self): + # delete tokens: + for access_token in AccessToken.objects.filter(user=self.user): + access_token.delete() + # delete app: self.app.delete() + # delete user: self.user.delete() def test_url(self): @@ -411,6 +420,8 @@ def test_url(self): def test_get_with_anonymous(self): sapelli_project = create_horniman_sapelli_project(self.user) + self.request.user = AnonymousUser() + response = self.view(self.request, project_id=sapelli_project.geokey_project.id) self.assertEqual(response.status_code, 403) @@ -420,5 +431,24 @@ def test_get_with_user(self): response = self.view(self.request, project_id=sapelli_project.geokey_project.id) + qr_link = SAPDownloadQRLink.objects.filter(access_token__user=self.user, sapelli_project=sapelli_project).latest('access_token__expires') + self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'], 'image/png') + self.assertEqual(response['X-QR-Access-Token'], qr_link.access_token.token) + self.assertEqual(response['X-QR-Access-Token-Expires'], qr_link.access_token.expires.isoformat()) + + def test_delete_with_user(self): + delete_request = HttpRequest() + delete_request.method = 'DELETE' + delete_request.user = self.user + force_authenticate(delete_request, user=self.user) + + sapelli_project = create_horniman_sapelli_project(self.user) + qr_link = create_qr_link(self.app, self.user, sapelli_project) + + response = self.view(delete_request, project_id=sapelli_project.geokey_project.id).render() + response_json = json.loads(response.content) + + self.assertTrue(response_json.get('deleted')) + self.assertNotIn(qr_link, SAPDownloadQRLink.objects.all()) diff --git a/geokey_sapelli/views.py b/geokey_sapelli/views.py index ef6f6aa..fff734b 100755 --- a/geokey_sapelli/views.py +++ b/geokey_sapelli/views.py @@ -18,7 +18,6 @@ from rest_framework.response import Response from oauth2_provider.views.base import TokenView -from oauth2_provider.models import AccessToken from geokey.core.decorators import ( handle_exceptions_for_ajax, @@ -28,7 +27,7 @@ from . import __version__ -from .models import SapelliProject +from .models import SapelliProject, SAPDownloadQRLink from .helper.sapelli_loader import load_from_sap from .helper.sapelli_exceptions import ( SapelliException, @@ -522,21 +521,30 @@ def get(self, request, project_id): raise PermissionDenied('API access not authorised, please login.') # Check if current user can contribute to the project: try: - SapelliProject.objects.get_single_for_contribution(self.request.user, project_id) + sapelli_project = SapelliProject.objects.get_single_for_contribution(request.user, project_id) except SapelliProject.DoesNotExist: return Response({'error': 'No such project (id: %s)' % project_id}, status=404) try: + qr_link = None + try: # Try getting previously generated link/token: + qr_link = SAPDownloadQRLink.objects.filter(access_token__user=request.user, sapelli_project=sapelli_project).latest('access_token__expires') + except BaseException, e: + pass + if qr_link is None or qr_link.access_token.is_expired(): + if(qr_link is not None): + qr_link.access_token.delete() # qr_link will be deleted as well + # Generate new access token (valid for 1 day): + qr_link = SAPDownloadQRLink.create(user=request.user, sapelli_project=sapelli_project, days_valid=1) # Generate download url: sap_download_url = ( request.build_absolute_uri(reverse('geokey_sapelli:sap_download_api', kwargs={'project_id': project_id})) + - '?access_token=' + AccessToken.objects.filter(user=request.user)[0].token) - # generate QR png image: + '?access_token=' + qr_link.access_token.token) + # Generate QR code PNG image: qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=5, - border=0, - ) + border=0) qr.add_data(sap_download_url) qr.make(fit=True) img_buffer = StringIO() @@ -545,6 +553,26 @@ def get(self, request, project_id): response = HttpResponse(img_buffer.getvalue(), content_type = 'image/png') # ..and correct content-disposition response['Content-Disposition'] = 'attachment; filename=%s' % request.path[request.path.rfind('/')+1:] + # Add additional info as response headers: + response['X-QR-URL'] = sap_download_url + response['X-QR-Access-Token'] = qr_link.access_token.token + response['X-QR-Access-Token-Expires'] = qr_link.access_token.expires.isoformat() return response except BaseException, e: return Response({'error': str(e)}) + + @handle_exceptions_for_ajax + def delete(self, request, project_id): + if request.user.is_anonymous(): + raise PermissionDenied('API access not authorised, please login.') + # Check if current user can contribute to the project: + try: + sapelli_project = SapelliProject.objects.get_single_for_contribution(request.user, project_id) + except SapelliProject.DoesNotExist: + return Response({'error': 'No such project (id: %s)' % project_id}, status=404) + try: # Try getting & deleting previously generated link/token: + qr_link = SAPDownloadQRLink.objects.filter(access_token__user=request.user, sapelli_project=sapelli_project).latest('access_token__expires') + qr_link.access_token.delete() # qr_link will be deleted as well + except BaseException, e: + return Response({'error': str(e)}) + return Response({'deleted': True}) diff --git a/requirements.txt b/requirements.txt index 5109b83..dca2dab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -geokey==0.8.5 +geokey==0.8.6 qrcode==5.1 From 310175eb80c396a30b09c06d5231b6116f8c59eb Mon Sep 17 00:00:00 2001 From: Matthias Stevens Date: Tue, 22 Dec 2015 16:25:01 +0100 Subject: [PATCH 11/11] v0.6.1-rc1 Signed-off-by: Matthias Stevens --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8a43c57..960f4a8 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages # Change geokey_sapelli version here (and here alone!): -VERSION_PARTS = (0, 6, 0) +VERSION_PARTS = (0, 6, 1) name = 'geokey-sapelli' version = '.'.join(map(str, VERSION_PARTS))