diff --git a/breccia_mapper/settings.py b/breccia_mapper/settings.py index e536c0f..d2424c7 100644 --- a/breccia_mapper/settings.py +++ b/breccia_mapper/settings.py @@ -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' diff --git a/breccia_mapper/templates/base.html b/breccia_mapper/templates/base.html index 3d2ddc0..1b93a01 100644 --- a/breccia_mapper/templates/base.html +++ b/breccia_mapper/templates/base.html @@ -25,8 +25,7 @@ crossorigin="anonymous" /> {% load staticfiles %} - + {% 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 %} -
- {% block navbar %} - - {% endblock %} + {% endif %} - {# Global banner if config.NOTICE_TEXT is set using Constance #} - {% if config.NOTICE_TEXT %} - - {% if request.user.is_authenticated and not request.user.has_person %} - - - - -{% 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 %} diff --git a/export/serializers/people.py b/export/serializers/people.py index 196f295..872761b 100644 --- a/export/serializers/people.py +++ b/export/serializers/people.py @@ -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,15 +22,17 @@ 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', ] - - + + class RelationshipSerializer(base.FlattenedModelSerializer): source = SimplePersonSerializer() target = SimplePersonSerializer() @@ -43,9 +46,14 @@ class RelationshipSerializer(base.FlattenedModelSerializer): ] +def underscore(slug: str) -> str: + """Replace hyphens with underscores in text.""" + return slug.replace('-', '_') + + class RelationshipAnswerSetSerializer(base.FlattenedModelSerializer): relationship = RelationshipSerializer() - + class Meta: model = models.RelationshipAnswerSet fields = [ @@ -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 diff --git a/people/forms.py b/people/forms.py index 5877ce5..5f7c5a3 100644 --- a/people/forms.py +++ b/people/forms.py @@ -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,13 +48,23 @@ 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): field_class = forms.ModelChoiceField field_widget = None field_required = True - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -73,8 +99,8 @@ class RelationshipAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase): self.instance.question_answers.add(value) return self.instance - - + + class NetworkFilterForm(DynamicAnswerSetBase): """ Form to provide filtering on the network view. @@ -82,9 +108,12 @@ class NetworkFilterForm(DynamicAnswerSetBase): field_class = forms.ModelMultipleChoiceField field_widget = Select2MultipleWidget field_required = False - + def __init__(self, *args, **kwargs): 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') diff --git a/people/migrations/0019_remove_person_core_member.py b/people/migrations/0019_remove_person_core_member.py new file mode 100644 index 0000000..ee02484 --- /dev/null +++ b/people/migrations/0019_remove_person_core_member.py @@ -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', + ), + ] diff --git a/people/migrations/0020_person_organisation_started_date.py b/people/migrations/0020_person_organisation_started_date.py new file mode 100644 index 0000000..e12a3a9 --- /dev/null +++ b/people/migrations/0020_person_organisation_started_date.py @@ -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'), + ), + ] diff --git a/people/migrations/0021_refactor_person_disciplines.py b/people/migrations/0021_refactor_person_disciplines.py new file mode 100644 index 0000000..5f7da69 --- /dev/null +++ b/people/migrations/0021_refactor_person_disciplines.py @@ -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', ), + ] diff --git a/people/models/person.py b/people/models/person.py index f7910f9..6127f66 100644 --- a/people/models/person.py +++ b/people/models/person.py @@ -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, diff --git a/people/templates/people/person/detail.html b/people/templates/people/person/detail.html index 349aaf5..5e49e83 100644 --- a/people/templates/people/person/detail.html +++ b/people/templates/people/person/detail.html @@ -14,56 +14,70 @@
-
- {% if person.gender %} -
Gender
-
{{ person.get_gender_display }}
+ {% if person.user == request.user or request.user.is_superuser %} + {% if person.user != request.user and request.user.is_superuser %} +
+ NB: 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. +
{% endif %} - {% if person.age_group %} -
Age Group
-
{{ person.get_age_group_display }}
- {% endif %} +
+ {% if person.gender %} +
Gender
+
{{ person.get_gender_display }}
+ {% endif %} - {% if person.nationality %} -
Nationality
-
{{ person.nationality.name }}
- {% endif %} + {% if person.age_group %} +
Age Group
+
{{ person.get_age_group_display }}
+ {% endif %} - {% if person.country_of_residence %} -
Country of Residence
-
{{ person.country_of_residence.name }}
- {% endif %} + {% if person.nationality %} +
Nationality
+
{{ person.nationality.name }}
+ {% endif %} - {% if person.organisation %} -
Organisation
-
{{ person.organisation }}
- {% endif %} + {% if person.country_of_residence %} +
Country of Residence
+
{{ person.country_of_residence.name }}
+ {% endif %} - {% if person.job_title %} -
Job Title
-
{{ person.job_title }}
- {% endif %} + {% if person.organisation %} +
Organisation
+
{{ person.organisation }}
- {% if person.role %} -
Role
-
{{ person.role }}
- {% endif %} + {% if person.organisation_started_date %} +
Started Date
+
{{ person.organisation_started_date }}
+ {% endif %} + {% endif %} - {% if person.dispipline %} -
Discipline
-
{{ person.discipline }}
- {% endif %} + {% if person.job_title %} +
Job Title
+
{{ person.job_title }}
+ {% endif %} - {% if person.themes.exists %} -
Project Themes
-
- {% for theme in person.themes.all %} - {{ theme }}{% if not forloop.last %}, {% endif %} - {% endfor %} -
- {% endif %} -
+ {% if person.role %} +
Role
+
{{ person.role }}
+ {% endif %} + + {% if person.disciplines %} +
Discipline(s)
+
{{ person.disciplines }}
+ {% endif %} + + {% if person.themes.exists %} +
Project Themes
+
+ {% for theme in person.themes.all %} + {{ theme }}{% if not forloop.last %}, {% endif %} + {% endfor %} +
+ {% endif %} +
+ {% endif %} Update