Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Privacy policy & Terms & Conditions #298

Merged
merged 18 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/migrations/0029_serviceuser_agreed_privacy_policy.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
1 change: 1 addition & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
31 changes: 29 additions & 2 deletions backend/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
ianroberts marked this conversation as resolved.
Show resolved Hide resolved
}

details.update(custom_docs)

url = {
'URL': request.headers['Host']
}

details.update(url)

return details


###############################
### Utility Methods ###
###############################
Expand Down
4 changes: 4 additions & 0 deletions backend/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions backend/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/privacy-policy.html
depends_on:
- db

Expand Down
57 changes: 56 additions & 1 deletion docs/docs/developerguide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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: "<a href='mailto:[email protected]'>Email</a>"
admin:
name: "Dr. Service Admin"
address: "Department of Example Studies, University of Example, City. Country."
contact: "<a href='mailto:[email protected]'>Email</a>"

backend:
# Name of the random secret you created above
djangoSecret: django-secret
Expand Down Expand Up @@ -304,3 +318,44 @@ 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`.

##### 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 HTML files in a `custom-policies` directory in the project root with the exact names `custom-policies/privacy-policy.html` and/or `custom-policies/terms-and-conditions.html` which will be rendered at the corresponding pages on the running web app.
davidwilby marked this conversation as resolved.
Show resolved Hide resolved

If deploying with docker compose, place files with these names 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:

```html
<h1>Organisation X Teamware Privacy Policy</h2>
...
...
<h2>Definitions of Roles and Terminology</h2>
...
...
```
4 changes: 3 additions & 1 deletion frontend/src/AnnotationApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
<div id="app">
<navbar></navbar>
<router-view :key="$route.fullPath"></router-view>
<pagefooter></pagefooter>
</div>
</template>
<script>
import navbar from './components/navbar.vue';
import pagefooter from './components/pagefooter.vue';
import {initToast} from "@/utils";

export default {
components: { navbar },
components: { navbar, pagefooter },
mounted() {
initToast(this)
}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
<b-icon-box-arrow-up-right
style="position: relative; font-size: 0.8em; padding-bottom: 0.2em;"></b-icon-box-arrow-up-right>
</b-nav-item>
<b-nav-item to="/about">About</b-nav-item>
</b-navbar-nav>


Expand Down
28 changes: 28 additions & 0 deletions frontend/src/components/pagefooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div id="footer">
<div>
<b-navbar type="light" variant="light" toggleable="md" fixed="bottom">
<b-navbar-nav align="right" :small=true>
<b-nav-text>v{{ appVersion }}</b-nav-text>
<b-nav-item to="/privacypolicy">Privacy Policy</b-nav-item>
<b-nav-item to="/cookies">Cookies Policy</b-nav-item>
<b-nav-item to="/terms">Terms & Conditions</b-nav-item>
<b-nav-item to="/about">About</b-nav-item>
</b-navbar-nav>
</b-navbar>
</div>
</div>
</template>
<script>

import {version} from '../../../package.json'

export default {
data: () => ({
appVersion: version
}),
}
</script>

<style>
</style>
12 changes: 12 additions & 0 deletions frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading