diff --git a/CodeChallenge/__init__.py b/CodeChallenge/__init__.py index 2e0b6f4..39a3add 100644 --- a/CodeChallenge/__init__.py +++ b/CodeChallenge/__init__.py @@ -11,6 +11,7 @@ from . import core from .api.eb import bp as eb_bp from .api.questions import bp as questions_bp +from .api.slack import bp as slack_bp from .api.users import bp as users_bp from .api.vote import bp as vote_bp from .auth import jwt @@ -62,6 +63,7 @@ def create_app(config): app.register_blueprint(q_cli_bp) app.register_blueprint(clock_cli_bp) app.register_blueprint(vote_bp) + app.register_blueprint(slack_bp) @app.errorhandler(429) def ratelimit_handler(e): diff --git a/CodeChallenge/api/eb.py b/CodeChallenge/api/eb.py index 9ea0c3f..fb0196f 100644 --- a/CodeChallenge/api/eb.py +++ b/CodeChallenge/api/eb.py @@ -1,5 +1,6 @@ from hmac import compare_digest +import requests from flask import Blueprint, request, current_app, render_template from flask_mail import Message @@ -41,4 +42,10 @@ def worker(): mail.send(msg) + webhook = current_app.config.get("SLACK_WEBHOOK") + if webhook is not None: + requests.post(webhook, json=dict( + text=f"*NEW RANK* {core.current_rank()}" + )) + return "", 200 diff --git a/CodeChallenge/api/slack.py b/CodeChallenge/api/slack.py new file mode 100644 index 0000000..3b569b9 --- /dev/null +++ b/CodeChallenge/api/slack.py @@ -0,0 +1,91 @@ +import hmac +import re +import time + +import requests +from flask import Blueprint, request, jsonify, current_app, abort +from sqlalchemy import func + +from .. import core +from ..auth import Users +from ..models import db + +bp = Blueprint("slackapi", __name__, url_prefix="/api/v1/slack") + +CMDRE = re.compile(r"^!([^ ]+)", re.I) + + +@bp.before_request +def slack_verify(): + ts = request.headers.get("X-Slack-Request-Timestamp") + secret = bytes(current_app.config.get("SLACK_SIGNING_SECRET"), "utf8") + body = request.data + + if abs(time.time() - int(ts)) > 60 * 5: + abort(401) + + sig_basestring = bytes(f"v0:{ts}:", "utf8") + sig_basestring += body + + my_sig = "v0=" + hmac.new(secret, sig_basestring, digestmod="SHA256").hexdigest() + slack_sig = request.headers.get("X-Slack-Signature") + if not hmac.compare_digest(my_sig, slack_sig): + abort(401) + + +def post_message(channel, text): + rv = requests.post("https://slack.com/api/chat.postMessage", + headers=dict( + Authorization="Bearer " + current_app.config["SLACK_OAUTH_TOKEN"] + ), + json=dict( + channel=channel, + text=text + )) + + rv.raise_for_status() + + +def handle_message(text, channel): + match = CMDRE.search(text) + + if match is None: + return + + command = match.group(1) + + if command == "status": + rank = core.current_rank() + resp = f"*Current Rank:* {rank if rank != -1 else '(challenge not started)'}\n" + resp += f"*Max Rank:* {core.max_rank()}\n" + resp += f"*Next Rank:* {core.time_until_next_rank()}\n" + resp += f"*Total Users:* {core.user_count()}" + + post_message(channel, resp) + + if command == "foundus": + found_us = db.session.query(Users.found_us, + func.count(Users.found_us)) \ + .group_by(Users.found_us) \ + .order_by(Users.found_us) \ + .all() + + resp = "" + for row in found_us: + resp += f"*{row[0]}*: {row[1]}\n" + + post_message(channel, resp) + + +@bp.route("/event", methods=["POST"]) +def slack_event(): + data = request.get_json() + + if "challenge" in data: + return jsonify(challenge=data["challenge"]) + + event = data["event"] + if event["type"] == "message": + handle_message(event["text"], event["channel"]) + + return "", 200 diff --git a/CodeChallenge/api/users.py b/CodeChallenge/api/users.py index cb7f754..2c64337 100644 --- a/CodeChallenge/api/users.py +++ b/CodeChallenge/api/users.py @@ -1,3 +1,4 @@ +import requests from flask import Blueprint, jsonify, request, current_app, render_template from flask_jwt_extended import (create_access_token, create_refresh_token, get_current_user, get_jwt_identity, @@ -6,6 +7,7 @@ unset_jwt_cookies) from flask_limiter.util import get_remote_address from flask_mail import Message +from sqlalchemy import func from .. import core from ..auth import (Users, hash_password, password_reset_token, @@ -146,8 +148,7 @@ def register(): name = new_u.studentfirstname or new_u.parentfirstname confirm_email.html = render_template("challenge_account_confirm.html", name=name, - username=new_u.username, - password=password) + username=new_u.username) confirm_email.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"} # welcome email @@ -162,6 +163,18 @@ def register(): mail.send(confirm_email) mail.send(welcome_email) + if "SLACK_WEBHOOK" in current_app.config and not current_app.config.get("TESTING", False): + regcount = db.session.query(func.count(Users.id)).scalar() + webhook = current_app.config.get("SLACK_WEBHOOK") + requests.post(webhook, json=dict( + text="Event: New Registration\n\n" + f"*User*: {new_u.username}\n" + f"*Student*: {new_u.studentfirstname} {new_u.studentlastname}\n" + f"*Parent*: {new_u.parentfirstname} {new_u.parentlastname}\n" + f"*How'd you find us?* {new_u.found_us}\n" + f"\n*Total Registrations*: {regcount}" + )) + return jsonify({"status": "success"}) diff --git a/CodeChallenge/api/vote.py b/CodeChallenge/api/vote.py index f9629a9..faf5817 100644 --- a/CodeChallenge/api/vote.py +++ b/CodeChallenge/api/vote.py @@ -55,10 +55,15 @@ def get_contestants(): display = f"{ans.user.studentfirstname} " \ f"{ans.user.studentlastname[0]}." + confirmed_votes = [] + for v in ans.votes: + if v.confirmed: + confirmed_votes.append(v) + contestants.append(dict( id=ans.id, text=ans.text, - numVotes=len(ans.votes), + numVotes=len(confirmed_votes), firstName=ans.user.studentfirstname, lastName=ans.user.studentlastname, username=ans.user.username, diff --git a/CodeChallenge/config.py b/CodeChallenge/config.py index 1434492..f52d8b6 100644 --- a/CodeChallenge/config.py +++ b/CodeChallenge/config.py @@ -30,6 +30,10 @@ class DefaultConfig: MG_PRIVATE_KEY = os.getenv("MG_PRIVATE_KEY") MG_LIST = "codechallenge@school.codewizardshq.com" WORKER_PASSWORD = os.getenv("WORKER_PASSWORD") + SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK") + SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET") + SLACK_OAUTH_TOKEN = os.getenv("SLACK_OAUTH_TOKEN") + SLACK_CHANNEL = os.getenv("SLACK_CHANNEL") # no trailing / EXTERNAL_URL = "https://challenge.codewizardshq.com" diff --git a/CodeChallenge/core.py b/CodeChallenge/core.py index 81c746c..cde9aa2 100644 --- a/CodeChallenge/core.py +++ b/CodeChallenge/core.py @@ -3,6 +3,7 @@ from flask import current_app from sqlalchemy import func +from .auth import Users from .models import Question, db @@ -68,3 +69,7 @@ def challenge_ended() -> bool: return True return False + + +def user_count() -> int: + return db.session.query(func.count(Users.id)).scalar() diff --git a/CodeChallenge/templates/challenge_account_confirm.html b/CodeChallenge/templates/challenge_account_confirm.html index c7c516d..2261017 100644 --- a/CodeChallenge/templates/challenge_account_confirm.html +++ b/CodeChallenge/templates/challenge_account_confirm.html @@ -295,7 +295,6 @@
{{name}}, your account has been created.
Login: {{username}}
-Password: {{password}}
Forgot your password? Reset your password.
diff --git a/src/api/voting.js b/src/api/voting.js index 5b07932..b2b0cfd 100644 --- a/src/api/voting.js +++ b/src/api/voting.js @@ -1,8 +1,10 @@ import routes from "./routes"; import request from "./request"; -async function getBallot() { - return request(routes.voting_ballot); +async function getBallot(page, per) { + return request(routes.voting_ballot, { + params: { page, per } + }); } async function cast(answerId, email) { diff --git a/src/components/MobileWarning.vue b/src/components/MobileWarning.vue new file mode 100644 index 0000000..20c0eee --- /dev/null +++ b/src/components/MobileWarning.vue @@ -0,0 +1,22 @@ + +
+ + Our Coding Challenge platform was not created for mobile phones. Please + visit challenge.codewizardshq.com from your desktop for the best + experience. +
+
@@ -82,28 +88,13 @@
diff --git a/src/views/Voting/Ballot.vue b/src/views/Voting/Ballot.vue
index f016711..4b69f6a 100644
--- a/src/views/Voting/Ballot.vue
+++ b/src/views/Voting/Ballot.vue
@@ -3,21 +3,34 @@
- Lorem Ipsum is simply dummy text of the printing and typesetting
- industry. Lorem Ipsum has been the industry's standard dummy text ever
- since the 1500s, when an unknown printer took a galley of type and
- scrambled it to make a type specimen book.
+ Cast your vote below!
+ Loading Results
+
Please Wait
+