mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 11:27:09 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -106,7 +106,6 @@ from django.urls import reverse_lazy
|
|||||||
from decouple import config, Csv
|
from decouple import config, Csv
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
|
|
||||||
|
|
||||||
# Settings exported to templates
|
# Settings exported to templates
|
||||||
# https://github.com/jakubroztocil/django-settings-export
|
# https://github.com/jakubroztocil/django-settings-export
|
||||||
|
|
||||||
@@ -116,15 +115,12 @@ SETTINGS_EXPORT = [
|
|||||||
'PROJECT_SHORT_NAME',
|
'PROJECT_SHORT_NAME',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
PROJECT_LONG_NAME = config('PROJECT_LONG_NAME', default='Project Long Name')
|
PROJECT_LONG_NAME = config('PROJECT_LONG_NAME', default='Project Long Name')
|
||||||
PROJECT_SHORT_NAME = config('PROJECT_SHORT_NAME', default='shortname')
|
PROJECT_SHORT_NAME = config('PROJECT_SHORT_NAME', default='shortname')
|
||||||
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR.joinpath(...)
|
# Build paths inside the project like this: BASE_DIR.joinpath(...)
|
||||||
BASE_DIR = pathlib.Path(__file__).parent.parent
|
BASE_DIR = pathlib.Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = config('SECRET_KEY')
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
@@ -134,9 +130,7 @@ DEBUG = config('DEBUG', default=False, cast=bool)
|
|||||||
ALLOWED_HOSTS = config(
|
ALLOWED_HOSTS = config(
|
||||||
'ALLOWED_HOSTS',
|
'ALLOWED_HOSTS',
|
||||||
default='*' if DEBUG else '127.0.0.1,localhost,localhost.localdomain',
|
default='*' if DEBUG else '127.0.0.1,localhost,localhost.localdomain',
|
||||||
cast=Csv()
|
cast=Csv())
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@@ -157,6 +151,7 @@ THIRD_PARTY_APPS = [
|
|||||||
'django_countries',
|
'django_countries',
|
||||||
'django_select2',
|
'django_select2',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'post_office',
|
||||||
]
|
]
|
||||||
|
|
||||||
FIRST_PARTY_APPS = [
|
FIRST_PARTY_APPS = [
|
||||||
@@ -199,16 +194,14 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'breccia_mapper.wsgi.application'
|
WSGI_APPLICATION = 'breccia_mapper.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': config(
|
'default':
|
||||||
'DATABASE_URL',
|
config('DATABASE_URL',
|
||||||
default='sqlite:///' + str(BASE_DIR.joinpath('db.sqlite3')),
|
default='sqlite:///' + str(BASE_DIR.joinpath('db.sqlite3')),
|
||||||
cast=dj_database_url.parse
|
cast=dj_database_url.parse)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Django DBBackup
|
# Django DBBackup
|
||||||
@@ -216,10 +209,11 @@ DATABASES = {
|
|||||||
|
|
||||||
DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||||
DBBACKUP_STORAGE_OPTIONS = {
|
DBBACKUP_STORAGE_OPTIONS = {
|
||||||
'location': config('DBBACKUP_STORAGE_LOCATION', default=BASE_DIR.joinpath('.dbbackup')),
|
'location':
|
||||||
|
config('DBBACKUP_STORAGE_LOCATION',
|
||||||
|
default=BASE_DIR.joinpath('.dbbackup')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
# https://www.django-rest-framework.org/
|
# https://www.django-rest-framework.org/
|
||||||
|
|
||||||
@@ -234,22 +228,25 @@ REST_FRAMEWORK = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
'NAME':
|
||||||
|
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
'NAME':
|
||||||
|
'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
'NAME':
|
||||||
|
'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
'NAME':
|
||||||
|
'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -264,7 +261,6 @@ LOGIN_URL = reverse_lazy('login')
|
|||||||
|
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
||||||
@@ -278,7 +274,6 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
@@ -286,10 +281,7 @@ STATIC_URL = '/static/'
|
|||||||
|
|
||||||
STATIC_ROOT = BASE_DIR.joinpath('static')
|
STATIC_ROOT = BASE_DIR.joinpath('static')
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [BASE_DIR.joinpath('breccia_mapper', 'static')]
|
||||||
BASE_DIR.joinpath('breccia_mapper', 'static')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Logging - NB the logger name is empty to capture all output
|
# Logging - NB the logger name is empty to capture all output
|
||||||
|
|
||||||
@@ -332,12 +324,14 @@ LOGGING_CONFIG = None
|
|||||||
logging.config.dictConfig(LOGGING)
|
logging.config.dictConfig(LOGGING)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Admin panel variables
|
# Admin panel variables
|
||||||
|
|
||||||
CONSTANCE_CONFIG = collections.OrderedDict([
|
CONSTANCE_CONFIG = collections.OrderedDict([
|
||||||
('NOTICE_TEXT', ('', 'Text to be displayed in a notice banner at the top of every page.')),
|
('NOTICE_TEXT',
|
||||||
('NOTICE_CLASS', ('alert-warning', 'CSS class to use for background of notice banner.')),
|
('',
|
||||||
|
'Text to be displayed in a notice banner at the top of every page.')),
|
||||||
|
('NOTICE_CLASS', ('alert-warning',
|
||||||
|
'CSS class to use for background of notice banner.')),
|
||||||
])
|
])
|
||||||
|
|
||||||
CONSTANCE_CONFIG_FIELDSETS = {
|
CONSTANCE_CONFIG_FIELDSETS = {
|
||||||
@@ -346,7 +340,6 @@ CONSTANCE_CONFIG_FIELDSETS = {
|
|||||||
|
|
||||||
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
||||||
|
|
||||||
|
|
||||||
# Bootstrap settings
|
# Bootstrap settings
|
||||||
# See https://django-bootstrap4.readthedocs.io/en/latest/settings.html
|
# See https://django-bootstrap4.readthedocs.io/en/latest/settings.html
|
||||||
|
|
||||||
@@ -354,17 +347,19 @@ BOOTSTRAP4 = {
|
|||||||
'include_jquery': 'full',
|
'include_jquery': 'full',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Email backend settings
|
# Email backend settings
|
||||||
# See https://docs.djangoproject.com/en/3.0/topics/email
|
# See https://docs.djangoproject.com/en/3.0/topics/email
|
||||||
|
|
||||||
EMAIL_HOST = config('EMAIL_HOST', default=None)
|
EMAIL_HOST = config('EMAIL_HOST', default=None)
|
||||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default=None)
|
DEFAULT_FROM_EMAIL = config(
|
||||||
|
'DEFAULT_FROM_EMAIL',
|
||||||
|
default=f'{PROJECT_SHORT_NAME}@localhost.localdomain')
|
||||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
if EMAIL_HOST is None:
|
if EMAIL_HOST is None:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
|
||||||
EMAIL_FILE_PATH = config('EMAIL_FILE_PATH', default=str(BASE_DIR.joinpath('mail.log')))
|
EMAIL_FILE_PATH = config('EMAIL_FILE_PATH',
|
||||||
|
default=str(BASE_DIR.joinpath('mail.log')))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
@@ -372,19 +367,22 @@ else:
|
|||||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default=None)
|
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default=None)
|
||||||
|
|
||||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=(EMAIL_PORT == 587), cast=bool)
|
EMAIL_USE_TLS = config('EMAIL_USE_TLS',
|
||||||
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=(EMAIL_PORT == 465), cast=bool)
|
default=(EMAIL_PORT == 587),
|
||||||
|
cast=bool)
|
||||||
|
EMAIL_USE_SSL = config('EMAIL_USE_SSL',
|
||||||
|
default=(EMAIL_PORT == 465),
|
||||||
|
cast=bool)
|
||||||
|
|
||||||
# Import customisation app settings if present
|
# Import customisation app settings if present
|
||||||
|
|
||||||
|
CUSTOMISATION_NAME = None
|
||||||
TEMPLATE_NAME_INDEX = 'index.html'
|
TEMPLATE_NAME_INDEX = 'index.html'
|
||||||
|
TEMPLATE_WELCOME_EMAIL_NAME = 'welcome-email'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from custom.settings import (
|
from custom.settings import (CUSTOMISATION_NAME, TEMPLATE_NAME_INDEX,
|
||||||
CUSTOMISATION_NAME,
|
TEMPLATE_WELCOME_EMAIL_NAME)
|
||||||
TEMPLATE_NAME_INDEX
|
|
||||||
)
|
|
||||||
logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME)
|
logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME)
|
||||||
|
|
||||||
INSTALLED_APPS.append('custom')
|
INSTALLED_APPS.append('custom')
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'people.apps.PeopleConfig'
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ from django.contrib.auth.admin import UserAdmin
|
|||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.User, UserAdmin)
|
@admin.register(models.User)
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
"""Add email address field to new user form."""
|
||||||
|
add_fieldsets = UserAdmin.add_fieldsets + (
|
||||||
|
('Details', {'fields': ('email', )}),
|
||||||
|
) # yapf: disable
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Organisation)
|
@admin.register(models.Organisation)
|
||||||
|
|||||||
@@ -1,5 +1,69 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import serializers
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
def load_welcome_template_fixture(fixture_path) -> bool:
|
||||||
|
"""Load welcome email template from a JSON fixture."""
|
||||||
|
try:
|
||||||
|
with open(fixture_path) as f:
|
||||||
|
for deserialized in serializers.deserialize('json', f):
|
||||||
|
if deserialized.object.name == settings.TEMPLATE_WELCOME_EMAIL_NAME:
|
||||||
|
deserialized.save()
|
||||||
|
logger.warning('Welcome email template \'%s\' loaded',
|
||||||
|
deserialized.object.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning('Email template fixture not found.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send_welcome_email(sender, instance, created, **kwargs):
|
||||||
|
from post_office import models
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
# If user already exists, don't send welcome message
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance.send_welcome_email()
|
||||||
|
|
||||||
|
except models.EmailTemplate.DoesNotExist:
|
||||||
|
logger.warning(
|
||||||
|
'Welcome email template \'%s\' not found - attempting to load from fixtures',
|
||||||
|
settings.TEMPLATE_WELCOME_EMAIL_NAME)
|
||||||
|
|
||||||
|
is_loaded = False
|
||||||
|
if settings.CUSTOMISATION_NAME:
|
||||||
|
# Customisation app present - try here first
|
||||||
|
is_loaded |= load_welcome_template_fixture(
|
||||||
|
settings.BASE_DIR.joinpath('custom', 'fixtures',
|
||||||
|
'email_templates.json'))
|
||||||
|
|
||||||
|
# |= operator shortcuts - only try here if we don't already have it
|
||||||
|
is_loaded |= load_welcome_template_fixture(
|
||||||
|
settings.BASE_DIR.joinpath('people', 'fixtures',
|
||||||
|
'email_templates.json'))
|
||||||
|
|
||||||
|
if is_loaded:
|
||||||
|
instance.send_welcome_email()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error('Welcome email template \'%s\' not found',
|
||||||
|
settings.TEMPLATE_WELCOME_EMAIL_NAME)
|
||||||
|
|
||||||
|
|
||||||
class PeopleConfig(AppConfig):
|
class PeopleConfig(AppConfig):
|
||||||
name = 'people'
|
name = 'people'
|
||||||
|
|
||||||
|
def ready(self) -> None:
|
||||||
|
# Activate signal handlers
|
||||||
|
post_save.connect(send_welcome_email, sender='people.user')
|
||||||
|
|||||||
16
people/fixtures/email_templates.json
Normal file
16
people/fixtures/email_templates.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "post_office.emailtemplate",
|
||||||
|
"fields": {
|
||||||
|
"name": "welcome-email",
|
||||||
|
"description": "Default welcome email template",
|
||||||
|
"created": "2020-04-27T12:13:30.448Z",
|
||||||
|
"last_updated": "2020-04-27T14:45:27.152Z",
|
||||||
|
"subject": "Welcome to {{settings.PROJECT_LONG_NAME}}",
|
||||||
|
"content": "Dear {{ user.get_full_name }},\r\n\r\nWelcome to {{ settings.PROJECT_LONG_NAME }}.\r\n\r\nThanks,\r\n\r\nThe {{ settings.PROJECT_LONG_NAME }} team",
|
||||||
|
"html_content": "<h1>{{ settings.PROJECT_LONG_NAME }}</h1>\r\n\r\nDear {{ user.get_full_name }},\r\n\r\nWelcome to {{ settings.PROJECT_LONG_NAME }}.\r\n\r\nThanks,\r\n\r\nThe {{ settings.PROJECT_LONG_NAME }} team",
|
||||||
|
"language": "",
|
||||||
|
"default_template": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
18
people/migrations/0018_require_user_email.py
Normal file
18
people/migrations/0018_require_user_email.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-05-28 15:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('people', '0017_answerset_replaced_timestamp'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(max_length=254, verbose_name='email address'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
from django_settings_export import settings_export
|
||||||
|
from post_office import mail
|
||||||
|
|
||||||
from backports.db.models.enums import TextChoices
|
from backports.db.models.enums import TextChoices
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'User',
|
'User',
|
||||||
'Organisation',
|
'Organisation',
|
||||||
@@ -22,19 +29,44 @@ class User(AbstractUser):
|
|||||||
"""
|
"""
|
||||||
Custom user model in case we need to make changes later.
|
Custom user model in case we need to make changes later.
|
||||||
"""
|
"""
|
||||||
|
email = models.EmailField(_('email address'), blank=False, null=False)
|
||||||
|
|
||||||
def has_person(self) -> bool:
|
def has_person(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Does this user have a linked :class:`Person` record?
|
Does this user have a linked :class:`Person` record?
|
||||||
"""
|
"""
|
||||||
return hasattr(self, 'person')
|
return hasattr(self, 'person')
|
||||||
|
|
||||||
|
def send_welcome_email(self):
|
||||||
|
"""Send a welcome email to a new user."""
|
||||||
|
# Get exported data from settings.py first
|
||||||
|
context = settings_export(None)
|
||||||
|
context.update({
|
||||||
|
'user': self,
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('Sending welcome mail to user \'%s\'', self.username)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail.send(
|
||||||
|
[self.email],
|
||||||
|
sender=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
template=settings.TEMPLATE_WELCOME_EMAIL_NAME,
|
||||||
|
context=context,
|
||||||
|
priority='now' # Send immediately - don't add to queue
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValidationError:
|
||||||
|
logger.error(
|
||||||
|
'Sending welcome mail failed, invalid email for user \'%s\'',
|
||||||
|
self.username)
|
||||||
|
|
||||||
|
|
||||||
class Organisation(models.Model):
|
class Organisation(models.Model):
|
||||||
"""
|
"""
|
||||||
Organisation to which a :class:`Person` belongs.
|
Organisation to which a :class:`Person` belongs.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
@@ -44,8 +76,7 @@ class Role(models.Model):
|
|||||||
"""
|
"""
|
||||||
Role which a :class:`Person` holds within the project.
|
Role which a :class:`Person` holds within the project.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
@@ -55,12 +86,10 @@ class Discipline(models.Model):
|
|||||||
"""
|
"""
|
||||||
Discipline within which a :class:`Person` works.
|
Discipline within which a :class:`Person` works.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
#: Short code using system such as JACS 3
|
#: Short code using system such as JACS 3
|
||||||
code = models.CharField(max_length=15,
|
code = models.CharField(max_length=15, blank=True, null=False)
|
||||||
blank=True, null=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
@@ -70,8 +99,7 @@ class Theme(models.Model):
|
|||||||
"""
|
"""
|
||||||
Project theme within which a :class:`Person` works.
|
Project theme within which a :class:`Person` works.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
@@ -88,21 +116,22 @@ class Person(models.Model):
|
|||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL,
|
user = models.OneToOneField(settings.AUTH_USER_MODEL,
|
||||||
related_name='person',
|
related_name='person',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
blank=True, null=True)
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
#: Name of the person
|
#: Name of the person
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
#: Is this person a member of the core project team?
|
#: Is this person a member of the core project team?
|
||||||
core_member = models.BooleanField(default=False,
|
core_member = models.BooleanField(default=False, blank=False, null=False)
|
||||||
blank=False, null=False)
|
|
||||||
|
|
||||||
#: People with whom this person has relationship - via intermediate :class:`Relationship` model
|
#: People with whom this person has relationship - via intermediate :class:`Relationship` model
|
||||||
relationship_targets = models.ManyToManyField('self', related_name='relationship_sources',
|
relationship_targets = models.ManyToManyField(
|
||||||
through='Relationship',
|
'self',
|
||||||
through_fields=('source', 'target'),
|
related_name='relationship_sources',
|
||||||
symmetrical=False)
|
through='Relationship',
|
||||||
|
through_fields=('source', 'target'),
|
||||||
|
symmetrical=False)
|
||||||
|
|
||||||
###############################################################
|
###############################################################
|
||||||
# Data collected for analysis of community makeup and structure
|
# Data collected for analysis of community makeup and structure
|
||||||
@@ -115,7 +144,8 @@ class Person(models.Model):
|
|||||||
|
|
||||||
gender = models.CharField(max_length=1,
|
gender = models.CharField(max_length=1,
|
||||||
choices=GenderChoices.choices,
|
choices=GenderChoices.choices,
|
||||||
blank=True, null=False)
|
blank=True,
|
||||||
|
null=False)
|
||||||
|
|
||||||
class AgeGroupChoices(TextChoices):
|
class AgeGroupChoices(TextChoices):
|
||||||
LTE_25 = '<=25', _('25 or under')
|
LTE_25 = '<=25', _('25 or under')
|
||||||
@@ -131,7 +161,8 @@ class Person(models.Model):
|
|||||||
|
|
||||||
age_group = models.CharField(max_length=5,
|
age_group = models.CharField(max_length=5,
|
||||||
choices=AgeGroupChoices.choices,
|
choices=AgeGroupChoices.choices,
|
||||||
blank=True, null=False)
|
blank=True,
|
||||||
|
null=False)
|
||||||
|
|
||||||
nationality = CountryField(blank=True, null=True)
|
nationality = CountryField(blank=True, null=True)
|
||||||
|
|
||||||
@@ -141,34 +172,33 @@ class Person(models.Model):
|
|||||||
organisation = models.ForeignKey(Organisation,
|
organisation = models.ForeignKey(Organisation,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='members',
|
related_name='members',
|
||||||
blank=True, null=True)
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
#: Job title this person holds within their organisation
|
#: Job title this person holds within their organisation
|
||||||
job_title = models.CharField(max_length=255,
|
job_title = models.CharField(max_length=255, blank=True, null=False)
|
||||||
blank=True, null=False)
|
|
||||||
|
|
||||||
#: Discipline within which this person works
|
#: Discipline within which this person works
|
||||||
discipline = models.ForeignKey(Discipline,
|
discipline = models.ForeignKey(Discipline,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='people',
|
related_name='people',
|
||||||
blank=True, null=True)
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
#: Role this person holds within the project
|
#: Role this person holds within the project
|
||||||
role = models.ForeignKey(Role,
|
role = models.ForeignKey(Role,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='holders',
|
related_name='holders',
|
||||||
blank=True, null=True)
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
#: Project themes within this person works
|
#: Project themes within this person works
|
||||||
themes = models.ManyToManyField(Theme,
|
themes = models.ManyToManyField(Theme, related_name='people', blank=True)
|
||||||
related_name='people',
|
|
||||||
blank=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relationships(self):
|
def relationships(self):
|
||||||
return self.relationships_as_source.all().union(
|
return self.relationships_as_source.all().union(
|
||||||
self.relationships_as_target.all()
|
self.relationships_as_target.all())
|
||||||
)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('people:person.detail', kwargs={'pk': self.pk})
|
return reverse('people:person.detail', kwargs={'pk': self.pk})
|
||||||
|
|||||||
@@ -2,15 +2,18 @@
|
|||||||
Views for displaying networks of :class:`People` and :class:`Relationship`s.
|
Views for displaying networks of :class:`People` and :class:`Relationship`s.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
|
||||||
from people import forms, models, serializers
|
from people import forms, models, serializers
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class NetworkView(LoginRequiredMixin, FormView):
|
class NetworkView(LoginRequiredMixin, FormView):
|
||||||
"""
|
"""
|
||||||
@@ -47,29 +50,39 @@ class NetworkView(LoginRequiredMixin, FormView):
|
|||||||
if not at_date:
|
if not at_date:
|
||||||
at_date = timezone.now().date()
|
at_date = timezone.now().date()
|
||||||
|
|
||||||
|
# Filter on timestamp__date doesn't seem to work on MySQL
|
||||||
|
# To compare datetimes we need at_date to be midnight at
|
||||||
|
# the *end* of the day in question - so add one day here
|
||||||
|
at_date += timezone.timedelta(days=1)
|
||||||
|
|
||||||
relationship_answerset_set = models.RelationshipAnswerSet.objects.filter(
|
relationship_answerset_set = models.RelationshipAnswerSet.objects.filter(
|
||||||
Q(replaced_timestamp__date__gte=at_date) | Q(replaced_timestamp__isnull=True),
|
Q(replaced_timestamp__gte=at_date)
|
||||||
timestamp__date__lte=at_date
|
| Q(replaced_timestamp__isnull=True),
|
||||||
)
|
timestamp__lte=at_date)
|
||||||
|
|
||||||
|
logger.info('Found %d relationship answer sets for %s',
|
||||||
|
relationship_answerset_set.count(), at_date)
|
||||||
|
|
||||||
# Filter answers to relationship questions
|
# Filter answers to relationship questions
|
||||||
for field, values in form.cleaned_data.items():
|
for field, values in form.cleaned_data.items():
|
||||||
if field.startswith('question_') and values:
|
if field.startswith('question_') and values:
|
||||||
relationship_answerset_set = relationship_answerset_set.filter(
|
relationship_answerset_set = relationship_answerset_set.filter(
|
||||||
question_answers__in=values
|
question_answers__in=values)
|
||||||
)
|
|
||||||
|
logger.info('Found %d relationship answer sets matching filters',
|
||||||
|
relationship_answerset_set.count())
|
||||||
|
|
||||||
context['person_set'] = serializers.PersonSerializer(
|
context['person_set'] = serializers.PersonSerializer(
|
||||||
models.Person.objects.all(),
|
models.Person.objects.all(), many=True).data
|
||||||
many=True
|
|
||||||
).data
|
|
||||||
|
|
||||||
context['relationship_set'] = serializers.RelationshipSerializer(
|
context['relationship_set'] = serializers.RelationshipSerializer(
|
||||||
models.Relationship.objects.filter(
|
models.Relationship.objects.filter(
|
||||||
pk__in=relationship_answerset_set.values_list('relationship', flat=True)
|
pk__in=relationship_answerset_set.values_list('relationship',
|
||||||
),
|
flat=True)),
|
||||||
many=True
|
many=True).data
|
||||||
).data
|
|
||||||
|
logger.info('Found %d distinct relationships matching filters',
|
||||||
|
len(context['relationship_set']))
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
@@ -16,5 +16,3 @@
|
|||||||
|
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: python2
|
ansible_python_interpreter: python2
|
||||||
db_user: 'breccia'
|
|
||||||
db_pass: 'breccia'
|
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ django-countries==5.5
|
|||||||
django-dbbackup==3.2.0
|
django-dbbackup==3.2.0
|
||||||
django-filter==2.2.0
|
django-filter==2.2.0
|
||||||
django-picklefield==2.1.1
|
django-picklefield==2.1.1
|
||||||
|
django-post-office==3.4.0
|
||||||
django-select2==7.2.0
|
django-select2==7.2.0
|
||||||
django-settings-export==1.2.1
|
django-settings-export==1.2.1
|
||||||
djangorestframework==3.11.0
|
djangorestframework==3.11.0
|
||||||
dodgy==0.2.1
|
dodgy==0.2.1
|
||||||
isort==4.3.21
|
isort==4.3.21
|
||||||
|
jsonfield==3.1.0
|
||||||
lazy-object-proxy==1.4.3
|
lazy-object-proxy==1.4.3
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
mysqlclient==1.4.6
|
mysqlclient==1.4.6
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
|
|
||||||
SECRET_KEY={{ secret_key }}
|
SECRET_KEY={{ secret_key }}
|
||||||
DEBUG={{ "True" if deploy_mode > 1 else "False" }}
|
DEBUG={{ "True" if deploy_mode > 1 else "False" }}
|
||||||
ALLOWED_HOSTS={{ inventory_hostname }},localhost,127.0.0.1
|
|
||||||
DATABASE_URL=mysql://{{ db_user }}:{{ db_pass }}@localhost:3306/{{ db_name }}
|
DATABASE_URL=mysql://{{ db_user }}:{{ db_pass }}@localhost:3306/{{ db_name }}
|
||||||
|
|
||||||
|
{% if allowed_hosts is defined %}
|
||||||
|
ALLOWED_HOSTS={% for h in allowed_hosts %}{{ h }},{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
ALLOWED_HOSTS={{ inventory_hostname }},localhost,127.0.0.1
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
PROJECT_SHORT_NAME={{ display_short_name }}
|
PROJECT_SHORT_NAME={{ display_short_name }}
|
||||||
PROJECT_LONG_NAME={{ display_long_name }}
|
PROJECT_LONG_NAME={{ display_long_name }}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user