mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 19:37:06 +00:00
Compare commits
68 Commits
v1.0.0
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 809894a04c | |||
| 9967c081c9 | |||
| 555aae5d1c | |||
| 905613d8e9 | |||
| e3ea56ce32 | |||
| d66e0ea21a | |||
| 50f5363b69 | |||
| de11df1999 | |||
| 35d8d9423a | |||
| 347e189a96 | |||
| e15de01cfa | |||
| 0d03678674 | |||
| 08c004d42d | |||
| 5fca39f32c | |||
| 8782b879a1 | |||
| d5eef1a296 | |||
| dcf08830f4 | |||
| 361ab675d3 | |||
| e1ba0df894 | |||
| f0dee9794f | |||
| 19698270e5 | |||
| f0f03c1175 | |||
| 8427b78ba6 | |||
| 62a28c6c66 | |||
| bb7568885d | |||
| 31a2367514 | |||
| b0e51bcfd6 | |||
| a8f119a1df | |||
| 2ad21765c0 | |||
| 7289774dc7 | |||
| 107e0f317b | |||
| 2d5f3e3b2e | |||
| ba44a34946 | |||
| 0c90b72c7f | |||
| 38d6e7e9cc | |||
| a57aee1ea7 | |||
| 04f9f81408 | |||
| e75631f8a5 | |||
| c0871a7b51 | |||
| 822b6e95aa | |||
| 7e9bbd2792 | |||
| b00a4dcb8e | |||
| 7555d4bd2b | |||
| bf896d07b0 | |||
| 539fb8daa3 | |||
| 35240b25ba | |||
| 4d38b2f8f0 | |||
| df0d9aa902 | |||
| 011a01d176 | |||
| 2a08ddba45 | |||
| bcba36a421 | |||
| d6bd47b4ea | |||
| 2c1ebbc399 | |||
| 330ec1afe1 | |||
| 2096534085 | |||
| 8608a350f3 | |||
| 4e82d363bc | |||
| cbd2158307 | |||
| 0355392675 | |||
| c5dca62b0b | |||
| 050a69d5ea | |||
| 07c15281c0 | |||
| 15bbf2f7d1 | |||
| 7e2491be76 | |||
| 770b4f1114 | |||
| 4b37c202f4 | |||
| de111bea55 | |||
| 6e2267a4b6 |
@@ -16,3 +16,6 @@ mail.log/
|
||||
*.sqlite3*
|
||||
*.log*
|
||||
deployment*
|
||||
|
||||
docs/
|
||||
.readthedocs.yaml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,3 +25,6 @@ deployment-key*
|
||||
/custom
|
||||
staging.yml
|
||||
production.yml
|
||||
|
||||
# Docs local builds
|
||||
/docs/build
|
||||
25
.readthedocs.yaml
Normal file
25
.readthedocs.yaml
Normal 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
5
Caddyfile
Normal file → Executable file
@@ -4,12 +4,13 @@
|
||||
|
||||
@proxy_paths {
|
||||
not path /static/*
|
||||
not path /media/*
|
||||
}
|
||||
|
||||
reverse_proxy @proxy_paths http://web:8000
|
||||
reverse_proxy @proxy_paths http://server:8000
|
||||
|
||||
log {
|
||||
output stderr
|
||||
format single_field common_log
|
||||
format console
|
||||
}
|
||||
}
|
||||
2
Dockerfile
Normal file → Executable file
2
Dockerfile
Normal file → Executable 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
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# BRECcIA Mapper
|
||||
|
||||
[](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.
|
||||
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.
|
||||
@@ -14,6 +16,7 @@ Deployment is managed using [Ansible](https://www.ansible.com/) and Docker (http
|
||||
## Contributors
|
||||
|
||||
- James Graham (@jag1g13) - developer
|
||||
- Matthew Grove (@mgrove36) - developer
|
||||
- Genevieve Agaba
|
||||
- Sebastian Reichel
|
||||
- Claire Bedelian
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -13,20 +13,8 @@ Before production deployment, see
|
||||
https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
|
||||
Many configuration settings are input from `settings.ini`.
|
||||
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
|
||||
Many configuration settings are input from `.env`.
|
||||
The most likely required settings are: SECRET_KEY, DEBUG, ALLOWED_HOSTS, PROJECT_*_NAME, EMAIL_*
|
||||
|
||||
- SECRET_KEY (REQUIRED)
|
||||
Used to generate CSRF tokens - must never be made public
|
||||
@@ -35,14 +23,38 @@ The most likely required settings are: SECRET_KEY, DEBUG, ALLOWED_HOSTS, DATABAS
|
||||
default: False
|
||||
Should the server run in debug mode? Provides information to users which is unsafe in production
|
||||
|
||||
- SITE_URL
|
||||
default: localhost
|
||||
The URL the site will be deployed on. Do not include http://, https://, or a trailing slash.
|
||||
|
||||
- SITE_PROTOCOL
|
||||
default: http
|
||||
The protocol the site uses. Valid options are http or https.
|
||||
|
||||
- PROJECT_LONG_NAME
|
||||
default: Project Network Mapper
|
||||
The project's full name.
|
||||
|
||||
- PROJECT_SHORT_NAME
|
||||
default: Network Mapper
|
||||
The project's short/abbreviated name. This will also be used as the app's name when installed as PWA.
|
||||
|
||||
- PROJECT_DESCRIPTION
|
||||
default: Application to map network relationships in the organisation.
|
||||
The project's description. Used when installed as a PWA.
|
||||
|
||||
- THEME_COLOR
|
||||
default: 212121
|
||||
The project's theme color, in hex format (excluding the leading #).
|
||||
|
||||
- BACKGROUND_COLOR
|
||||
default: ffffff
|
||||
The project's background color, in hex format (excluding the leading #).
|
||||
|
||||
- ALLOWED_HOSTS
|
||||
default: * if DEBUG else localhost
|
||||
Accepted values for server header in request - protects against CSRF and CSS attacks
|
||||
|
||||
- DATABASE_URL
|
||||
default: sqlite://db.sqlite3
|
||||
URL to database - uses format described at https://github.com/jacobian/dj-database-url
|
||||
|
||||
- DBBACKUP_STORAGE_LOCATION
|
||||
default: .dbbackup
|
||||
Directory where database backups should be stored
|
||||
@@ -118,16 +130,16 @@ import dj_database_url
|
||||
|
||||
SETTINGS_EXPORT = [
|
||||
'DEBUG',
|
||||
'PARENT_PROJECT_NAME',
|
||||
'SITE_URL',
|
||||
'SITE_PROTOCOL',
|
||||
'GOOGLE_MAPS_API_KEY',
|
||||
'PROJECT_LONG_NAME',
|
||||
'PROJECT_SHORT_NAME',
|
||||
'GOOGLE_MAPS_API_KEY',
|
||||
'PROJECT_DESCRIPTION',
|
||||
'THEME_COLOR',
|
||||
'BACKGROUND_COLOR',
|
||||
]
|
||||
|
||||
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(...)
|
||||
BASE_DIR = pathlib.Path(__file__).parent.parent
|
||||
@@ -143,6 +155,27 @@ ALLOWED_HOSTS = config(
|
||||
default='*' if DEBUG else '127.0.0.1,localhost,localhost.localdomain',
|
||||
cast=Csv())
|
||||
|
||||
# Site URL
|
||||
SITE_URL = config('SITE_URL', default='localhost')
|
||||
SITE_PROTOCOL = config('SITE_PROTOCOL', default='http')
|
||||
|
||||
# CORS settings
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CSRF_TRUSTED_ORIGINS = config(
|
||||
'TRUSTED_ORIGINS',
|
||||
default='*' if DEBUG else 'http://127.0.0.1,http://localhost,http://localhost.localdomain',
|
||||
cast=Csv())
|
||||
CORS_REPLACE_HTTPS_REFERER = True
|
||||
CSRF_COOKIE_DOMAIN = config(
|
||||
'SITE_URL',
|
||||
default='localhost')
|
||||
CORS_ORIGIN_WHITELIST = config(
|
||||
'TRUSTED_ORIGINS',
|
||||
default='*' if DEBUG else 'http://127.0.0.1,http://localhost,http://localhost.localdomain',
|
||||
cast=Csv())
|
||||
|
||||
# Application definition
|
||||
|
||||
DJANGO_APPS = [
|
||||
@@ -165,7 +198,7 @@ THIRD_PARTY_APPS = [
|
||||
'post_office',
|
||||
'bootstrap_datepicker_plus',
|
||||
'hijack',
|
||||
'compat',
|
||||
'pwa',
|
||||
]
|
||||
|
||||
FIRST_PARTY_APPS = [
|
||||
@@ -184,6 +217,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'hijack.middleware.HijackUserMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'breccia_mapper.urls'
|
||||
@@ -212,10 +246,7 @@ WSGI_APPLICATION = 'breccia_mapper.wsgi.application'
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default':
|
||||
config('DATABASE_URL',
|
||||
default='sqlite:///' + str(BASE_DIR.joinpath('db.sqlite3')),
|
||||
cast=dj_database_url.parse)
|
||||
'default': dj_database_url.parse('sqlite:///' + str(BASE_DIR.joinpath('db.sqlite3')))
|
||||
}
|
||||
|
||||
# Django DBBackup
|
||||
@@ -297,6 +328,10 @@ STATIC_ROOT = BASE_DIR.joinpath('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 = {
|
||||
@@ -340,6 +375,10 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
# Admin panel variables
|
||||
|
||||
CONSTANCE_ADDITIONAL_FIELDS = {
|
||||
'image_field': ['django.forms.ImageField', {}]
|
||||
}
|
||||
|
||||
CONSTANCE_CONFIG = {
|
||||
'NOTICE_TEXT': (
|
||||
'',
|
||||
@@ -359,17 +398,91 @@ CONSTANCE_CONFIG = {
|
||||
'RELATIONSHIP_FORM_HELP': (
|
||||
'',
|
||||
'Help text to display at the top of relationship forms.'),
|
||||
'PARENT_PROJECT_NAME': (
|
||||
'',
|
||||
'Parent project 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_HEADER_IMAGE_SHRINK': (
|
||||
False,
|
||||
'Shrink the homepage header image to display the whole image at all times'),
|
||||
'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
|
||||
|
||||
CONSTANCE_CONFIG_FIELDSETS = {
|
||||
'Notice Banner': (
|
||||
'Project options': (
|
||||
'PARENT_PROJECT_NAME',
|
||||
'PROJECT_LEAD',
|
||||
'PROJECT_TAGLINE',
|
||||
),
|
||||
'Homepage configuration': (
|
||||
'HOMEPAGE_HEADER_IMAGE_SHRINK',
|
||||
'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_CLASS',
|
||||
),
|
||||
'Data Collection': (
|
||||
'CONSENT_TEXT',
|
||||
),
|
||||
'Help Text': (
|
||||
'Help text': (
|
||||
'PERSON_LIST_HELP',
|
||||
'ORGANISATION_LIST_HELP',
|
||||
'RELATIONSHIP_FORM_HELP',
|
||||
@@ -378,6 +491,12 @@ CONSTANCE_CONFIG_FIELDSETS = {
|
||||
|
||||
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
||||
|
||||
PROJECT_LONG_NAME = config('PROJECT_LONG_NAME', 'Project Network Mapper')
|
||||
PROJECT_SHORT_NAME = config('PROJECT_SHORT_NAME', 'Network Mapper')
|
||||
PROJECT_DESCRIPTION = config('PROJECT_DESCRIPTION', 'Application to map network relationships in the organisation.')
|
||||
THEME_COLOR = '#' + config('THEME_COLOR', '212121')
|
||||
BACKGROUND_COLOR = '#' + config('BACKGROUND_COLOR', 'ffffff')
|
||||
|
||||
# Django Hijack settings
|
||||
# See https://django-hijack.readthedocs.io/en/stable/
|
||||
|
||||
@@ -417,6 +536,53 @@ else:
|
||||
default=(EMAIL_PORT == 465),
|
||||
cast=bool)
|
||||
|
||||
# Bootstrap Datepicker Plus Settings
|
||||
BOOTSTRAP_DATEPICKER_PLUS = {
|
||||
"variant_options": {
|
||||
"date": {
|
||||
"format": "YYYY-MM-DD",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# PWA settings
|
||||
|
||||
PWA_SERVICE_WORKER_PATH = BASE_DIR.joinpath('static/js', 'serviceworker.js')
|
||||
|
||||
PWA_APP_NAME = PROJECT_SHORT_NAME
|
||||
PWA_APP_DESCRIPTION = PROJECT_DESCRIPTION
|
||||
PWA_APP_THEME_COLOR = THEME_COLOR
|
||||
PWA_APP_BACKGROUND_COLOR = BACKGROUND_COLOR
|
||||
PWA_APP_DISPLAY = 'standalone'
|
||||
PWA_APP_SCOPE = '/'
|
||||
PWA_APP_ORIENTATION = 'any'
|
||||
PWA_APP_START_URL = '/'
|
||||
PWA_APP_STATUS_BAR_COLOR = 'default'
|
||||
PWA_APP_ICONS = [
|
||||
{
|
||||
'src': '/media/icon-192x192.png',
|
||||
'sizes': '192x192'
|
||||
}
|
||||
]
|
||||
PWA_APP_ICONS_APPLE = [
|
||||
{
|
||||
'src': '/media/icon-192x192.png',
|
||||
'sizes': '192x192'
|
||||
}
|
||||
]
|
||||
PWA_APP_SPLASH_SCREEN = [
|
||||
{
|
||||
'src': '/media/icon-192x192.png',
|
||||
'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)'
|
||||
}
|
||||
]
|
||||
PWA_APP_DIR = 'ltr'
|
||||
PWA_APP_LANG = 'en-GB'
|
||||
PWA_APP_DEBUG_MODE = DEBUG
|
||||
|
||||
# Database default automatic primary key
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Upstream API keys
|
||||
|
||||
GOOGLE_MAPS_API_KEY = config('GOOGLE_MAPS_API_KEY', default=None)
|
||||
|
||||
@@ -18,4 +18,11 @@ body {
|
||||
line-height: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer .container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
/* end of sticky footer styles */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
header.masthead {
|
||||
position: relative;
|
||||
background: #343a40 no-repeat center;
|
||||
-webkit-background-size: contain;
|
||||
-moz-background-size: contain;
|
||||
-o-background-size: contain;
|
||||
background-size: contain;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
background-size: cover;
|
||||
padding-top: 8rem;
|
||||
padding-bottom: 8rem;
|
||||
min-height: 400px;
|
||||
@@ -12,6 +12,13 @@ header.masthead {
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
header.masthead.masthead-shrink {
|
||||
-webkit-background-size: contain;
|
||||
-moz-background-size: contain;
|
||||
-o-background-size: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
header.masthead .overlay {
|
||||
position: absolute;
|
||||
background-color: #212529;
|
||||
|
||||
47
breccia_mapper/static/js/serviceworker.js
Normal file
47
breccia_mapper/static/js/serviceworker.js
Normal file
@@ -0,0 +1,47 @@
|
||||
var staticCacheName = "django-pwa-v" + new Date().getTime();
|
||||
var filesToCache = [
|
||||
"/offline",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/solid.min.css",
|
||||
"/static/css/global.css",
|
||||
"/static/hijack/hijack.min.css",
|
||||
"/media/icon-192x192.png",
|
||||
];
|
||||
|
||||
// Cache on install
|
||||
self.addEventListener("install", event => {
|
||||
this.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(staticCacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
// Clear cache on activate
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => (cacheName.startsWith("django-pwa-")))
|
||||
.filter(cacheName => (cacheName !== staticCacheName))
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Serve from Cache
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('offline');
|
||||
})
|
||||
)
|
||||
});
|
||||
60
breccia_mapper/templates/base.html
Normal file → Executable file
60
breccia_mapper/templates/base.html
Normal file → Executable file
@@ -6,6 +6,10 @@
|
||||
{% endif %}
|
||||
<html lang="{{ LANGUAGE_CODE|default:'en_us' }}">
|
||||
|
||||
{% load pwa %}
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
@@ -16,21 +20,23 @@
|
||||
{% bootstrap_css %}
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/fontawesome.min.css"
|
||||
integrity="sha256-/sdxenK1NDowSNuphgwjv8wSosSNZB0t5koXqd7XqOI="
|
||||
crossorigin="anonymous" />
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css"
|
||||
integrity="sha512-giQeaPns4lQTBMRpOOHsYnGw1tGVzbAIHUyHRgn7+6FmiEgGGjaG0T2LZJmAPMzRCl+Cug0ItQ2xDZpTmEc+CQ=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css"
|
||||
integrity="sha256-8DcgqUGhWHHsTLj1qcGr0OuPbKkN1RwDjIbZ6DKh/RA="
|
||||
crossorigin="anonymous" />
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/solid.min.css"
|
||||
integrity="sha512-6mc0R607di/biCutMUtU9K7NtNewiGQzrvWX4bWTeqmljZdJrwYvKJtnhgR+Ryvj+NRJ8+NnnCM/biGqMe/iRA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/global.css' %}">
|
||||
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{% static 'hijack/hijack-styles.css' %}" />
|
||||
href="{% static 'hijack/hijack.min.css' %}" />
|
||||
|
||||
{% if 'javascript_in_head'|bootstrap_setting %}
|
||||
{% if 'include_jquery'|bootstrap_setting %}
|
||||
@@ -46,6 +52,9 @@
|
||||
{{ form.media.css }}
|
||||
{% endif %}
|
||||
|
||||
<!-- PWA metadata -->
|
||||
{% progressive_web_app_meta %}
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
|
||||
</head>
|
||||
@@ -107,13 +116,13 @@
|
||||
<li class="nav-item">
|
||||
{% if request.user.person %}
|
||||
<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 }}
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
<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 }}
|
||||
</a>
|
||||
|
||||
@@ -122,7 +131,7 @@
|
||||
|
||||
<li class="nav-item">
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
@@ -130,7 +139,7 @@
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
@@ -149,8 +158,30 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% load hijack_tags %}
|
||||
{% hijack_notification %}
|
||||
{% load hijack %}
|
||||
|
||||
{# 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 %}
|
||||
<div class="alert alert-info rounded-0" role="alert">
|
||||
@@ -190,6 +221,7 @@
|
||||
<footer class="footer bg-light">
|
||||
<div class="container">
|
||||
<span class="text-muted">{{ settings.PROJECT_LONG_NAME }}</span>
|
||||
<span class="text-muted">Developed by the <a href="https://gcrf-breccia.com">BRECcIA</a> team</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -1,62 +1,89 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'css/masthead.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block before_content %}
|
||||
<header class="container-fluid masthead text-white text-left"
|
||||
style="background-image: url('https://via.placeholder.com/800x500')">
|
||||
|
||||
{% get_media_prefix as MEDIA_URL %}
|
||||
<header class="container-fluid masthead text-white text-left {% if config.HOMEPAGE_HEADER_IMAGE_SHRINK %}masthead-shrink{% endif %}"
|
||||
style="background-image: url('{{ MEDIA_URL }}{{ config.HOMEPAGE_HEADER_IMAGE }}')">
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="ml-5 px-4 mt-3 pt-3 textbox-container">
|
||||
<div class="mx-5 px-4 my-3 pt-3 pb-2 textbox-container">
|
||||
<h1 class="display-1">{{ settings.PROJECT_LONG_NAME }}</h1>
|
||||
<p class="lead">Snappy leader here...</p>
|
||||
<p class="lead">{{ config.PROJECT_LEAD }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="bg-secondary py-3">
|
||||
<div class="container text-white">
|
||||
<h2>Snappy tagline here...</h2>
|
||||
<h2>{{ config.PROJECT_TAGLINE }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-light py-2 mb-4">
|
||||
<div class="container">
|
||||
<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-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 %}
|
||||
<p>{{ config.HOMEPAGE_CARD_1_DESCRIPTION|safe }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if config.HOMEPAGE_CARD_1_ICON %}
|
||||
<span class="fa-solid fa-5x fa-{{ config.HOMEPAGE_CARD_1_ICON }}"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-4">
|
||||
{% if config.HOMEPAGE_CARD_2_TITLE %}
|
||||
<div class="col-md-4 mx-auto">
|
||||
<div class="card text-center">
|
||||
<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 %}
|
||||
<p>{{ config.HOMEPAGE_CARD_2_DESCRIPTION|safe }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if config.HOMEPAGE_CARD_2_ICON %}
|
||||
<span class="fa-solid fa-5x fa-{{ config.HOMEPAGE_CARD_2_ICON }}"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-4">
|
||||
{% if config.HOMEPAGE_CARD_3_TITLE %}
|
||||
<div class="col-md-4 mx-auto">
|
||||
<div class="card text-center">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,15 +92,16 @@
|
||||
{% block content %}
|
||||
<div class="row align-items-center" style="min-height: 400px;">
|
||||
<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>
|
||||
{{ settings.PROJECT_LONG_NAME }} is...
|
||||
{{ config.HOMEPAGE_ABOUT_CONTENT|safe }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<input type="submit" class="btn btn-info" value="Login">
|
||||
{% endbuttons %}
|
||||
<p>Forgot your password? Reset it <a href="{% url 'password_reset' %}">here</a>.</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password Reset Complete</h1>
|
||||
|
||||
<p>Your password has been successfully reset. You can now <a href="{% url 'login' %}">log in using it</a>.</p> If you haven't yet completed your profile, please do this now.
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block content %}
|
||||
{% if validlink %}
|
||||
|
||||
<h1>Choose a New Password</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<input type="submit" class="btn btn-info" value="Change password">
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>That link isn't valid - has it already been used? You can request a new link <a href="{% url 'login' %}">here</a>.</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Check Your Inbox</h1>
|
||||
|
||||
<p>We've emailed you instructions for how to reset your password.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password Reset</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<input type="submit" class="btn btn-info" value="Send reset link">
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -14,10 +14,15 @@ Including another URLconf
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.urls import include, path
|
||||
from django.conf.urls.static import static
|
||||
|
||||
from . import views
|
||||
|
||||
admin.site.site_header = settings.PROJECT_LONG_NAME + " Admin"
|
||||
admin.site.site_title = settings.PROJECT_SHORT_NAME + " Admin"
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/',
|
||||
admin.site.urls),
|
||||
@@ -47,4 +52,7 @@ urlpatterns = [
|
||||
|
||||
path('',
|
||||
include('activities.urls')),
|
||||
|
||||
path('',
|
||||
include('pwa.urls')),
|
||||
] # yapf: disable
|
||||
|
||||
4
deploy/Vagrantfile
vendored
4
deploy/Vagrantfile
vendored
@@ -21,9 +21,7 @@ Vagrant.configure("2") do |config|
|
||||
ansible.playbook = "playbook.yml"
|
||||
ansible.host_vars = {
|
||||
"default" => {
|
||||
"deploy_environment" => "vagrant",
|
||||
"django_debug" => 1,
|
||||
"django_secret_key" => "debug_only_g62WlORMbo8iAcV7vKCKBQ=="
|
||||
"deploy_environment" => "vagrant"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
130
deploy/example.env
Normal file
130
deploy/example.env
Normal file
@@ -0,0 +1,130 @@
|
||||
# REQUIRED=Secret key
|
||||
# Used to generate CSRF tokens - must never be made public
|
||||
SECRET_KEY=changeme
|
||||
|
||||
# Debug
|
||||
# Should the server run in debug mode? Provides information to users which is unsafe in production
|
||||
# Default: False
|
||||
DEBUG=False
|
||||
|
||||
# Project long name
|
||||
# The project's full name.
|
||||
# Default: Project Network Mapper
|
||||
# PROJECT_LONG_NAME=Project Network Mapper
|
||||
|
||||
# Project short name
|
||||
# The project's short/abbreviated name. This will also be used as the app's name when installed as PWA.
|
||||
# Default: Network Mapper
|
||||
# PROJECT_SHORT_NAME=Network Mapper
|
||||
|
||||
# Project description
|
||||
# The project's description. Used when installed as a PWA.
|
||||
# Default: Application to map network relationships in the organisation.
|
||||
# PROJECT_DESCRIPTION=Application to map network relationships in the organisation.
|
||||
|
||||
# Theme color
|
||||
# The project's theme color, in hex format (excluding the leading #).
|
||||
# Default: 212121
|
||||
# THEME_COLOR=212121
|
||||
|
||||
# Background color
|
||||
# The project's background color, in hex format (excluding the leading #).
|
||||
# Default: ffffff
|
||||
# BACKGROUND_COLOR=ffffff
|
||||
|
||||
# Allowed hosts
|
||||
# Accepted values for server header in request - protects against CSRF and CSS attacks
|
||||
# Default: * if DEBUG else localhost
|
||||
# ALLOWED_HOSTS=127.0.0.1,localhost,localhost.localdomain
|
||||
|
||||
# Site URL
|
||||
# The URL the site will be deployed on. Do not include http://, https://, or a trailing slash.
|
||||
# Default: localhost
|
||||
# SITE_URL=localhost
|
||||
|
||||
# Site protocol
|
||||
# The protocol the site uses. Valid options are http or https.
|
||||
# Default: http
|
||||
# SITE_PROTOCOL=http
|
||||
|
||||
# Trusted origins
|
||||
# The trusted origin domains of requests - protects against CSRF and CSS attacks
|
||||
# Default: '*' if DEBUG else 'http://127.0.0.1,http://localhost,http://localhost.localdomain'
|
||||
# TRUSTED_ORIGINS=http://127.0.0.1,http://localhost,http://localhost.localdomain
|
||||
|
||||
# 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
|
||||
@@ -1,5 +1,4 @@
|
||||
all:
|
||||
hosts:
|
||||
example.com:
|
||||
django_debug: 1
|
||||
django_secret_key: debug_only_g62WlORMbo8iAcV7vKCKBQ==
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
register: vagrant_dir
|
||||
|
||||
vars:
|
||||
project_name: mapper
|
||||
project_name: network-mapper
|
||||
project_dir: /srv/{{ project_name }}
|
||||
project_src_dir: "{{ project_dir }}/src"
|
||||
provision_superuser: false
|
||||
superuser_username: admin
|
||||
superuser_password: admin
|
||||
superuser_email: email@example.com
|
||||
|
||||
tasks:
|
||||
- name: Vagrant specific tasks
|
||||
@@ -43,7 +47,7 @@
|
||||
ansible.builtin.git:
|
||||
repo: 'https://github.com/Southampton-RSG/breccia-mapper.git'
|
||||
dest: '{{ project_src_dir }}'
|
||||
version: docker
|
||||
version: dev # master
|
||||
accept_hostkey: yes
|
||||
|
||||
- name: Copy template files
|
||||
@@ -55,6 +59,18 @@
|
||||
- Caddyfile
|
||||
- docker-compose.yml
|
||||
|
||||
- name: Copy settings file
|
||||
ansible.builtin.copy:
|
||||
src: '.env'
|
||||
dest: '{{ project_dir }}/.env'
|
||||
mode: 0600
|
||||
|
||||
- name: Copy site icon
|
||||
ansible.builtin.copy:
|
||||
src: 'icon-192x192.png'
|
||||
dest: '{{ project_dir }}/icon-192x192.png'
|
||||
mode: 0600
|
||||
|
||||
- name: Create database file
|
||||
ansible.builtin.file:
|
||||
path: "{{ project_dir }}/db.sqlite3"
|
||||
@@ -72,15 +88,21 @@
|
||||
cmd: docker compose pull {{ item }}
|
||||
loop:
|
||||
- caddy
|
||||
|
||||
- name: Build custom images
|
||||
ansible.builtin.command:
|
||||
chdir: "{{ project_dir }}"
|
||||
cmd: docker compose build {{ item }}
|
||||
loop:
|
||||
- web
|
||||
- server
|
||||
|
||||
- name: Start containers
|
||||
ansible.builtin.command:
|
||||
chdir: "{{ project_dir }}"
|
||||
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 the password that was provided. This user has unlimited access to the network mapper."
|
||||
when: provision_superuser
|
||||
7
deploy/templates/Caddyfile.j2
Normal file → Executable file
7
deploy/templates/Caddyfile.j2
Normal file → Executable file
@@ -1,15 +1,16 @@
|
||||
http://* {
|
||||
:80 :443 {
|
||||
root * /srv
|
||||
file_server
|
||||
|
||||
@proxy_paths {
|
||||
not path /static/*
|
||||
not path /media/*
|
||||
}
|
||||
|
||||
reverse_proxy @proxy_paths http://web:8000
|
||||
reverse_proxy @proxy_paths http://server:8000
|
||||
|
||||
log {
|
||||
output stderr
|
||||
format single_field common_log
|
||||
format console
|
||||
}
|
||||
}
|
||||
16
deploy/templates/docker-compose.yml.j2
Normal file → Executable file
16
deploy/templates/docker-compose.yml.j2
Normal file → Executable file
@@ -1,18 +1,19 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: breccia-mapper
|
||||
server:
|
||||
image: mgrove36/breccia-mapper:latest
|
||||
build: {{ project_src_dir }}
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
DEBUG: {{ django_debug }}
|
||||
DATABASE_URL: sqlite:////app/db.sqlite3
|
||||
SECRET_KEY: {{ django_secret_key }}
|
||||
DJANGO_DEBUG: ${DEBUG}
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- {{ project_dir }}/db.sqlite3:/app/db.sqlite3:z
|
||||
- static_files:/app/static
|
||||
- media_files:/app/media
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
@@ -24,12 +25,15 @@ services:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:z
|
||||
# Caddy serves static files collected by Django
|
||||
- static_files:/srv/static:ro
|
||||
- media_files:/srv/media:ro
|
||||
- {{ project_dir }}/icon-192x192.png:/srv/media/icon-192x192.png:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- web
|
||||
- server
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
static_files:
|
||||
media_files:
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: breccia-mapper
|
||||
server:
|
||||
image: mgrove36/breccia-mapper:latest
|
||||
build: .
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
DEBUG: ${DJANGO_DEBUG}
|
||||
DATABASE_URL: sqlite:////app/db.sqlite3
|
||||
SECRET_KEY: ${DJANGO_SECRET_KEY}
|
||||
DJANGO_DEBUG: ${DEBUG}
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./db.sqlite3:/app/db.sqlite3:z
|
||||
- static_files:/app/static
|
||||
- media_files:/app/media
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
@@ -24,12 +25,15 @@ services:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:z
|
||||
# Caddy serves static files collected by Django
|
||||
- static_files:/srv/static:ro
|
||||
- media_files:/srv/media:ro
|
||||
- ./icon-192x192.png:/srv/media/icon-192x192.png:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- web
|
||||
- server
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
static_files:
|
||||
media_files:
|
||||
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal 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
35
docs/make.bat
Normal 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
|
||||
32
docs/source/conf.py
Normal file
32
docs/source/conf.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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']
|
||||
186
docs/source/deployment.md
Normal file
186
docs/source/deployment.md
Normal file
@@ -0,0 +1,186 @@
|
||||
|
||||
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.
|
||||
|
||||
|
||||
# Ansible
|
||||
|
||||
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:
|
||||
|
||||
<!-- -------------------------------------------------------
|
||||
NOTES
|
||||
pull deploy folder only
|
||||
navigate to folder
|
||||
copy icon to icon-192x192.png in folder
|
||||
copy example.env to .env and edit
|
||||
copy inventory.example.yml to inventory.yml and edit
|
||||
edit playbook if superuser desired
|
||||
run playbook
|
||||
set provision_superuser to false if was changed
|
||||
------------------------------------------------------- -->
|
||||
|
||||
1. Download and extract the deployment files from [the latest release](https://github.com/Southampton-RSG/breccia-mapper/releases/latest):
|
||||
|
||||
```bash
|
||||
curl https://github.com/Southampton-RSG/breccia-mapper/releases/latest/download/deploy-ansible.tar | tar xzv && cd deploy-ansible
|
||||
```
|
||||
|
||||
2. Copy your logo (192x192 pixels) to `icon-192x192.png` in this folder.
|
||||
|
||||
3. Copy `example.env` to `.env`:
|
||||
|
||||
```bash
|
||||
cp example.env .env
|
||||
```
|
||||
|
||||
4. Edit this file as desired. Note that some variables are required.
|
||||
5. Copy `inventory.example.yml` to `inventory.yml`:
|
||||
|
||||
```bash
|
||||
cp inventory.example.yml inventory.yml
|
||||
```
|
||||
|
||||
6. Edit this file to reflect your Ansible setup:
|
||||
- Use your server's hostname instead of `example.com`
|
||||
7. If you would like a new superuser to be provisioned (e.g. during initial install), edit the `provision_superuser` variable in `playbook.yml` to `true`.
|
||||
- Then change the `superuser_*` options below it as desired.
|
||||
8. Run the Ansible playbook `playbook.yml` with this inventory file using:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
:::{warning}
|
||||
If you changed the `provision_superuser` variable in `playbook.yml` to `true`, remember to change it back to `false`.
|
||||
:::
|
||||
|
||||
|
||||
# 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
|
||||
:::
|
||||
|
||||
<!-- -------------------------------------------------------
|
||||
NOTES
|
||||
create folder
|
||||
pull docker compose file and example.env only
|
||||
copy icon to icon-192x192.png in folder
|
||||
copy example.env to .env and edit
|
||||
touch db file? (is this needed?)
|
||||
run docker compose up -d
|
||||
create superuser if desired
|
||||
------------------------------------------------------- -->
|
||||
|
||||
To deploy the BRECcIA Network Mapper with Docker:
|
||||
|
||||
1. Download and extract the deployment files from [the latest release](https://github.com/Southampton-RSG/breccia-mapper/releases/latest):
|
||||
|
||||
```bash
|
||||
curl https://github.com/mgrove36/Southampton-RSG/releases/latest/download/deploy-docker.tar | tar xzv && cd deploy-docker
|
||||
```
|
||||
|
||||
2. Copy your logo (192x192 pixels) to `icon-192x192.png` in this folder.
|
||||
3. Copy `example.env` to `.env`:
|
||||
|
||||
```bash
|
||||
cp example.env .env
|
||||
```
|
||||
|
||||
4. Edit this file as desired. Note that some variables are required.
|
||||
3. Create the database using:
|
||||
|
||||
```bash
|
||||
touch db.sqlite3
|
||||
```
|
||||
|
||||
5. Start the containers with the following command (you may need to use `sudo`):
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
6. If desired (e.g. on initial deployment), 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"
|
||||
```
|
||||
|
||||
:::{important}
|
||||
If you don't create a superuser when you first deploy the app, you will be unable to log in.
|
||||
:::
|
||||
|
||||
# Vagrant
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [Vagrant](https://www.vagrantup.com/)
|
||||
- [Ansible](https://www.ansible.com/)
|
||||
|
||||
<!-- -------------------------------------------------------
|
||||
NOTES
|
||||
pull deploy folder only
|
||||
navigate to folder
|
||||
copy icon to icon-192x192.png in folder
|
||||
copy example.env to .env and edit
|
||||
edit playbook if superuser desired
|
||||
run vagrant up and/or vagrant provision
|
||||
set provision_superuser to false if was changed
|
||||
------------------------------------------------------- -->
|
||||
|
||||
To deploy the BRECcIA Network Mapper with Vagrant:
|
||||
|
||||
1. Download and extract the deployment files from [the latest release](https://github.com/Southampton-RSG/breccia-mapper/releases/latest):
|
||||
|
||||
```bash
|
||||
curl https://github.com/mgrove36/Southampton-RSG/releases/latest/download/deploy-vagrant.tar | tar xzv && cd deploy-vagrant
|
||||
```
|
||||
|
||||
2. Copy your logo (192x192 pixels) to `icon-192x192.png` in this folder.
|
||||
3. Copy `example.env` to `.env`:
|
||||
|
||||
```bash
|
||||
cp example.env .env
|
||||
```
|
||||
|
||||
4. Edit this file as desired. Note that some variables are required.
|
||||
5. If you would like a new superuser to be provisioned (e.g. during initial install), edit the `provision_superuser` variable in `playbook.yml` to `true`.
|
||||
- Then change the `superuser_*` options below it as desired.
|
||||
6. To change where the app is accessible from, edit the `config.vm.network` line in `Vagrantfile`.
|
||||
- By default, the app is accessible only from `http://localhost:8080`.
|
||||
- To make it available from any IP address, replace `host: 8080, host_ip: "127.0.0.1"` with `host: 8080`.
|
||||
- To change the port the app is available on, edit `host: 8080`.
|
||||
- More details are available in the [Vagrant docs](https://developer.hashicorp.com/vagrant/docs/networking).
|
||||
6. Start the virtual machine:
|
||||
|
||||
```bash
|
||||
vagrant up
|
||||
```
|
||||
|
||||
7. Deploy the Network Mapper on the virtual machine:
|
||||
|
||||
```bash
|
||||
vagrant provision
|
||||
```
|
||||
|
||||
|
||||
:::{note}
|
||||
To stop the virtual machine, run `vagrant halt` in this directory. More commands are explained in the [Vagrant docs](https://www.vagrantup.com/docs/cli).
|
||||
:::
|
||||
|
||||
<!-- # Build From Source -->
|
||||
17
docs/source/index.md
Normal file
17
docs/source/index.md
Normal 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:
|
||||
*
|
||||
```
|
||||
1
docs/source/requirements.txt
Normal file
1
docs/source/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
myst-parser
|
||||
BIN
media/400x400.png
Normal file
BIN
media/400x400.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
media/800x500.png
Normal file
BIN
media/800x500.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -7,8 +7,8 @@
|
||||
"created": "2020-04-27T12:13:30.448Z",
|
||||
"last_updated": "2020-04-27T14:45:27.152Z",
|
||||
"subject": "Welcome to {{settings.PROJECT_LONG_NAME}}",
|
||||
"content": "Dear {{ user.get_full_name }},\r\n\r\nWelcome to {{ settings.PROJECT_LONG_NAME }}.\r\n\r\nThanks,\r\n\r\nThe {{ settings.PROJECT_LONG_NAME }} team",
|
||||
"html_content": "<h1>{{ settings.PROJECT_LONG_NAME }}</h1>\r\n\r\nDear {{ user.get_full_name }},\r\n\r\nWelcome to {{ settings.PROJECT_LONG_NAME }}.\r\n\r\nThanks,\r\n\r\nThe {{ settings.PROJECT_LONG_NAME }} team",
|
||||
"content": "Dear user,\r\n\r\nWelcome to {{ settings.PROJECT_LONG_NAME }}. You can set your password at {{ settings.SITE_PROTOCOL }}://{{ settings.SITE_URL }}/password_reset/.\r\n\r\nYour username is {{ user.username }}.\r\nThanks,\r\n\r\nThe {{ settings.PROJECT_SHORT_NAME }} team",
|
||||
"html_content": "<h1>{{ settings.PROJECT_LONG_NAME }}</h1><br/><p>Dear user,</p><br/><p>Welcome to {{ settings.PROJECT_LONG_NAME }}. You can set your password <a href='{{ settings.SITE_PROTOCOL }}://{{ settings.SITE_URL }}/password_reset/'>here</a>.</p><p>Your username is {{ user.username }}.</p><br/><p>Thanks,</p><p>The {{ settings.PROJECT_SHORT_NAME }} team</p>",
|
||||
"language": "",
|
||||
"default_template": null
|
||||
}
|
||||
|
||||
15
people/forms.py
Normal file → Executable file
15
people/forms.py
Normal file → Executable file
@@ -3,9 +3,10 @@
|
||||
import typing
|
||||
|
||||
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 . import models
|
||||
@@ -121,7 +122,7 @@ class OrganisationAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
]
|
||||
labels = {
|
||||
'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 = {
|
||||
'countries': Select2MultipleWidget(),
|
||||
@@ -185,14 +186,14 @@ class PersonAnswerSetForm(forms.ModelForm, DynamicAnswerSetBase):
|
||||
widgets = {
|
||||
'nationality': Select2MultipleWidget(),
|
||||
'country_of_residence': Select2Widget(),
|
||||
'organisation_started_date': DatePickerInput(format='%Y-%m-%d'),
|
||||
'project_started_date': DatePickerInput(format='%Y-%m-%d'),
|
||||
'organisation_started_date': DatePickerInput(),
|
||||
'project_started_date': DatePickerInput(),
|
||||
'latitude': forms.HiddenInput,
|
||||
'longitude': forms.HiddenInput,
|
||||
}
|
||||
labels = {
|
||||
'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':
|
||||
'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):
|
||||
date = forms.DateField(
|
||||
required=False,
|
||||
widget=DatePickerInput(format='%Y-%m-%d'),
|
||||
widget=DatePickerInput(),
|
||||
help_text='Show relationships as they were on this date'
|
||||
)
|
||||
|
||||
|
||||
4
people/migrations/0002_add_relationship_models.py
Normal file → Executable file
4
people/migrations/0002_add_relationship_models.py
Normal file → Executable file
@@ -50,10 +50,10 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
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(
|
||||
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
2
people/migrations/0022_refactor_person_questions.py
Normal file → Executable file
@@ -50,6 +50,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
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
2
people/migrations/0035_add_organisation_questions.py
Normal file → Executable file
@@ -56,6 +56,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='organisationquestionchoice',
|
||||
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer'),
|
||||
constraint=models.UniqueConstraint(fields=('question', 'text'), name='unique_question_answer_organisationquestionchoice'),
|
||||
),
|
||||
]
|
||||
|
||||
4
people/migrations/0039_add_organisation_relationship.py
Normal file → Executable file
4
people/migrations/0039_add_organisation_relationship.py
Normal file → Executable file
@@ -67,10 +67,10 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
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(
|
||||
model_name='organisationrelationship',
|
||||
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship'),
|
||||
constraint=models.UniqueConstraint(fields=('source', 'target'), name='unique_relationship_organisationrelationship'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
5
people/models/organisation.py
Normal file → Executable file
@@ -35,6 +35,11 @@ class OrganisationQuestionChoice(QuestionChoice):
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
class Meta(QuestionChoice.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer_organisationquestionchoice')
|
||||
]
|
||||
|
||||
|
||||
class Organisation(models.Model):
|
||||
|
||||
15
people/models/person.py
Normal file → Executable file
15
people/models/person.py
Normal file → Executable file
@@ -8,9 +8,10 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
from django_settings_export import settings_export
|
||||
from post_office import mail
|
||||
|
||||
from constance import config
|
||||
|
||||
from .organisation import Organisation
|
||||
from .question import AnswerSet, Question, QuestionChoice
|
||||
|
||||
@@ -43,10 +44,11 @@ class User(AbstractUser):
|
||||
def send_welcome_email(self) -> None:
|
||||
"""Send a welcome email to a new user."""
|
||||
# Get exported data from settings.py first
|
||||
context = settings_export(None)
|
||||
context.update({
|
||||
context = {
|
||||
'user': self,
|
||||
})
|
||||
'config': config,
|
||||
'settings': settings,
|
||||
}
|
||||
|
||||
logger.info('Sending welcome mail to user \'%s\'', self.username)
|
||||
|
||||
@@ -77,6 +79,11 @@ class PersonQuestionChoice(QuestionChoice):
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
class Meta(QuestionChoice.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer_personquestionchoice')
|
||||
]
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
|
||||
2
people/models/question.py
Normal file → Executable file
2
people/models/question.py
Normal file → Executable file
@@ -92,7 +92,7 @@ class QuestionChoice(models.Model):
|
||||
abstract = True
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer')
|
||||
name='unique_question_answer_modelquestionchoice')
|
||||
]
|
||||
ordering = [
|
||||
'question__order',
|
||||
|
||||
14
people/models/relationship.py
Normal file → Executable file
14
people/models/relationship.py
Normal file → Executable file
@@ -33,6 +33,11 @@ class RelationshipQuestionChoice(QuestionChoice):
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
class Meta(QuestionChoice.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer_relationshipquestionchoice')
|
||||
]
|
||||
|
||||
|
||||
class Relationship(models.Model):
|
||||
@@ -40,7 +45,7 @@ class Relationship(models.Model):
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['source', 'target'],
|
||||
name='unique_relationship'),
|
||||
name='unique_relationship_modelrelationship'),
|
||||
]
|
||||
|
||||
#: Person reporting the relationship
|
||||
@@ -122,6 +127,11 @@ class OrganisationRelationshipQuestionChoice(QuestionChoice):
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
class Meta(QuestionChoice.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['question', 'text'],
|
||||
name='unique_question_answer_organisationrelationshipquestionchoice')
|
||||
]
|
||||
|
||||
|
||||
class OrganisationRelationship(models.Model):
|
||||
@@ -129,7 +139,7 @@ class OrganisationRelationship(models.Model):
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['source', 'target'],
|
||||
name='unique_relationship'),
|
||||
name='unique_relationship_modelorganisationrelationship'),
|
||||
]
|
||||
|
||||
#: Person reporting the relationship
|
||||
|
||||
2
people/templates/people/map.html
Normal file → Executable file
2
people/templates/people/map.html
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
{% block extra_head %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<script src="{% static 'js/map.js' %}"></script>
|
||||
|
||||
<script async defer
|
||||
|
||||
2
people/templates/people/network.html
Normal file → Executable file
2
people/templates/people/network.html
Normal file → Executable file
@@ -97,6 +97,6 @@
|
||||
integrity="sha512-Qlv6VSKh1gDKGoJbnyA5RMXYcvnpIqhO++MhIM2fStMcGT9i2T//tSwYFlcyoRRDcDZ+TYHpH8azBBCyhpSeqw=="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<script src="{% static 'js/network.js' %}"></script>
|
||||
{% endblock %}
|
||||
2
people/templates/people/organisation/detail.html
Normal file → Executable file
2
people/templates/people/organisation/detail.html
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
{% block extra_head %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<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"
|
||||
|
||||
2
people/templates/people/organisation/update.html
Normal file → Executable file
2
people/templates/people/organisation/update.html
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
<script src="{% static 'js/map.js' %}"></script>
|
||||
|
||||
14
people/templates/people/person/detail_full.html
Normal file → Executable file
14
people/templates/people/person/detail_full.html
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
{% block extra_head %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<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"
|
||||
@@ -64,17 +64,19 @@
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'people:person.update' pk=person.pk %}">Update</a>
|
||||
|
||||
{% load hijack_tags %}
|
||||
{% if person.user == request.user and not request|is_hijacked %}
|
||||
{% load hijack %}
|
||||
{% if person.user == request.user and not request.user.is_hijacked %}
|
||||
<a class="btn btn-info"
|
||||
href="{% url 'password_change' %}?next={{ person.get_absolute_url }}">Change Password</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_superuser and person.user and person.user != request.user %}
|
||||
<form style="display: inline;" action="/hijack/{{ person.user.pk }}/" method="post">
|
||||
{% csrf_token %}
|
||||
<form action="{% url 'hijack:acquire' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user_pk" value="{{ person.pk }}">
|
||||
<button class="btn btn-warning" type="submit">Become {{ person.name }}</button>
|
||||
</form>
|
||||
<input type="hidden" name="next" value="{{ request.path }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
2
people/templates/people/person/detail_partial.html
Normal file → Executable file
2
people/templates/people/person/detail_partial.html
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
{% block extra_head %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<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"
|
||||
|
||||
2
people/templates/people/person/update.html
Normal file → Executable file
2
people/templates/people/person/update.html
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{{ map_markers|json_script:'map-markers' }}
|
||||
|
||||
<script src="{% static 'js/map.js' %}"></script>
|
||||
|
||||
2
people/templates/people/relationship/detail.html
Normal file → Executable file
2
people/templates/people/relationship/detail.html
Normal file → Executable file
@@ -47,7 +47,7 @@
|
||||
<div class="col-md-2 text-center">
|
||||
{% if relationship.reverse %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
2
people/templates/people/relationship/update.html
Normal file → Executable file
2
people/templates/people/relationship/update.html
Normal file → Executable file
@@ -23,6 +23,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<script async defer src="{% static 'js/hide_free_text.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
|
||||
from django.conf import settings
|
||||
from constance import config
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils import timezone
|
||||
@@ -49,11 +50,11 @@ class OrganisationListView(LoginRequiredMixin, ListView):
|
||||
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')
|
||||
|
||||
special = {
|
||||
f'{settings.PARENT_PROJECT_NAME} partners', 'International',
|
||||
f'{config.PARENT_PROJECT_NAME} partners', 'International',
|
||||
'Unknown'
|
||||
}
|
||||
for country in sorted(k for k in orgs_by_country.keys()
|
||||
@@ -81,7 +82,7 @@ class OrganisationListView(LoginRequiredMixin, ListView):
|
||||
country = 'International'
|
||||
|
||||
if answers.is_partner_organisation:
|
||||
country = f'{settings.PARENT_PROJECT_NAME} partners'
|
||||
country = f'{config.PARENT_PROJECT_NAME} partners'
|
||||
|
||||
except AttributeError:
|
||||
# Organisation has no AnswerSet - country is 'Unknown'
|
||||
|
||||
@@ -25,8 +25,11 @@ class PersonCreateView(LoginRequiredMixin, CreateView):
|
||||
form_class = forms.PersonForm
|
||||
|
||||
def form_valid(self, form):
|
||||
if 'user' in self.request.GET:
|
||||
form.instance.user = self.request.user
|
||||
try:
|
||||
self.request.user.person
|
||||
except ObjectDoesNotExist:
|
||||
if 'user' in self.request.GET:
|
||||
form.instance.user = self.request.user
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
@@ -118,6 +121,10 @@ class ProfileView(LoginRequiredMixin, DetailView):
|
||||
except models.Relationship.DoesNotExist:
|
||||
pass
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
# No linked Person yet
|
||||
pass
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import typing
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic import DetailView, RedirectView, UpdateView
|
||||
@@ -44,9 +46,22 @@ class RelationshipCreateView(LoginRequiredMixin, RedirectView):
|
||||
"""
|
||||
def get_redirect_url(self, *args: typing.Any,
|
||||
**kwargs: typing.Any) -> typing.Optional[str]:
|
||||
target = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||
relationship, _ = models.Relationship.objects.get_or_create(
|
||||
source=self.request.user.person, target=target)
|
||||
target = None
|
||||
try:
|
||||
target = models.Person.objects.get(pk=self.kwargs.get('person_pk'))
|
||||
if target is None: raise ObjectDoesNotExist
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
# target doesn't exist
|
||||
return reverse('people:person.list')
|
||||
|
||||
try:
|
||||
relationship, _ = models.Relationship.objects.get_or_create(
|
||||
source=self.request.user.person, target=target)
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
# User has no linked Person yet
|
||||
return reverse('people:person.create') + '?user'
|
||||
|
||||
return reverse('people:relationship.update',
|
||||
kwargs={'pk': relationship.pk})
|
||||
|
||||
77
requirements.txt
Normal file → Executable file
77
requirements.txt
Normal file → Executable file
@@ -1,46 +1,47 @@
|
||||
astroid==2.3.3
|
||||
beautifulsoup4==4.8.2
|
||||
dj-database-url==0.5.0
|
||||
Django==2.2.10
|
||||
django-appconf==1.0.3
|
||||
django-bootstrap4==1.1.1
|
||||
django-bootstrap-datepicker-plus==3.0.5
|
||||
django-compat==1.0.15
|
||||
django-constance==2.6.0
|
||||
django-countries==5.5
|
||||
django-dbbackup==3.2.0
|
||||
django-filter==2.2.0
|
||||
django-hijack==2.2.1
|
||||
django-picklefield==2.1.1
|
||||
django-post-office==3.4.0
|
||||
django-select2==7.2.0
|
||||
astroid==2.12.13
|
||||
beautifulsoup4==4.11.1
|
||||
dj-database-url==1.2.0
|
||||
Django==4.1.4
|
||||
django-appconf==1.0.5
|
||||
django-bootstrap4==22.3
|
||||
django-bootstrap-datepicker-plus==5.0.2
|
||||
django-constance==2.9.1
|
||||
django-countries==7.5
|
||||
django-dbbackup==4.0.2
|
||||
django-filter==22.1
|
||||
django-hijack==3.2.6
|
||||
django-picklefield==3.1
|
||||
django-post-office==3.6.3
|
||||
django-pwa==1.1.0
|
||||
django-select2==8.0.0
|
||||
django-settings-export==1.2.1
|
||||
djangorestframework==3.11.0
|
||||
djangorestframework==3.14.0
|
||||
dodgy==0.2.1
|
||||
isort==4.3.21
|
||||
isort==5.11.4
|
||||
jsonfield==3.1.0
|
||||
lazy-object-proxy==1.4.3
|
||||
mccabe==0.6.1
|
||||
lazy-object-proxy==1.8.0
|
||||
mccabe==0.7.0
|
||||
# mysqlclient==1.4.6
|
||||
pep8-naming==0.4.1
|
||||
prospector==1.2.0
|
||||
pycodestyle==2.4.0
|
||||
pydocstyle==5.0.2
|
||||
pyflakes==2.1.1
|
||||
pylint==2.4.4
|
||||
pep8-naming==0.10.0
|
||||
prospector==1.8.3
|
||||
pycodestyle==2.10.0
|
||||
pydocstyle==6.1.1
|
||||
pyflakes==2.5.0
|
||||
pylint==2.15.9
|
||||
pylint-celery==0.3
|
||||
pylint-django==2.0.12
|
||||
pylint-django==2.5.3
|
||||
pylint-flask==0.6
|
||||
pylint-plugin-utils==0.6
|
||||
python-decouple==3.3
|
||||
pytz==2019.3
|
||||
pylint-plugin-utils==0.7
|
||||
python-decouple==3.6
|
||||
pytz==2022.7
|
||||
pyuca==1.2
|
||||
PyYAML==5.3
|
||||
requirements-detector==0.6
|
||||
setoptconf==0.2.0
|
||||
six==1.14.0
|
||||
snowballstemmer==2.0.0
|
||||
soupsieve==1.9.5
|
||||
sqlparse==0.3.0
|
||||
PyYAML==6.0
|
||||
requirements-detector==1.0.3
|
||||
setoptconf==0.3.0
|
||||
six==1.16.0
|
||||
snowballstemmer==2.2.0
|
||||
soupsieve==2.3.2.post1
|
||||
sqlparse==0.4.3
|
||||
typed-ast
|
||||
wrapt==1.11.2
|
||||
wrapt==1.14.1
|
||||
Pillow==9.4.0
|
||||
|
||||
Reference in New Issue
Block a user