Merge pull request #37 from Southampton-RSG/dev

Merge first batch of changes from user acceptance testing
This commit is contained in:
James Graham
2020-06-26 12:10:39 +01:00
committed by GitHub
9 changed files with 342 additions and 204 deletions

View File

@@ -376,16 +376,20 @@ else:
# Import customisation app settings if present # Import customisation app settings if present
CUSTOMISATION_NAME = None
TEMPLATE_NAME_INDEX = 'index.html'
TEMPLATE_WELCOME_EMAIL_NAME = 'welcome-email'
try: try:
from custom.settings import (CUSTOMISATION_NAME, TEMPLATE_NAME_INDEX, from custom.settings import (
TEMPLATE_WELCOME_EMAIL_NAME) CUSTOMISATION_NAME,
TEMPLATE_NAME_INDEX,
TEMPLATE_WELCOME_EMAIL_NAME
)
logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME) logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME)
INSTALLED_APPS.append('custom') INSTALLED_APPS.append('custom')
except ImportError as e: except ImportError as exc:
logger.info("No customisation app loaded: %s", e) logger.info("No customisation app loaded: %s", exc)
# Set default values if no customisations loaded
CUSTOMISATION_NAME = None
TEMPLATE_NAME_INDEX = 'index.html'
TEMPLATE_WELCOME_EMAIL_NAME = 'welcome-email'

View File

@@ -25,8 +25,7 @@
crossorigin="anonymous" /> crossorigin="anonymous" />
{% load staticfiles %} {% load staticfiles %}
<link rel="stylesheet" <link rel="stylesheet" href="{% static 'css/global.css' %}">
href="{% static 'css/global.css' %}">
{% if 'javascript_in_head'|bootstrap_setting %} {% if 'javascript_in_head'|bootstrap_setting %}
{% if 'include_jquery'|bootstrap_setting %} {% if 'include_jquery'|bootstrap_setting %}
@@ -38,147 +37,152 @@
{% bootstrap_javascript %} {% bootstrap_javascript %}
{% endif %} {% endif %}
{{ form.media.css }} {% if form %}
{{ form.media.css }}
{% endif %}
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
</head> </head>
<body> <body>
<div class="content" style="display: flex; flex-direction: column"> <div class="content" style="display: flex; flex-direction: column">
{% block navbar %} {% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container"> <div class="container">
<a href="{% url 'index' %}" class="navbar-brand"> <a href="{% url 'index' %}" class="navbar-brand">
{{ settings.PROJECT_SHORT_NAME }} {{ settings.PROJECT_SHORT_NAME }}
</a> </a>
<button type="button" class="navbar-toggler" <button type="button" class="navbar-toggler"
data-toggle="collapse" data-target="#navbarCollapse" data-toggle="collapse" data-target="#navbarCollapse"
aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navbar"> aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navbar">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="navbar-collapse collapse" id="navbarCollapse"> <div class="navbar-collapse collapse" id="navbarCollapse">
<ul class="navbar-nav mt-2 mt-lg-0"> <ul class="navbar-nav mt-2 mt-lg-0">
<li class="nav-item">
<a href="{% url 'people:person.list' %}" class="nav-link">People</a>
</li>
<li class="nav-item">
<a href="{% url 'activities:activity-series.list' %}" class="nav-link">Activity Series</a>
</li>
<li class="nav-item">
<a href="{% url 'activities:activity.list' %}" class="nav-link">Activities</a>
</li>
<li class="nav-item">
<a href="{% url 'people:network' %}" class="nav-link">Network</a>
</li>
{% if request.user.is_superuser %}
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'export:index' %}" class="nav-link">Export</a> <a href="{% url 'people:person.list' %}" class="nav-link">People</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'admin:index' %}" class="nav-link">Admin</a> <a href="{% url 'activities:activity-series.list' %}" class="nav-link">Activity Series</a>
</li> </li>
{% endif %}
</ul>
<ul class="navbar-nav mt-2 mt-lg-0 ml-auto">
{% if request.user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
{% if request.user.person %} <a href="{% url 'activities:activity.list' %}" class="nav-link">Activities</a>
<a href="{% url 'people:person.profile' %}" class="nav-link"> </li>
<i class="fas fa-user-circle"></i>
{{ request.user }} <li class="nav-item">
<a href="{% url 'people:network' %}" class="nav-link">Network</a>
</li>
{% if request.user.is_superuser %}
<li class="nav-item">
<a href="{% url 'export:index' %}" class="nav-link">Export</a>
</li>
<li class="nav-item">
<a href="{% url 'admin:index' %}" class="nav-link">Admin</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav mt-2 mt-lg-0 ml-auto">
{% if request.user.is_authenticated %}
<li class="nav-item">
{% if request.user.person %}
<a href="{% url 'people:person.profile' %}" class="nav-link">
<i class="fas fa-user-circle"></i>
{{ request.user }}
</a>
{% else %}
<a href="{% url 'people:person.create' %}?user" class="nav-link">
<i class="fas fa-user-circle"></i>
{{ request.user }}
</a>
{% endif %}
</li>
<li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link">
<i class="fas fa-sign-out-alt"></i>
Log Out
</a> </a>
</li>
{% else %} {% else %}
<a href="{% url 'people:person.create' %}?user" class="nav-link"> <li class="nav-item">
<i class="fas fa-user-circle"></i> <a href="{% url 'login' %}" class="nav-link">
{{ request.user }} <i class="fas fa-sign-in-alt"></i>
Log In
</a> </a>
</li>
{% endif %} {% endif %}
</li> </ul>
</div>
<li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link">
<i class="fas fa-sign-out-alt"></i>
Log Out
</a>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="nav-link">
<i class="fas fa-sign-in-alt"></i>
Log In
</a>
</li>
{% endif %}
</ul>
</div> </div>
</nav>
{% endblock %}
{# Global banner if config.NOTICE_TEXT is set using Constance #}
{% if config.NOTICE_TEXT %}
<div class="alert {{ config.NOTICE_CLASS }} rounded-0 mb-3" role="alert">
<h4 class="alert-heading text-center mb-0">{{ config.NOTICE_TEXT }}</h4>
</div> </div>
</nav> {% endif %}
{% endblock %}
{# Global banner if config.NOTICE_TEXT is set using Constance #} {% if request.user.is_authenticated and not request.user.has_person %}
{% if config.NOTICE_TEXT %} <div class="alert alert-info rounded-0" role="alert">
<div class="alert {{ config.NOTICE_CLASS }} rounded-0 mb-3" role="alert"> <p class="text-center mb-0">
<h4 class="alert-heading text-center mb-0">{{ config.NOTICE_TEXT }}</h4> Your profile is currently blank.
Please fill in your details so you can be part of the network.
<a class="btn btn-success"
href="{% url 'people:person.create' %}?user">Profile</a>
</p>
</div>
{% endif %}
{% block before_content %}{% endblock %}
<main class="container">
{# Display Django messages as Bootstrap alerts #}
{% bootstrap_messages %}
{% block content %}{% endblock %}
</main>
<div class="container">
{% block after_content %}{% endblock %}
</div> </div>
{% endif %} </div>
{% if request.user.is_authenticated and not request.user.has_person %} <footer class="footer bg-light">
<div class="alert alert-info rounded-0" role="alert"> <div class="container">
<p class="text-center mb-0"> <span class="text-muted">{{ settings.PROJECT_LONG_NAME }}</span>
Your profile is currently blank. Please fill in your details so you can be part of the network.
<a class="btn btn-success"
href="{% url 'people:person.create' %}?user">Profile</a>
</p>
</div> </div>
</footer>
{% if not 'javascript_in_head'|bootstrap_setting %}
{% if 'include_jquery'|bootstrap_setting %}
{# jQuery JavaScript if it is in body #}
{% bootstrap_jquery jquery='include_jquery'|bootstrap_setting %}
{% endif %}
{# Bootstrap JavaScript if it is in body #}
{% bootstrap_javascript %}
{% endif %} {% endif %}
{% block before_content %}{% endblock %} {% if form %}
{{ form.media.js }}
<main class="container">
{# Display Django messages as Bootstrap alerts #}
{% bootstrap_messages %}
{% block content %}{% endblock %}
</main>
<div class="container">
{% block after_content %}{% endblock %}
</div>
</div>
<footer class="footer bg-light">
<div class="container">
<span class="text-muted">{{ settings.PROJECT_LONG_NAME }}</span>
</div>
</footer>
{% if not 'javascript_in_head'|bootstrap_setting %}
{% if 'include_jquery'|bootstrap_setting %}
{# jQuery JavaScript if it is in body #}
{% bootstrap_jquery jquery='include_jquery'|bootstrap_setting %}
{% endif %} {% endif %}
{# Bootstrap JavaScript if it is in body #} {% block extra_script %}{% endblock %}
{% bootstrap_javascript %}
{% endif %}
{{ form.media.js }}
{% block extra_script %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -12,7 +12,8 @@ class SimplePersonSerializer(serializers.ModelSerializer):
model = models.Person model = models.Person
fields = [ fields = [
'id', 'id',
'name', # Name is excluded from exports
# See https://github.com/Southampton-RSG/breccia-mapper/issues/35
] ]
@@ -21,12 +22,14 @@ class PersonSerializer(base.FlattenedModelSerializer):
model = models.Person model = models.Person
fields = [ fields = [
'id', 'id',
'name', # Name is excluded from exports
'core_member', # See https://github.com/Southampton-RSG/breccia-mapper/issues/35
'gender', 'gender',
'age_group', 'age_group',
'nationality', 'nationality',
'country_of_residence', 'country_of_residence',
'organisation',
'organisation_started_date',
] ]
@@ -43,6 +46,11 @@ class RelationshipSerializer(base.FlattenedModelSerializer):
] ]
def underscore(slug: str) -> str:
"""Replace hyphens with underscores in text."""
return slug.replace('-', '_')
class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer): class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
relationship = RelationshipSerializer() relationship = RelationshipSerializer()
@@ -61,7 +69,7 @@ class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
# Add relationship questions to columns # Add relationship questions to columns
for question in models.RelationshipQuestion.objects.all(): for question in models.RelationshipQuestion.objects.all():
headers.append(question.slug.replace('-', '_')) headers.append(underscore(question.slug))
return headers return headers
@@ -71,7 +79,7 @@ class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
try: try:
# Add relationship question answers to data # Add relationship question answers to data
for answer in instance.question_answers.all(): for answer in instance.question_answers.all():
rep[answer.question.slug.replace('-', '_')] = answer.slug.replace('-', '_') rep[underscore(answer.question.slug)] = underscore(answer.slug)
except AttributeError: except AttributeError:
pass pass

View File

@@ -1,13 +1,29 @@
""" """
Forms for creating / updating models belonging to the 'people' app. Forms for creating / updating models belonging to the 'people' app.
""" """
import typing
from django import forms from django import forms
from django.forms.widgets import SelectDateWidget
from django.utils import timezone
from django_select2.forms import Select2Widget, Select2MultipleWidget from django_select2.forms import Select2Widget, Select2MultipleWidget
from . import models from . import models
def get_date_year_range() -> typing.Iterable[int]:
"""
Get sensible year range for SelectDateWidgets in the past.
By default these widgets show 10 years in the future.
"""
num_years_display = 60
this_year = timezone.datetime.now().year
return range(this_year, this_year - num_years_display, -1)
class PersonForm(forms.ModelForm): class PersonForm(forms.ModelForm):
""" """
Form for creating / updating an instance of :class:`Person`. Form for creating / updating an instance of :class:`Person`.
@@ -16,14 +32,14 @@ class PersonForm(forms.ModelForm):
model = models.Person model = models.Person
fields = [ fields = [
'name', 'name',
'core_member',
'gender', 'gender',
'age_group', 'age_group',
'nationality', 'nationality',
'country_of_residence', 'country_of_residence',
'organisation', 'organisation',
'organisation_started_date',
'job_title', 'job_title',
'discipline', 'disciplines',
'role', 'role',
'themes', 'themes',
] ]
@@ -32,6 +48,16 @@ class PersonForm(forms.ModelForm):
'country_of_residence': Select2Widget(), 'country_of_residence': Select2Widget(),
'themes': Select2MultipleWidget(), 'themes': Select2MultipleWidget(),
} }
help_texts = {
'organisation_started_date':
'If you don\'t know the exact date, an approximate date is okay.',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['organisation_started_date'].widget = SelectDateWidget(
years=get_date_year_range())
class DynamicAnswerSetBase(forms.Form): class DynamicAnswerSetBase(forms.Form):
@@ -87,4 +113,7 @@ class NetworkFilterForm(DynamicAnswerSetBase):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Add date field to select relationships at a particular point in time # Add date field to select relationships at a particular point in time
self.fields['date'] = forms.DateField(required=False) self.fields['date'] = forms.DateField(
required=False,
widget=SelectDateWidget(years=get_date_year_range()),
help_text='Show relationships as they were on this date')

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.10 on 2020-06-24 11:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('people', '0018_require_user_email'),
]
operations = [
migrations.RemoveField(
model_name='person',
name='core_member',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-06-24 12:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('people', '0019_remove_person_core_member'),
]
operations = [
migrations.AddField(
model_name='person',
name='organisation_started_date',
field=models.DateField(null=True, verbose_name='Date started at this organisation'),
),
]

View File

@@ -0,0 +1,61 @@
# Generated by Django 2.2.10 on 2020-06-24 14:19
from django.db import migrations, models
def migrate_forward(apps, schema_editor):
Person = apps.get_model('people', 'Person')
for person in Person.objects.all():
try:
person.disciplines = person.discipline.name
person.save()
except AttributeError:
pass
def migrate_backward(apps, schema_editor):
Person = apps.get_model('people', 'Person')
Discipline = apps.get_model('people', 'Discipline')
for person in Person.objects.all():
try:
discipline_str = person.disciplines.split(',')[0]
except AttributeError:
pass
else:
# Returns None if not found - doesn't raise exception
discipline = Discipline.objects.filter(name=discipline_str).first()
if not discipline:
discipline = Discipline.objects.create(
name=discipline_str,
code=discipline_str
if len(discipline_str) < 15 else discipline_str[:15])
person.discipline = discipline
person.save()
class Migration(migrations.Migration):
dependencies = [
('people', '0020_person_organisation_started_date'),
]
operations = [
migrations.AddField(
model_name='person',
name='disciplines',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.RunPython(migrate_forward, migrate_backward),
migrations.RemoveField(
model_name='person',
name='discipline',
),
migrations.DeleteModel(name='Discipline', ),
]

View File

@@ -19,7 +19,6 @@ __all__ = [
'User', 'User',
'Organisation', 'Organisation',
'Role', 'Role',
'Discipline',
'Theme', 'Theme',
'Person', 'Person',
] ]
@@ -82,19 +81,6 @@ class Role(models.Model):
return self.name return self.name
class Discipline(models.Model):
"""
Discipline within which a :class:`Person` works.
"""
name = models.CharField(max_length=255, blank=False, null=False)
#: Short code using system such as JACS 3
code = models.CharField(max_length=15, blank=True, null=False)
def __str__(self) -> str:
return self.name
class Theme(models.Model): class Theme(models.Model):
""" """
Project theme within which a :class:`Person` works. Project theme within which a :class:`Person` works.
@@ -122,9 +108,6 @@ class Person(models.Model):
#: Name of the person #: Name of the person
name = models.CharField(max_length=255, blank=False, null=False) name = models.CharField(max_length=255, blank=False, null=False)
#: Is this person a member of the core project team?
core_member = models.BooleanField(default=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( relationship_targets = models.ManyToManyField(
'self', 'self',
@@ -175,15 +158,15 @@ class Person(models.Model):
blank=True, blank=True,
null=True) null=True)
#: When did this person start at their current organisation?
organisation_started_date = models.DateField(
'Date started at this organisation', blank=False, 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, blank=True, null=False) job_title = models.CharField(max_length=255, blank=True, null=False)
#: Discipline within which this person works #: Discipline(s) within which this person works
discipline = models.ForeignKey(Discipline, disciplines = models.CharField(max_length=255, blank=True, null=True)
on_delete=models.PROTECT,
related_name='people',
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,

View File

@@ -14,56 +14,70 @@
<hr> <hr>
<dl> {% if person.user == request.user or request.user.is_superuser %}
{% if person.gender %} {% if person.user != request.user and request.user.is_superuser %}
<dt>Gender</dt> <div class="alert alert-warning">
<dd>{{ person.get_gender_display }}</dd> <strong>NB:</strong> You are able to see the details of this person because you are an admin.
Regular users are not able to see this information for people other than themselves.
</div>
{% endif %} {% endif %}
{% if person.age_group %} <dl>
<dt>Age Group</dt> {% if person.gender %}
<dd>{{ person.get_age_group_display }}</dd> <dt>Gender</dt>
{% endif %} <dd>{{ person.get_gender_display }}</dd>
{% endif %}
{% if person.nationality %} {% if person.age_group %}
<dt>Nationality</dt> <dt>Age Group</dt>
<dd>{{ person.nationality.name }}</dd> <dd>{{ person.get_age_group_display }}</dd>
{% endif %} {% endif %}
{% if person.country_of_residence %} {% if person.nationality %}
<dt>Country of Residence</dt> <dt>Nationality</dt>
<dd>{{ person.country_of_residence.name }}</dd> <dd>{{ person.nationality.name }}</dd>
{% endif %} {% endif %}
{% if person.organisation %} {% if person.country_of_residence %}
<dt>Organisation</dt> <dt>Country of Residence</dt>
<dd>{{ person.organisation }}</dd> <dd>{{ person.country_of_residence.name }}</dd>
{% endif %} {% endif %}
{% if person.job_title %} {% if person.organisation %}
<dt>Job Title</dt> <dt>Organisation</dt>
<dd>{{ person.job_title }}</dd> <dd>{{ person.organisation }}</dd>
{% endif %}
{% if person.role %} {% if person.organisation_started_date %}
<dt>Role</dt> <dt>Started Date</dt>
<dd>{{ person.role }}</dd> <dd>{{ person.organisation_started_date }}</dd>
{% endif %} {% endif %}
{% endif %}
{% if person.dispipline %} {% if person.job_title %}
<dt>Discipline</dt> <dt>Job Title</dt>
<dd>{{ person.discipline }}</dd> <dd>{{ person.job_title }}</dd>
{% endif %} {% endif %}
{% if person.themes.exists %} {% if person.role %}
<dt>Project Themes</dt> <dt>Role</dt>
<dd> <dd>{{ person.role }}</dd>
{% for theme in person.themes.all %} {% endif %}
{{ theme }}{% if not forloop.last %}, {% endif %}
{% endfor %} {% if person.disciplines %}
</dd> <dt>Discipline(s)</dt>
{% endif %} <dd>{{ person.disciplines }}</dd>
</dl> {% endif %}
{% if person.themes.exists %}
<dt>Project Themes</dt>
<dd>
{% for theme in person.themes.all %}
{{ theme }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</dd>
{% endif %}
</dl>
{% endif %}
<a class="btn btn-success" <a class="btn btn-success"
href="{% url 'people:person.update' pk=person.pk %}">Update</a> href="{% url 'people:person.update' pk=person.pk %}">Update</a>