TOS and Privacy Policy Pages (#1632)

* Adds a legal section where users can add a terms of service and privacy policy
* Optionally show links to the TOS and Privacy Policy on the registration page
* Closes #1621
This commit is contained in:
Kevin Chung
2020-09-07 17:25:47 -04:00
committed by GitHub
parent 37ddfa3bc3
commit 2c505f366d
10 changed files with 174 additions and 5 deletions

View File

@@ -1,5 +1,7 @@
import json
from flask import url_for
from CTFd.constants import JinjaEnum, RawEnum
from CTFd.utils import get_config
@@ -63,5 +65,19 @@ class _ConfigsWrapper:
def theme_settings(self):
return json.loads(get_config("theme_settings", default="null"))
@property
def tos_or_privacy(self):
tos = bool(get_config("tos_url") or get_config("tos_text"))
privacy = bool(get_config("privacy_url") or get_config("privacy_text"))
return tos or privacy
@property
def tos_link(self):
return get_config("tos_url", default=url_for("views.tos"))
@property
def privacy_link(self):
return get_config("privacy_url", default=url_for("views.privacy"))
Configs = _ConfigsWrapper()

View File

@@ -1,5 +1,5 @@
from wtforms import BooleanField, SelectField, StringField
from wtforms.fields.html5 import IntegerField
from wtforms import BooleanField, SelectField, StringField, TextAreaField
from wtforms.fields.html5 import IntegerField, URLField
from wtforms.widgets.html5 import NumberInput
from CTFd.forms import BaseForm
@@ -60,3 +60,21 @@ class ExportCSVForm(BaseForm):
),
)
submit = SubmitField("Download CSV")
class LegalSettingsForm(BaseForm):
tos_url = URLField(
"Terms of Service URL",
description="External URL to a Terms of Service document hosted elsewhere",
)
tos_text = TextAreaField(
"Terms of Service", description="Text shown on the Terms of Service page",
)
privacy_url = URLField(
"Privacy Policy URL",
description="External URL to a Privacy Policy document hosted elsewhere",
)
privacy_text = TextAreaField(
"Privacy Policy", description="Text shown on the Privacy Policy page",
)
submit = SubmitField("Update")

View File

@@ -268,6 +268,17 @@ $(() => {
theme_settings_editor.refresh();
});
$(
"a[href='#legal'], a[href='#tos-config'], a[href='#privacy-policy-config']"
).on("shown.bs.tab", function(_e) {
$("#tos-config .CodeMirror").each(function(i, el) {
el.CodeMirror.refresh();
});
$("#privacy-policy-config .CodeMirror").each(function(i, el) {
el.CodeMirror.refresh();
});
});
$("#theme-settings-modal form").submit(function(e) {
e.preventDefault();
theme_settings_editor

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -38,6 +38,9 @@
<li class="nav-item">
<a class="nav-link rounded-0" href="#ctftime" role="tab" data-toggle="tab">Time</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-0" href="#legal" role="tab" data-toggle="tab">Legal</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-0" href="#backup" role="tab" data-toggle="tab">Backup</a>
</li>
@@ -74,6 +77,8 @@
{% include "admin/configs/time.html" %}
{% include "admin/configs/legal.html" %}
{% include "admin/configs/backup.html" %}
</div>
</div>

View File

@@ -0,0 +1,56 @@
<div role="tabpanel" class="tab-pane config-section" id="legal">
{% with form = Forms.config.LegalSettingsForm(tos_url=tos_url, tos_text=tos_text, privacy_url=privacy_url, privacy_text=privacy_text) %}
<form method="POST" autocomplete="off" class="w-100">
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link active" href="#tos-config" role="tab" data-toggle="tab">
Terms of Service
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#privacy-policy-config" role="tab" data-toggle="tab">
Privacy Policy
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tos-config">
<div class="form-group">
{{ form.tos_url.label }}
{{ form.tos_url(class="form-control") }}
<small class="form-text text-muted">
{{ form.tos_url.description }}
</small>
</div>
<div class="form-group">
{{ form.tos_text.label }}
<small class="form-text text-muted">
{{ form.tos_text.description }}
</small>
{{ form.tos_text(class="form-control markdown", rows=15) }}
</div>
</div>
<div role="tabpanel" class="tab-pane" id="privacy-policy-config">
<div class="form-group">
{{ form.privacy_url.label }}
{{ form.privacy_url(class="form-control") }}
<small class="form-text text-muted">
{{ form.privacy_url.description }}
</small>
</div>
<div class="form-group">
{{ form.privacy_text.label }}
<small class="form-text text-muted">
{{ form.privacy_text.description }}
</small>
{{ form.privacy_text(class="form-control markdown", rows=15) }}
</div>
</div>
</div>
{{ form.submit(class="btn btn-md btn-primary float-right") }}
</form>
{% endwith %}
</div>

View File

@@ -55,6 +55,18 @@
{{ form.submit(class="btn btn-md btn-primary btn-outlined float-right") }}
</div>
</div>
{% if Configs.tos_or_privacy %}
<div class="row pt-3">
<div class="col-md-12 text-center">
<small class="text-muted text-center">
By registering, you agree to the
<a href="{{ Configs.privacy_link }}" rel="noopener" target="_blank">privacy policy</a>
and <a href="{{ Configs.tos_link }}" rel="noopener" target="_blank">terms of service</a>
</small>
</div>
</div>
{% endif %}
</form>
{% endwith %}
</div>

View File

@@ -28,7 +28,7 @@ from CTFd.utils import config, get_config, set_config
from CTFd.utils import user as current_user
from CTFd.utils import validators
from CTFd.utils.config import is_setup
from CTFd.utils.config.pages import get_page
from CTFd.utils.config.pages import build_html, get_page
from CTFd.utils.config.visibility import challenges_visible
from CTFd.utils.dates import ctf_ended, ctftime, view_after_ctf
from CTFd.utils.decorators import authed_only
@@ -332,6 +332,30 @@ def static_html(route):
return render_template("page.html", content=page.content)
@views.route("/tos")
def tos():
tos_url = get_config("tos_url")
tos_text = get_config("tos_text")
if tos_url:
return redirect(tos_url)
elif tos_text:
return render_template("page.html", content=build_html(tos_text))
else:
abort(404)
@views.route("/privacy")
def privacy():
privacy_url = get_config("privacy_url")
privacy_text = get_config("privacy_text")
if privacy_url:
return redirect(privacy_url)
elif privacy_text:
return render_template("page.html", content=build_html(privacy_text))
else:
abort(404)
@views.route("/files", defaults={"path": ""})
@views.route("/files/<path:path>")
def files(path):

27
tests/test_legal.py Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.utils import set_config
from tests.helpers import create_ctfd, destroy_ctfd
def test_legal_settings():
app = create_ctfd()
with app.app_context():
set_config("tos_text", "Terms of Service")
set_config("privacy_text", "Privacy Policy")
with app.test_client() as client:
r = client.get("/register")
assert r.status_code == 200
assert "privacy policy" in r.get_data(as_text=True)
assert "terms of service" in r.get_data(as_text=True)
r = client.get("/tos")
assert r.status_code == 200
assert "Terms of Service" in r.get_data(as_text=True)
r = client.get("/privacy")
assert r.status_code == 200
assert "Privacy Policy" in r.get_data(as_text=True)
destroy_ctfd(app)