def setUpClass(cls):
     super(PhotoReviewTests, cls).setUpClass()
     storage = FileSystemStorage()
     desired_storage_path = join('queued-images', 'pilot.jpg')
     with open(cls.example_image_filename, 'rb') as f:
         cls.storage_filename = storage.save(desired_storage_path, f)
     mkdir_p(TEST_MEDIA_ROOT)
示例#2
0
 def setUpClass(cls):
     super(PhotoReviewTests, cls).setUpClass()
     storage = FileSystemStorage()
     desired_storage_path = join('queued-images', 'pilot.jpg')
     with open(cls.example_image_filename, 'rb') as f:
         cls.storage_filename = storage.save(desired_storage_path, f)
     mkdir_p(TEST_MEDIA_ROOT)
 def handle(self, *args, **options):
     self.client = Client()
     self.directory_url = options['DIRECTORY-URL']
     if not self.directory_url.endswith('/'):
         self.directory_url = self.directory_url + '/'
     self.hostname, self.secure = hostname_and_secure(self.directory_url)
     mkdir_p(options['OUTPUT-DIRECTORY'])
     self.timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
     json_directory = join(options['OUTPUT-DIRECTORY'], self.timestamp)
     mkdir_p(json_directory)
     page_size = options['page_size']
     if not page_size:
         page_size = 200
     for endpoint in self.endpoints:
         self.get_api_results_to_directory(endpoint, json_directory,
                                           page_size)
     update_latest_symlink(options['OUTPUT-DIRECTORY'], self.timestamp)
     if options['prune']:
         prune(options['OUTPUT-DIRECTORY'])
def get_settings(conf_file_leafname, election_app=None, tests=False):
    conf = get_conf(conf_file_leafname)

    debug = bool(int(conf.get('STAGING')))

    # Get the requested ELECTION_APP:
    if election_app is None:
        election_app = conf['ELECTION_APP']
    election_app_fully_qualified = 'elections.' + election_app
    election_settings_module = election_app_fully_qualified + '.settings'
    elections_module = importlib.import_module(election_settings_module)

    language_code = conf.get('LANGUAGE_CODE', 'en-gb')

    # Internationalization
    # https://docs.djangoproject.com/en/1.6/topics/i18n/
    locale_paths = [
        join(BASE_DIR, 'locale')
    ]
    # The code below sets LANGUAGES to only those we have translations
    # for, so at the time of writing that will be:
    #   [('en', 'English'), ('es-ar', 'Argentinian Spanish')]
    # whereas the default setting is a long list of languages which
    # includes:
    #   ('es', 'Spanish').
    # If someone's browser sends 'Accept-Language: es', that means that it
    # will be found in this list, but since there are no translations for 'es'
    # it'll fall back to LANGUAGE_CODE.  However, if there is no 'es' in
    # LANGUAGES, then Django will attempt to do a best match, so if
    # Accept-Language is 'es' then it will use the 'es-ar' translation.  We think
    # this is generally desirable (e.g. so someone can see YourNextMP in Spanish
    # if their browser asks for Spanish).
    languages = [
        l for l in LANGUAGES
        if exists(join(locale_paths[0], to_locale(l[0])))
    ]
    languages.append(('cy-gb', 'Welsh'))
    languages.append(('es-cr', 'Costa Rican Spanish'))

    # The language selection has been slightly complicated now that we
    # have two es- languages: es-ar and es-cr.  Chrome doesn't offer
    # Costa Rican Spanish as one of its language choices, so the best
    # you can do is choose 'Spanish - español'. (This might well be
    # the case in other browsers too.)  Since 'es-ar' comes first in
    # 'languages' after the preceding code, this means that someone
    # viewing the Costa Rica site with Chrome's preferred language set
    # to Spanish (i.e. with 'es' first in Accept-Language) will get
    # the Argentinian Spanish translations instead of Costa Rican
    # Spanish.  To get around this, look for the default language code
    # for the site, and if that's present, move it to the front of
    # 'languages'.  This should be generally helpful behaviour: the
    # default language code of the site should take precedence over
    # another language that happens to match based on the generic part
    # of the language code.
    language_code_index = next(
        (i for i, l in enumerate(languages) if l[0] == language_code),
        None
    )
    if language_code_index is not None:
        languages.insert(0, languages.pop(language_code_index))

    # Make sure the MEDIA_ROOT directory actually exists:
    media_root = conf.get('MEDIA_ROOT') or join(BASE_DIR, 'media')
    # Make sure that the MEDIA_ROOT and subdirectory for archived CSV
    # files exist:
    mkdir_p(join(media_root, 'csv-archives'))

    # Database
    # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
    if conf.get('DATABASE_SYSTEM') == 'postgresql':
        databases = {
            'default': {
                'ENGINE':   'django.db.backends.postgresql_psycopg2',
                'NAME':     conf.get('YNMP_DB_NAME'),
                'USER':     conf.get('YNMP_DB_USER'),
                'PASSWORD': conf.get('YNMP_DB_PASS'),
                'HOST':     conf.get('YNMP_DB_HOST'),
                'PORT':     conf.get('YNMP_DB_PORT'),
            }
        }
    else:
        databases = {
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': join(BASE_DIR, 'db.sqlite3'),
            }
        }

    # Setup caches depending on DEBUG:
    if debug:
        cache = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
        cache_thumbnails = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
    else:
        cache = {
            'TIMEOUT': None, # cache keys never expire; we invalidate them
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'KEY_PREFIX': databases['default']['NAME'],
        }
        cache_thumbnails = {
            'TIMEOUT': 60 * 60 * 24 * 2, # expire after two days
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'KEY_PREFIX': databases['default']['NAME'] + "-thumbnails",
        }

    # Create a dictionary with these settings and other simpler ones:
    result = {
        'BASE_DIR': BASE_DIR,
        'ALLOWED_HOSTS': conf.get('ALLOWED_HOSTS'),
        'DEBUG': debug,
        'RUNNING_TESTS': tests,

        # Google analytics settings:
        'GOOGLE_ANALYTICS_ACCOUNT': conf.get('GOOGLE_ANALYTICS_ACCOUNT'),
        'USE_UNIVERSAL_ANALYTICS': conf.get('USE_UNIVERSAL_ANALYTICS', True),

        # The Twitter account referenced in the Twitter card data:
        'TWITTER_USERNAME': conf.get('TWITTER_USERNAME', ''),

        # The email address which is made public on the site for sending
        # support email to:
        'SUPPORT_EMAIL': conf['SUPPORT_EMAIL'],

        # Email addresses that error emails are sent to when DEBUG = False
        'ADMINS': conf['ADMINS'],

        # The From: address for all emails except error emails
        'DEFAULT_FROM_EMAIL': conf['DEFAULT_FROM_EMAIL'],

        # The From: address for error emails
        'SERVER_EMAIL': conf['SERVER_EMAIL'],

        # SECURITY WARNING: keep the secret key used in production secret!
        'SECRET_KEY': conf['SECRET_KEY'],

        'TEMPLATE_DEBUG': True,
        'TEMPLATE_DIRS': (
            join(BASE_DIR, 'mysite', 'templates'),
        ),
        'TEMPLATE_CONTEXT_PROCESSORS': TEMPLATE_CONTEXT_PROCESSORS + (
            # Required by allauth template tags
            "django.core.context_processors.request",
            # allauth specific context processors
            "allauth.account.context_processors.account",
            "allauth.socialaccount.context_processors.socialaccount",
            "django.contrib.messages.context_processors.messages",
            "mysite.context_processors.add_settings",
            "mysite.context_processors.election_date",
            "mysite.context_processors.add_group_permissions",
            "mysite.context_processors.add_notification_data",
            "mysite.context_processors.locale",
            "mysite.context_processors.add_site",
        ),

        'ELECTION_APP': election_app,
        'ELECTION_APP_FULLY_QUALIFIED': election_app_fully_qualified,

        # The Django applications in use:
        'INSTALLED_APPS': (
            'django.contrib.admin',
            'django.contrib.auth',
            'django.contrib.contenttypes',
            'django.contrib.humanize',
            'django.contrib.sessions',
            'django.contrib.messages',
            'django.contrib.staticfiles',
            'django.contrib.sites',
            'django_nose',
            'django_extensions',
            'pipeline',
            'statici18n',
            'sorl.thumbnail',
            'rest_framework',
            'rest_framework.authtoken',
            'images',
            'haystack',
            'elections',
            'popolo',
            election_app_fully_qualified,
            'candidates',
            'tasks',
            'alerts',
            'cached_counts',
            'moderation_queue',
            'auth_helpers',
            'debug_toolbar',
            'template_timings_panel',
            'official_documents',
            'results',
            'notifications',
            'allauth',
            'allauth.account',
            'allauth.socialaccount',
            'allauth.socialaccount.providers.google',
            'allauth.socialaccount.providers.facebook',
            'allauth.socialaccount.providers.twitter',
            'corsheaders',
            'crispy_forms',
        ),

        'SITE_ID': 1,

        'MIDDLEWARE_CLASSES': (
            'debug_toolbar.middleware.DebugToolbarMiddleware',
            'corsheaders.middleware.CorsMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            'django.middleware.locale.LocaleMiddleware',
            'django.middleware.common.CommonMiddleware',
            'django.middleware.csrf.CsrfViewMiddleware',
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'candidates.middleware.CopyrightAssignmentMiddleware',
            'candidates.middleware.DisallowedUpdateMiddleware',
            'django.contrib.messages.middleware.MessageMiddleware',
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
        ),

        # django-allauth settings:
        'AUTHENTICATION_BACKENDS': (
            # Needed to login by username in Django admin, regardless of `allauth`
            "django.contrib.auth.backends.ModelBackend",
            # `allauth` specific authentication methods, such as login by e-mail
            "allauth.account.auth_backends.AuthenticationBackend",
        ),
        'SOCIALACCOUNT_PROVIDERS': {
            'google': {'SCOPE': ['https://www.googleapis.com/auth/userinfo.profile'],
                       'AUTH_PARAMS': {'access_type': 'online'}},
            'facebook': {'SCOPE': ['email',]},
        },
        'LOGIN_REDIRECT_URL': '/',
        'ACCOUNT_EMAIL_VERIFICATION': 'mandatory',
        'ACCOUNT_EMAIL_REQUIRED': True,
        'ACCOUNT_USERNAME_REQUIRED': True,
        'SOCIALACCOUNT_AUTO_SIGNUP': True,

        'ROOT_URLCONF': 'mysite.urls',
        'WSGI_APPLICATION': 'mysite.wsgi.application',

        # Django Debug Toolbar settings:
        'DEBUG_TOOLBAR_PATCH_SETTINGS': False,
        'DEBUG_TOOLBAR_PANELS': [
            'debug_toolbar.panels.versions.VersionsPanel',
            'debug_toolbar.panels.timer.TimerPanel',
            'debug_toolbar.panels.settings.SettingsPanel',
            'debug_toolbar.panels.headers.HeadersPanel',
            'debug_toolbar.panels.request.RequestPanel',
            'debug_toolbar.panels.sql.SQLPanel',
            'debug_toolbar.panels.staticfiles.StaticFilesPanel',
            'debug_toolbar.panels.templates.TemplatesPanel',
            'debug_toolbar.panels.cache.CachePanel',
            'debug_toolbar.panels.signals.SignalsPanel',
            'debug_toolbar.panels.logging.LoggingPanel',
            'debug_toolbar.panels.redirects.RedirectsPanel',
            'template_timings_panel.panels.TemplateTimings.TemplateTimings',
        ],
        'INTERNAL_IPS': ['127.0.0.1'],

        # Language settings (calculated above):
        'LOCALE_PATHS': locale_paths,
        'LANGUAGES': languages,
        'LANGUAGE_CODE': language_code,
        'TIME_ZONE': conf.get('TIME_ZONE', 'Europe/London'),
        'USE_I18N': True,
        'USE_L10N': True,
        'USE_TZ': True,
        'DD_MM_DATE_FORMAT_PREFERRED': conf.get('DD_MM_DATE_FORMAT_PREFERRED', True),

        # The media and static file settings:
        'MEDIA_ROOT': media_root,
        'MEDIA_URL': '/media/',

        # Settings for staticfiles and Django pipeline:
        'STATIC_URL': '/static/',
        'STATIC_ROOT': join(BASE_DIR, 'static'),
        'STATICI18N_ROOT': join(BASE_DIR, 'mysite', 'static'),
        'STATICFILES_DIRS': (
            join(BASE_DIR, 'mysite', 'static'),
        ),
        'STATICFILES_FINDERS': (
            'django.contrib.staticfiles.finders.FileSystemFinder',
            'django.contrib.staticfiles.finders.AppDirectoriesFinder',
            'pipeline.finders.PipelineFinder',
        ),
        'PIPELINE_CSS': {
            'image-review': {
                'source_filenames': (
                    'moderation_queue/css/jquery.Jcrop.css',
                    'moderation_queue/css/crop.scss',
                ),
                'output_filename': 'css/image-review.css',
            },
            'official_documents': {
                'source_filenames': (
                    'official_documents/css/official_documents.scss',
                ),
                'output_filename': 'css/official_documents.css',
            },
            'all': {
                'source_filenames': (
                    'candidates/style.scss',
                    'cached_counts/style.scss',
                    'select2/select2.css',
                    'jquery/jquery-ui.css',
                    'jquery/jquery-ui.structure.css',
                    'jquery/jquery-ui.theme.css',
                    'moderation_queue/css/photo-upload.scss',
                ),
                'output_filename': 'css/all.css',
            }
        },
        'PIPELINE_JS': {
            'image-review': {
                'source_filenames': (
                    'moderation_queue/js/jquery.color.js',
                    'moderation_queue/js/jquery.Jcrop.js',
                    'moderation_queue/js/crop.js',
                ),
                'output_filename': 'js/image-review.js',
            },
            'all': {
                'source_filenames': (
                    'jquery/jquery-1.11.1.js',
                    'jquery/jquery-ui.js',
                    'foundation/js/foundation/foundation.js',
                    'foundation/js/foundation/foundation.equalizer.js',
                    'foundation/js/foundation/foundation.dropdown.js',
                    'foundation/js/foundation/foundation.tooltip.js',
                    'foundation/js/foundation/foundation.offcanvas.js',
                    'foundation/js/foundation/foundation.accordion.js',
                    'foundation/js/foundation/foundation.joyride.js',
                    'foundation/js/foundation/foundation.alert.js',
                    'foundation/js/foundation/foundation.topbar.js',
                    'foundation/js/foundation/foundation.reveal.js',
                    'foundation/js/foundation/foundation.slider.js',
                    'foundation/js/foundation/foundation.magellan.js',
                    'foundation/js/foundation/foundation.clearing.js',
                    'foundation/js/foundation/foundation.orbit.js',
                    'foundation/js/foundation/foundation.interchange.js',
                    'foundation/js/foundation/foundation.abide.js',
                    'foundation/js/foundation/foundation.tab.js',
                    'select2/select2.js',
                    'js/constituency.js',
                    'js/person_form.js',
                    'js/home_geolocation_form.js',
                    'js/versions.js',
                ),
                'output_filename': 'js/all.js'
            }
        },
        'PIPELINE_COMPILERS': (
            'pipeline.compilers.sass.SASSCompiler',
        ),
        'PIPELINE_SASS_ARGUMENTS': '--trace --quiet',
        'PIPELINE_CSS_COMPRESSOR': 'pipeline.compressors.yui.YUICompressor',
        'PIPELINE_JS_COMPRESSOR': 'pipeline.compressors.yui.YUICompressor',
        # On some platforms this might be called "yuicompressor", so it may be
        # necessary to symlink it into your PATH as "yui-compressor".
        'PIPELINE_YUI_BINARY': '/usr/bin/env yui-compressor',

        'TEST_RUNNER': 'django_nose.NoseTestSuiteRunner',

        'SOURCE_HINTS': _(
            "Please don't quote third-party candidate sites \u2014 "
            "we prefer URLs of news stories or official candidate pages."
        ),

        # By default, cache successful results from MapIt for a day
        'MAPIT_CACHE_SECONDS': 86400,
        'DATABASES': databases,
        'CACHES': {
            'default': cache,
            'thumbnails': cache_thumbnails,
        },

        # sorl-thumbnail settings:
        'THUMBNAIL_CACHE': 'thumbnails',
        'THUMBNAIL_DEBUG': debug,

        # Settings for restricting user activity to reduce abuse:
        'RESTRICT_RENAMES': conf.get('RESTRICT_RENAMES'),
        'EDITS_ALLOWED': conf.get('EDITS_ALLOWED', True),

        # Django Rest Framework settings:
        'REST_FRAMEWORK': {
            'DEFAULT_PERMISSION_CLASSES': ('candidates.api_permissions.ReadOnly',),
            'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
            'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
            'PAGE_SIZE': 10,
        },

        # allow attaching extra data to notifications:
        'NOTIFICATIONS_USE_JSONFIELD': True,

        'HAYSTACK_SIGNAL_PROCESSOR': 'haystack.signals.RealtimeSignalProcessor',

        'HAYSTACK_CONNECTIONS': {
            'default': {
                'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
                'URL': 'http://127.0.0.1:9200/',
                'INDEX_NAME': '{0}_{1}'.format(conf.get('YNMP_DB_NAME'), conf.get('YNMP_DB_HOST')),
            },
        },

        # CORS config
        'CORS_ORIGIN_ALLOW_ALL': True,
        'CORS_URLS_REGEX': r'^/api/.*$',
        'CORS_ALLOW_METHODS': (
            'GET',
            'OPTIONS',
        ),
    }
    if not conf.get('NEW_ACCOUNTS_ALLOWED', True):
        result['ACCOUNT_ADAPTER'] = \
            'mysite.account_adapter.NoNewUsersAccountAdapter'
    if tests:
        result['NOSE_ARGS'] = [
            '--nocapture',
            '--with-yanc',
            # There are problems with OpenCV on Travis, so don't even try to
            # import moderation_queue/faces.py
            '--ignore-files=faces',
        ]
        if election_app == 'example':
            result['NOSE_ARGS'].append('--with-doctest')
    else:
        # If we're not testing, use PipelineCachedStorage
        result['STATICFILES_STORAGE'] = \
            'pipeline.storage.PipelineCachedStorage'
    if conf.get('NGINX_SSL'):
        result['SECURE_PROXY_SSL_HEADER'] = ('HTTP_X_FORWARDED_PROTO', 'https')
        result['ACCOUNT_DEFAULT_HTTP_PROTOCOL'] = 'https'
    for required_election_app_setting in (
            'SITE_OWNER',
            'COPYRIGHT_HOLDER',
    ):
        result[required_election_app_setting] = \
            getattr(elections_module, required_election_app_setting)
    for optional_election_app_setting, default in (
            ('SITE_OWNER_URL', ''),
            ('AREAS_TO_ALWAYS_RETURN', []),
            ('IMAGE_PROXY_URL', ''),
    ):
        try:
            result[optional_election_app_setting] = \
                getattr(elections_module, optional_election_app_setting)
        except AttributeError:
            result[optional_election_app_setting] = default
    # Make sure there's a trailing slash at the end of base MapIt URL:
    result['MAPIT_BASE_URL'] = \
        re.sub(r'/*$', '/', elections_module.MAPIT_BASE_URL)

    result['INSTALLED_APPS'] = list(result['INSTALLED_APPS'])
    result['INSTALLED_APPS'].extend(
        getattr(elections_module, 'INSTALLED_APPS', [])
    )

    return result
示例#5
0
def get_settings(conf_file_leafname, election_app=None, tests=False):
    conf = get_conf(conf_file_leafname)

    debug = bool(int(conf.get('STAGING')))

    # Get the requested ELECTION_APP:
    if election_app is None:
        election_app = conf['ELECTION_APP']
    election_app_fully_qualified = 'elections.' + election_app

    language_code = conf.get('LANGUAGE_CODE', 'en-gb')

    # Internationalization
    # https://docs.djangoproject.com/en/1.6/topics/i18n/
    locale_paths = [
        join(BASE_DIR, 'locale')
    ]
    # The code below sets LANGUAGES to only those we have translations
    # for, so at the time of writing that will be:
    #   [('en', 'English'), ('es-ar', 'Argentinian Spanish')]
    # whereas the default setting is a long list of languages which
    # includes:
    #   ('es', 'Spanish').
    # If someone's browser sends 'Accept-Language: es', that means that it
    # will be found in this list, but since there are no translations for 'es'
    # it'll fall back to LANGUAGE_CODE.  However, if there is no 'es' in
    # LANGUAGES, then Django will attempt to do a best match, so if
    # Accept-Language is 'es' then it will use the 'es-ar' translation.  We think
    # this is generally desirable (e.g. so someone can see YourNextMP in Spanish
    # if their browser asks for Spanish).
    languages = [
        l for l in LANGUAGES
        if exists(join(locale_paths[0], to_locale(l[0])))
    ]
    languages.append(('cy-gb', 'Welsh'))
    languages.append(('es-cr', 'Costa Rican Spanish'))

    # The language selection has been slightly complicated now that we
    # have two es- languages: es-ar and es-cr.  Chrome doesn't offer
    # Costa Rican Spanish as one of its language choices, so the best
    # you can do is choose 'Spanish - español'. (This might well be
    # the case in other browsers too.)  Since 'es-ar' comes first in
    # 'languages' after the preceding code, this means that someone
    # viewing the Costa Rica site with Chrome's preferred language set
    # to Spanish (i.e. with 'es' first in Accept-Language) will get
    # the Argentinian Spanish translations instead of Costa Rican
    # Spanish.  To get around this, look for the default language code
    # for the site, and if that's present, move it to the front of
    # 'languages'.  This should be generally helpful behaviour: the
    # default language code of the site should take precedence over
    # another language that happens to match based on the generic part
    # of the language code.
    language_code_index = next(
        (i for i, l in enumerate(languages) if l[0] == language_code),
        None
    )
    if language_code_index is not None:
        languages.insert(0, languages.pop(language_code_index))

    # Make sure the MEDIA_ROOT directory actually exists:
    media_root = conf.get('MEDIA_ROOT') or join(BASE_DIR, 'media')
    # Make sure that the MEDIA_ROOT and subdirectory for archived CSV
    # files exist:
    mkdir_p(join(media_root, 'csv-archives'))

    # Database
    # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
    if conf.get('DATABASE_SYSTEM') == 'postgresql':
        databases = {
            'default': {
                'ENGINE':   'django.db.backends.postgresql_psycopg2',
                'NAME':     conf.get('YNMP_DB_NAME'),
                'USER':     conf.get('YNMP_DB_USER'),
                'PASSWORD': conf.get('YNMP_DB_PASS'),
                'HOST':     conf.get('YNMP_DB_HOST'),
                'PORT':     conf.get('YNMP_DB_PORT'),
            }
        }
    else:
        databases = {
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': join(BASE_DIR, 'db.sqlite3'),
            }
        }

    # Setup caches depending on DEBUG:
    if debug:
        cache = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
        cache_thumbnails = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
    else:
        cache = {
            'TIMEOUT': None, # cache keys never expire; we invalidate them
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'KEY_PREFIX': databases['default']['NAME'],
        }
        cache_thumbnails = {
            'TIMEOUT': 60 * 60 * 24 * 2, # expire after two days
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'KEY_PREFIX': databases['default']['NAME'] + "-thumbnails",
        }

    # Create a dictionary with these settings and other simpler ones:
    result = {
        'BASE_DIR': BASE_DIR,
        'ALLOWED_HOSTS': conf.get('ALLOWED_HOSTS'),
        'DEBUG': debug,
        'RUNNING_TESTS': tests,

        # Google analytics settings:
        'GOOGLE_ANALYTICS_ACCOUNT': conf.get('GOOGLE_ANALYTICS_ACCOUNT'),
        'USE_UNIVERSAL_ANALYTICS': conf.get('USE_UNIVERSAL_ANALYTICS', True),

        # The Twitter account referenced in the Twitter card data:
        'TWITTER_USERNAME': conf.get('TWITTER_USERNAME', ''),

        # The email address which is made public on the site for sending
        # support email to:
        'SUPPORT_EMAIL': conf['SUPPORT_EMAIL'],

        # Email addresses that error emails are sent to when DEBUG = False
        'ADMINS': conf['ADMINS'],

        # The From: address for all emails except error emails
        'DEFAULT_FROM_EMAIL': conf['DEFAULT_FROM_EMAIL'],

        # The From: address for error emails
        'SERVER_EMAIL': conf['SERVER_EMAIL'],

        # SECURITY WARNING: keep the secret key used in production secret!
        'SECRET_KEY': conf['SECRET_KEY'],

        'TEMPLATE_DEBUG': True,
        'TEMPLATE_DIRS': (
            join(BASE_DIR, 'mysite', 'templates'),
        ),
        'TEMPLATE_CONTEXT_PROCESSORS': TEMPLATE_CONTEXT_PROCESSORS + (
            # Required by allauth template tags
            "django.core.context_processors.request",
            # allauth specific context processors
            "allauth.account.context_processors.account",
            "allauth.socialaccount.context_processors.socialaccount",
            "django.contrib.messages.context_processors.messages",
            "mysite.context_processors.add_settings",
            "mysite.context_processors.election_date",
            "mysite.context_processors.add_group_permissions",
            "mysite.context_processors.add_notification_data",
            "mysite.context_processors.locale",
            "mysite.context_processors.add_site",
        ),

        'ELECTION_APP': election_app,
        'ELECTION_APP_FULLY_QUALIFIED': election_app_fully_qualified,

        # The Django applications in use:
        'INSTALLED_APPS': [
            'django.contrib.admin',
            'django.contrib.auth',
            'django.contrib.contenttypes',
            'django.contrib.humanize',
            'django.contrib.sessions',
            'django.contrib.messages',
            'django.contrib.staticfiles',
            'django.contrib.sites',
            'django_nose',
            'django_extensions',
            'pipeline',
            'statici18n',
            'sorl.thumbnail',
            'rest_framework',
            'rest_framework.authtoken',
            'images',
            'haystack',
            'elections',
            'popolo',
            election_app_fully_qualified,
            'candidates',
            'tasks',
            'alerts',
            'cached_counts',
            'moderation_queue',
            'auth_helpers',
            'debug_toolbar',
            'template_timings_panel',
            'official_documents',
            'results',
            'notifications',
            'allauth',
            'allauth.account',
            'allauth.socialaccount',
            'allauth.socialaccount.providers.google',
            'allauth.socialaccount.providers.facebook',
            'allauth.socialaccount.providers.twitter',
            'corsheaders',
            'crispy_forms',
        ],

        'SITE_ID': 1,

        'MIDDLEWARE_CLASSES': (
            'debug_toolbar.middleware.DebugToolbarMiddleware',
            'corsheaders.middleware.CorsMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            'django.middleware.locale.LocaleMiddleware',
            'django.middleware.common.CommonMiddleware',
            'django.middleware.csrf.CsrfViewMiddleware',
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'candidates.middleware.CopyrightAssignmentMiddleware',
            'candidates.middleware.DisallowedUpdateMiddleware',
            'django.contrib.messages.middleware.MessageMiddleware',
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
        ),

        # django-allauth settings:
        'AUTHENTICATION_BACKENDS': (
            # Needed to login by username in Django admin, regardless of `allauth`
            "django.contrib.auth.backends.ModelBackend",
            # `allauth` specific authentication methods, such as login by e-mail
            "allauth.account.auth_backends.AuthenticationBackend",
        ),
        'SOCIALACCOUNT_PROVIDERS': {
            'google': {'SCOPE': ['https://www.googleapis.com/auth/userinfo.profile'],
                       'AUTH_PARAMS': {'access_type': 'online'}},
            'facebook': {'SCOPE': ['email',]},
        },
        'LOGIN_REDIRECT_URL': '/',
        'ACCOUNT_EMAIL_VERIFICATION': 'mandatory',
        'ACCOUNT_EMAIL_REQUIRED': True,
        'ACCOUNT_USERNAME_REQUIRED': True,
        'SOCIALACCOUNT_AUTO_SIGNUP': True,

        'ROOT_URLCONF': 'mysite.urls',
        'WSGI_APPLICATION': 'mysite.wsgi.application',

        # Django Debug Toolbar settings:
        'DEBUG_TOOLBAR_PATCH_SETTINGS': False,
        'DEBUG_TOOLBAR_PANELS': [
            'debug_toolbar.panels.versions.VersionsPanel',
            'debug_toolbar.panels.timer.TimerPanel',
            'debug_toolbar.panels.settings.SettingsPanel',
            'debug_toolbar.panels.headers.HeadersPanel',
            'debug_toolbar.panels.request.RequestPanel',
            'debug_toolbar.panels.sql.SQLPanel',
            'debug_toolbar.panels.staticfiles.StaticFilesPanel',
            'debug_toolbar.panels.templates.TemplatesPanel',
            'debug_toolbar.panels.cache.CachePanel',
            'debug_toolbar.panels.signals.SignalsPanel',
            'debug_toolbar.panels.logging.LoggingPanel',
            'debug_toolbar.panels.redirects.RedirectsPanel',
            'template_timings_panel.panels.TemplateTimings.TemplateTimings',
        ],
        'INTERNAL_IPS': ['127.0.0.1'],

        # Language settings (calculated above):
        'LOCALE_PATHS': locale_paths,
        'LANGUAGES': languages,
        'LANGUAGE_CODE': language_code,
        'TIME_ZONE': conf.get('TIME_ZONE', 'Europe/London'),
        'USE_I18N': True,
        'USE_L10N': True,
        'USE_TZ': True,
        'DD_MM_DATE_FORMAT_PREFERRED': conf.get('DD_MM_DATE_FORMAT_PREFERRED', True),

        # The media and static file settings:
        'MEDIA_ROOT': media_root,
        'MEDIA_URL': '/media/',

        # Settings for staticfiles and Django pipeline:
        'STATIC_URL': '/static/',
        'STATIC_ROOT': join(BASE_DIR, 'static'),
        'STATICI18N_ROOT': join(BASE_DIR, 'mysite', 'static'),
        'STATICFILES_DIRS': (
            join(BASE_DIR, 'mysite', 'static'),
        ),
        'STATICFILES_FINDERS': (
            'django.contrib.staticfiles.finders.FileSystemFinder',
            'django.contrib.staticfiles.finders.AppDirectoriesFinder',
            'pipeline.finders.PipelineFinder',
        ),
        'PIPELINE': {
            'STYLESHEETS': {
                'image-review': {
                    'source_filenames': (
                        'moderation_queue/css/jquery.Jcrop.css',
                        'moderation_queue/css/crop.scss',
                    ),
                    'output_filename': 'css/image-review.css',
                },
                'official_documents': {
                    'source_filenames': (
                        'official_documents/css/official_documents.scss',
                    ),
                    'output_filename': 'css/official_documents.css',
                },
                'all': {
                    'source_filenames': (
                        'candidates/style.scss',
                        'cached_counts/style.scss',
                        'select2/select2.css',
                        'jquery/jquery-ui.css',
                        'jquery/jquery-ui.structure.css',
                        'jquery/jquery-ui.theme.css',
                        'moderation_queue/css/photo-upload.scss',
                    ),
                    'output_filename': 'css/all.css',
                }
            },
            'JAVASCRIPT': {
                'image-review': {
                    'source_filenames': (
                        'moderation_queue/js/jquery.color.js',
                        'moderation_queue/js/jquery.Jcrop.js',
                        'moderation_queue/js/crop.js',
                    ),
                    'output_filename': 'js/image-review.js',
                },
                'all': {
                    'source_filenames': (
                        'jquery/jquery-1.11.1.js',
                        'jquery/jquery-ui.js',
                        'foundation/js/foundation/foundation.js',
                        'foundation/js/foundation/foundation.equalizer.js',
                        'foundation/js/foundation/foundation.dropdown.js',
                        'foundation/js/foundation/foundation.tooltip.js',
                        'foundation/js/foundation/foundation.offcanvas.js',
                        'foundation/js/foundation/foundation.accordion.js',
                        'foundation/js/foundation/foundation.joyride.js',
                        'foundation/js/foundation/foundation.alert.js',
                        'foundation/js/foundation/foundation.topbar.js',
                        'foundation/js/foundation/foundation.reveal.js',
                        'foundation/js/foundation/foundation.slider.js',
                        'foundation/js/foundation/foundation.magellan.js',
                        'foundation/js/foundation/foundation.clearing.js',
                        'foundation/js/foundation/foundation.orbit.js',
                        'foundation/js/foundation/foundation.interchange.js',
                        'foundation/js/foundation/foundation.abide.js',
                        'foundation/js/foundation/foundation.tab.js',
                        'select2/select2.js',
                        'js/constituency.js',
                        'js/person_form.js',
                        'js/home_geolocation_form.js',
                        'js/versions.js',
                    ),
                    'output_filename': 'js/all.js'
                }
            },

            'COMPILERS': (
                'pipeline.compilers.sass.SASSCompiler',
            ),
            'SASS_BINARY': 'sassc',
            'CSS_COMPRESSOR': 'pipeline.compressors.yui.YUICompressor',
            'JS_COMPRESSOR': 'pipeline.compressors.yui.YUICompressor',
            # On some platforms this might be called "yuicompressor", so it may be
            # necessary to symlink it into your PATH as "yui-compressor".
            'YUI_BINARY': '/usr/bin/env yui-compressor',
        },


        'TEST_RUNNER': 'django_nose.NoseTestSuiteRunner',

        'SOURCE_HINTS': _(
            u"Please don't quote third-party candidate sites \u2014 "
            u"we prefer URLs of news stories or official candidate pages."
        ),

        # By default, cache successful results from MapIt for a day
        'MAPIT_CACHE_SECONDS': 86400,
        'DATABASES': databases,
        'CACHES': {
            'default': cache,
            'thumbnails': cache_thumbnails,
        },

        # sorl-thumbnail settings:
        'THUMBNAIL_CACHE': 'thumbnails',
        'THUMBNAIL_DEBUG': debug,

        # Settings for restricting user activity to reduce abuse:
        'RESTRICT_RENAMES': conf.get('RESTRICT_RENAMES'),
        'EDITS_ALLOWED': conf.get('EDITS_ALLOWED', True),

        # A bearer token for the Twitter API for mapping between
        # Twitter usernames and IDs.
        'TWITTER_APP_ONLY_BEARER_TOKEN': conf.get('TWITTER_APP_ONLY_BEARER_TOKEN'),

        # Django Rest Framework settings:
        'REST_FRAMEWORK': {
            'DEFAULT_PERMISSION_CLASSES': ('candidates.api_permissions.ReadOnly',),
            'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
            'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
            'DEFAULT_RENDERER_CLASSES': (
                'rest_framework.renderers.JSONRenderer',
                'rest_framework_jsonp.renderers.JSONPRenderer',
                'rest_framework.renderers.BrowsableAPIRenderer',
            ),
            'PAGE_SIZE': 10,
        },

        # allow attaching extra data to notifications:
        'NOTIFICATIONS_USE_JSONFIELD': True,

        'HAYSTACK_SIGNAL_PROCESSOR': 'haystack.signals.RealtimeSignalProcessor',

        'HAYSTACK_CONNECTIONS': {
            'default': {
                'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
                'URL': 'http://127.0.0.1:9200/',
                'INDEX_NAME': '{0}_{1}'.format(conf.get('YNMP_DB_NAME'), conf.get('YNMP_DB_HOST')),
            },
        },

        # CORS config
        'CORS_ORIGIN_ALLOW_ALL': True,
        'CORS_URLS_REGEX': r'^/(api|upcoming-elections)/.*$',
        'CORS_ALLOW_METHODS': (
            'GET',
            'OPTIONS',
        ),
    }
    if not conf.get('NEW_ACCOUNTS_ALLOWED', True):
        result['ACCOUNT_ADAPTER'] = \
            'mysite.account_adapter.NoNewUsersAccountAdapter'
    result['CANDIDATES_REQUIRED_FOR_WEIGHTED_PARTY_LIST'] = \
        conf.get('CANDIDATES_REQUIRED_FOR_WEIGHTED_PARTY_LIST', 20)
    result['HOIST_ELECTED_CANDIDATES'] = \
        conf.get('HOIST_ELECTED_CANDIDATES', True)
    if tests:
        result['NOSE_ARGS'] = [
            '--nocapture',
            '--with-yanc',
            # There are problems with OpenCV on Travis, so don't even try to
            # import moderation_queue/faces.py
            '--ignore-files=faces',
        ]
        if election_app == 'example':
            result['NOSE_ARGS'].append('--with-doctest')
    else:
        # If we're not testing, use PipelineCachedStorage
        result['STATICFILES_STORAGE'] = \
            'pipeline.storage.PipelineCachedStorage'
    if conf.get('NGINX_SSL'):
        result['SECURE_PROXY_SSL_HEADER'] = ('HTTP_X_FORWARDED_PROTO', 'https')
        result['ACCOUNT_DEFAULT_HTTP_PROTOCOL'] = 'https'

    add_election_specific_settings(result, election_app_fully_qualified)

    result['RESULTS_FEATURE_ACTIVE'] = True

    result['DATE_FORMAT'] = conf.get('DATE_FORMAT', "jS E Y")

    return result
 def setUp(self):
     super(TestMergePeopleView, self).setUp()
     example_image_filename = join(settings.BASE_DIR, 'moderation_queue',
                                   'tests', 'example-image.jpg')
     mkdir_p(TEST_MEDIA_ROOT)
     # Create Tessa Jowell (the primary person)
     person_extra = factories.PersonExtraFactory.create(
         base__id=2009,
         base__name='Tessa Jowell',
         base__gender='female',
         base__honorific_suffix='DBE',
         base__email='*****@*****.**',
         versions='''
             [
               {
                 "username": "******",
                 "information_source": "Just adding example data",
                 "ip": "127.0.0.1",
                 "version_id": "35ec2d5821176ccc",
                 "timestamp": "2014-10-28T14:32:36.835429",
                 "data": {
                   "name": "Tessa Jowell",
                   "id": "2009",
                   "honorific_suffix": "DBE",
                   "twitter_username": "",
                   "standing_in": {
                     "2010": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     },
                     "2015": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     }
                   },
                   "gender": "female",
                   "homepage_url": "",
                   "birth_date": null,
                   "wikipedia_url": "https://en.wikipedia.org/wiki/Tessa_Jowell",
                   "party_memberships": {
                     "2010": {
                       "id": "party:53",
                       "name": "Labour Party"
                     },
                     "2015": {
                       "id": "party:53",
                       "name": "Labour Party"
                     }
                   },
                   "email": "*****@*****.**"
                 }
               },
               {
                 "username": "******",
                 "information_source": "An initial version",
                 "ip": "127.0.0.1",
                 "version_id": "5469de7db0cbd155",
                 "timestamp": "2014-10-01T15:12:34.732426",
                 "data": {
                   "name": "Tessa Jowell",
                   "id": "2009",
                   "twitter_username": "",
                   "standing_in": {
                     "2010": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     }
                   },
                   "homepage_url": "http://example.org/tessajowell",
                   "birth_date": "1947-09-17",
                   "wikipedia_url": "",
                   "party_memberships": {
                     "2010": {
                       "id": "party:53",
                       "name": "Labour Party"
                     }
                   },
                   "email": "*****@*****.**"
                 }
               }
             ]
         ''',
     )
     ImageExtra.objects.create_from_file(
         example_image_filename,
         'images/jowell-pilot.jpg',
         base_kwargs={
             'content_object': person_extra,
             'is_primary': True,
             'source': 'Taken from Wikipedia',
         },
         extra_kwargs={
             'copyright': 'example-license',
             'uploading_user': self.user,
             'user_notes': 'A photo of Tessa Jowell',
         },
     )
     factories.CandidacyExtraFactory.create(
         election=self.election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.labour_party_extra.base)
     factories.CandidacyExtraFactory.create(
         election=self.earlier_election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.labour_party_extra.base)
     # Now create Shane Collins (who we'll merge into Tessa Jowell)
     person_extra = factories.PersonExtraFactory.create(
         base__id=2007,
         base__name='Shane Collins',
         base__gender='male',
         base__honorific_prefix='Mr',
         base__email='*****@*****.**',
         versions='''
             [
               {
                 "data": {
                   "birth_date": null,
                   "email": "*****@*****.**",
                   "facebook_page_url": "",
                   "facebook_personal_url": "",
                   "gender": "male",
                   "homepage_url": "",
                   "honorific_prefix": "Mr",
                   "honorific_suffix": "",
                   "id": "2007",
                   "identifiers": [
                     {
                       "id": "547786cc737edc5252ce5af1",
                       "identifier": "2961",
                       "scheme": "yournextmp-candidate"
                     }
                   ],
                   "image": null,
                   "linkedin_url": "",
                   "name": "Shane Collins",
                   "other_names": [],
                   "party_memberships": {
                     "2010": {
                       "id": "party:63",
                       "name": "Green Party"
                     }
                   },
                   "party_ppc_page_url": "",
                   "proxy_image": null,
                   "standing_in": {
                     "2010": {
                       "mapit_url": "http://mapit.mysociety.org/area/65808",
                       "name": "Dulwich and West Norwood",
                       "post_id": "65808"
                     },
                     "2015": null
                   },
                   "twitter_username": "",
                   "wikipedia_url": ""
                 },
                 "information_source": "http://www.lambeth.gov.uk/sites/default/files/ec-dulwich-and-west-norwood-candidates-and-notice-of-poll-2015.pdf",
                 "timestamp": "2015-04-09T20:32:09.237610",
                 "username": "******",
                 "version_id": "274e50504df330e4"
               },
               {
                 "data": {
                   "birth_date": null,
                   "email": "*****@*****.**",
                   "facebook_page_url": null,
                   "facebook_personal_url": null,
                   "gender": "male",
                   "homepage_url": null,
                   "id": "2007",
                   "identifiers": [
                     {
                       "identifier": "2961",
                       "scheme": "yournextmp-candidate"
                     }
                   ],
                   "name": "Shane Collins",
                   "party_memberships": {
                     "2010": {
                       "id": "party:63",
                       "name": "Green Party"
                     }
                   },
                   "party_ppc_page_url": null,
                   "phone": "07939 196612",
                   "slug": "shane-collins",
                   "standing_in": {
                     "2010": {
                       "mapit_url": "http://mapit.mysociety.org/area/65808",
                       "name": "Dulwich and West Norwood",
                       "post_id": "65808"
                     }
                   },
                   "twitter_username": null,
                   "wikipedia_url": null
                 },
                 "information_source": "Imported from YourNextMP data from 2010",
                 "timestamp": "2014-11-21T18:16:47.670167",
                 "version_id": "68a452284d95d9ab"
               }
             ]
         ''')
     ImageExtra.objects.create_from_file(
         example_image_filename,
         'images/collins-pilot.jpg',
         base_kwargs={
             'content_object': person_extra,
             'is_primary': True,
             'source': 'Taken from Twitter',
         },
         extra_kwargs={
             'copyright': 'profile-photo',
             'uploading_user': self.user,
             'user_notes': 'A photo of Shane Collins',
         },
     )
     factories.CandidacyExtraFactory.create(
         election=self.election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.green_party_extra.base)
     factories.CandidacyExtraFactory.create(
         election=self.earlier_election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.green_party_extra.base)
 def setUp(self):
     super(TestMergePeopleView, self).setUp()
     example_image_filename = join(
         settings.BASE_DIR, 'moderation_queue', 'tests', 'example-image.jpg'
     )
     mkdir_p(TEST_MEDIA_ROOT)
     # Create Tessa Jowell (the primary person)
     person_extra = factories.PersonExtraFactory.create(
         base__id=2009,
         base__name='Tessa Jowell',
         base__gender='female',
         base__honorific_suffix='DBE',
         base__email='*****@*****.**',
         versions='''
             [
               {
                 "username": "******",
                 "information_source": "Just adding example data",
                 "ip": "127.0.0.1",
                 "version_id": "35ec2d5821176ccc",
                 "timestamp": "2014-10-28T14:32:36.835429",
                 "data": {
                   "name": "Tessa Jowell",
                   "id": "2009",
                   "honorific_suffix": "DBE",
                   "twitter_username": "",
                   "standing_in": {
                     "2010": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     },
                     "2015": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     }
                   },
                   "gender": "female",
                   "homepage_url": "",
                   "birth_date": null,
                   "wikipedia_url": "https://en.wikipedia.org/wiki/Tessa_Jowell",
                   "party_memberships": {
                     "2010": {
                       "id": "party:53",
                       "name": "Labour Party"
                     },
                     "2015": {
                       "id": "party:53",
                       "name": "Labour Party"
                     }
                   },
                   "email": "*****@*****.**"
                 }
               },
               {
                 "username": "******",
                 "information_source": "An initial version",
                 "ip": "127.0.0.1",
                 "version_id": "5469de7db0cbd155",
                 "timestamp": "2014-10-01T15:12:34.732426",
                 "data": {
                   "name": "Tessa Jowell",
                   "id": "2009",
                   "twitter_username": "",
                   "standing_in": {
                     "2010": {
                       "post_id": "65808",
                       "name": "Dulwich and West Norwood",
                       "mapit_url": "http://mapit.mysociety.org/area/65808"
                     }
                   },
                   "homepage_url": "http://example.org/tessajowell",
                   "birth_date": "1947-09-17",
                   "wikipedia_url": "",
                   "party_memberships": {
                     "2010": {
                       "id": "party:53",
                       "name": "Labour Party"
                     }
                   },
                   "email": "*****@*****.**"
                 }
               }
             ]
         ''',
     )
     ImageExtra.objects.create_from_file(
         example_image_filename,
         'images/jowell-pilot.jpg',
         base_kwargs={
             'content_object': person_extra,
             'is_primary': True,
             'source': 'Taken from Wikipedia',
         },
         extra_kwargs={
             'copyright': 'example-license',
             'uploading_user': self.user,
             'user_notes': 'A photo of Tessa Jowell',
         },
     )
     factories.CandidacyExtraFactory.create(
         election=self.election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.labour_party_extra.base
     )
     factories.CandidacyExtraFactory.create(
         election=self.earlier_election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.labour_party_extra.base
     )
     # Now create Shane Collins (who we'll merge into Tessa Jowell)
     person_extra = factories.PersonExtraFactory.create(
         base__id=2007,
         base__name='Shane Collins',
         base__gender='male',
         base__honorific_prefix='Mr',
         base__email='*****@*****.**',
         versions='''
             [
               {
                 "data": {
                   "birth_date": null,
                   "email": "*****@*****.**",
                   "facebook_page_url": "",
                   "facebook_personal_url": "",
                   "gender": "male",
                   "homepage_url": "",
                   "honorific_prefix": "Mr",
                   "honorific_suffix": "",
                   "id": "2007",
                   "identifiers": [
                     {
                       "id": "547786cc737edc5252ce5af1",
                       "identifier": "2961",
                       "scheme": "yournextmp-candidate"
                     }
                   ],
                   "image": null,
                   "linkedin_url": "",
                   "name": "Shane Collins",
                   "other_names": [],
                   "party_memberships": {
                     "2010": {
                       "id": "party:63",
                       "name": "Green Party"
                     }
                   },
                   "party_ppc_page_url": "",
                   "proxy_image": null,
                   "standing_in": {
                     "2010": {
                       "mapit_url": "http://mapit.mysociety.org/area/65808",
                       "name": "Dulwich and West Norwood",
                       "post_id": "65808"
                     },
                     "2015": null
                   },
                   "twitter_username": "",
                   "wikipedia_url": ""
                 },
                 "information_source": "http://www.lambeth.gov.uk/sites/default/files/ec-dulwich-and-west-norwood-candidates-and-notice-of-poll-2015.pdf",
                 "timestamp": "2015-04-09T20:32:09.237610",
                 "username": "******",
                 "version_id": "274e50504df330e4"
               },
               {
                 "data": {
                   "birth_date": null,
                   "email": "*****@*****.**",
                   "facebook_page_url": null,
                   "facebook_personal_url": null,
                   "gender": "male",
                   "homepage_url": null,
                   "id": "2007",
                   "identifiers": [
                     {
                       "identifier": "2961",
                       "scheme": "yournextmp-candidate"
                     }
                   ],
                   "name": "Shane Collins",
                   "party_memberships": {
                     "2010": {
                       "id": "party:63",
                       "name": "Green Party"
                     }
                   },
                   "party_ppc_page_url": null,
                   "phone": "07939 196612",
                   "slug": "shane-collins",
                   "standing_in": {
                     "2010": {
                       "mapit_url": "http://mapit.mysociety.org/area/65808",
                       "name": "Dulwich and West Norwood",
                       "post_id": "65808"
                     }
                   },
                   "twitter_username": null,
                   "wikipedia_url": null
                 },
                 "information_source": "Imported from YourNextMP data from 2010",
                 "timestamp": "2014-11-21T18:16:47.670167",
                 "version_id": "68a452284d95d9ab"
               }
             ]
         ''')
     ImageExtra.objects.create_from_file(
         example_image_filename,
         'images/collins-pilot.jpg',
         base_kwargs={
             'content_object': person_extra,
             'is_primary': True,
             'source': 'Taken from Twitter',
         },
         extra_kwargs={
             'copyright': 'profile-photo',
             'uploading_user': self.user,
             'user_notes': 'A photo of Shane Collins',
         },
     )
     factories.CandidacyExtraFactory.create(
         election=self.election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.green_party_extra.base
     )
     factories.CandidacyExtraFactory.create(
         election=self.earlier_election,
         base__person=person_extra.base,
         base__post=self.dulwich_post_extra.base,
         base__on_behalf_of=self.green_party_extra.base
     )