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 import json
from flask import url_for
from CTFd.constants import JinjaEnum, RawEnum from CTFd.constants import JinjaEnum, RawEnum
from CTFd.utils import get_config from CTFd.utils import get_config
@@ -63,5 +65,19 @@ class _ConfigsWrapper:
def theme_settings(self): def theme_settings(self):
return json.loads(get_config("theme_settings", default="null")) 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() Configs = _ConfigsWrapper()

View File

@@ -1,5 +1,5 @@
from wtforms import BooleanField, SelectField, StringField from wtforms import BooleanField, SelectField, StringField, TextAreaField
from wtforms.fields.html5 import IntegerField from wtforms.fields.html5 import IntegerField, URLField
from wtforms.widgets.html5 import NumberInput from wtforms.widgets.html5 import NumberInput
from CTFd.forms import BaseForm from CTFd.forms import BaseForm
@@ -60,3 +60,21 @@ class ExportCSVForm(BaseForm):
), ),
) )
submit = SubmitField("Download CSV") 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(); 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) { $("#theme-settings-modal form").submit(function(e) {
e.preventDefault(); e.preventDefault();
theme_settings_editor 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"> <li class="nav-item">
<a class="nav-link rounded-0" href="#ctftime" role="tab" data-toggle="tab">Time</a> <a class="nav-link rounded-0" href="#ctftime" role="tab" data-toggle="tab">Time</a>
</li> </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"> <li class="nav-item">
<a class="nav-link rounded-0" href="#backup" role="tab" data-toggle="tab">Backup</a> <a class="nav-link rounded-0" href="#backup" role="tab" data-toggle="tab">Backup</a>
</li> </li>
@@ -74,6 +77,8 @@
{% include "admin/configs/time.html" %} {% include "admin/configs/time.html" %}
{% include "admin/configs/legal.html" %}
{% include "admin/configs/backup.html" %} {% include "admin/configs/backup.html" %}
</div> </div>
</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") }} {{ form.submit(class="btn btn-md btn-primary btn-outlined float-right") }}
</div> </div>
</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> </form>
{% endwith %} {% endwith %}
</div> </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 user as current_user
from CTFd.utils import validators from CTFd.utils import validators
from CTFd.utils.config import is_setup 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.config.visibility import challenges_visible
from CTFd.utils.dates import ctf_ended, ctftime, view_after_ctf from CTFd.utils.dates import ctf_ended, ctftime, view_after_ctf
from CTFd.utils.decorators import authed_only from CTFd.utils.decorators import authed_only
@@ -332,6 +332,30 @@ def static_html(route):
return render_template("page.html", content=page.content) 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", defaults={"path": ""})
@views.route("/files/<path:path>") @views.route("/files/<path:path>")
def files(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)