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
CUSTOMISATION_NAME = None
TEMPLATE_NAME_INDEX = 'index.html'
TEMPLATE_WELCOME_EMAIL_NAME = 'welcome-email'
try:
from custom.settings import (CUSTOMISATION_NAME, TEMPLATE_NAME_INDEX,
TEMPLATE_WELCOME_EMAIL_NAME)
from custom.settings import (
CUSTOMISATION_NAME,
TEMPLATE_NAME_INDEX,
TEMPLATE_WELCOME_EMAIL_NAME
)
logger.info("Loaded customisation app: %s", CUSTOMISATION_NAME)
INSTALLED_APPS.append('custom')
except ImportError as e:
logger.info("No customisation app loaded: %s", e)
except ImportError as exc:
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" />
{% load staticfiles %}
<link rel="stylesheet"
href="{% static 'css/global.css' %}">
<link rel="stylesheet" href="{% static 'css/global.css' %}">
{% if 'javascript_in_head'|bootstrap_setting %}
{% if 'include_jquery'|bootstrap_setting %}
@@ -38,147 +37,152 @@
{% bootstrap_javascript %}
{% endif %}
{{ form.media.css }}
{% if form %}
{{ form.media.css }}
{% endif %}
{% block extra_head %}{% endblock %}
</head>
<body>
<div class="content" style="display: flex; flex-direction: column">
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a href="{% url 'index' %}" class="navbar-brand">
{{ settings.PROJECT_SHORT_NAME }}
</a>
<div class="content" style="display: flex; flex-direction: column">
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a href="{% url 'index' %}" class="navbar-brand">
{{ settings.PROJECT_SHORT_NAME }}
</a>
<button type="button" class="navbar-toggler"
data-toggle="collapse" data-target="#navbarCollapse"
aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navbar">
<span class="navbar-toggler-icon"></span>
</button>
<button type="button" class="navbar-toggler"
data-toggle="collapse" data-target="#navbarCollapse"
aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarCollapse">
<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 %}
<div class="navbar-collapse collapse" id="navbarCollapse">
<ul class="navbar-nav mt-2 mt-lg-0">
<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 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>
{% 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 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">
<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>
</li>
{% else %}
<a href="{% url 'people:person.create' %}?user" class="nav-link">
<i class="fas fa-user-circle"></i>
{{ request.user }}
{% 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 %}
</li>
<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>
{% 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>
</nav>
{% endblock %}
{% endif %}
{# 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>
{% if request.user.is_authenticated and not request.user.has_person %}
<div class="alert alert-info rounded-0" role="alert">
<p class="text-center mb-0">
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>
{% endif %}
</div>
{% if request.user.is_authenticated and not request.user.has_person %}
<div class="alert alert-info rounded-0" role="alert">
<p class="text-center mb-0">
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>
<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 %}
{# Bootstrap JavaScript if it is in body #}
{% bootstrap_javascript %}
{% 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>
<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 %}
{% if form %}
{{ form.media.js }}
{% endif %}
{# Bootstrap JavaScript if it is in body #}
{% bootstrap_javascript %}
{% endif %}
{{ form.media.js }}
{% block extra_script %}{% endblock %}
{% block extra_script %}{% endblock %}
</body>
</html>

View File

@@ -12,7 +12,8 @@ class SimplePersonSerializer(serializers.ModelSerializer):
model = models.Person
fields = [
'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
fields = [
'id',
'name',
'core_member',
# Name is excluded from exports
# See https://github.com/Southampton-RSG/breccia-mapper/issues/35
'gender',
'age_group',
'nationality',
'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):
relationship = RelationshipSerializer()
@@ -61,7 +69,7 @@ class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
# Add relationship questions to columns
for question in models.RelationshipQuestion.objects.all():
headers.append(question.slug.replace('-', '_'))
headers.append(underscore(question.slug))
return headers
@@ -71,7 +79,7 @@ class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer):
try:
# Add relationship question answers to data
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:
pass

View File

@@ -1,13 +1,29 @@
"""
Forms for creating / updating models belonging to the 'people' app.
"""
import typing
from django import forms
from django.forms.widgets import SelectDateWidget
from django.utils import timezone
from django_select2.forms import Select2Widget, Select2MultipleWidget
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):
"""
Form for creating / updating an instance of :class:`Person`.
@@ -16,14 +32,14 @@ class PersonForm(forms.ModelForm):
model = models.Person
fields = [
'name',
'core_member',
'gender',
'age_group',
'nationality',
'country_of_residence',
'organisation',
'organisation_started_date',
'job_title',
'discipline',
'disciplines',
'role',
'themes',
]
@@ -32,6 +48,16 @@ class PersonForm(forms.ModelForm):
'country_of_residence': Select2Widget(),
'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):
@@ -87,4 +113,7 @@ class NetworkFilterForm(DynamicAnswerSetBase):
super().__init__(*args, **kwargs)
# 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',
'Organisation',
'Role',
'Discipline',
'Theme',
'Person',
]
@@ -82,19 +81,6 @@ class Role(models.Model):
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):
"""
Project theme within which a :class:`Person` works.
@@ -122,9 +108,6 @@ class Person(models.Model):
#: Name of the person
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
relationship_targets = models.ManyToManyField(
'self',
@@ -175,15 +158,15 @@ class Person(models.Model):
blank=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 = models.CharField(max_length=255, blank=True, null=False)
#: Discipline within which this person works
discipline = models.ForeignKey(Discipline,
on_delete=models.PROTECT,
related_name='people',
blank=True,
null=True)
#: Discipline(s) within which this person works
disciplines = models.CharField(max_length=255, blank=True, null=True)
#: Role this person holds within the project
role = models.ForeignKey(Role,

View File

@@ -14,56 +14,70 @@
<hr>
<dl>
{% if person.gender %}
<dt>Gender</dt>
<dd>{{ person.get_gender_display }}</dd>
{% if person.user == request.user or request.user.is_superuser %}
{% if person.user != request.user and request.user.is_superuser %}
<div class="alert alert-warning">
<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 %}
{% if person.age_group %}
<dt>Age Group</dt>
<dd>{{ person.get_age_group_display }}</dd>
{% endif %}
<dl>
{% if person.gender %}
<dt>Gender</dt>
<dd>{{ person.get_gender_display }}</dd>
{% endif %}
{% if person.nationality %}
<dt>Nationality</dt>
<dd>{{ person.nationality.name }}</dd>
{% endif %}
{% if person.age_group %}
<dt>Age Group</dt>
<dd>{{ person.get_age_group_display }}</dd>
{% endif %}
{% if person.country_of_residence %}
<dt>Country of Residence</dt>
<dd>{{ person.country_of_residence.name }}</dd>
{% endif %}
{% if person.nationality %}
<dt>Nationality</dt>
<dd>{{ person.nationality.name }}</dd>
{% endif %}
{% if person.organisation %}
<dt>Organisation</dt>
<dd>{{ person.organisation }}</dd>
{% endif %}
{% if person.country_of_residence %}
<dt>Country of Residence</dt>
<dd>{{ person.country_of_residence.name }}</dd>
{% endif %}
{% if person.job_title %}
<dt>Job Title</dt>
<dd>{{ person.job_title }}</dd>
{% endif %}
{% if person.organisation %}
<dt>Organisation</dt>
<dd>{{ person.organisation }}</dd>
{% if person.role %}
<dt>Role</dt>
<dd>{{ person.role }}</dd>
{% endif %}
{% if person.organisation_started_date %}
<dt>Started Date</dt>
<dd>{{ person.organisation_started_date }}</dd>
{% endif %}
{% endif %}
{% if person.dispipline %}
<dt>Discipline</dt>
<dd>{{ person.discipline }}</dd>
{% endif %}
{% if person.job_title %}
<dt>Job Title</dt>
<dd>{{ person.job_title }}</dd>
{% 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>
{% if person.role %}
<dt>Role</dt>
<dd>{{ person.role }}</dd>
{% endif %}
{% if person.disciplines %}
<dt>Discipline(s)</dt>
<dd>{{ person.disciplines }}</dd>
{% 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"
href="{% url 'people:person.update' pk=person.pk %}">Update</a>