diff --git a/backend/migrations/0029_serviceuser_agreed_privacy_policy.py b/backend/migrations/0029_serviceuser_agreed_privacy_policy.py new file mode 100644 index 00000000..d09c0570 --- /dev/null +++ b/backend/migrations/0029_serviceuser_agreed_privacy_policy.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2023-02-22 10:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend', '0028_project_uuid'), + ] + + operations = [ + migrations.AddField( + model_name='serviceuser', + name='agreed_privacy_policy', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/models.py b/backend/models.py index ee452c42..cbcc7749 100644 --- a/backend/models.py +++ b/backend/models.py @@ -53,6 +53,7 @@ class ServiceUser(AbstractUser): receive_mail_notifications = models.BooleanField(default=True) doc_format_pref = models.IntegerField(choices=UserDocumentFormatPreference.USER_DOC_FORMAT_PREF, default=UserDocumentFormatPreference.JSON) + agreed_privacy_policy = models.BooleanField(default=False) @property def has_active_project(self): diff --git a/backend/rpc.py b/backend/rpc.py index d4981312..22e8bd26 100644 --- a/backend/rpc.py +++ b/backend/rpc.py @@ -3,6 +3,7 @@ import datetime import json +import os from urllib.parse import urljoin from django.conf import settings from django.contrib.auth import authenticate, get_user_model, login as djlogin, logout as djlogout @@ -26,7 +27,7 @@ from backend.rpcserver import rpc_method, rpc_method_auth, rpc_method_manager, rpc_method_admin from backend.models import Project, Document, DocumentType, Annotation, AnnotatorProject, AnnotationChangeHistory, \ UserDocumentFormatPreference -from backend.utils.misc import get_value_from_key_path, insert_value_to_key_path +from backend.utils.misc import get_value_from_key_path, insert_value_to_key_path, read_custom_document from backend.utils.serialize import ModelSerializer log = logging.getLogger(__name__) @@ -98,9 +99,10 @@ def register(request, payload): username = payload.get("username") password = payload.get("password") email = payload.get("email") + agreed_privacy_policy = True if not get_user_model().objects.filter(username=username).exists(): - user = get_user_model().objects.create_user(username=username, password=password, email=email) + user = get_user_model().objects.create_user(username=username, password=password, email=email, agreed_privacy_policy=agreed_privacy_policy) _generate_user_activation(user) djlogin(request, user) context["username"] = payload["username"] @@ -941,6 +943,31 @@ def admin_update_user_password(request, username, password): user.save() +################################## +### Privacy Policy/T&C Methods ### +################################## + +@rpc_method +def get_privacy_policy_details(request): + + details = settings.PRIVACY_POLICY + + custom_docs = { + 'CUSTOM_PP_DOCUMENT': read_custom_document(settings.CUSTOM_PP_DOCUMENT_PATH) if os.path.isfile(settings.CUSTOM_PP_DOCUMENT_PATH) else None, + 'CUSTOM_TC_DOCUMENT': read_custom_document(settings.CUSTOM_TC_DOCUMENT_PATH) if os.path.isfile(settings.CUSTOM_TC_DOCUMENT_PATH) else None + } + + details.update(custom_docs) + + url = { + 'URL': request.headers['Host'] + } + + details.update(url) + + return details + + ############################### ### Utility Methods ### ############################### diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index f895a463..016989fa 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -23,6 +23,10 @@ def check_model_fields(self, model_class, field_name_types_dict): class TestUserModel(TestCase): + def test_agree_privacy_policy(self): + user = get_user_model().objects.create(username="test1", agreed_privacy_policy=True) + self.assertTrue(user.agreed_privacy_policy) + def test_document_association_check(self): user = get_user_model().objects.create(username="test1") user2 = get_user_model().objects.create(username="test2") diff --git a/backend/utils/misc.py b/backend/utils/misc.py index 7cd91153..d50c686b 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -39,3 +39,13 @@ def insert_value_to_key_path(obj_dict, key_path, value, delimiter="."): return True return False + + +def read_custom_document(path): + """ + Reads in a text file and returns as a string. + Primarily used for reading in custom privacy policy and/or terms & conditions documents. + """ + with open(path) as file: + doc_str = file.read() + return doc_str \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 746a030e..9b3a896c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,14 @@ services: - SUPERUSER_USERNAME - SUPERUSER_PASSWORD - SUPERUSER_EMAIL + - PP_HOST_NAME + - PP_HOST_ADDRESS + - PP_HOST_CONTACT + - PP_ADMIN_NAME + - PP_ADMIN_ADDRESS + - PP_ADMIN_CONTACT + volumes: + - ./custom-policies:/app/custom-policies/ depends_on: - db diff --git a/docs/docs/developerguide/README.md b/docs/docs/developerguide/README.md index 60aaa6da..d0853340 100644 --- a/docs/docs/developerguide/README.md +++ b/docs/docs/developerguide/README.md @@ -120,7 +120,7 @@ To run separately: ``` ## Deployment using Docker -Deployment is via [docker-compose](https://docs.docker.com/compose/), using [NGINX](https://www.nginx.com/) to serve static content, a separate [postgreSQL](https://hub.docker.com/_/postgres) service containing the database and a database backup service (see `docker-compose.yml` for details). +Teamware can be deployed via [docker-compose](https://docs.docker.com/compose/), using [NGINX](https://www.nginx.com/) to serve static content, a separate [postgreSQL](https://hub.docker.com/_/postgres) service containing the database and a database backup service (see `docker-compose.yml` for details). 1. Run `./generate-docker-env.sh` to create a `.env` file containing randomly generated secrets which are mounted as environment variables into the container. See [below](#env-config) for details. @@ -218,6 +218,20 @@ email: # You will also need to set user and passwordSecret if your # mail server requires authentication +privacyPolicy: +# Contact details of the host and administrator of the teamware instance, if no admin defined, defaults to the host values. + host: + # Name of the host + name: "Service Host" + # Host's physical address + address: "123 Example Street, City. Country." + # A method of contacting the host, field supports HTML for e.g. linking to a form + contact: "Email" + admin: + name: "Dr. Service Admin" + address: "Department of Example Studies, University of Example, City. Country." + contact: "Email" + backend: # Name of the random secret you created above djangoSecret: django-secret @@ -304,3 +318,46 @@ This package includes the script linked in the documentation above, which simpli DJANGO_GMAIL_API_CLIENT_SECRET='google_assigned_secret' DJANGO_GMAIL_API_REFRESH_TOKEN='google_assigned_token' ``` + + +#### Teamware Privacy Policy and Terms & Conditions + +Teamware includes a default privacy policy and terms & conditions, which are required for running the application. + +The default privacy policy is intended to be compliant with UK GDPR regulations, which may comply with the rights of users of your deployment, however it is your responsibility to ensure that this is the case. + +If the default privacy policy covers your use case, then you will need to include configuration for a few contact details. + +Contact details are required for the **host** and the **administrator**: the **host** is the organisation or individual responsible for managing the deployment of the teamware instance and the **administrator** is the organisation or individual responsible for managing users, projects and data on the instance. In many cases these roles will be filled by the same organisation or individual, so in this case specifying just the **host** details is sufficient. + +For deployment from source, set the following environment variables: + +* `PP_HOST_NAME` +* `PP_HOST_ADDRESS` +* `PP_HOST_CONTACT` +* `PP_ADMIN_NAME` +* `PP_ADMIN_ADDRESS` +* `PP_ADMIN_CONTACT` + +For deployment using docker-compose, set these values in `.env`. + +If the host and administrator are the same, you can just set the `PP_HOST_*` variables above which will be used for both. + +##### Including a custom Privacy Policy and/or Terms & Conditions + +If the default privacy policy or terms & conditions do not cover your use case, you can easily replace these with your own documents. + +If deploying from source, include markdown (`.md`) files in a `custom-policies` directory in the project root with the exact names `custom-policies/privacy-policy.md` and/or `custom-policies/terms-and-conditions.md` which will be rendered at the corresponding pages on the running web app. If you are not familiar with the Markdown language there are a number of free WYSIWYG-style editor tools available including [StackEdit](https://stackedit.io/app) (browser based) and [Zettlr](https://www.zettlr.com) (desktop app). + +If deploying with docker compose, place the `custom-policies` directory at the same location as the `docker-compose.yml` file before running `./deploy.sh` as above. + +An example custom privacy policy file contents might look like: + +```md +# Organisation X Teamware Privacy Policy +... +... +## Definitions of Roles and Terminology +... +... +``` \ No newline at end of file diff --git a/frontend/src/AnnotationApp.vue b/frontend/src/AnnotationApp.vue index fa13225d..2f0f0cf9 100644 --- a/frontend/src/AnnotationApp.vue +++ b/frontend/src/AnnotationApp.vue @@ -2,14 +2,16 @@
+
+ + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 16a5c424..1a2afd17 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -21,6 +21,18 @@ const routes = [ component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), meta: {guest: true}, }, + { + path: '/privacypolicy', + name: 'Privacy Policy', + component: () => import('../views/PrivacyPolicy.vue'), + meta: {guest: true}, + }, + { + path: '/terms', + name: 'Terms & Conditions', + component: () => import('../views/TermsAndConditions.vue'), + meta: {guest: true}, + }, { path: '/login', name: 'Login', diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index cd4455b2..e5355b3f 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -72,6 +72,15 @@ export default new Vuex.Store({ await rpc.call("logout"); commit("updateUser", params); }, + async getPrivacyPolicyDetails() { + try{ + let response = await rpc.call("get_privacy_policy_details"); + return response + }catch (e){ + console.error(e) + throw e + } + }, async register({dispatch, commit}, params) { try{ const payload = { diff --git a/frontend/src/views/PrivacyPolicy.vue b/frontend/src/views/PrivacyPolicy.vue new file mode 100644 index 00000000..f705e2cc --- /dev/null +++ b/frontend/src/views/PrivacyPolicy.vue @@ -0,0 +1,157 @@ + + diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue index 33f42d16..2c866837 100644 --- a/frontend/src/views/Register.vue +++ b/frontend/src/views/Register.vue @@ -19,6 +19,8 @@ +

By registering to use Teamware, you confirm that you are over 18 years of age and have read and agreed to Teamware's privacy policy and terms & conditions.

+