From be65669503ec108c0d65bd2f7bf2083fd6e4ef38 Mon Sep 17 00:00:00 2001 From: Samuel Hoffman Date: Fri, 6 Mar 2020 01:51:22 -0500 Subject: [PATCH 1/4] allow MAIL_SUPPRESS_SEND in Production --- CodeChallenge/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeChallenge/config.py b/CodeChallenge/config.py index b2d6cd8..201a9ec 100644 --- a/CodeChallenge/config.py +++ b/CodeChallenge/config.py @@ -65,7 +65,7 @@ class ProductionConfig(DefaultConfig): JWT_COOKIE_SECURE = True JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") CODE_CHALLENGE_START = os.getenv("CODE_CHALLENGE_START") - MAIL_SUPPRESS_SEND = False + MAIL_SUPPRESS_SEND = os.getenv("MAIL_SUPPRESS_SEND", False) MAIL_USERNAME = os.getenv("MAIL_USERNAME") MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") JWT_ACCESS_TOKEN_EXPIRES = 604800 From 8f7b790d908e1085763811302229ca0a20cc7040 Mon Sep 17 00:00:00 2001 From: Samuel Hoffman Date: Fri, 6 Mar 2020 01:59:44 -0500 Subject: [PATCH 2/4] Don't try to post to a None if SLACK_WEBHOOK isn't an envvar --- CodeChallenge/api/users.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/CodeChallenge/api/users.py b/CodeChallenge/api/users.py index 580f167..2063145 100644 --- a/CodeChallenge/api/users.py +++ b/CodeChallenge/api/users.py @@ -163,17 +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() + if not current_app.config.get("TESTING", False): 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}" - )) + if webhook is not None: + regcount = db.session.query(func.count(Users.id)).scalar() + 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"}) From 25306763a8c2c6118d04b94c5d1089979fd2806e Mon Sep 17 00:00:00 2001 From: Samuel Hoffman Date: Tue, 10 Mar 2020 16:29:09 -0400 Subject: [PATCH 3/4] voteapi: add /api/v1/vote/search --- CodeChallenge/api/vote.py | 48 ++++++++++++++++++++++++++++----------- CodeChallenge/auth.py | 7 ++++++ CodeChallenge/models.py | 8 +++++++ tests/test_question.py | 18 +++++++++++++++ 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/CodeChallenge/api/vote.py b/CodeChallenge/api/vote.py index faf5817..5c37287 100644 --- a/CodeChallenge/api/vote.py +++ b/CodeChallenge/api/vote.py @@ -2,6 +2,7 @@ from flask_jwt_extended import get_current_user, jwt_optional from flask_mail import Message from itsdangerous import URLSafeSerializer +from sqlalchemy import or_ from .. import core from ..auth import Users @@ -49,25 +50,14 @@ def get_contestants(): contestants = [] for ans in p.items: # type: Answer - display = None - if ans.user.studentfirstname \ - and ans.user.studentlastname: - 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(confirmed_votes), + numVotes=ans.confirmed_votes(), firstName=ans.user.studentfirstname, lastName=ans.user.studentlastname, username=ans.user.username, - display=display + display=ans.user.display() )) return jsonify( @@ -188,3 +178,35 @@ def vote_confirm(): return jsonify(status="success", reason="vote confirmed") + + +@bp.route("/search", methods=["GET"]) +def search(): + keyword = request.args.get("q") + + if keyword is None: + return jsonify(status="error", reason="missing 'q' parameter"), 400 + + keyword = f"%{keyword}%" + + answers = Answer.query \ + .join(Answer.question) \ + .join(Answer.user) \ + .filter(Question.rank == core.max_rank(), + Answer.correct, or_(Users.username.ilike(keyword), Users.studentlastname.ilike(keyword), + Users.studentlastname.ilike(keyword))) + + results = [] + + for ans in answers.all(): # type: Answer + results.append(dict( + id=ans.id, + text=ans.text, + numVotes=ans.confirmed_votes(), + firstName=ans.user.studentfirstname, + lastName=ans.user.studentlastname, + username=ans.user.username, + display=ans.user.display() + )) + + return jsonify(results=results) diff --git a/CodeChallenge/auth.py b/CodeChallenge/auth.py index eb9a1bd..248cf86 100644 --- a/CodeChallenge/auth.py +++ b/CodeChallenge/auth.py @@ -53,6 +53,13 @@ def votes(self): .all() return v + def display(self): + if self.studentfirstname is not None \ + and self.studentlastname is not None \ + and len(self.studentlastname): + return f"{self.studentfirstname} " \ + f"{self.studentlastname[0]}." + def hash_password(plaintext): ph = argon2.PasswordHasher() diff --git a/CodeChallenge/models.py b/CodeChallenge/models.py index 395ec01..48a556b 100644 --- a/CodeChallenge/models.py +++ b/CodeChallenge/models.py @@ -40,6 +40,14 @@ class Answer(db.Model): votes = db.relationship("Vote", cascade="all,delete", lazy=True, uselist=True) + def confirmed_votes(self) -> int: + confirmed = 0 + for vote in self.votes: + if vote.confirmed: + confirmed += 1 + + return confirmed + class Vote(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/tests/test_question.py b/tests/test_question.py index 1e06a3f..b342f68 100644 --- a/tests/test_question.py +++ b/tests/test_question.py @@ -380,6 +380,24 @@ def test_cast_vote(client_challenge_lastq): assert rv.json["status"] == "success" +@pytest.mark.skipif(not os.getenv("SANDBOX_API_URL"), reason="no final question") +def test_vote_search(client_challenge_lastq): + rv = client_challenge_lastq.get("/api/v1/vote/search?q=sam") + assert rv.status_code == 200 + + results = rv.json["results"] + assert len(results) == 1 + assert results[0]["username"] == "cwhqsam" + assert results[0]["numVotes"] == 1 + + rv2 = client_challenge_lastq.get("/api/v1/vote/search?q=hOffMan") + assert rv2.status_code == 200 + + results2 = rv.json["results"] + assert len(results2) == 1 + assert results2[0]["username"] == "cwhqsam" + + @pytest.mark.skipif(not os.getenv("SANDBOX_API_URL"), reason="no final question") def test_cast_notregistered(client_challenge_lastq): client_challenge_lastq.cookie_jar.clear() # logout From d30a15c4d1342e168b2d4af2eedeac3af517b678 Mon Sep 17 00:00:00 2001 From: Samuel Hoffman Date: Tue, 10 Mar 2020 16:35:53 -0400 Subject: [PATCH 4/4] voteapi: paginate search results --- CodeChallenge/api/vote.py | 24 ++++++++++++++++++++---- tests/test_question.py | 4 ++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CodeChallenge/api/vote.py b/CodeChallenge/api/vote.py index 03ab820..f919381 100644 --- a/CodeChallenge/api/vote.py +++ b/CodeChallenge/api/vote.py @@ -183,22 +183,29 @@ def vote_confirm(): @bp.route("/search", methods=["GET"]) def search(): keyword = request.args.get("q") + try: + page = int(request.args.get("page", 1)) + per = int(request.args.get("per", 20)) + except ValueError: + return jsonify(status="error", + reason="invalid 'page' or 'per' parameter"), 400 if keyword is None: return jsonify(status="error", reason="missing 'q' parameter"), 400 keyword = f"%{keyword}%" - answers = Answer.query \ + p = Answer.query \ .join(Answer.question) \ .join(Answer.user) \ .filter(Question.rank == core.max_rank(), Answer.correct, or_(Users.username.ilike(keyword), Users.studentlastname.ilike(keyword), - Users.studentlastname.ilike(keyword))) + Users.studentlastname.ilike(keyword))) \ + .paginate(page=page, per_page=per) results = [] - for ans in answers.all(): # type: Answer + for ans in p.items: # type: Answer results.append(dict( id=ans.id, text=ans.text, @@ -209,4 +216,13 @@ def search(): display=ans.user.display() )) - return jsonify(results=results) + return jsonify( + items=results, + totalItems=p.total, + page=p.page, + totalPages=p.pages, + hasNext=p.has_next, + nextNum=p.next_num, + hasPrev=p.has_prev, + prevNum=p.prev_num + ) diff --git a/tests/test_question.py b/tests/test_question.py index b342f68..71b401d 100644 --- a/tests/test_question.py +++ b/tests/test_question.py @@ -385,7 +385,7 @@ def test_vote_search(client_challenge_lastq): rv = client_challenge_lastq.get("/api/v1/vote/search?q=sam") assert rv.status_code == 200 - results = rv.json["results"] + results = rv.json["items"] assert len(results) == 1 assert results[0]["username"] == "cwhqsam" assert results[0]["numVotes"] == 1 @@ -393,7 +393,7 @@ def test_vote_search(client_challenge_lastq): rv2 = client_challenge_lastq.get("/api/v1/vote/search?q=hOffMan") assert rv2.status_code == 200 - results2 = rv.json["results"] + results2 = rv.json["items"] assert len(results2) == 1 assert results2[0]["username"] == "cwhqsam"