Repository: CTFd/CTFd

Issue:
[Feature] Admin Team List - New Team
Admins should be able to create teams quickly for the admin team list page.

**TODO:**
- [x] MVP https://github.com/deckar01/CTFd/commit/c5df9994cbe61bc4815d71247eaa21ee5ed54d3c
- [ ] Fix inline style issue
- [ ] Add tests

![screen shot 2017-11-18 at 4 41 31 pm](https://user-images.githubusercontent.com/3108007/32985404-7a3b5ccc-cc7f-11e7-906f-9dcc20fc2dff.png)
![screen shot 2017-11-18 at 4 41 47 pm](https://user-images.githubusercontent.com/3108007/32985403-7a2946ae-cc7f-11e7-8fe5-fef5c83860a6.png)




Discussion:

Resolving patch:
diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py
--- a/CTFd/admin/teams.py
+++ b/CTFd/admin/teams.py
@@ -6,6 +6,8 @@
 
 from CTFd import utils
 
+import re
+
 admin_teams = Blueprint('admin_teams', __name__)
 
 
@@ -45,6 +47,54 @@ def admin_teams_view(page):
     return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
 
 
+@admin_teams.route('/admin/team/new', methods=['POST'])
+@admins_only
+def admin_create_team():
+    name = request.form.get('name', None)
+    password = request.form.get('password', None)
+    email = request.form.get('email', None)
+    website = request.form.get('website', None)
+    affiliation = request.form.get('affiliation', None)
+    country = request.form.get('country', None)
+
+    errors = []
+
+    if not name:
+        errors.append('The team requires a name')
+    elif Teams.query.filter(Teams.name == name).first():
+        errors.append('That name is taken')
+
+    if not email:
+        errors.append('The team requires an email')
+    elif Teams.query.filter(Teams.email == email).first():
+        errors.append('That email is taken')
+
+    if email:
+        valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)
+        if not valid_email:
+            errors.append("That email address is invalid")
+
+    if not password:
+        errors.append('The team requires a password')
+
+    if website and (website.startswith('http://') or website.startswith('https://')) is False:
+        errors.append('Websites must start with http:// or https://')
+
+    if errors:
+        db.session.close()
+        return jsonify({'data': errors})
+
+    team = Teams(name, email, password)
+    team.website = website
+    team.affiliation = affiliation
+    team.country = country
+
+    db.session.add(team)
+    db.session.commit()
+    db.session.close()
+    return jsonify({'data': ['success']})
+
+
 @admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
 @admins_only
 def admin_team(teamid):
@@ -93,6 +143,11 @@ def admin_team(teamid):
 
         errors = []
 
+        if email:
+            valid_email = re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)
+            if not valid_email:
+                errors.append("That email address is invalid")
+
         name_used = Teams.query.filter(Teams.name == name).first()
         if name_used and int(name_used.id) != int(teamid):
             errors.append('That name is taken')
@@ -101,6 +156,9 @@ def admin_team(teamid):
         if email_used and int(email_used.id) != int(teamid):
             errors.append('That email is taken')
 
+        if website and (website.startswith('http://') or website.startswith('https://')) is False:
+            errors.append('Websites must start with http:// or https://')
+
         if errors:
             db.session.close()
             return jsonify({'data': errors})


Test patch:
diff --git a/tests/admin/test_admin_facing.py b/tests/admin/test_admin_facing.py
--- a/tests/admin/test_admin_facing.py
+++ b/tests/admin/test_admin_facing.py
@@ -235,3 +235,138 @@ def test_admin_chal_detail_returns_proper_data():
         assert data == response
 
     destroy_ctfd(app)
+
+
+def test_admins_can_create_teams():
+    '''Test that admins can create new teams'''
+    app = create_ctfd()
+    with app.app_context():
+        client = login_as_user(app, name="admin", password="password")
+
+        with client.session_transaction() as sess:
+            data = {
+                'name': 'TunnelBunnies',
+                'password': 'fUnn3lJuNK135',
+                'email': 'scary.hares@trace.us',
+                'website': 'https://scary-hares.trace.us/',
+                'affiliation': 'Energizer',
+                'country': 'USA',
+                'nonce': sess.get('nonce'),
+            }
+            r = client.post('/admin/team/new', data=data)
+            assert r.status_code == 200
+
+        team = Teams.query.filter_by(id=2).first()
+        assert team
+        assert team.name == 'TunnelBunnies'
+        assert team.email == 'scary.hares@trace.us'
+        assert team.website == 'https://scary-hares.trace.us/'
+        assert team.affiliation == 'Energizer'
+        assert team.country == 'USA'
+    destroy_ctfd(app)
+
+
+def test_admin_create_team_without_required_fields():
+    '''Test that an admin can't create a new team without the required fields'''
+    app = create_ctfd()
+    with app.app_context():
+        client = login_as_user(app, name="admin", password="password")
+
+        with client.session_transaction() as sess:
+            data = {
+                'name': '',
+                'password': '',
+                'email': '',
+                'website': '',
+                'affiliation': '',
+                'country': '',
+                'nonce': sess.get('nonce'),
+            }
+            r = client.post('/admin/team/new', data=data)
+            assert r.status_code == 200
+
+        response = json.loads(r.get_data(as_text=True))
+        assert 'data' in response
+        assert len(response['data']) == 3
+        assert 'The team requires a name' in response['data']
+        assert 'The team requires an email' in response['data']
+        assert 'The team requires a password' in response['data']
+    destroy_ctfd(app)
+
+
+def test_admin_create_team_with_existing_name():
+    '''Test that an admin can't create a new team with an existing name'''
+    app = create_ctfd()
+    with app.app_context():
+        client = login_as_user(app, name="admin", password="password")
+
+        with client.session_transaction() as sess:
+            data = {
+                'name': 'admin',
+                'password': 'fUnn3lJuNK135',
+                'email': 'scary.hares@trace.us',
+                'website': 'https://scary-hares.trace.us/',
+                'affiliation': 'Energizer',
+                'country': 'USA',
+                'nonce': sess.get('nonce'),
+            }
+            r = client.post('/admin/team/new', data=data)
+            assert r.status_code == 200
+
+        response = json.loads(r.get_data(as_text=True))
+        assert 'data' in response
+        assert len(response['data']) == 1
+        assert 'That name is taken' in response['data']
+    destroy_ctfd(app)
+
+
+def test_admin_create_team_with_existing_email():
+    '''Test that an admin can't create a new team with an existing email'''
+    app = create_ctfd()
+    with app.app_context():
+        client = login_as_user(app, name="admin", password="password")
+
+        with client.session_transaction() as sess:
+            data = {
+                'name': 'TunnelBunnies',
+                'password': 'fUnn3lJuNK135',
+                'email': 'admin@ctfd.io',
+                'website': 'https://scary-hares.trace.us/',
+                'affiliation': 'Energizer',
+                'country': 'USA',
+                'nonce': sess.get('nonce'),
+            }
+            r = client.post('/admin/team/new', data=data)
+            assert r.status_code == 200
+
+        response = json.loads(r.get_data(as_text=True))
+        assert 'data' in response
+        assert len(response['data']) == 1
+        assert 'That email is taken' in response['data']
+    destroy_ctfd(app)
+
+
+def test_admin_create_team_with_invalid_website():
+    '''Test that an admin can't create a new team with an invalid website'''
+    app = create_ctfd()
+    with app.app_context():
+        client = login_as_user(app, name="admin", password="password")
+
+        with client.session_transaction() as sess:
+            data = {
+                'name': 'TunnelBunnies',
+                'password': 'fUnn3lJuNK135',
+                'email': 'scary.hares@trace.us',
+                'website': 'ftp://scary-hares.trace.us/',
+                'affiliation': 'Energizer',
+                'country': 'USA',
+                'nonce': sess.get('nonce'),
+            }
+            r = client.post('/admin/team/new', data=data)
+            assert r.status_code == 200
+
+        response = json.loads(r.get_data(as_text=True))
+        assert 'data' in response
+        assert len(response['data']) == 1
+        assert 'Websites must start with http:// or https://' in response['data']
+    destroy_ctfd(app)