Repository: CTFd/CTFd
Pull request #477 (2017-11-22 07:59:45)

Workshop mode
Create a config to allow people to hide teams and scores. To be used for recruiting events or class homework. 


Patch:
diff --git a/.travis.yml b/.travis.yml
index 08f66424..202fc150 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,4 +15,4 @@ before_script:
     - psql -c 'create database ctfd;' -U postgres
 script:
     - pep8 --ignore E501,E712 CTFd/ tests/
-    - nosetests
+    - nosetests -d
diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py
index 2504e9e7..93b05496 100644
--- a/CTFd/admin/__init__.py
+++ b/CTFd/admin/__init__.py
@@ -117,6 +117,7 @@ def admin_config():
             mail_tls = bool(request.form.get('mail_tls', None))
             mail_ssl = bool(request.form.get('mail_ssl', None))
             mail_useauth = bool(request.form.get('mail_useauth', None))
+            workshop_mode = bool(request.form.get('workshop_mode', None))
         except (ValueError, TypeError):
             view_challenges_unregistered = None
             view_scoreboard_if_authed = None
@@ -128,6 +129,7 @@ def admin_config():
             mail_tls = None
             mail_ssl = None
             mail_useauth = None
+            workshop_mode = None
         finally:
             view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
             view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
@@ -139,6 +141,7 @@ def admin_config():
             mail_tls = utils.set_config('mail_tls', mail_tls)
             mail_ssl = utils.set_config('mail_ssl', mail_ssl)
             mail_useauth = utils.set_config('mail_useauth', mail_useauth)
+            workshop_mode = utils.set_config('workshop_mode', workshop_mode)
 
         mail_server = utils.set_config("mail_server", request.form.get('mail_server', None))
         mail_port = utils.set_config("mail_port", request.form.get('mail_port', None))
@@ -207,6 +210,8 @@ def admin_config():
     prevent_name_change = utils.get_config('prevent_name_change')
     verify_emails = utils.get_config('verify_emails')
 
+    workshop_mode = utils.get_config('workshop_mode')
+
     db.session.commit()
     db.session.close()
 
@@ -236,4 +241,5 @@ def admin_config():
                            prevent_name_change=prevent_name_change,
                            verify_emails=verify_emails,
                            view_after_ctf=view_after_ctf,
-                           themes=themes)
+                           themes=themes,
+                           workshop_mode=workshop_mode)
diff --git a/CTFd/challenges.py b/CTFd/challenges.py
index 9ced0812..f7831155 100644
--- a/CTFd/challenges.py
+++ b/CTFd/challenges.py
@@ -153,6 +153,11 @@ def solves_per_chal():
     if not utils.user_can_view_challenges():
         return redirect(url_for('auth.login', next=request.path))
 
+    chals = Challenges.query\
+        .filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
+        .order_by(Challenges.value)\
+        .all()
+
     solves_sub = db.session.query(
         Solves.chalid,
         db.func.count(Solves.chalid).label('solves')
@@ -168,15 +173,21 @@ def solves_per_chal():
     ) \
         .join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
 
-    json = {}
+    data = {}
     if utils.hide_scores():
         for chal, count, name in solves:
-            json[chal] = -1
+            data[chal] = -1
+        for c in chals:
+            if c.id not in data:
+                data[c.id] = -1
     else:
         for chal, count, name in solves:
-            json[chal] = count
+            data[chal] = count
+        for c in chals:
+            if c.id not in data:
+                data[c.id] = 0
     db.session.close()
-    return jsonify(json)
+    return jsonify(data)
 
 
 @challenges.route('/solves')
diff --git a/CTFd/themes/admin/templates/config.html b/CTFd/themes/admin/templates/config.html
index 00a074b3..d93d9d65 100644
--- a/CTFd/themes/admin/templates/config.html
+++ b/CTFd/themes/admin/templates/config.html
@@ -53,6 +53,16 @@
 						</select>
 					</div>
 
+					<div class="checkbox">
+						<label>
+							<input id="workshop_mode" name="workshop_mode" type="checkbox"
+								   {% if workshop_mode %}checked{% endif %}>
+							Workshop Mode <i class="fa fa-question-circle gray-text" data-toggle="tooltip"
+											 data-placement="right"
+											 title="Hide all public team and score information."></i>
+						</label>
+					</div>
+
 					<div class="checkbox">
 						<label>
 							<input id="hide_scores" name="hide_scores" type="checkbox" {% if hide_scores %}checked{% endif %}>
diff --git a/CTFd/themes/original/static/css/style.css b/CTFd/themes/original/static/css/style.css
index 2240eddd..a975595e 100644
--- a/CTFd/themes/original/static/css/style.css
+++ b/CTFd/themes/original/static/css/style.css
@@ -123,7 +123,7 @@ table{
 }
 
 .spinner-error {
-    padding-top: 210px;
+    padding-top: 20vh;
     text-align: center;
     opacity: 0.5;
 }
diff --git a/CTFd/themes/original/templates/base.html b/CTFd/themes/original/templates/base.html
index ae1b9797..8077d493 100644
--- a/CTFd/themes/original/templates/base.html
+++ b/CTFd/themes/original/templates/base.html
@@ -53,8 +53,9 @@
 								<li><a href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a></li>
 							{% endif %}
 						{% endfor %}
-
+						{% if not get_config('workshop_mode') %}
 						<li><a href="{{ request.script_root }}/teams">Teams</a></li>
+						{% endif %}
 						{% if not hide_scores() %}
 						<li><a href="{{ request.script_root }}/scoreboard">Scoreboard</a></li>
 						{% endif %}
@@ -65,7 +66,7 @@
 						{% if admin %}
 						<li><a href="{{ request.script_root }}/admin">Admin</a></li>
 						{% endif %}
-						<li><a href="{{ request.script_root }}/team/{{ id }}">Team</a></li>
+						<li><a href="{{ request.script_root }}/team">Team</a></li>
 						<li><a href="{{ request.script_root }}/profile">Profile</a></li>
 						<li><a href="{{ request.script_root }}/logout">Logout</a></li>
 					{% else %}
diff --git a/CTFd/utils.py b/CTFd/utils.py
index eb4fe512..bf1274f1 100644
--- a/CTFd/utils.py
+++ b/CTFd/utils.py
@@ -217,7 +217,7 @@ def ctf_theme():
 
 @cache.memoize()
 def hide_scores():
-    return get_config('hide_scores')
+    return get_config('hide_scores') or get_config('workshop_mode')
 
 
 def override_template(template, html):
diff --git a/CTFd/views.py b/CTFd/views.py
index fd5a2549..8a877cf6 100644
--- a/CTFd/views.py
+++ b/CTFd/views.py
@@ -120,6 +120,8 @@ def static_html(template):
 @views.route('/teams', defaults={'page': '1'})
 @views.route('/teams/<int:page>')
 def teams(page):
+    if utils.get_config('workshop_mode'):
+        abort(404)
     page = abs(int(page))
     results_per_page = 50
     page_start = results_per_page * (page - 1)
@@ -164,6 +166,9 @@ def private_team():
 
 @views.route('/team/<int:teamid>', methods=['GET', 'POST'])
 def team(teamid):
+    if utils.get_config('workshop_mode'):
+        abort(404)
+
     if utils.get_config('view_scoreboard_if_utils.authed') and not utils.authed():
         return redirect(url_for('auth.login', next=request.path))
     errors = []


Test patch:
diff --git a/tests/user/test_challenges.py b/tests/user/test_challenges.py
index 83ad6324..254a32a9 100644
--- a/tests/user/test_challenges.py
+++ b/tests/user/test_challenges.py
@@ -56,7 +56,7 @@ def test_chals_solves():
             register_user(app, name=name, email=email, password="password")
 
         # Generate 5 challenges
-        for c in range(5):
+        for c in range(6):
             chal1 = gen_challenge(app.db, value=100)
 
         user_ids = list(range(2, 7))
@@ -76,7 +76,23 @@ def test_chals_solves():
               "2": 4,
               "3": 3,
               "4": 2,
-              "5": 1
+              "5": 1,
+              "6": 0
+            }
+            ''')
+            received = json.loads(output)
+            assert saved == received
+        set_config('hide_scores', True)
+        with client.session_transaction() as sess:
+            r = client.get('/chals/solves')
+            output = r.get_data(as_text=True)
+            saved = json.loads('''{
+              "1": -1,
+              "2": -1,
+              "3": -1,
+              "4": -1,
+              "5": -1,
+              "6": -1
             }
             ''')
             received = json.loads(output)
@@ -435,7 +451,10 @@ def test_that_view_challenges_unregistered_works():
 
         r = client.get('/chals/solves')
         data = r.get_data(as_text=True)
-        assert json.loads(data) == {}
+        assert json.loads(data) == json.loads('''{
+              "1": 0
+            }
+            ''')
 
         r = client.get('/chal/1/solves')
         data = r.get_data(as_text=True)
diff --git a/tests/user/test_user_facing.py b/tests/user/test_user_facing.py
index ab209a4a..29623c69 100644
--- a/tests/user/test_user_facing.py
+++ b/tests/user/test_user_facing.py
@@ -565,3 +565,53 @@ http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdw
             team = Teams.query.filter_by(email="user@user.com").first()
             assert team.password != team_password_saved
     destroy_ctfd(app)
+
+
+def test_workshop_mode():
+    """Test that workshop mode hides the appropriate data"""
+    app = create_ctfd()
+    with app.app_context():
+        # Set CTFd to only allow confirmed users and send emails
+        set_config('workshop_mode', True)
+
+        register_user(app)
+
+        chal = gen_challenge(app.db, value=100)
+        solve = gen_solve(app.db, teamid=2, chalid=1)
+
+        client = login_as_user(app, name="user", password="password")
+        r = client.get('/scoreboard')
+        output = r.get_data(as_text=True)
+        assert "Scores are currently hidden" in output
+
+        r = client.get('/scoreboard')
+        output = r.get_data(as_text=True)
+        assert "Scores are currently hidden" in output
+
+        r = client.get('/scores')
+        received = r.get_data(as_text=True)
+        saved = '''{
+          "standings": []
+        }
+        '''
+        assert json.loads(saved) == json.loads(received)
+
+        r = client.get('/teams')
+        assert r.status_code == 404
+
+        r = client.get('/teams/1')
+        assert r.status_code == 404
+
+        r = client.get('/teams/2')
+        assert r.status_code == 404
+
+        r = client.get('/chals/solves')
+        output = r.get_data(as_text=True)
+        saved = json.loads('''{
+          "1": -1
+        }
+        ''')
+        received = json.loads(output)
+        assert saved == received
+
+    destroy_ctfd(app)