From 55d24e96cb2c815b3d10910225b2cbc67dc61314 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 15 May 2017 20:40:03 +0100 Subject: [PATCH 1/5] Adds basic tests to check that versioning views load successfully More comprehensive tests should be added when versioning.py is updated for the new version of django-reversion --- RIGS/test_unit.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py index 5b531636..c264b9c1 100644 --- a/RIGS/test_unit.py +++ b/RIGS/test_unit.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.test.utils import override_settings from RIGS import models - +from reversion import revisions as reversion class TestAdminMergeObjects(TestCase): @classmethod @@ -249,6 +249,52 @@ class TestPrintPaperwork(TestCase): self.assertEqual(response.status_code, 200) +class TestVersioningViews(TestCase): + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + + cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + + cls.events = {} + + with reversion.create_revision(): + reversion.set_user(cls.profile) + cls.events[1] = models.Event.objects.create(name="TE E1", start_date=date.today()) + + with reversion.create_revision(): + reversion.set_user(cls.profile) + cls.events[2] = models.Event.objects.create(name="TE E2", start_date='2014-03-05') + + with reversion.create_revision(): + reversion.set_user(cls.profile) + cls.events[1].description = "A test description" + cls.events[1].save() + + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + + def test_event_history_loads_successfully(self): + request_url = reverse('event_history', kwargs={'pk': self.events[1].pk}) + + response = self.client.get(request_url, follow=True) + self.assertEqual(response.status_code, 200) + + def test_activity_feed_loads_successfully(self): + request_url = reverse('activity_feed') + + response = self.client.get(request_url, follow=True) + self.assertEqual(response.status_code, 200) + + def test_activity_table_loads_successfully(self): + request_url = reverse('activity_table') + + response = self.client.get(request_url, follow=True) + self.assertEqual(response.status_code, 200) + + class TestEmbeddedViews(TestCase): @classmethod def setUpTestData(cls): From fdce2fa53d24de5452d201a1018b5c180b7c0e2d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 15 May 2017 21:41:33 +0100 Subject: [PATCH 2/5] Update selenium, use chrome for tests, and use sauce-labs for CI integration tests --- .travis.yml | 15 +++-- README.md | 13 +++++ RIGS/test_functional.py | 124 +++++++++++++++++++++++----------------- requirements.txt | 2 +- 4 files changed, 97 insertions(+), 57 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1efb7729..0e93cb50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,27 @@ +sudo: false +dist: trusty + language: python python: "2.7" +cache: pip -before_install: - - "export DISPLAY=:99.0" - - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" +addons: + sauce_connect: + username: davidtaylorhq + access_key: + secure: "ibpjQ19LfjwfQruiJmX0j6NzyNwsS3PvRFdfXUYcfCEa9Eh20QQ/S8pOdFhRh70KIEuwN5oGuPqDkJPPTjkdY3/NCjuA7/NMTp14jAIX4XjpeNcsPFupp31vEy7KBuX4iAGpenrHJssFCwurpvrlWfwSOrk7bVZKaGUowVOXmyth1FSNQvr5c3YnlxmGvNzNBMMBDcJ3ixSlS9pBRLnHIJ1w3/f9Lx2uONkVMeGM6rVyuHholWvanIyNVYtO9JkXkoie6n1R3gNbXCyJdxSRn2OLppdryUaA0wUPJSu3hqEM3R5EsRDiFJszkJLTwSBG8x4k/dbqim7stjsu1qpUhCIG5mT6e+UI9auPi/5nlwlVmPhSq58qBP53vH3hs++02wjDlgvTGB1p4PqFblHhVaslaQ166bo9skGMZb0fXLlM1aCmmwFTpC5ofiPTSRTdJcljHG/d3JabKX03ME+nX2LFPIMnSLXgrjrfh2ppI6LFESiX3Z8jYUdsgTFeN3nQZ8U0kyb5X9Ay9YFnAaYD9OuxaqweTmqAJQj093GK38+79WMN2jnvEUzM1ZjI8Y4L/f3rHvhNIwYvZjQ+gJRhUqJh2Qruk7ke7uQ1oecxIqRHj8hIFEkuBcM3e86MkRiYQXXI9jOX3JrhI/jivAjFuw0flU2tjLNgM7tUYzjMyqk=" install: - pip install -r requirements.txt - pip install coveralls codeclimate-test-reporter before_script: + - export PATH=$PATH:/usr/lib/chromium-browser/ - python manage.py collectstatic --noinput script: - - coverage run manage.py test RIGS + - coverage run manage.py test --verbosity=2 after_success: - coveralls diff --git a/README.md b/README.md index 625bac90..3401782f 100644 --- a/README.md +++ b/README.md @@ -95,5 +95,18 @@ python manage.py generateSampleData |keyholder|keyholder| |basic |basic | +### Testing ### +Tests are contained in 3 files. `RIGS/test_models.py` contains tests for logic within the data models. `RIGS/test_unit.py` contains "Live server" tests, using raw web requests. `RIGS/test_integration.py` contains user interface tests which take control of a web browser. For automated Travis tests, we use [Sauce Labs](https://saucelabs.com). When debugging locally, ensure that you have the latest version of Google Chrome installed, then install [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and ensure it is on the `PATH`. + +You can run the entire test suite, or you can run specific sections individually. For example, in order of specificity: + +``` +python manage.py test +python manage.py test RIGS.test_models +python manage.py test RIGS.test_models.EventTestCase +python manage.py test RIGS.test_models.EventTestCase.test_current_events + +``` + ### Committing, pushing and testing ### Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person. diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index 3b867652..9452e41c 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -3,7 +3,6 @@ import os import re from datetime import date, timedelta -import reversion from django.core import mail from django.db import transaction from django.test import LiveServerTestCase @@ -15,20 +14,52 @@ from selenium.webdriver.support.ui import WebDriverWait from RIGS import models -import re -import os -from datetime import date, timedelta -from django.db import transaction from reversion import revisions as reversion -import json + +import time +import sys + +browsers = [{"platform": "macOS 10.12", + "browserName": "chrome", + "version": "latest"}, + ] +def on_platforms(platforms): + if os.environ.get("TRAVIS"): + def decorator(base_class): + module = sys.modules[base_class.__module__].__dict__ + for i, platform in enumerate(platforms): + d = dict(base_class.__dict__) + d['desired_capabilities'] = platform + name = "%s_%s" % (base_class.__name__, i + 1) + module[name] = type(name, (base_class,), d) + return decorator + +def create_browser(test_name, desired_capabilities): + # return webdriver.Chrome() + if os.environ.get("TRAVIS"): + username = os.environ["SAUCE_USERNAME"] + access_key = os.environ["SAUCE_ACCESS_KEY"] + caps = {'browserName': desired_capabilities['browserName']} + caps['platform'] = desired_capabilities['platform'] + caps['version'] = desired_capabilities['version'] + caps["tunnel-identifier"] = os.environ["TRAVIS_JOB_NUMBER"] + caps["name"] = '#' + os.environ["TRAVIS_JOB_NUMBER"] + ": " + test_name + hub_url = "%s:%s@localhost:4445" % (username, access_key) + driver = webdriver.Remote(desired_capabilities=caps, command_executor="http://%s/wd/hub" % hub_url) + return driver + else: + return webdriver.Chrome() + + +@on_platforms(browsers) class UserRegistrationTest(LiveServerTestCase): def setUp(self): - self.browser = webdriver.Firefox() - self.browser.implicitly_wait(3) # Set implicit wait session wide + self.browser = create_browser(self.id(), self.desired_capabilities) + self.browser.implicitly_wait(3) # Set implicit wait session wide os.environ['RECAPTCHA_TESTING'] = 'True' def tearDown(self): @@ -155,7 +186,7 @@ class UserRegistrationTest(LiveServerTestCase): # All is well - +@on_platforms(browsers) class EventTest(LiveServerTestCase): def setUp(self): @@ -166,9 +197,9 @@ class EventTest(LiveServerTestCase): self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1') - self.browser = webdriver.Firefox() - self.browser.implicitly_wait(3) # Set implicit wait session wide - self.browser.maximize_window() + self.browser = create_browser(self.id(), self.desired_capabilities) + self.browser.implicitly_wait(10) # Set implicit wait session wide + # self.browser.maximize_window() os.environ['RECAPTCHA_TESTING'] = 'True' def tearDown(self): @@ -211,7 +242,7 @@ class EventTest(LiveServerTestCase): # Gets redirected to login and back self.authenticate('/event/create/') - wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations) + wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations) wait.until(animation_is_finished()) @@ -366,11 +397,11 @@ class EventTest(LiveServerTestCase): self.assertEqual(obj.pk, int(option.get_attribute("value"))) # Set start date/time - form.find_element_by_id('id_start_date').send_keys('3015-05-25') + form.find_element_by_id('id_start_date').send_keys('25/05/3015') form.find_element_by_id('id_start_time').send_keys('06:59') # Set end date/time - form.find_element_by_id('id_end_date').send_keys('4000-06-27') + form.find_element_by_id('id_end_date').send_keys('27/06/4000') form.find_element_by_id('id_end_time').send_keys('07:00') # Add item @@ -467,7 +498,7 @@ class EventTest(LiveServerTestCase): self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk) + '/duplicate/') self.authenticate('/event/' + str(testEvent.pk) + '/duplicate/') - wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations) + wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations) save = self.browser.find_element_by_xpath( '(//button[@type="submit"])[3]') @@ -540,7 +571,7 @@ class EventTest(LiveServerTestCase): # Gets redirected to login and back self.authenticate('/event/create/') - wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations) + wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations) wait.until(animation_is_finished()) @@ -555,14 +586,13 @@ class EventTest(LiveServerTestCase): e.send_keys('Test Event Name') # Both dates, no times, end before start - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") - form.find_element_by_id('id_end_date').clear() - form.find_element_by_id('id_end_date').send_keys('3015-04-23') + self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-23'") # Attempt to save - should fail save.click() + error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]') self.assertTrue(error.is_displayed()) self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) @@ -571,16 +601,14 @@ class EventTest(LiveServerTestCase): # Same date, end time before start time form = self.browser.find_element_by_tag_name('form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') - form.find_element_by_id('id_end_date').clear() - form.find_element_by_id('id_end_date').send_keys('3015-04-23') + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") + self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-23'") - form.find_element_by_id('id_start_time').clear() + form.find_element_by_id('id_start_time').send_keys(Keys.DELETE) form.find_element_by_id('id_start_time').send_keys('06:59') - form.find_element_by_id('id_end_time').clear() + form.find_element_by_id('id_end_time').send_keys(Keys.DELETE) form.find_element_by_id('id_end_time').send_keys('06:00') # Attempt to save - should fail @@ -593,31 +621,28 @@ class EventTest(LiveServerTestCase): # Same date, end time before start time form = self.browser.find_element_by_tag_name('form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') - form.find_element_by_id('id_end_date').clear() - form.find_element_by_id('id_end_date').send_keys('3015-04-23') + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") + self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-24'") - form.find_element_by_id('id_start_time').clear() + form.find_element_by_id('id_start_time').send_keys(Keys.DELETE) form.find_element_by_id('id_start_time').send_keys('06:59') - form.find_element_by_id('id_end_time').clear() + form.find_element_by_id('id_end_time').send_keys(Keys.DELETE) form.find_element_by_id('id_end_time').send_keys('06:00') # No end date, end time before start time form = self.browser.find_element_by_tag_name('form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') + + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") + self.browser.execute_script("document.getElementById('id_end_date').value=''") - form.find_element_by_id('id_end_date').clear() - - form.find_element_by_id('id_start_time').clear() + form.find_element_by_id('id_start_time').send_keys(Keys.DELETE) form.find_element_by_id('id_start_time').send_keys('06:59') - form.find_element_by_id('id_end_time').clear() + form.find_element_by_id('id_end_time').send_keys(Keys.DELETE) form.find_element_by_id('id_end_time').send_keys('06:00') # Attempt to save - should fail @@ -630,15 +655,11 @@ class EventTest(LiveServerTestCase): # 2 dates, end after start form = self.browser.find_element_by_tag_name('form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") + self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'") - form.find_element_by_id('id_end_date').clear() - form.find_element_by_id('id_end_date').send_keys('3015-04-26') - - form.find_element_by_id('id_start_time').clear() - - form.find_element_by_id('id_end_time').clear() + self.browser.execute_script("document.getElementById('id_start_time').value=''") + self.browser.execute_script("document.getElementById('id_end_time').value=''") # Attempt to save - should succeed save.click() @@ -653,7 +674,7 @@ class EventTest(LiveServerTestCase): # Gets redirected to login and back self.authenticate('/event/create/') - wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations) + wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations) self.browser.implicitly_wait(3) #Set session-long wait (only works for non-existant DOM objects) wait.until(animation_is_finished()) @@ -672,8 +693,7 @@ class EventTest(LiveServerTestCase): e.send_keys('Test Event Name') # Set an arbitrary date - form.find_element_by_id('id_start_date').clear() - form.find_element_by_id('id_start_date').send_keys('3015-04-24') + self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") # Save the rig save.click() @@ -728,6 +748,7 @@ class EventTest(LiveServerTestCase): organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..') +@on_platforms(browsers) class IcalTest(LiveServerTestCase): def setUp(self): @@ -767,7 +788,7 @@ class IcalTest(LiveServerTestCase): models.Event.objects.create(name="TE E17", start_date=date.today()-timedelta(days=1), is_rig=False, description="non rig yesterday") models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, description="non rig today cancelled") - self.browser = webdriver.Firefox() + self.browser = create_browser(self.id(), self.desired_capabilities) self.browser.implicitly_wait(3) # Set implicit wait session wide os.environ['RECAPTCHA_TESTING'] = 'True' @@ -926,6 +947,5 @@ class animation_is_finished(object): numberAnimating = driver.execute_script('return $(":animated").length') finished = numberAnimating == 0 if finished: - import time time.sleep(0.1) return finished diff --git a/requirements.txt b/requirements.txt index 5e6106d3..3e7d8d17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ python-dateutil==2.6.0 pytz==2017.2 raven==6.0.0 reportlab==3.4.0 -selenium==2.53.1 +selenium==3.4.1 simplejson==3.10.0 six==1.10.0 sqlparse==0.2.3 From fbc039c27422428ed5889adda449f69d65076255 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 16 May 2017 13:58:05 +0100 Subject: [PATCH 3/5] Fix tests so they can actually run locally (I failed) --- RIGS/test_functional.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index 9452e41c..a804ddd5 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -26,15 +26,20 @@ browsers = [{"platform": "macOS 10.12", def on_platforms(platforms): - if os.environ.get("TRAVIS"): - def decorator(base_class): - module = sys.modules[base_class.__module__].__dict__ - for i, platform in enumerate(platforms): - d = dict(base_class.__dict__) - d['desired_capabilities'] = platform - name = "%s_%s" % (base_class.__name__, i + 1) - module[name] = type(name, (base_class,), d) - return decorator + if not os.environ.get("TRAVIS"): + platforms = {'local'} + + def decorator(base_class): + module = sys.modules[base_class.__module__].__dict__ + for i, platform in enumerate(platforms): + d = dict(base_class.__dict__) + d['desired_capabilities'] = platform + name = "%s_%s" % (base_class.__name__, i + 1) + module[name] = type(name, (base_class,), d) + + return decorator + + def create_browser(test_name, desired_capabilities): From cb23fd183e7ed8e23a336a280e66fc8c507b8c04 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 16 May 2017 14:50:18 +0100 Subject: [PATCH 4/5] Add failing test --- RIGS/test_functional.py | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index a804ddd5..7e9595cb 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import os import re -from datetime import date, timedelta +import pytz +from datetime import date, time, datetime, timedelta from django.core import mail from django.db import transaction @@ -16,7 +17,8 @@ from RIGS import models from reversion import revisions as reversion -import time +from django.conf import settings + import sys browsers = [{"platform": "macOS 10.12", @@ -753,6 +755,65 @@ class EventTest(LiveServerTestCase): organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..') + def testEventEdit(self): + person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save() + organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save() + venue = models.Venue(name="Event Detail Venue").save() + + eventData = { + 'name': "Detail Test", + 'description': "This is an event to test the detail view", + 'notes': "It is going to be awful", + 'person': person, + 'organisation': organisation, + 'venue': venue, + 'mic': self.profile, + 'start_date': date(2015, 06, 04), + 'end_date': date(2015, 06, 05), + 'start_time': time(10, 00), + 'end_time': time(15, 00), + 'meet_at': self.create_datetime(2015, 06, 04, 10, 00), + 'access_at': self.create_datetime(2015, 06, 04, 10, 00), + 'collector': 'A Person' + } + + event = models.Event(**eventData) + event.save() + + item1Data = { + 'event': event, + 'name': "Detail Item 1", + 'cost': "10.00", + 'quantity': "1", + 'order': 1 + } + + models.EventItem(**item1Data).save() + + self.browser.get(self.live_server_url + '/event/%d/edit/' % event.pk) + self.authenticate('/event/%d/edit/' % event.pk) + + save = self.browser.find_element_by_xpath('(//button[@type="submit"])[1]') + save.click() + + successTitle = self.browser.find_element_by_xpath('//h1').text + self.assertIn("N%05d | Detail Test" % event.pk, successTitle) + + reloadedEvent = models.Event.objects.get(name='Detail Test') + reloadedItem = models.EventItem.objects.get(name='Detail Item 1') + + # Check the event + for key, value in eventData.iteritems(): + self.assertEqual(str(getattr(reloadedEvent, key)), str(value)) + + # Check the item + for key, value in item1Data.iteritems(): + self.assertEqual(str(getattr(reloadedItem, key)), str(value)) + + def create_datetime(self, year, month, day, hour, min): + tz = pytz.timezone(settings.TIME_ZONE) + return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc) + @on_platforms(browsers) class IcalTest(LiveServerTestCase): @@ -952,5 +1013,6 @@ class animation_is_finished(object): numberAnimating = driver.execute_script('return $(":animated").length') finished = numberAnimating == 0 if finished: + import time time.sleep(0.1) return finished From 4b032944ac567ced90271562e623183b473b70f7 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 16 May 2017 14:50:33 +0100 Subject: [PATCH 5/5] Fix the time formatting --- RIGS/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/forms.py b/RIGS/forms.py index e0d57f70..85fd0394 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -12,8 +12,8 @@ from RIGS import models # Override the django form defaults to use the HTML date/time/datetime UI elements forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) -forms.TimeField.widget = forms.DateInput(attrs={'type': 'time'}) -forms.DateTimeField.widget = forms.DateInput(attrs={'type': 'datetime-local'}) +forms.TimeField.widget = forms.TextInput(attrs={'type': 'time'}) +forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}) # Registration class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):