Пример #1
0
    def patch(self, puzzle_id):
        data = request.get_json(silent=True)
        response_msg = update_puzzle_details(puzzle_id, data)

        if (
            response_msg.get("rowcount")
            and response_msg.get("status_code") == 200
            and data.get("status")
        ):
            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
                current_app.config.get("PURGEURLLIST"),
            )
        return make_response(json.jsonify(response_msg), response_msg["status_code"])
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # Verify args
        action = args.get("action")
        if action not in ACTIONS:
            abort(400)

        name_register_ids = request.form.getlist("name_register_id")
        if len(name_register_ids) == 0:
            abort(400)
        if not isinstance(name_register_ids, list):
            name_register_ids = [name_register_ids]

        name_register_users = request.form.getlist("name_register_user")
        if len(name_register_users) == 0:
            abort(400)
        if not isinstance(name_register_users, list):
            name_register_users = [name_register_users]

        cur = db.cursor()

        if action == "reject":

            def each(name_register_ids):
                for id in name_register_ids:
                    yield {"id": id}

            cur.executemany(
                fetch_query_string("reject-name-on-name-register-for-id.sql"),
                each(name_register_ids),
            )

        db.commit()
        cur.close()

        routes_to_purge = []
        for user in name_register_users:
            routes_to_purge.append(
                "/chill/site/internal/player-bit/{}/".format(user))
        purge_route_from_nginx_cache(
            "\n".join(routes_to_purge),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect("/chill/site/admin/name-register-review/")
Пример #3
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # TODO: Check user to see if role matches?
        # user = current_app.secure_cookie.get(u'user')
        # if not user:
        #     abort(403)

        # Verify args
        action = args.get("action")
        if action not in ACTIONS:
            abort(400)

        reject = args.get("reject")
        if action == "reject" and reject not in ("license", "attribution"):
            abort(400)

        delete = args.get("delete")
        if action == "delete" and delete not in ("license", "inapt", "old",
                                                 "request"):
            abort(400)

        # abort if tag value not set
        tag = args.get("tag")
        if action == "tag" and not tag:
            abort(400)

        puzzle_ids = request.form.getlist("montage_puzzle_id")
        if len(puzzle_ids) == 0 or len(puzzle_ids) > 20:
            abort(400)
        if not isinstance(puzzle_ids, list):
            puzzle_ids = [puzzle_ids]

        cur = db.cursor()
        status = None

        if action == "approve":
            # TODO: May need to be set to REBUILD if it is an existing puzzle,
            # otherwise the preview_full.jpg will be recreated.  Use new
            # "rebuild" action instead of just "approve".
            status = IN_RENDER_QUEUE

        if action == "reject":
            if reject == "license":
                status = FAILED_LICENSE
            elif reject == "attribution":
                status = NO_ATTRIBUTION

        if action == "delete":
            if delete == "license":
                status = DELETED_LICENSE
            elif delete == "inapt":
                status = DELETED_INAPT
            elif delete == "old":
                status = DELETED_OLD
            elif delete == "request":
                status = DELETED_REQUEST

            for puzzle_id in puzzle_ids:
                delete_puzzle_resources(puzzle_id)
                id = cur.execute(
                    fetch_query_string("select_puzzle_id_by_puzzle_id.sql"),
                    {
                        "puzzle_id": puzzle_id
                    },
                ).fetchone()[0]
                # current_app.logger.info('deleting puzzle resources for id {}'.format(id))
                cur.execute(
                    fetch_query_string("delete_puzzle_file_for_puzzle.sql"),
                    {"puzzle": id},
                )
                cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"),
                            {"puzzle": id})
                cur.execute(fetch_query_string("delete_puzzle_timeline.sql"),
                            {"puzzle": id})
                redis_connection.delete("timeline:{puzzle}".format(puzzle=id))
                redis_connection.delete("score:{puzzle}".format(puzzle=id))
            db.commit()

        def each(puzzle_ids):
            for puzzle_id in puzzle_ids:
                yield {"puzzle_id": puzzle_id, "status": status}

        cur.executemany(
            fetch_query_string("update_puzzle_status_for_puzzle_id.sql"),
            each(puzzle_ids),
        )
        db.commit()

        for puzzle_id in puzzle_ids:
            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
                current_app.config.get("PURGEURLLIST"),
            )

        if action == "approve":
            puzzles = rowify(
                cur.execute(
                    fetch_query_string("select-puzzles-in-render-queue.sql"),
                    {
                        "IN_RENDER_QUEUE": IN_RENDER_QUEUE,
                        "REBUILD": REBUILD
                    },
                ).fetchall(),
                cur.description,
            )[0]
            print("found {0} puzzles to render".format(len(puzzles)))

            # push each puzzle to artist job queue
            for puzzle in puzzles:
                job = current_app.createqueue.enqueue_call(
                    func="api.jobs.pieceRenderer.render",
                    args=([puzzle]),
                    result_ttl=0,
                    timeout="24h",
                )

        # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances

        cur.close()
        return make_response("204", 204)
Пример #4
0
    def patch(self, puzzle_id):
        ip = request.headers.get("X-Real-IP")
        user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip))

        # validate the args and headers
        args = {}
        xhr_data = request.get_json()
        if xhr_data:
            args.update(xhr_data)
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # Verify args
        action = args.get("action")
        if action not in INSTANCE_ACTIONS:
            abort(400)

        cur = db.cursor()

        # validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select-puzzle-details-for-puzzle_id.sql"),
            {"puzzle_id": puzzle_id},
        ).fetchall()
        if not result:
            # 400 if puzzle does not exist
            err_msg = {
                "msg": "No puzzle found",
            }
            cur.close()
            return make_response(json.jsonify(err_msg), 400)
        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]

        if puzzleData["owner"] != user or puzzleData["is_original"]:
            cur.close()
            abort(400)

        if puzzleData["status"] not in (
            FROZEN,
            ACTIVE,
            COMPLETED,
            BUGGY_UNLISTED,
            RENDERING_FAILED,
            REBUILD,
            IN_RENDER_QUEUE,
            MAINTENANCE,
        ):
            cur.close()
            abort(400)

        if action in ("freeze", "unfreeze") and puzzleData["status"] not in (
            FROZEN,
            BUGGY_UNLISTED,
            ACTIVE,
        ):
            cur.close()
            abort(400)

        response = {}

        if action == "delete":

            (
                delete_penalty,
                can_delete,
                delete_disabled_message,
            ) = self.get_delete_prereq(puzzleData)
            if not can_delete:
                response = {"msg": delete_disabled_message}
                cur.close()
                return make_response(json.jsonify(response), 400)

            if delete_penalty > 0:
                cur.execute(
                    fetch_query_string("decrease-user-points.sql"),
                    {"user": user, "points": delete_penalty},
                )

            delete_puzzle_resources(puzzle_id)
            cur.execute(
                fetch_query_string("delete_puzzle_file_for_puzzle.sql"),
                {"puzzle": puzzleData["id"]},
            )
            cur.execute(
                fetch_query_string("delete_piece_for_puzzle.sql"),
                {"puzzle": puzzleData["id"]},
            )

            msg = delete_puzzle_timeline(puzzle_id)
            if msg.get("status_code") >= 400:
                current_app.logger.error(msg.get("msg"))
                current_app.logger.error(
                    f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}"
                )

            cur.execute(
                fetch_query_string("update_puzzle_status_for_puzzle.sql"),
                {"status": DELETED_REQUEST, "puzzle": puzzleData["id"]},
            )
            cur.execute(
                fetch_query_string("empty-user-puzzle-slot.sql"),
                {"player": user, "puzzle": puzzleData["id"]},
            )

            db.commit()
            sse.publish(
                "status:{}".format(DELETED_REQUEST),
                channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
            )

            response = {
                "status": DELETED_REQUEST,
            }

        elif action == "freeze":
            cur.execute(
                fetch_query_string("update_puzzle_status_for_puzzle.sql"),
                {"status": FROZEN, "puzzle": puzzleData["id"]},
            )
            db.commit()
            sse.publish(
                "status:{}".format(FROZEN),
                channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
            )

            response = {
                "status": FROZEN,
            }

        elif action == "unfreeze":
            # TODO: set status to COMPLETE if puzzle has been completed instead of ACTIVE
            cur.execute(
                fetch_query_string("update_puzzle_status_for_puzzle.sql"),
                {"status": ACTIVE, "puzzle": puzzleData["id"]},
            )
            db.commit()

            sse.publish(
                "status:{}".format(ACTIVE),
                channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
            )
            response = {
                "status": ACTIVE,
            }

        elif action == "reset":
            if not (
                puzzleData.get("permission") == PRIVATE
                and not puzzleData.get("is_original")
                and puzzleData.get("status")
                in (FROZEN, BUGGY_UNLISTED, ACTIVE, COMPLETED)
            ):

                response = {"msg": "Only unlisted puzzle instances can be reset"}
                cur.close()
                return make_response(json.jsonify(response), 400)

            if puzzleData.get("status") not in (
                ACTIVE,
                COMPLETED,
                FROZEN,
                BUGGY_UNLISTED,
            ):
                response = {
                    "msg": "Puzzle is not in acceptable state in order to be reset"
                }
                cur.close()
                return make_response(json.jsonify(response), 400)

            if puzzleData.get("status") != ACTIVE:
                # Only update the response status if puzzle status is changing.
                # This way any response will trigger the "Reload" button since
                # the active puzzle status will have an empty response status.
                response = {"status": ACTIVE}

            job = current_app.cleanupqueue.enqueue(
                "api.jobs.piece_reset.reset_puzzle_pieces_and_handle_errors",
                puzzleData.get("id"),
                result_ttl=0,
            )

        cur.close()

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )
        return make_response(json.jsonify(response), 202)
Пример #5
0
    def patch(self, puzzle_id):
        ip = request.headers.get("X-Real-IP")
        user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip))

        # validate the args and headers
        args = {}
        xhr_data = request.get_json()
        if xhr_data:
            args.update(xhr_data)
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # Verify args
        action = args.get("action")
        if action not in ORIGINAL_ACTIONS:
            abort(400)

        cur = db.cursor()

        # validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select-puzzle-details-for-puzzle_id.sql"),
            {"puzzle_id": puzzle_id},
        ).fetchall()
        if not result:
            # 400 if puzzle does not exist
            err_msg = {
                "msg": "No puzzle found",
            }
            cur.close()
            return make_response(json.jsonify(err_msg), 400)
        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]

        if not puzzleData["is_original"]:
            cur.close()
            abort(400)

        response = {}
        if action == "bump":
            bid_amount = self.get_bid_amount(cur, puzzleData)
            low, high = next(
                filter(
                    lambda x: x[0] <= puzzleData["pieces"]
                    and x[1] > puzzleData["pieces"],
                    current_app.config["SKILL_LEVEL_RANGES"],
                )
            )

            # check if player has enough dots for bumping puzzle up in queue
            player_points_result = cur.execute(
                fetch_query_string("select-minimum-points-for-user.sql"),
                {"points": bid_amount, "user": user},
            ).fetchone()
            if not player_points_result:
                cur.close()
                return make_response(json.jsonify({}), 400)

            # bump any puzzle that is currently at QUEUE_WINNING_BID to be QUEUE_BUMPED_BID
            cur.execute(
                fetch_query_string("bump-puzzle-winning-bid-to-bumped-bid.sql"),
                {"low": low, "high": high},
            )

            # bump this puzzle to be the winning bid and decrease player dots
            cur.execute(
                fetch_query_string("bump-puzzle-queue-winning-bid-for-puzzle.sql"),
                {"puzzle": puzzleData["id"]},
            )
            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {"points": bid_amount, "user": user},
            )
            db.commit()

        else:
            cur.close()
            return make_response(json.jsonify({}), 400)

        cur.close()

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )
        return make_response(json.jsonify(response), 202)
Пример #6
0
    def publishMessage(msg, karma_change, karma, points=0, complete=False):
        # print(topic)
        # print(msg)
        if current_app.config.get("PUZZLE_PIECES_CACHE_TTL"):
            stamp = redis_connection.get(f"pzstamp:{puzzle}")
            if stamp:
                pcu_key = f"pcu:{stamp}"
                redis_connection.rpushx(pcu_key, msg)
        sse.publish(
            msg,
            type="move",
            channel="puzzle:{puzzle_id}".format(
                puzzle_id=puzzleData["puzzle_id"]),
        )

        if user != ANONYMOUS_USER_ID:
            points_key = "points:{user}".format(user=user)
            recent_points = int(redis_connection.get(points_key) or 0)
            if karma_change < 0 and karma <= 0 and recent_points > 0:
                redis_connection.decr(points_key)

        redis_connection.zadd("pcupdates", {puzzle: now})

        if user != ANONYMOUS_USER_ID:
            # bump the m_date for this player on the puzzle and timeline
            redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle),
                                  {user: now})
            redis_connection.zadd("timeline", {user: now})

        # Update player points
        if points != 0 and user is not None and user != ANONYMOUS_USER_ID:
            redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle),
                                     amount=1,
                                     value=user)
            redis_connection.sadd("batchuser", user)
            redis_connection.sadd("batchpuzzle", puzzle)
            redis_connection.incr("batchscore:{user}".format(user=user),
                                  amount=1)
            redis_connection.incr(
                "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user),
                amount=points,
            )
            redis_connection.zincrby("rank", amount=1, value=user)
            pieces = int(puzzleData["pieces"])
            # Skip increasing dots if puzzle is private
            earns = get_earned_points(pieces,
                                      permission=puzzleData.get("permission"))

            # karma = int(redis_connection.get(karma_key))
            ## Max out recent points
            if (earns != 0 and karma >= current_app.config["MAX_KARMA"] and
                    recent_points < current_app.config["MAX_RECENT_POINTS"]):
                recent_points = redis_connection.incr(points_key)
            # Doing small puzzles doesn't increase recent points, just extends points expiration.
            redis_connection.expire(points_key,
                                    current_app.config["RECENT_POINTS_EXPIRE"])

            # Extend the karma points expiration since it has increased
            redis_connection.expire(karma_key,
                                    current_app.config["KARMA_POINTS_EXPIRE"])
            # Max out karma
            if karma < current_app.config["MAX_KARMA"]:
                karma = redis_connection.incr(karma_key)
            karma_change += 1

            redis_connection.incr("batchpoints:{user}".format(user=user),
                                  amount=earns)

        if complete:
            current_app.logger.info("puzzle {puzzle_id} is complete".format(
                puzzle_id=puzzleData["puzzle_id"]))

            sse.publish(
                "status:{}".format(COMPLETED),
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )

            r = requests.patch(
                "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                .format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    puzzle_id=puzzleData["puzzle_id"],
                ),
                json={
                    "status": COMPLETED,
                    "m_date": time.strftime("%Y-%m-%d %H:%M:%S",
                                            time.gmtime()),
                    "queue": QUEUE_END_OF_LINE,
                },
            )
            if r.status_code != 200:
                raise Exception(
                    "Puzzle details api error when updating puzzle to be complete"
                )
            # Delaying helps avoid issues for players that are moving the last
            # piece of the puzzle as someone else completes it.
            delay = (current_app.config["MAX_PAUSE_PIECES_TIMEOUT"] +
                     current_app.config["PIECE_MOVE_TIMEOUT"] + 2)
            current_app.logger.info(
                f"Delaying puzzle transfer on completed puzzle ({puzzleData['puzzle_id']}) for {delay} seconds"
            )
            job = current_app.cleanupqueue.enqueue_in(
                timedelta(seconds=delay),
                "api.jobs.convertPiecesToDB.transfer",
                puzzle,
                result_ttl=0,
            )

            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(
                    puzzle_id=puzzleData["puzzle_id"]),
                current_app.config.get("PURGEURLLIST"),
            )

        if karma_change and user != ANONYMOUS_USER_ID:
            sse.publish(
                "{user}:{piece}:{karma}:{karma_change}".format(
                    user=user,
                    piece=piece,
                    karma=karma + recent_points,
                    karma_change=karma_change,
                ),
                type="karma",
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )

        # end = time.perf_counter()
        # duration = end - start
        # redis_connection.rpush("testdata:translate", duration)
        # return topic and msg mostly for testing
        return (msg, karma_change)
    def post(self):
        ""
        response = {"message": "", "name": "error"}

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))
        if user == None:
            response["message"] = "User not signed in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)
        user = int(user)

        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        display_name = args.get("name", "").strip()
        name = normalize_name_from_display_name(display_name)

        # name is always converted to lowercase and display_name preserves
        # original case.
        display_name = args.get("name", "").strip()
        name = normalize_name_from_display_name(display_name)
        if len(display_name) > USER_NAME_MAXLENGTH:
            response["message"] = "Submitted name is too long."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        cur = db.cursor()

        result = cur.execute(
            fetch_query_string("select-minimum-points-for-user.sql"),
            {
                "user": user,
                "points": current_app.config["POINT_COST_FOR_CHANGING_NAME"],
            },
        ).fetchone()
        if not result:
            response["message"] = "Not enough points to change name."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        else:
            if name == "":
                cur.execute(
                    fetch_query_string(
                        "remove-user-name-on-name-register-for-player.sql"),
                    {
                        "player_id": user,
                    },
                )
                cur.execute(
                    fetch_query_string("decrease-user-points.sql"),
                    {
                        "points":
                        current_app.config["POINT_COST_FOR_CHANGING_NAME"],
                        "user":
                        user,
                    },
                )
                response["message"] = "Removed name."
                response["name"] = "success"
            else:
                result = cur.execute(
                    fetch_query_string(
                        "check-status-of-name-on-name-register.sql"),
                    {
                        "name": name
                    },
                ).fetchall()
                if result:
                    (result, col_names) = rowify(result, cur.description)
                    name_status = result[0]
                    if name_status["rejected"] == 1:
                        response[
                            "message"] = "Submitted name has been rejected before."
                        response["name"] = "rejected"
                    elif name_status["claimed"] == 0 or name_status[
                            "user"] == user:
                        # The name is available and can be claimed. If owned by the
                        # user the casing of the letters can be modified.
                        cur.execute(
                            fetch_query_string(
                                "remove-user-name-on-name-register-for-player.sql"
                            ),
                            {
                                "player_id": user,
                            },
                        )

                        # Also updates the display_name if casing has changed
                        cur.execute(
                            fetch_query_string(
                                "claim-user-name-on-name-register-for-player.sql"
                            ),
                            {
                                "player_id": user,
                                "name": name,
                                "display_name": display_name,
                                "time": "+5 minutes",
                            },
                        )
                        cur.execute(
                            fetch_query_string("decrease-user-points.sql"),
                            {
                                "points":
                                current_app.
                                config["POINT_COST_FOR_CHANGING_NAME"],
                                "user":
                                user,
                            },
                        )
                        response[
                            "message"] = "Submitted name ({}) reclaimed.".format(
                                display_name)
                        response["name"] = "success"
                    else:
                        response[
                            "message"] = "Submitted name ({}) is currently used by another player.  Please try a different name.".format(
                                display_name)
                        response["name"] = "rejected"
                else:
                    # name is new
                    cur.execute(
                        fetch_query_string(
                            "remove-user-name-on-name-register-for-player.sql"
                        ),
                        {
                            "player_id": user,
                        },
                    )
                    cur.execute(
                        fetch_query_string(
                            "add-user-name-on-name-register-for-player-to-be-reviewed.sql"
                        ),
                        {
                            "player_id": user,
                            "name": name,
                            "display_name": display_name,
                            "time": "+5 minutes",
                        },
                    )

                    cur.execute(
                        fetch_query_string("decrease-user-points.sql"),
                        {
                            "points":
                            current_app.config["POINT_COST_FOR_CHANGING_NAME"],
                            "user":
                            user,
                        },
                    )
                    response[
                        "message"] = 'Thank you for submitting a new name.  "{}" will be shown next to your bit icon in about 10 minutes.'.format(
                            display_name)
                    response["name"] = "success"

        db.commit()
        cur.close()

        purge_route_from_nginx_cache(
            "/chill/site/internal/player-bit/{}/".format(user),
            current_app.config.get("PURGEURLLIST"),
        )

        return make_response(json.jsonify(response), 202)
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # TODO: Check user to see if role matches?
        # user = current_app.secure_cookie.get(u'user')
        # if not user:
        #     abort(403)

        # Verify args
        action = args.get("action")
        if action not in ACTIONS:
            abort(400)

        reject = args.get("reject")
        if action == "reject" and reject not in ("license", "attribution"):
            abort(400)

        delete = args.get("delete")
        if action == "delete" and delete not in ("license", "inapt", "old",
                                                 "request"):
            abort(400)

        edit = args.get("edit")
        if action == "edit" and edit not in ("private", ):
            abort(400)

        # abort if tag value not set
        tag = args.get("tag")
        if action == "tag" and not tag:
            abort(400)

        puzzle_ids = request.form.getlist("montage_puzzle_id")
        if len(puzzle_ids) == 0:
            abort(400)
        if not isinstance(puzzle_ids, list):
            puzzle_ids = [puzzle_ids]

        cur = db.cursor()
        status = None

        if action == "approve":
            status = IN_RENDER_QUEUE

        if action == "rebuild":
            status = REBUILD

        if action == "reject":
            if reject == "license":
                status = FAILED_LICENSE
            elif reject == "attribution":
                status = NO_ATTRIBUTION

        if action == "delete":
            if delete == "license":
                status = DELETED_LICENSE
            elif delete == "inapt":
                status = DELETED_INAPT
            elif delete == "old":
                status = DELETED_OLD
            elif delete == "request":
                status = DELETED_REQUEST

            for puzzle_id in puzzle_ids:
                delete_puzzle_resources(puzzle_id)
                id = cur.execute(
                    fetch_query_string("select_puzzle_id_by_puzzle_id.sql"),
                    {
                        "puzzle_id": puzzle_id
                    },
                ).fetchone()[0]
                # current_app.logger.info('deleting puzzle resources for id {}'.format(id))
                cur.execute(
                    fetch_query_string("delete_puzzle_file_for_puzzle.sql"),
                    {"puzzle": id},
                )
                cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"),
                            {"puzzle": id})

                msg = delete_puzzle_timeline(puzzle_id)
                if msg.get("status_code") >= 400:
                    current_app.logger.error(msg.get("msg"))
                    current_app.logger.error(
                        f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}"
                    )

                cur.execute(
                    fetch_query_string(
                        "remove-puzzle-from-all-user-puzzle-slots.sql"),
                    {"puzzle": id},
                )

            db.commit()

        if action == "edit":
            if edit == "private":

                def each(puzzle_ids):
                    for puzzle_id in puzzle_ids:
                        yield {"puzzle_id": puzzle_id, "permission": PRIVATE}

                cur.executemany(
                    fetch_query_string(
                        "update_puzzle_permission_for_puzzle_id.sql"),
                    each(puzzle_ids),
                )
                db.commit()

        def each(puzzle_ids):
            for puzzle_id in puzzle_ids:
                yield {"puzzle_id": puzzle_id, "status": status}

        if status is not None:
            cur.executemany(
                fetch_query_string("update_puzzle_status_for_puzzle_id.sql"),
                each(puzzle_ids),
            )
            db.commit()

        for puzzle_id in puzzle_ids:
            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
                current_app.config.get("PURGEURLLIST"),
            )

        if action in ("approve", "rebuild"):
            puzzles = rowify(
                cur.execute(
                    fetch_query_string("select-puzzles-in-render-queue.sql"),
                    {
                        "IN_RENDER_QUEUE": IN_RENDER_QUEUE,
                        "REBUILD": REBUILD
                    },
                ).fetchall(),
                cur.description,
            )[0]
            print("found {0} puzzles to render or rebuild".format(
                len(puzzles)))

            # push each puzzle to artist job queue
            for puzzle in puzzles:
                job = current_app.createqueue.enqueue(
                    "api.jobs.pieceRenderer.render",
                    [puzzle],
                    result_ttl=0,
                    job_timeout="24h",
                )

        # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances

        cur.close()
        return redirect("/chill/site/admin/puzzle/", code=303)
Пример #9
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        player = args.get("player")
        if not player:
            abort(400)
        email_verified = int(args.get("email_verified", "0"))
        if not email_verified in (0, 1):
            abort(400)
        name_approved = int(args.get("name_approved", "0"))
        if not name_approved in (0, 1):
            abort(400)

        # name is always converted to lowercase and display_name preserves
        # original case.
        display_name = args.get("name", "").strip()
        name = normalize_name_from_display_name(display_name)
        if len(display_name) > USER_NAME_MAXLENGTH:
            abort(400)

        email = args.get("email", "").strip().lower()
        if len(email) > EMAIL_MAXLENGTH:
            abort(400)

        cur = db.cursor()

        result = cur.execute(fetch_query_string("user-has-player-account.sql"),
                             {
                                 "player_id": player
                             }).fetchone()
        if not result or result[0] == 0:
            cur.execute(
                fetch_query_string("init-player-account-for-user.sql"),
                {"player_id": player},
            )
            db.commit()

        result = cur.execute(
            fetch_query_string(
                "select-admin-player-details-for-player-id.sql"),
            {
                "player_id": player
            },
        ).fetchall()
        if not result:
            cur.close()
            db.commit()
            abort(400)
        (result, col_names) = rowify(result, cur.description)
        existing_player_data = result[0]

        if email == "":
            cur.execute(
                fetch_query_string("remove-player-account-email.sql"),
                {"player_id": player},
            )
        else:
            cur.execute(
                fetch_query_string("update-player-account-email.sql"),
                {
                    "player_id": player,
                    "email": email
                },
            )

        cur.execute(
            fetch_query_string("update-player-account-email-verified.sql"),
            {
                "player_id": player,
                "email_verified": email_verified,
            },
        )

        cur.execute(
            fetch_query_string("update-user-points.sql"),
            {
                "player_id": player,
                "points": int(args.get("dots", existing_player_data["dots"])),
                "POINTS_CAP": current_app.config["POINTS_CAP"],
            },
        )

        if name == "":
            cur.execute(
                fetch_query_string(
                    "remove-user-name-on-name-register-for-player.sql"),
                {
                    "player_id": player,
                },
            )
        else:
            if existing_player_data["name"] != name:
                result = cur.execute(
                    fetch_query_string(
                        "select-unclaimed-name-on-name-register.sql"),
                    {
                        "name": name,
                    },
                ).fetchall()
                if result:
                    (result, col_names) = rowify(result, cur.description)
                    unclaimed_name_data = result[0]
                    if unclaimed_name_data["approved_date"] == None:
                        # name has been rejected
                        if name_approved == 1:
                            # override the rejected name and let player claim it
                            cur.execute(
                                fetch_query_string(
                                    "remove-user-name-on-name-register-for-player.sql"
                                ),
                                {
                                    "player_id": player,
                                },
                            )
                            cur.execute(
                                fetch_query_string(
                                    "claim-rejected-user-name-on-name-register-for-player.sql"
                                ),
                                {
                                    "player_id": player,
                                    "name": name,
                                },
                            )
                    else:
                        # name can be claimed
                        cur.execute(
                            fetch_query_string(
                                "remove-user-name-on-name-register-for-player.sql"
                            ),
                            {
                                "player_id": player,
                            },
                        )
                        cur.execute(
                            fetch_query_string(
                                "claim-user-name-on-name-register-for-player.sql"
                            ),
                            {
                                "player_id": player,
                                "display_name": display_name,
                                "name": name,
                                "time": "+1 second",
                            },
                        )
                else:
                    # The name is new and not in the NameRegister.  Add it and
                    # mark it for auto-approval.
                    cur.execute(
                        fetch_query_string(
                            "remove-user-name-on-name-register-for-player.sql"
                        ),
                        {
                            "player_id": player,
                        },
                    )
                    cur.execute(
                        fetch_query_string(
                            "add-user-name-on-name-register-for-player.sql"),
                        {
                            "player_id": player,
                            "name": name,
                            "display_name": display_name,
                            "time": "+1 second",
                        },
                    )

        if existing_player_data["name_approved"] == 1 and name_approved == 0:
            # Place this name on reject list
            cur.execute(fetch_query_string("reject-name-on-name-register.sql"),
                        {
                            "name": name,
                        })

        if existing_player_data["name_approved"] == 0 and name_approved == 1:
            cur.execute(
                fetch_query_string("update-user-name-approved.sql"),
                {
                    "player_id": player,
                    "name_approved": name_approved,
                },
            )

        cur.close()
        db.commit()

        purge_route_from_nginx_cache(
            "/chill/site/internal/player-bit/{}/".format(player),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect(
            "/chill/site/admin/player/details/{player}/".format(player=player))
Пример #10
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # TODO: Check user to see if role matches?
        # user = current_app.secure_cookie.get(u'user')
        # if not user:
        #     abort(403)

        # Verify args
        action = args.get("action")
        if action not in ACTIONS:
            abort(400)

        reject = args.get("reject")
        if action == "reject" and reject not in ("license", "attribution"):
            abort(400)

        delete = args.get("delete")
        if action == "delete" and delete not in ("license", "inapt", "old",
                                                 "request"):
            abort(400)

        redo = args.get("redo")
        if action == "redo" and redo not in ("delete_and_redo", ):
            abort(400)

        edit = args.get("edit")
        if action == "edit" and edit not in ("private", "status_active"):
            abort(400)

        # abort if tag value not set
        tag = args.get("tag")
        if action == "tag" and not tag:
            abort(400)

        puzzle_ids = request.form.getlist("montage_puzzle_id")
        if len(puzzle_ids) == 0:
            abort(400)
        if not isinstance(puzzle_ids, list):
            puzzle_ids = [puzzle_ids]

        cur = db.cursor()
        status = None

        if action == "approve":
            status = IN_RENDER_QUEUE

        if action == "rebuild":
            status = REBUILD

        if action == "buggy_unlisted":
            status = BUGGY_UNLISTED

        if action == "reject":
            if reject == "license":
                status = FAILED_LICENSE
            elif reject == "attribution":
                status = NO_ATTRIBUTION

        if action == "delete" or action == "redo":
            if action == "delete":
                if delete == "license":
                    status = DELETED_LICENSE
                elif delete == "inapt":
                    status = DELETED_INAPT
                elif delete == "old":
                    status = DELETED_OLD
                elif delete == "request":
                    status = DELETED_REQUEST
            elif action == "redo":
                if redo == "delete_and_redo":
                    # Set it back to SUGGESTED since that way a new puzzle_id
                    # will be created.
                    status = SUGGESTED

            for puzzle_id in puzzle_ids:
                result = cur.execute(
                    fetch_query_string(
                        "select-puzzle-details-for-puzzle_id.sql"),
                    {
                        "puzzle_id": puzzle_id
                    },
                ).fetchall()
                if not result:
                    # Could be a puzzle that failed to render and doesn't have
                    # any resources.
                    continue
                (result, col_names) = rowify(result, cur.description)
                puzzle_details = result[0]

                delete_puzzle_resources(
                    puzzle_id,
                    is_local_resource=not puzzle_details["url"].startswith(
                        "http") and not puzzle_details["url"].startswith("//"))
                id = puzzle_details["id"]
                # current_app.logger.info('deleting puzzle resources for id {}'.format(id))
                cur.execute(
                    fetch_query_string("delete_puzzle_file_for_puzzle.sql"),
                    {"puzzle": id},
                )
                cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"),
                            {"puzzle": id})

                msg = delete_puzzle_timeline(puzzle_id)
                if msg.get("status_code") >= 400:
                    current_app.logger.error(msg.get("msg"))
                    current_app.logger.error(
                        f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}"
                    )

                cur.execute(
                    fetch_query_string(
                        "remove-puzzle-from-all-user-puzzle-slots.sql"),
                    {"puzzle": id},
                )

            db.commit()

        if action == "edit":
            if edit == "private":

                def each(puzzle_ids):
                    for puzzle_id in puzzle_ids:
                        yield {"puzzle_id": puzzle_id, "permission": PRIVATE}

                cur.executemany(
                    fetch_query_string(
                        "update_puzzle_permission_for_puzzle_id.sql"),
                    each(puzzle_ids),
                )
                db.commit()

            elif edit == "status_active":
                m_date_now = strftime("%Y-%m-%d %H:%M:%S", gmtime(time()))
                for puzzle_id in puzzle_ids:
                    data = {"status": ACTIVE, "m_date": m_date_now}
                    response_msg = update_puzzle_details(puzzle_id, data)
                    if (response_msg.get("rowcount")
                            and response_msg.get("status_code") >= 300):
                        current_app.logger.warning(
                            f"Failed to update puzzle details {puzzle_id} {data}"
                        )

        def each(puzzle_ids):
            for puzzle_id in puzzle_ids:
                yield {"puzzle_id": puzzle_id, "status": status}

        if status is not None:
            cur.executemany(
                fetch_query_string("update_puzzle_status_for_puzzle_id.sql"),
                each(puzzle_ids),
            )
            db.commit()

        for puzzle_id in puzzle_ids:
            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
                current_app.config.get("PURGEURLLIST"),
            )

        if action in ("approve", "rebuild"):
            puzzles = rowify(
                cur.execute(
                    fetch_query_string("select-puzzles-in-render-queue.sql"),
                    {
                        "IN_RENDER_QUEUE": IN_RENDER_QUEUE,
                        "REBUILD": REBUILD
                    },
                ).fetchall(),
                cur.description,
            )[0]
            print("found {0} puzzles to render or rebuild".format(
                len(puzzles)))

            # push each puzzle to artist job queue
            for puzzle in puzzles:
                job = current_app.createqueue.enqueue(
                    "api.jobs.pieceRenderer.render",
                    [puzzle],
                    result_ttl=0,
                    job_timeout="24h",
                )

        # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances

        cur.close()
        return redirect("/chill/site/admin/puzzle/", code=303)
Пример #11
0
    def publishMessage(msg, karma_change, points=0, complete=False):
        # print(topic)
        # print(msg)
        sse.publish(
            msg,
            type="move",
            channel="puzzle:{puzzle_id}".format(
                puzzle_id=puzzleData["puzzle_id"]),
        )

        now = int(time.time())

        redis_connection.zadd("pcupdates", {puzzle: now})

        # TODO:
        # return (topic, msg)

        # bump the m_date for this player on the puzzle and timeline
        redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle),
                              {user: now})
        redis_connection.zadd("timeline", {user: now})

        # Update player points
        if points != 0 and user != None:
            redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle),
                                     amount=1,
                                     value=user)
            redis_connection.sadd("batchuser", user)
            redis_connection.sadd("batchpuzzle", puzzle)
            redis_connection.incr("batchscore:{user}".format(user=user),
                                  amount=1)
            redis_connection.incr(
                "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user),
                amount=points,
            )
            redis_connection.zincrby("rank", amount=1, value=user)
            points_key = "points:{user}".format(user=user)
            pieces = int(puzzleData["pieces"])
            # Skip increasing dots if puzzle is private
            earns = get_earned_points(pieces,
                                      permission=puzzleData.get("permission"))

            karma = int(redis_connection.get(karma_key))
            ## Max out recent points
            # if earns != 0:
            #    recent_points = int(redis_connection.get(points_key) or 0)
            #    if karma + 1 + recent_points + earns < MAX_KARMA:
            #        redis_connection.incr(points_key, amount=earns)
            # Doing small puzzles doesn't increase recent points, just extends points expiration.
            redis_connection.expire(points_key, RECENT_POINTS_EXPIRE)

            karma_change += 1
            # Extend the karma points expiration since it has increased
            redis_connection.expire(karma_key, KARMA_POINTS_EXPIRE)
            # Max out karma
            if karma < MAX_KARMA:
                redis_connection.incr(karma_key)
            else:
                # Max out points
                if earns != 0:
                    recent_points = int(redis_connection.get(points_key) or 0)
                    if recent_points + earns <= MAX_RECENT_POINTS:
                        redis_connection.incr(points_key, amount=earns)

            redis_connection.incr("batchpoints:{user}".format(user=user),
                                  amount=earns)

        # TODO: Optimize by using redis for puzzle status
        if complete:
            current_app.logger.info("puzzle {puzzle_id} is complete".format(
                puzzle_id=puzzleData["puzzle_id"]))
            cur = db.cursor()

            cur.execute(
                fetch_query_string("update_puzzle_status_for_puzzle.sql"),
                {
                    "puzzle": puzzle,
                    "status": COMPLETED
                },
            )
            cur.execute(
                fetch_query_string("update_puzzle_m_date_to_now.sql"),
                {
                    "puzzle": puzzle,
                    "modified": now
                },
            )
            cur.execute(
                fetch_query_string("update_puzzle_queue_for_puzzle.sql"),
                {
                    "puzzle": puzzle,
                    "queue": QUEUE_END_OF_LINE
                },
            )
            db.commit()
            sse.publish(
                "status:{}".format(COMPLETED),
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )
            job = current_app.cleanupqueue.enqueue_call(
                func="api.jobs.convertPiecesToDB.transfer",
                args=(puzzle, ),
                result_ttl=0)

            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(
                    puzzle_id=puzzleData["puzzle_id"]),
                current_app.config.get("PURGEURLLIST"),
            )

            db.commit()
            cur.close()

        # return topic and msg mostly for testing
        return (msg, karma_change)
Пример #12
0
    def post(self):
        """If the bit icon is available; claim it for the user."""

        data = {"message": "", "name": "error"}

        icon = request.args.get("icon")
        if not icon:
            data["message"] = "No icon param passed"
            data["name"] = "error"
            return make_response(json.jsonify(data), 400)

        # Prevent creating a new user if no support for cookies. Player should
        # have 'ot' already set by viewing the page.
        uses_cookies = current_app.secure_cookie.get(u"ot")
        if not uses_cookies:
            data["message"] = "No ot cookie present"
            data["name"] = "error"
            return make_response(json.jsonify(data), 400)

        cur = db.cursor()

        # Check if bit icon is available
        result = cur.execute(
            fetch_query_string("select_available_bit_icon.sql"), {"icon": icon}
        ).fetchone()
        if not result:
            cur.close()
            db.commit()
            data["message"] = "That bit icon is no longer available."
            data["name"] = "error"
            return make_response(json.jsonify(data), 400)

        user = current_app.secure_cookie.get(u"user")
        if not user:
            user = user_id_from_ip(request.headers.get("X-Real-IP"))
            if user == None:
                data["message"] = "Not logged in."
                data["name"] = "error"
                cur.close()
                db.commit()
                return make_response(json.jsonify(data), 400)
            user = int(user)

        else:
            user = int(user)

        data["message"] = "Bit icon claimed by using {} of your dots.".format(
            current_app.config["POINT_COST_FOR_CHANGING_BIT"]
        )
        data["name"] = "success"
        response = make_response(json.jsonify(data), 200)

        # Unclaim any bit icon that the player already has
        cur.execute(fetch_query_string("unclaim_bit_icon.sql"), {"user": user})

        # Claim the bit icon
        cur.execute(
            fetch_query_string("update_bit_icon_user.sql"), {"user": user, "icon": icon}
        )
        cur.execute(
            fetch_query_string("decrease-user-points.sql"),
            {
                "points": current_app.config["POINT_COST_FOR_CHANGING_BIT"],
                "user": user,
            },
        )

        cur.close()
        db.commit()

        purge_route_from_nginx_cache(
            "/chill/site/internal/player-bit/{}/".format(user),
            current_app.config.get("PURGEURLLIST"),
        )

        return response
Пример #13
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        puzzle_id = args.get("puzzle_id")
        if not puzzle_id:
            abort(400)

        # Check pieces arg
        try:
            pieces = int(
                args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"]))
        except ValueError as err:
            abort(400)
        if pieces < current_app.config["MINIMUM_PIECE_COUNT"]:
            abort(400)

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        cur = db.cursor()
        result = cur.execute(
            fetch_query_string(
                "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": COMPLETED
            },
        ).fetchall()
        if not result:
            # Puzzle does not exist or is not completed status.
            # Reload the page as the status may have been changed.
            cur.close()
            return redirect(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))

        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]
        puzzle = puzzleData["id"]

        userCanRebuildPuzzle = cur.execute(
            fetch_query_string("select-user-rebuild-puzzle-prereq.sql"),
            {
                "user": user,
                "puzzle": puzzle,
                "pieces": pieces
            },
        ).fetchall()
        if not userCanRebuildPuzzle:
            cur.close()
            abort(400)

        original_puzzle_id = puzzleData["original_puzzle_id"]
        # Get the adjusted piece count depending on the size of the original and
        # the minimum piece size.
        original_puzzle_dir = os.path.join(
            current_app.config["PUZZLE_RESOURCES"], original_puzzle_id)
        # TODO: get path of original.jpg via the PuzzleFile query
        # TODO: use requests.get to get original.jpg and run in another thread
        imagefile = os.path.join(original_puzzle_dir, "original.jpg")
        im = Image.open(imagefile)
        (width, height) = im.size
        im.close()
        max_pieces_that_will_fit = int((old_div(width, MIN_PIECE_SIZE)) *
                                       (old_div(height, MIN_PIECE_SIZE)))

        # The user points for rebuilding the puzzle is decreased by the piece
        # count for the puzzle. Use at least minimum piece count (20) points for
        # smaller puzzles.  Players that own a puzzle instance do not decrease
        # any points (dots) if the puzzle is complete.
        point_cost = max(
            current_app.config["MINIMUM_PIECE_COUNT"],
            min(
                max_pieces_that_will_fit,
                pieces,
                current_app.config["MAX_POINT_COST_FOR_REBUILDING"],
            ),
        )
        if not (puzzleData["owner"] == user and puzzleData["puzzle_id"]
                == puzzleData["original_puzzle_id"]):
            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {
                    "user": user,
                    "points": point_cost
                },
            )

        # Update puzzle status to be REBUILD and change the piece count
        cur.execute(
            fetch_query_string("update_status_puzzle_for_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": REBUILD,
                "pieces": pieces,
                "queue": QUEUE_REBUILD,
            },
        )
        puzzleData["status"] = REBUILD
        puzzleData["pieces"] = pieces

        db.commit()

        # Delete any piece data from redis since it is no longer needed.
        (all_pieces, col_names) = rowify(
            cur.execute(
                fetch_query_string("select_all_piece_ids_for_puzzle.sql"),
                {
                    "puzzle": puzzle
                },
            ).fetchall(),
            cur.description,
        )
        cur.close()
        deletePieceDataFromRedis(redis_connection, puzzle, all_pieces)

        job = current_app.createqueue.enqueue_call(
            func="api.jobs.pieceRenderer.render",
            args=([puzzleData]),
            result_ttl=0,
            timeout="24h",
        )

        job = current_app.cleanupqueue.enqueue_call(
            func="api.jobs.timeline_archive.archive_and_clear",
            kwargs=({
                "puzzle": puzzle
            }),
            result_ttl=0,
            timeout="24h",
        )

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))
Пример #14
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        puzzle_id = args.get("puzzle_id")
        if not puzzle_id:
            abort(400)

        # Check pieces arg
        try:
            pieces = int(
                args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"]))
        except ValueError as err:
            abort(400)
        if pieces < current_app.config["MINIMUM_PIECE_COUNT"]:
            abort(400)

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        cur = db.cursor()
        result = cur.execute(
            fetch_query_string(
                "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": COMPLETED
            },
        ).fetchall()
        if not result:
            # Puzzle does not exist or is not completed status.
            # Reload the page as the status may have been changed.
            cur.close()
            return redirect(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))

        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]
        puzzle = puzzleData["id"]

        userCanRebuildPuzzle = cur.execute(
            fetch_query_string("select-user-rebuild-puzzle-prereq.sql"),
            {
                "user": user,
                "puzzle": puzzle,
                "pieces": pieces
            },
        ).fetchall()
        if not userCanRebuildPuzzle:
            cur.close()
            abort(400)

        if (puzzleData["permission"] == PRIVATE and
                puzzleData["original_puzzle_id"] == puzzleData["puzzle_id"]):
            current_app.logger.warning(
                "Original puzzles that are private can not be rebuilt")
            cur.close()
            abort(400)

        # The user points for rebuilding the puzzle is decreased by the piece
        # count for the puzzle. Use at least minimum piece count (20) points for
        # smaller puzzles.  Players that own a puzzle instance do not decrease
        # any points (dots) if the puzzle is complete.
        point_cost = max(
            current_app.config["MINIMUM_PIECE_COUNT"],
            min(
                pieces,
                current_app.config["MAX_POINT_COST_FOR_REBUILDING"],
            ),
        )
        if not (puzzleData["owner"] == user and puzzleData["puzzle_id"]
                == puzzleData["original_puzzle_id"]):
            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {
                    "user": user,
                    "points": point_cost
                },
            )

        # Update puzzle status to be REBUILD and change the piece count
        cur.execute(
            fetch_query_string("update_status_puzzle_for_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": REBUILD,
                "pieces": pieces,
                "queue": QUEUE_REBUILD,
            },
        )
        puzzleData["status"] = REBUILD
        puzzleData["pieces"] = pieces

        db.commit()

        # Delete any piece data from redis since it is no longer needed.
        (all_pieces, col_names) = rowify(
            cur.execute(
                fetch_query_string("select_all_piece_ids_for_puzzle.sql"),
                {
                    "puzzle": puzzle
                },
            ).fetchall(),
            cur.description,
        )
        cur.close()
        deletePieceDataFromRedis(redis_connection, puzzle, all_pieces)

        delete_puzzle_resources(
            puzzle_id,
            is_local_resource=not puzzleData["preview_full"].startswith("http")
            and not puzzleData["preview_full"].startswith("//"),
            exclude_regex=r"(original|preview_full).([^.]+\.)?jpg")

        job = current_app.createqueue.enqueue(
            "api.jobs.pieceRenderer.render",
            [puzzleData],
            result_ttl=0,
            job_timeout="24h",
        )

        job = current_app.cleanupqueue.enqueue(
            "api.jobs.timeline_archive.archive_and_clear",
            puzzle,
            result_ttl=0,
            job_timeout="24h",
        )

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))