Merge pull request #123 from mgrove36/dev

Merge development from personal fork
This commit is contained in:
2023-01-19 21:41:55 +00:00
committed by GitHub
48 changed files with 889 additions and 158 deletions

View File

@@ -16,3 +16,6 @@ mail.log/
*.sqlite3* *.sqlite3*
*.log* *.log*
deployment* deployment*
docs/
.readthedocs.yaml

3
.gitignore vendored
View File

@@ -25,3 +25,6 @@ deployment-key*
/custom /custom
staging.yml staging.yml
production.yml production.yml
# Docs local builds
/docs/build

25
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,25 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/source/requirements.txt

5
Caddyfile Normal file → Executable file
View File

@@ -4,12 +4,13 @@
@proxy_paths { @proxy_paths {
not path /static/* not path /static/*
not path /media/*
} }
reverse_proxy @proxy_paths http://web:8000 reverse_proxy @proxy_paths http://server:8000
log { log {
output stderr output stderr
format single_field common_log format console
} }
} }

2
Dockerfile Normal file → Executable file
View File

@@ -1,4 +1,4 @@
FROM python:3.8-slim FROM python:3.9-slim
RUN groupadd -r mapper && useradd --no-log-init -r -g mapper mapper RUN groupadd -r mapper && useradd --no-log-init -r -g mapper mapper

View File

@@ -1,5 +1,7 @@
# BRECcIA Mapper # BRECcIA Mapper
[![Documentation Status](https://readthedocs.org/projects/breccia/badge/?version=latest)](https://breccia.readthedocs.io/en/latest/?badge=latest)
BRECcIA Mapper is a web app to collect and explore data about the relationships between researchers and their stakeholders on large-scale, multi-site research projects. BRECcIA Mapper is a web app to collect and explore data about the relationships between researchers and their stakeholders on large-scale, multi-site research projects.
This allows researchers to visually represent the relationships between project staff and stakeholders involved in the their project at different points in time. This allows researchers to visually represent the relationships between project staff and stakeholders involved in the their project at different points in time.
Through this it is possible to explore the extent of networks and change over time, and identify where new relationships can be developed or existing ones strengthened. Through this it is possible to explore the extent of networks and change over time, and identify where new relationships can be developed or existing ones strengthened.
@@ -14,6 +16,7 @@ Deployment is managed using [Ansible](https://www.ansible.com/) and Docker (http
## Contributors ## Contributors
- James Graham (@jag1g13) - developer - James Graham (@jag1g13) - developer
- Matthew Grove (@mgrove36) - developer
- Genevieve Agaba - Genevieve Agaba
- Sebastian Reichel - Sebastian Reichel
- Claire Bedelian - Claire Bedelian

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.1.4 on 2023-01-05 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('activities', '0006_activity_attendance_optional'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='activitymedium',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='activityseries',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='activitytype',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -16,18 +16,6 @@ https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
Many configuration settings are input from `settings.ini`. Many configuration settings are input from `settings.ini`.
The most likely required settings are: SECRET_KEY, DEBUG, ALLOWED_HOSTS, DATABASE_URL, PROJECT_*_NAME, EMAIL_* The most likely required settings are: SECRET_KEY, DEBUG, ALLOWED_HOSTS, DATABASE_URL, PROJECT_*_NAME, EMAIL_*
- PARENT_PROJECT_NAME
default: Parent Project Name
Displayed in templates where the name of the parent project should be used
- PROJECT_LONG_NAME
default: Project Long Name
Displayed in templates where the full name of the project should be used
- PROJECT_SHORT_NAME
default: shortname
Displayed in templates where a short identifier for the project should be used
- SECRET_KEY (REQUIRED) - SECRET_KEY (REQUIRED)
Used to generate CSRF tokens - must never be made public Used to generate CSRF tokens - must never be made public
@@ -118,16 +106,9 @@ import dj_database_url
SETTINGS_EXPORT = [ SETTINGS_EXPORT = [
'DEBUG', 'DEBUG',
'PARENT_PROJECT_NAME',
'PROJECT_LONG_NAME',
'PROJECT_SHORT_NAME',
'GOOGLE_MAPS_API_KEY', 'GOOGLE_MAPS_API_KEY',
] ]
PARENT_PROJECT_NAME = config('PARENT_PROJECT_NAME',
default='Parent Project Name')
PROJECT_LONG_NAME = config('PROJECT_LONG_NAME', default='Project Long Name')
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
@@ -165,7 +146,6 @@ THIRD_PARTY_APPS = [
'post_office', 'post_office',
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'hijack', 'hijack',
'compat',
] ]
FIRST_PARTY_APPS = [ FIRST_PARTY_APPS = [
@@ -184,6 +164,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'hijack.middleware.HijackUserMiddleware',
] ]
ROOT_URLCONF = 'breccia_mapper.urls' ROOT_URLCONF = 'breccia_mapper.urls'
@@ -297,6 +278,10 @@ STATIC_ROOT = BASE_DIR.joinpath('static')
STATICFILES_DIRS = [BASE_DIR.joinpath('breccia_mapper', 'static')] STATICFILES_DIRS = [BASE_DIR.joinpath('breccia_mapper', 'static')]
# Media uploads
MEDIA_ROOT = BASE_DIR.joinpath('media')
MEDIA_URL = "/media/"
# Logging - NB the logger name is empty to capture all output # Logging - NB the logger name is empty to capture all output
LOGGING = { LOGGING = {
@@ -340,6 +325,10 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name
# Admin panel variables # Admin panel variables
CONSTANCE_ADDITIONAL_FIELDS = {
'image_field': ['django.forms.ImageField', {}]
}
CONSTANCE_CONFIG = { CONSTANCE_CONFIG = {
'NOTICE_TEXT': ( 'NOTICE_TEXT': (
'', '',
@@ -359,21 +348,105 @@ CONSTANCE_CONFIG = {
'RELATIONSHIP_FORM_HELP': ( 'RELATIONSHIP_FORM_HELP': (
'', '',
'Help text to display at the top of relationship forms.'), 'Help text to display at the top of relationship forms.'),
'DEPLOYMENT_URL': (
'http://localhost',
'URL at which this mapper tool is accessible'),
'PARENT_PROJECT_NAME': (
'',
'Parent project name'),
'PROJECT_LONG_NAME': (
'Project Network Mapper',
'Project long name'),
'PROJECT_SHORT_NAME': (
'Network Mapper',
'Project short name'),
'PROJECT_LEAD': (
'Project Lead',
'Project lead'),
'PROJECT_TAGLINE': (
'Here is your project\'s tagline.',
'Project tagline'),
'HOMEPAGE_HEADER_IMAGE': (
'800x500.png',
'Homepage header image',
'image_field'),
'HOMEPAGE_CARD_1_TITLE': (
'Step 1',
'Homepage card #1 title'),
'HOMEPAGE_CARD_1_DESCRIPTION': (
'Tell us about your position within the project',
'Homepage card #1 description'),
'HOMEPAGE_CARD_1_ICON': (
'building-user',
'Homepage card #1 icon'),
'HOMEPAGE_CARD_2_TITLE': (
'Step 2',
'Homepage card #2 title'),
'HOMEPAGE_CARD_2_DESCRIPTION': (
'Describe your relationships with other stakeholders',
'Homepage card #2 description'),
'HOMEPAGE_CARD_2_ICON': (
'handshake-simple',
'Homepage card #2 icon'),
'HOMEPAGE_CARD_3_TITLE': (
'Step 3',
'Homepage card #3 title'),
'HOMEPAGE_CARD_3_DESCRIPTION': (
'Use the network view to build new relationships',
'Homepage card #3 description'),
'HOMEPAGE_CARD_3_ICON': (
'diagram-project',
'Homepage card #3 icon'),
'HOMEPAGE_ABOUT_TITLE': (
'About Us',
'Homepage about section title'),
'HOMEPAGE_ABOUT_CONTENT': (
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In massa tempor nec feugiat nisl. Eget dolor morbi non arcu risus quis varius quam quisque. Nisl pretium fusce id velit ut tortor pretium viverra suspendisse. Vitae auctor eu augue ut lectus arcu. Tellus molestie nunc non blandit massa enim nec. At consectetur lorem donec massa sapien. Placerat orci nulla pellentesque dignissim enim sit. Sit amet mauris commodo quis imperdiet. Tellus at urna condimentum mattis pellentesque.<br/>In vitae turpis massa sed. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Ut consequat semper viverra nam libero justo laoreet. Velit ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nunc id cursus metus aliquam eleifend mi in nulla posuere. Aliquam eleifend mi in nulla posuere sollicitudin aliquam. Est ante in nibh mauris cursus mattis molestie a iaculis. Nunc id cursus metus aliquam. Auctor urna nunc id cursus metus aliquam. Porttitor lacus luctus accumsan tortor posuere ac ut consequat semper. Volutpat consequat mauris nunc congue nisi. Leo vel fringilla est ullamcorper eget. Vitae purus faucibus ornare suspendisse sed nisi lacus sed. Massa id neque aliquam vestibulum morbi blandit. Iaculis nunc sed augue lacus viverra vitae congue. Sodales neque sodales ut etiam.""",
'Homepage about section content'),
'HOMEPAGE_ABOUT_IMAGE': (
'400x400.png',
'Homepage about section image',
'image_field'),
} # yapf: disable } # yapf: disable
CONSTANCE_CONFIG_FIELDSETS = { CONSTANCE_CONFIG_FIELDSETS = {
'Notice Banner': ( 'Project options': (
'PARENT_PROJECT_NAME',
'PROJECT_LONG_NAME',
'PROJECT_SHORT_NAME',
'PROJECT_LEAD',
'PROJECT_TAGLINE',
),
'Homepage configuration': (
'HOMEPAGE_HEADER_IMAGE',
'HOMEPAGE_CARD_1_TITLE',
'HOMEPAGE_CARD_1_DESCRIPTION',
'HOMEPAGE_CARD_1_ICON',
'HOMEPAGE_CARD_2_TITLE',
'HOMEPAGE_CARD_2_DESCRIPTION',
'HOMEPAGE_CARD_2_ICON',
'HOMEPAGE_CARD_3_TITLE',
'HOMEPAGE_CARD_3_DESCRIPTION',
'HOMEPAGE_CARD_3_ICON',
'HOMEPAGE_ABOUT_TITLE',
'HOMEPAGE_ABOUT_CONTENT',
'HOMEPAGE_ABOUT_IMAGE',
),
'Notice banner': (
'NOTICE_TEXT', 'NOTICE_TEXT',
'NOTICE_CLASS', 'NOTICE_CLASS',
), ),
'Data Collection': ( 'Data Collection': (
'CONSENT_TEXT', 'CONSENT_TEXT',
), ),
'Help Text': ( 'Help text': (
'PERSON_LIST_HELP', 'PERSON_LIST_HELP',
'ORGANISATION_LIST_HELP', 'ORGANISATION_LIST_HELP',
'RELATIONSHIP_FORM_HELP', 'RELATIONSHIP_FORM_HELP',
), ),
'Deployment': (
'DEPLOYMENT_URL',
),
} # yapf: disable } # yapf: disable
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
@@ -396,7 +469,7 @@ BOOTSTRAP4 = {
EMAIL_HOST = config('EMAIL_HOST', default=None) EMAIL_HOST = config('EMAIL_HOST', default=None)
DEFAULT_FROM_EMAIL = config( DEFAULT_FROM_EMAIL = config(
'DEFAULT_FROM_EMAIL', 'DEFAULT_FROM_EMAIL',
default=f'{PROJECT_SHORT_NAME}@localhost.localdomain') default=f'{CONSTANCE_CONFIG["PROJECT_SHORT_NAME"][0]}@localhost.localdomain')
SERVER_EMAIL = DEFAULT_FROM_EMAIL SERVER_EMAIL = DEFAULT_FROM_EMAIL
if EMAIL_HOST is None: if EMAIL_HOST is None:
@@ -417,6 +490,18 @@ else:
default=(EMAIL_PORT == 465), default=(EMAIL_PORT == 465),
cast=bool) cast=bool)
# Bootstrap Datepicker Plus Settings
BOOTSTRAP_DATEPICKER_PLUS = {
"variant_options": {
"date": {
"format": "%Y-%m-%d",
},
}
}
# Database default automatic primary key
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Upstream API keys # Upstream API keys
GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None) GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None)

View File

@@ -1,10 +1,10 @@
header.masthead { header.masthead {
position: relative; position: relative;
background: #343a40 no-repeat center; background: #343a40 no-repeat center;
-webkit-background-size: contain; -webkit-background-size: cover;
-moz-background-size: contain; -moz-background-size: cover;
-o-background-size: contain; -o-background-size: cover;
background-size: contain; background-size: cover;
padding-top: 8rem; padding-top: 8rem;
padding-bottom: 8rem; padding-bottom: 8rem;
min-height: 400px; min-height: 400px;

58
breccia_mapper/templates/base.html Normal file → Executable file
View File

@@ -10,27 +10,29 @@
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ settings.PROJECT_LONG_NAME }}</title> <title>{{ config.PROJECT_LONG_NAME }}</title>
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
{% bootstrap_css %} {% bootstrap_css %}
<link rel="stylesheet" <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/fontawesome.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css"
integrity="sha256-/sdxenK1NDowSNuphgwjv8wSosSNZB0t5koXqd7XqOI=" integrity="sha512-giQeaPns4lQTBMRpOOHsYnGw1tGVzbAIHUyHRgn7+6FmiEgGGjaG0T2LZJmAPMzRCl+Cug0ItQ2xDZpTmEc+CQ=="
crossorigin="anonymous" /> crossorigin="anonymous"
referrerpolicy="no-referrer" />
<link rel="stylesheet" <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/solid.min.css"
integrity="sha256-8DcgqUGhWHHsTLj1qcGr0OuPbKkN1RwDjIbZ6DKh/RA=" integrity="sha512-6mc0R607di/biCutMUtU9K7NtNewiGQzrvWX4bWTeqmljZdJrwYvKJtnhgR+Ryvj+NRJ8+NnnCM/biGqMe/iRA=="
crossorigin="anonymous" /> crossorigin="anonymous"
referrerpolicy="no-referrer" />
{% load staticfiles %} {% load static %}
<link rel="stylesheet" href="{% static 'css/global.css' %}"> <link rel="stylesheet" href="{% static 'css/global.css' %}">
<link rel="stylesheet" <link rel="stylesheet"
type="text/css" type="text/css"
href="{% static 'hijack/hijack-styles.css' %}" /> href="{% static 'hijack/hijack.min.css' %}" />
{% if 'javascript_in_head'|bootstrap_setting %} {% if 'javascript_in_head'|bootstrap_setting %}
{% if 'include_jquery'|bootstrap_setting %} {% if 'include_jquery'|bootstrap_setting %}
@@ -56,7 +58,7 @@
<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 }} {{ config.PROJECT_SHORT_NAME }}
</a> </a>
<button type="button" class="navbar-toggler" <button type="button" class="navbar-toggler"
@@ -107,13 +109,13 @@
<li class="nav-item"> <li class="nav-item">
{% if request.user.person %} {% if request.user.person %}
<a href="{% url 'people:person.profile' %}" class="nav-link"> <a href="{% url 'people:person.profile' %}" class="nav-link">
<i class="fas fa-user-circle"></i> <i class="fa-solid fa-circle-user"></i>
{{ request.user }} {{ request.user }}
</a> </a>
{% else %} {% else %}
<a href="{% url 'people:person.create' %}?user" class="nav-link"> <a href="{% url 'people:person.create' %}?user" class="nav-link">
<i class="fas fa-user-circle"></i> <i class="fa-solid fa-circle-user"></i>
{{ request.user }} {{ request.user }}
</a> </a>
@@ -122,7 +124,7 @@
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link"> <a href="{% url 'logout' %}" class="nav-link">
<i class="fas fa-sign-out-alt"></i> <i class="fa-solid fa-right-from-bracket"></i>
Log Out Log Out
</a> </a>
</li> </li>
@@ -130,7 +132,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'login' %}" class="nav-link"> <a href="{% url 'login' %}" class="nav-link">
<i class="fas fa-sign-in-alt"></i> <i class="fa-solid fa-right-to-bracket"></i>
Log In Log In
</a> </a>
</li> </li>
@@ -149,8 +151,30 @@
</div> </div>
{% endif %} {% endif %}
{% load hijack_tags %} {% load hijack %}
{% hijack_notification %}
{# Hijack notification if user is hijacked #}
{% if person.user == request.user and request.user.is_hijacked %}
<div class="djhj" id="djhj">
<div class="djhj-notification">
<div class="djhj-message">
{% blocktrans trimmed with user=request.user %}
You are currently working on behalf of <em>{{ user }}</em>.
{% endblocktrans %}
</div>
<form action="{% url 'hijack:release' %}" method="POST" class="djhj-actions">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="djhj-button" onclick="document.getElementById('djhj').style.display = 'none';" type="button">
{% trans 'hide' %}
</button>
<button class="djhj-button" type="submit">
{% trans 'release' %}
</button>
</form>
</div>
</div>
{% endif %}
{% if request.user.is_authenticated and not request.user.has_person %} {% if request.user.is_authenticated and not request.user.has_person %}
<div class="alert alert-info rounded-0" role="alert"> <div class="alert alert-info rounded-0" role="alert">
@@ -189,7 +213,7 @@
<footer class="footer bg-light"> <footer class="footer bg-light">
<div class="container"> <div class="container">
<span class="text-muted">{{ settings.PROJECT_LONG_NAME }}</span> <span class="text-muted">{{ config.PROJECT_LONG_NAME }}</span>
</div> </div>
</footer> </footer>

View File

@@ -1,62 +1,89 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block extra_head %} {% block extra_head %}
{% load staticfiles %} {% load static %}
<link rel="stylesheet" <link rel="stylesheet"
href="{% static 'css/masthead.css' %}"> href="{% static 'css/masthead.css' %}">
{% endblock %} {% endblock %}
{% block before_content %} {% block before_content %}
{% get_media_prefix as MEDIA_URL %}
<header class="container-fluid masthead text-white text-left" <header class="container-fluid masthead text-white text-left"
style="background-image: url('https://via.placeholder.com/800x500')"> style="background-image: url('{{ MEDIA_URL }}{{ config.HOMEPAGE_HEADER_IMAGE }}')">
<div class="overlay"></div> <div class="overlay"></div>
<div class="row"> <div class="row">
<div class="ml-5 px-4 mt-3 pt-3 textbox-container"> <div class="ml-5 px-4 mt-3 pt-3 textbox-container">
<h1 class="display-1">{{ settings.PROJECT_LONG_NAME }}</h1> <h1 class="display-1">{{ config.PROJECT_LONG_NAME }}</h1>
<p class="lead">Snappy leader here...</p> <p class="lead">{{ config.PROJECT_LEAD }}</p>
</div> </div>
</div> </div>
</header> </header>
<div class="bg-secondary py-3"> <div class="bg-secondary py-3">
<div class="container text-white"> <div class="container text-white">
<h2>Snappy tagline here...</h2> <h2>{{ config.PROJECT_TAGLINE }}</h2>
</div> </div>
</div> </div>
<div class="bg-light py-2 mb-4"> <div class="bg-light py-2 mb-4">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-4"> {% if config.HOMEPAGE_CARD_1_TITLE %}
<div class="col-md-4 mx-auto">
<div class="card text-center"> <div class="card text-center">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Do Feature 1</h2> <h2 class="card-title">{{ config.HOMEPAGE_CARD_1_TITLE }}</h2>
<span class="fas fa-5x fa-atlas"></span> {% if config.HOMEPAGE_CARD_1_DESCRIPTION %}
</div> <p>{{ config.HOMEPAGE_CARD_1_DESCRIPTION|safe }}</p>
</div> {% endif %}
</div>
<div class="col-md-4"> {% if config.HOMEPAGE_CARD_1_ICON %}
<span class="fa-solid fa-5x fa-{{ config.HOMEPAGE_CARD_1_ICON }}"></span>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if config.HOMEPAGE_CARD_2_TITLE %}
<div class="col-md-4 mx-auto">
<div class="card text-center"> <div class="card text-center">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Do Feature 2</h2> <h2 class="card-title">{{ config.HOMEPAGE_CARD_2_TITLE }}</h2>
<span class="fas fa-5x fa-atlas"></span> {% if config.HOMEPAGE_CARD_2_DESCRIPTION %}
</div> <p>{{ config.HOMEPAGE_CARD_2_DESCRIPTION|safe }}</p>
</div> {% endif %}
</div>
<div class="col-md-4"> {% if config.HOMEPAGE_CARD_2_ICON %}
<span class="fa-solid fa-5x fa-{{ config.HOMEPAGE_CARD_2_ICON }}"></span>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if config.HOMEPAGE_CARD_3_TITLE %}
<div class="col-md-4 mx-auto">
<div class="card text-center"> <div class="card text-center">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Do Feature 3</h2> <h2 class="card-title">{{ config.HOMEPAGE_CARD_3_TITLE }}</h2>
<span class="fas fa-5x fa-atlas"></span> {% if config.HOMEPAGE_CARD_3_DESCRIPTION %}
<p>{{ config.HOMEPAGE_CARD_3_DESCRIPTION|safe }}</p>
{% endif %}
{% if config.HOMEPAGE_CARD_3_ICON %}
<span class="fa-solid fa-5x fa-{{ config.HOMEPAGE_CARD_3_ICON }}"></span>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -65,15 +92,16 @@
{% block content %} {% block content %}
<div class="row align-items-center" style="min-height: 400px;"> <div class="row align-items-center" style="min-height: 400px;">
<div class="col-sm-8"> <div class="col-sm-8">
<h2 class="pb-2">About {{ settings.PROJECT_LONG_NAME }}</h2> <h2 class="pb-2">{{ config.HOMEPAGE_ABOUT_TITLE }}</h2>
<p> <p>
{{ settings.PROJECT_LONG_NAME }} is... {{ config.HOMEPAGE_ABOUT_CONTENT|safe }}
</p> </p>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<img class="img-fluid py-3" src="https://via.placeholder.com/400x400"> {% get_media_prefix as MEDIA_URL %}
<img class="img-fluid py-3" src="{{ MEDIA_URL }}{{ config.HOMEPAGE_ABOUT_IMAGE }}">
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -14,10 +14,17 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.conf import settings
from django.urls import include, path from django.urls import include, path
from django.conf.urls.static import static
from . import views from . import views
from constance import config
admin.site.site_header = config.PROJECT_LONG_NAME + " Admin"
admin.site.site_title = config.PROJECT_SHORT_NAME + " Admin"
urlpatterns = [ urlpatterns = [
path('admin/', path('admin/',
admin.site.urls), admin.site.urls),

View File

@@ -1,5 +1,5 @@
all: all:
hosts: hosts:
example.com: example.com:
django_debug: 1 django_debug: 0
django_secret_key: debug_only_g62WlORMbo8iAcV7vKCKBQ== django_secret_key: debug_only_g62WlORMbo8iAcV7vKCKBQ==

View File

@@ -11,9 +11,13 @@
register: vagrant_dir register: vagrant_dir
vars: vars:
project_name: mapper project_name: network-mapper
project_dir: /srv/{{ project_name }} project_dir: /srv/{{ project_name }}
project_src_dir: "{{ project_dir }}/src" project_src_dir: "{{ project_dir }}/src"
provision_superuser: false
superuser_username: admin
superuser_password: admin
superuser_email: email@example.com
tasks: tasks:
- name: Vagrant specific tasks - name: Vagrant specific tasks
@@ -41,9 +45,10 @@
- name: Clone / update from source repos - name: Clone / update from source repos
ansible.builtin.git: ansible.builtin.git:
repo: 'https://github.com/Southampton-RSG/breccia-mapper.git' # repo: 'https://github.com/Southampton-RSG/breccia-mapper.git'
repo: 'https://github.com/mgrove36/breccia-network-mapper.git'
dest: '{{ project_src_dir }}' dest: '{{ project_src_dir }}'
version: docker version: dev # master
accept_hostkey: yes accept_hostkey: yes
- name: Copy template files - name: Copy template files
@@ -55,6 +60,12 @@
- Caddyfile - Caddyfile
- docker-compose.yml - docker-compose.yml
- name: Copy settings file
ansible.builtin.copy:
src: 'settings.ini'
dest: '{{ project_src_dir }}/breccia_mapper/settings.ini'
mode: 0600
- name: Create database file - name: Create database file
ansible.builtin.file: ansible.builtin.file:
path: "{{ project_dir }}/db.sqlite3" path: "{{ project_dir }}/db.sqlite3"
@@ -78,9 +89,21 @@
chdir: "{{ project_dir }}" chdir: "{{ project_dir }}"
cmd: docker compose build {{ item }} cmd: docker compose build {{ item }}
loop: loop:
- web - server
- name: Start containers - name: Start containers
ansible.builtin.command: ansible.builtin.command:
chdir: "{{ project_dir }}" chdir: "{{ project_dir }}"
cmd: docker compose up -d cmd: docker compose up -d
- name: Provision superuser
ansible.builtin.command:
chdir: "{{ project_dir }}"
cmd: sudo docker compose exec -it server /bin/bash -c "DJANGO_SUPERUSER_USERNAME='{{ superuser_username }}' DJANGO_SUPERUSER_PASSWORD='{{ superuser_password }}' DJANGO_SUPERUSER_EMAIL='{{ superuser_email }}' /app/manage.py createsuperuser --no-input"
when: provision_superuser
- name: Display warning about new superuser
debug:
msg:
- "[WARNING] A superuser has been provisioned with the username \"{{ superuser_username }}\" and password that was provided. This user has unlimited access to the network mapper."
when: provision_superuser

View File

@@ -0,0 +1,88 @@
[settings]
; Allowed hosts
; Accepted values for server header in request - protects against CSRF and CSS attacks
; Default: * if DEBUG else localhost
# ALLOWED_HOSTS=* if DEBUG else localhost
; Database URL
; URL to database - uses format described at https://github.com/jacobian/dj-database-url
; Default: sqlite://db.sqlite3
# DATABASE_URL=sqlite://db.sqlite3
; Database backup storage location
; Directory where database backups should be stored
; Default: .dbbackup
# DBBACKUP_STORAGE_LOCATION=.dbbackup
; Default language
; Default language - used for translation - has not been enabled
; Default: en-gb
# LANGUAGE_CODE=en-gb
; Timezone
; Default timezone
; Default: UTC
# TIME_ZONE=UTC
; Logging level
; Level of messages written to log file
; Default: INFO
# LOG_LEVEL=INFO
; Logging filename
; Path to logfile
; Default: debug.log
# LOG_FILENAME=debug.log
; Logging duration
; Number of days of logs to keep - logfile is rotated out at the end of each day
; Default: 14
# LOG_DAYS=14
; STMP host
; Hostname of SMTP server
; Default: None
# EMAIL_HOST=None
; Default from email address
; Email address from which messages are sent
; Default: None
# DEFAULT_FROM_EMAIL=None
; [DEBUG ONLY] Email file path
; Directory where emails will be stored if not using an SMTP server
; Default: mail.log
# EMAIL_FILE_PATH=mail.log
; SMTP username
; Username to authenticate with SMTP server
; Default: None
# EMAIL_HOST_USER=None
; SMTP password
; Password to authenticate with SMTP server
; Default: None
# EMAIL_HOST_PASSWORD=None
; SMTP port
; Port to access on SMTP server
; Default: 25
# EMAIL_PORT=25
; SMTP use TLS
; Use TLS to communicate with SMTP server? Usually on port 587
; Cannot be enabled at the same time as EMAIL_USE_SSL
; Default: True if EMAIL_PORT == 587 else False
# EMAIL_USE_TLS=True if EMAIL_PORT == 587 else False
; SMTP use SSL
; Use SSL to communicate with SMTP server? Usually on port 465
; Cannot be enabled at the same time as EMAIL_USE_TLS
; Default: True if EMAIL_PORT == 465 else False
# EMAIL_USE_SSL=True if EMAIL_PORT == 465 else False
; Google Maps API key
; Google Maps API key to display maps of people's locations
; Default: None
# GOOGLE_MAPS_API_KEY=None

7
deploy/templates/Caddyfile.j2 Normal file → Executable file
View File

@@ -1,15 +1,16 @@
http://* { :80 :443 {
root * /srv root * /srv
file_server file_server
@proxy_paths { @proxy_paths {
not path /static/* not path /static/*
not path /media/*
} }
reverse_proxy @proxy_paths http://web:8000 reverse_proxy @proxy_paths http://server:8000
log { log {
output stderr output stderr
format single_field common_log format console
} }
} }

11
deploy/templates/docker-compose.yml.j2 Normal file → Executable file
View File

@@ -1,8 +1,9 @@
version: '3.1' version: '3.1'
services: services:
web: server:
image: breccia-mapper image: breccia-network-mapper
container_name: network-mapper-server
build: {{ project_src_dir }} build: {{ project_src_dir }}
ports: ports:
- 8000:8000 - 8000:8000
@@ -13,9 +14,11 @@ services:
volumes: volumes:
- {{ project_dir }}/db.sqlite3:/app/db.sqlite3:z - {{ project_dir }}/db.sqlite3:/app/db.sqlite3:z
- static_files:/app/static - static_files:/app/static
- media_files:/app/media
caddy: caddy:
image: caddy:2 image: caddy:2
container_name: network-mapper-caddy
restart: unless-stopped restart: unless-stopped
ports: ports:
- 80:80 - 80:80
@@ -24,12 +27,14 @@ services:
- ./Caddyfile:/etc/caddy/Caddyfile:z - ./Caddyfile:/etc/caddy/Caddyfile:z
# Caddy serves static files collected by Django # Caddy serves static files collected by Django
- static_files:/srv/static:ro - static_files:/srv/static:ro
- media_files:/srv/media:ro
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
depends_on: depends_on:
- web - server
volumes: volumes:
caddy_data: caddy_data:
caddy_config: caddy_config:
static_files: static_files:
media_files:

View File

@@ -1,8 +1,9 @@
version: '3.1' version: '3.1'
services: services:
web: server:
image: breccia-mapper image: breccia-network-mapper
container_name: network-mapper-server
build: . build: .
ports: ports:
- 8000:8000 - 8000:8000
@@ -13,9 +14,11 @@ services:
volumes: volumes:
- ./db.sqlite3:/app/db.sqlite3:z - ./db.sqlite3:/app/db.sqlite3:z
- static_files:/app/static - static_files:/app/static
- media_files:/app/media
caddy: caddy:
image: caddy:2 image: caddy:2
container_name: network-mapper-caddy
restart: unless-stopped restart: unless-stopped
ports: ports:
- 80:80 - 80:80
@@ -24,12 +27,14 @@ services:
- ./Caddyfile:/etc/caddy/Caddyfile:z - ./Caddyfile:/etc/caddy/Caddyfile:z
# Caddy serves static files collected by Django # Caddy serves static files collected by Django
- static_files:/srv/static:ro - static_files:/srv/static:ro
- media_files:/srv/media:ro
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
depends_on: depends_on:
- web - server
volumes: volumes:
caddy_data: caddy_data:
caddy_config: caddy_config:
static_files: static_files:
media_files:

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

33
docs/source/conf.py Normal file
View File

@@ -0,0 +1,33 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'BRECcIA Network Mapper'
copyright = 'Matthew Grove, University of Southampton'
author = 'Matthew Grove'
release = '1.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.duration',
'myst_parser',
]
myst_enable_extensions = ['colon_fence']
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

147
docs/source/deployment.md Normal file
View File

@@ -0,0 +1,147 @@
# Deployment
The BRECcIA Network Mapper can be deployed in a variety of ways, most of which utilise Docker.
Ansible deployment has been tested on RHEL7 and RHEL8.
## Development Deployment
Prerequisites:
- [Vagrant](https://www.vagrantup.com/)
- [Ansible](https://www.ansible.com/)
Using Vagrant, we can create a virtual machine and deploy BRECcIA Mapper using the same provisioning scripts as a production deployment.
To deploy a local development version of BRECcIA Mapper inside a virtual machine, first navigate to the `deploy` folder:
```bash
cd deploy
```
And then set your config options for the deployment, by copying `settings.example.ini` to `settings.ini` and changing the options contained within as required.
And then start the virtual machine using:
```bash
vagrant up
```
If you would like a new superuser to be provisioned when deploying the network mapper, change the following line in `playbook.yml`:
```yaml
provision_superuser: false
```
to
```yaml
provision_superuser: true
```
And change the `superuser_*` options below it as desired.
Then provision the virtual machine (deploying the network mapper) using:
```bash
vagrant provision
```
This installs the network mapper and makes it available on the local machine at `http://localhost:8080`.
If you wish to make this accessible from other devices on your local network, replace the following line in `deploy/Vagrantfile`:
```ruby
config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
```
with:
```ruby
config.vm.network "forwarded_port", guest: 80, host: 8080
```
To stop the virtual machine run the following, in the `deploy` directory:
```
vagrant halt
```
For further commands see the [Vagrant documentation](https://www.vagrantup.com/docs/cli).
## Production Deployment
### Ansible (Recommended)
Prerequisites:
- [Ansible](https://www.ansible.com/)
:::{note}
Deployment with Ansible has been tested on RHEL7 and RHEL8, but is compatible with other Linux distributions with minor changes to the playbook (`deploy/playbook.yml`)
:::
To deploy the BRECcIA Network Mapper with Ansible:
1. Copy `settings.example.ini` to `settings.ini`
2. Edit this file as desired. Note there is no requirement to change any of these variables, but it is recommended.
3. Copy `inventory.example.yml` to `inventory.yml`
4. Edit this file to reflect your Ansible setup:
- Use your server's hostname instead of `example.com`
- Replace the secret key with some text known only to you
5. If you would like a new superuser to be provisioned for the network mapper (e.g. during initial install), edit the following line of `playbook.yml`:
```yaml
provision_superuser: false
```
to
```yaml
provision_superuser: true
```
And change the `superuser_*` options below it as desired.
6. Run the Ansible playbook `playbook.yml` with this inventory file using:
```
ansible-playbook playbook.yml -i inventory.yml -K -k -u <SSH username>
```
This will ask for your SSH and sudo passwords for the server before deploying.
To redeploy updates, the same command can be run again - it's safe to redeploy on top of an existing deployment.
### Docker Compose
Prerequisites:
- [Docker Compose](https://docs.docker.com/compose) (installed by default with most [Docker](https://docker.com/) installs)
:::{note}
Deployment with Docker has been tested on RHEL7, RHEL8, and Ubuntu 22.04 LTS
:::
To deploy the BRECcIA Network Mapper with Docker:
1. Copy `deploy/settings.example.ini` to `breccia_mapper/settings.ini`
2. Edit this file as desired. Note there is no requirement to change any of these variables, but it is recommended.
3. Create the database using:
```bash
touch db.sqlite3
```
4. Set the `DEBUG` and `SECRET_KEY` values in `docker-compose.yml`.
- The secret key should be a long, random string that only you know. Replace `${DJANGO_SECRET_KEY}` with this key.
- Debug can be `True` or `False`. Replace `${DJANGO_DEBUG}` with this value.
- You can also set these via environment variables on the host machine. The appropriate environment variables are `DJANGO_SECRET_KEY` and `DJANGO_DEBUG`.
5. Start the containers with the following command (you may need to use `sudo`):
```bash
docker compose up -d
```
6. Create a superuser by running the following, and enter their details when prompted:
```bash
docker compose exec -it server /bin/bash -c "/app/manage.py createsuperuser"
```

17
docs/source/index.md Normal file
View File

@@ -0,0 +1,17 @@
# BRECcIA Network Mapper documentation
The BRECcIA Network Mapper is a web app designed to track and quantify personnel networks & relationships - primarily developed for use in research projects. It is designed for global use across many different organisations involved in a single project, and has been utilised as part of [BRECcIA](https://gcrf-breccia.com) itself.
This work was funded through the "Building REsearch Capacity for sustainable water and food security In drylands of sub-saharan Africa" (BRECcIA) project which is supported by UK Research and Innovation as part of the Global Challenges Research Fund, grant number NE/P021093/1.
:::{note}
This project is still under development until April 2023.
:::
## Contents
```{toctree}
:maxdepth: 4
:glob:
*
```

View File

@@ -0,0 +1 @@
myst-parser

BIN
media/400x400.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
media/800x500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -6,9 +6,9 @@
"description": "Default welcome email template", "description": "Default welcome email template",
"created": "2020-04-27T12:13:30.448Z", "created": "2020-04-27T12:13:30.448Z",
"last_updated": "2020-04-27T14:45:27.152Z", "last_updated": "2020-04-27T14:45:27.152Z",
"subject": "Welcome to {{settings.PROJECT_LONG_NAME}}", "subject": "Welcome to {{config.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", "content": "Dear user,\r\n\r\nWelcome to {{ config.PROJECT_LONG_NAME }}. You can sign in at {{ config.DEPLOYMENT_URL }}.\r\n\r\nThanks,\r\n\r\nThe {{ config.PROJECT_SHORT_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", "html_content": "<h1>{{ config.PROJECT_LONG_NAME }}</h1><br/><p>Dear user,</p><br/><p>Welcome to {{ config.PROJECT_LONG_NAME }}. You can sign in <a href='{{ config.DEPLOYMENT_URL }}'>here</a>.</p><br/><p>Thanks,</p><p>The {{ config.PROJECT_SHORT_NAME }} team</p>",
"language": "", "language": "",
"default_template": null "default_template": null
} }

15
people/forms.py Normal file → Executable file
View File

@@ -3,9 +3,10 @@
import typing import typing
from django import forms from django import forms
from django.conf import settings
from bootstrap_datepicker_plus import DatePickerInput from constance import config
from bootstrap_datepicker_plus.widgets import DatePickerInput
from django_select2.forms import ModelSelect2Widget, Select2Widget, Select2MultipleWidget from django_select2.forms import ModelSelect2Widget, Select2Widget, Select2MultipleWidget
from . import models from . import models
@@ -121,7 +122,7 @@ class OrganisationAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
] ]
labels = { labels = {
'is_partner_organisation': 'is_partner_organisation':
f'Is this organisation a {settings.PARENT_PROJECT_NAME} partner organisation?' f'Is this organisation a {config.PARENT_PROJECT_NAME} partner organisation?'
} }
widgets = { widgets = {
'countries': Select2MultipleWidget(), 'countries': Select2MultipleWidget(),
@@ -185,14 +186,14 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
widgets = { widgets = {
'nationality': Select2MultipleWidget(), 'nationality': Select2MultipleWidget(),
'country_of_residence': Select2Widget(), 'country_of_residence': Select2Widget(),
'organisation_started_date': DatePickerInput(format='%Y-%m-%d'), 'organisation_started_date': DatePickerInput(),
'project_started_date': DatePickerInput(format='%Y-%m-%d'), 'project_started_date': DatePickerInput(),
'latitude': forms.HiddenInput, 'latitude': forms.HiddenInput,
'longitude': forms.HiddenInput, 'longitude': forms.HiddenInput,
} }
labels = { labels = {
'project_started_date': 'project_started_date':
f'Date started on the {settings.PARENT_PROJECT_NAME} project', f'Date started on the {config.PARENT_PROJECT_NAME} project',
'external_organisations': 'external_organisations':
'Please list the main organisations external to BRECcIA work that you have been working with since 1st January 2019 that are involved in food/water security in African dryland regions' 'Please list the main organisations external to BRECcIA work that you have been working with since 1st January 2019 that are involved in food/water security in African dryland regions'
} }
@@ -325,7 +326,7 @@ class OrganisationRelationshipAnswerSetForm(forms.ModelForm,
class DateForm(forms.Form): class DateForm(forms.Form):
date = forms.DateField( date = forms.DateField(
required=False, required=False,
widget=DatePickerInput(format='%Y-%m-%d'), widget=DatePickerInput(),
help_text='Show relationships as they were on this date' help_text='Show relationships as they were on this date'
) )

4
people/migrations/0002_add_relationship_models.py Normal file → Executable file
View File

@@ -50,10 +50,10 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='relationshipquestionchoice', model_name='relationshipquestionchoice',
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'), constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer_relationshipquestionchoice'),
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='relationship', model_name='relationship',
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship'), constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship_relationship'),
), ),
] ]

2
people/migrations/0022_refactor_person_questions.py Normal file → Executable file
View File

@@ -50,6 +50,6 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='personquestionchoice', model_name='personquestionchoice',
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'), constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer_personquestionchoice'),
), ),
] ]

2
people/migrations/0035_add_organisation_questions.py Normal file → Executable file
View File

@@ -56,6 +56,6 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='organisationquestionchoice', model_name='organisationquestionchoice',
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'), constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer_organisationquestionchoice'),
), ),
] ]

View File

@@ -67,10 +67,10 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='organisationrelationshipquestionchoice', model_name='organisationrelationshipquestionchoice',
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'), constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer_organisationrelationshipquestionchoice'),
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='organisationrelationship', model_name='organisationrelationship',
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship'), constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship_organisationrelationship'),
), ),
] ]

View File

@@ -0,0 +1,119 @@
# Generated by Django 4.1.4 on 2023-01-05 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('people', '0054_add_option_for_auto_negative_response'),
]
operations = [
migrations.RemoveConstraint(
model_name='organisationrelationship',
name='unique_relationship_organisationrelationship',
),
migrations.RemoveConstraint(
model_name='relationship',
name='unique_relationship_relationship',
),
migrations.AlterField(
model_name='organisation',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationanswerset',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationquestion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationquestionchoice',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationrelationship',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationrelationshipanswerset',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationrelationshipquestion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='organisationrelationshipquestionchoice',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='person',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='personanswerset',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='personquestion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='personquestionchoice',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='relationship',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='relationshipanswerset',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='relationshipquestion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='relationshipquestionchoice',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='user',
name='first_name',
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AddConstraint(
model_name='organisationrelationship',
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship_modelorganisationrelationship'),
),
migrations.AddConstraint(
model_name='relationship',
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship_modelrelationship'),
),
]

5
people/models/organisation.py Normal file → Executable file
View File

@@ -35,6 +35,11 @@ class OrganisationQuestionChoice(QuestionChoice):
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False)
class Meta(QuestionChoice.Meta):
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer_organisationquestionchoice')
]
class Organisation(models.Model): class Organisation(models.Model):

14
people/models/person.py Normal file → Executable file
View File

@@ -8,9 +8,10 @@ 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 post_office import mail
from constance import config
from .organisation import Organisation from .organisation import Organisation
from .question import AnswerSet, Question, QuestionChoice from .question import AnswerSet, Question, QuestionChoice
@@ -43,10 +44,10 @@ class User(AbstractUser):
def send_welcome_email(self) -> None: def send_welcome_email(self) -> None:
"""Send a welcome email to a new user.""" """Send a welcome email to a new user."""
# Get exported data from settings.py first # Get exported data from settings.py first
context = settings_export(None) context = {
context.update({
'user': self, 'user': self,
}) 'config': config,
}
logger.info('Sending welcome mail to user \'%s\'', self.username) logger.info('Sending welcome mail to user \'%s\'', self.username)
@@ -77,6 +78,11 @@ class PersonQuestionChoice(QuestionChoice):
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False)
class Meta(QuestionChoice.Meta):
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer_personquestionchoice')
]
class Person(models.Model): class Person(models.Model):

2
people/models/question.py Normal file → Executable file
View File

@@ -92,7 +92,7 @@ class QuestionChoice(models.Model):
abstract = True abstract = True
constraints = [ constraints = [
models.UniqueConstraint(fields=['question', 'text'], models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer') name='unique_question_answer_modelquestionchoice')
] ]
ordering = [ ordering = [
'question__order', 'question__order',

14
people/models/relationship.py Normal file → Executable file
View File

@@ -33,6 +33,11 @@ class RelationshipQuestionChoice(QuestionChoice):
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False)
class Meta(QuestionChoice.Meta):
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer_relationshipquestionchoice')
]
class Relationship(models.Model): class Relationship(models.Model):
@@ -40,7 +45,7 @@ class Relationship(models.Model):
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=['source', 'target'], models.UniqueConstraint(fields=['source', 'target'],
name='unique_relationship'), name='unique_relationship_modelrelationship'),
] ]
#: Person reporting the relationship #: Person reporting the relationship
@@ -122,6 +127,11 @@ class OrganisationRelationshipQuestionChoice(QuestionChoice):
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False)
class Meta(QuestionChoice.Meta):
constraints = [
models.UniqueConstraint(fields=['question', 'text'],
name='unique_question_answer_organisationrelationshipquestionchoice')
]
class OrganisationRelationship(models.Model): class OrganisationRelationship(models.Model):
@@ -129,7 +139,7 @@ class OrganisationRelationship(models.Model):
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=['source', 'target'], models.UniqueConstraint(fields=['source', 'target'],
name='unique_relationship'), name='unique_relationship_modelorganisationrelationship'),
] ]
#: Person reporting the relationship #: Person reporting the relationship

2
people/templates/people/map.html Normal file → Executable file
View File

@@ -3,7 +3,7 @@
{% block extra_head %} {% block extra_head %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
{% load staticfiles %} {% load static %}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>
<script async defer <script async defer

2
people/templates/people/network.html Normal file → Executable file
View File

@@ -97,6 +97,6 @@
integrity="sha512-Qlv6VSKh1gDKGoJbnyA5RMXYcvnpIqhO++MhIM2fStMcGT9i2T//tSwYFlcyoRRDcDZ+TYHpH8azBBCyhpSeqw==" integrity="sha512-Qlv6VSKh1gDKGoJbnyA5RMXYcvnpIqhO++MhIM2fStMcGT9i2T//tSwYFlcyoRRDcDZ+TYHpH8azBBCyhpSeqw=="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
{% load staticfiles %} {% load static %}
<script src="{% static 'js/network.js' %}"></script> <script src="{% static 'js/network.js' %}"></script>
{% endblock %} {% endblock %}

2
people/templates/people/organisation/detail.html Normal file → Executable file
View File

@@ -3,7 +3,7 @@
{% block extra_head %} {% block extra_head %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
{% load staticfiles %} {% load static %}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap" <script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"

2
people/templates/people/organisation/update.html Normal file → Executable file
View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block extra_head %} {% block extra_head %}
{% load staticfiles %} {% load static %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>

10
people/templates/people/person/detail_full.html Normal file → Executable file
View File

@@ -3,7 +3,7 @@
{% block extra_head %} {% block extra_head %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
{% load staticfiles %} {% load static %}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap" <script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"
@@ -64,16 +64,18 @@
<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>
{% load hijack_tags %} {% load hijack %}
{% if person.user == request.user and not request|is_hijacked %} {% if person.user == request.user and not request.user.is_hijacked %}
<a class="btn btn-info" <a class="btn btn-info"
href="{% url 'password_change' %}?next={{ person.get_absolute_url }}">Change Password</a> href="{% url 'password_change' %}?next={{ person.get_absolute_url }}">Change Password</a>
{% endif %} {% endif %}
{% if request.user.is_superuser and person.user and person.user != request.user %} {% if request.user.is_superuser and person.user and person.user != request.user %}
<form style="display: inline;" action="/hijack/{{ person.user.pk }}/" method="post"> <form action="{% url 'hijack:acquire' %}" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user_pk" value="{{ person.pk }}">
<button class="btn btn-warning" type="submit">Become {{ person.name }}</button> <button class="btn btn-warning" type="submit">Become {{ person.name }}</button>
<input type="hidden" name="next" value="{{ request.path }}">
</form> </form>
{% endif %} {% endif %}

2
people/templates/people/person/detail_partial.html Normal file → Executable file
View File

@@ -3,7 +3,7 @@
{% block extra_head %} {% block extra_head %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
{% load staticfiles %} {% load static %}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap" <script async defer src="https://maps.googleapis.com/maps/api/js?key={{ settings.GOOGLE_MAPS_API_KEY }}&callback=initMap"

2
people/templates/people/person/update.html Normal file → Executable file
View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block extra_head %} {% block extra_head %}
{% load staticfiles %} {% load static %}
{{ map_markers|json_script:'map-markers' }} {{ map_markers|json_script:'map-markers' }}
<script src="{% static 'js/map.js' %}"></script> <script src="{% static 'js/map.js' %}"></script>

2
people/templates/people/relationship/detail.html Normal file → Executable file
View File

@@ -47,7 +47,7 @@
<div class="col-md-2 text-center"> <div class="col-md-2 text-center">
{% if relationship.reverse %} {% if relationship.reverse %}
<a href="{% url 'people:relationship.detail' pk=relationship.reverse.pk %}"> <a href="{% url 'people:relationship.detail' pk=relationship.reverse.pk %}">
<span class="fas fa-exchange-alt fa-5x"></span> <span class="fa-solid fa-right-left fa-5x"></span>
</a> </a>
{% endif %} {% endif %}
</div> </div>

2
people/templates/people/relationship/update.html Normal file → Executable file
View File

@@ -23,6 +23,6 @@
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}
{% load staticfiles %} {% load static %}
<script async defer src="{% static 'js/hide_free_text.js' %}"></script> <script async defer src="{% static 'js/hide_free_text.js' %}"></script>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,7 @@
import typing import typing
from django.conf import settings from constance import config
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone from django.utils import timezone
@@ -49,11 +50,11 @@ class OrganisationListView(LoginRequiredMixin, ListView):
orgs_sorted = {} orgs_sorted = {}
try_copy_by_key(orgs_by_country, orgs_sorted, try_copy_by_key(orgs_by_country, orgs_sorted,
f'{settings.PARENT_PROJECT_NAME} partners') f'{config.PARENT_PROJECT_NAME} partners')
try_copy_by_key(orgs_by_country, orgs_sorted, 'International') try_copy_by_key(orgs_by_country, orgs_sorted, 'International')
special = { special = {
f'{settings.PARENT_PROJECT_NAME} partners', 'International', f'{config.PARENT_PROJECT_NAME} partners', 'International',
'Unknown' 'Unknown'
} }
for country in sorted(k for k in orgs_by_country.keys() for country in sorted(k for k in orgs_by_country.keys()
@@ -81,7 +82,7 @@ class OrganisationListView(LoginRequiredMixin, ListView):
country = 'International' country = 'International'
if answers.is_partner_organisation: if answers.is_partner_organisation:
country = f'{settings.PARENT_PROJECT_NAME} partners' country = f'{config.PARENT_PROJECT_NAME} partners'
except AttributeError: except AttributeError:
# Organisation has no AnswerSet - country is 'Unknown' # Organisation has no AnswerSet - country is 'Unknown'

76
requirements.txt Normal file → Executable file
View File

@@ -1,46 +1,46 @@
astroid==2.3.3 astroid==2.12.13
beautifulsoup4==4.8.2 beautifulsoup4==4.11.1
dj-database-url==0.5.0 dj-database-url==1.2.0
Django==2.2.10 Django==4.1.4
django-appconf==1.0.3 django-appconf==1.0.5
django-bootstrap4==1.1.1 django-bootstrap4==22.3
django-bootstrap-datepicker-plus==3.0.5 django-bootstrap-datepicker-plus==5.0.2
django-compat==1.0.15 django-constance==2.9.1
django-constance==2.6.0 django-countries==7.5
django-countries==5.5 django-dbbackup==4.0.2
django-dbbackup==3.2.0 django-filter==22.1
django-filter==2.2.0 django-hijack==3.2.6
django-hijack==2.2.1 django-picklefield==3.1
django-picklefield==2.1.1 django-post-office==3.6.3
django-post-office==3.4.0 django-select2==8.0.0
django-select2==7.2.0
django-settings-export==1.2.1 django-settings-export==1.2.1
djangorestframework==3.11.0 djangorestframework==3.14.0
dodgy==0.2.1 dodgy==0.2.1
isort==4.3.21 isort==5.11.4
jsonfield==3.1.0 jsonfield==3.1.0
lazy-object-proxy==1.4.3 lazy-object-proxy==1.8.0
mccabe==0.6.1 mccabe==0.7.0
# mysqlclient==1.4.6 # mysqlclient==1.4.6
pep8-naming==0.4.1 pep8-naming==0.10.0
prospector==1.2.0 prospector==1.8.3
pycodestyle==2.4.0 pycodestyle==2.10.0
pydocstyle==5.0.2 pydocstyle==6.1.1
pyflakes==2.1.1 pyflakes==2.5.0
pylint==2.4.4 pylint==2.15.9
pylint-celery==0.3 pylint-celery==0.3
pylint-django==2.0.12 pylint-django==2.5.3
pylint-flask==0.6 pylint-flask==0.6
pylint-plugin-utils==0.6 pylint-plugin-utils==0.7
python-decouple==3.3 python-decouple==3.6
pytz==2019.3 pytz==2022.7
pyuca==1.2 pyuca==1.2
PyYAML==5.3 PyYAML==6.0
requirements-detector==0.6 requirements-detector==1.0.3
setoptconf==0.2.0 setoptconf==0.3.0
six==1.14.0 six==1.16.0
snowballstemmer==2.0.0 snowballstemmer==2.2.0
soupsieve==1.9.5 soupsieve==2.3.2.post1
sqlparse==0.3.0 sqlparse==0.4.3
typed-ast typed-ast
wrapt==1.11.2 wrapt==1.14.1
Pillow==9.4.0