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

        # Only allow valid contributor
        if args.get("contributor",
                    None) != current_app.config.get("NEW_PUZZLE_CONTRIB"):
            abort(403)

        # 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)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

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

        # All puzzles are public by default, but allow the player to set to
        # PRIVATE if they have the role of membership.
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        puzzle_id = submit_puzzle(pieces, bg_color, user, permission,
                                  description, link, upload_file)

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
Пример #2
0
    def post(self):
        "Route is protected by basic auth in nginx"
        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)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        # All puzzles are public by default, but allow the admin to set
        # permission to PRIVATE.
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        # Get the owner of the suggested puzzle
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("_select-owner-for-suggested-puzzle.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchone()
        if not result:
            cur.close()
            abort(400)
        owner = result[0]

        new_puzzle_id = submit_puzzle(pieces, bg_color, owner, permission,
                                      description, link, upload_file)

        # Update the status of this suggested puzzle to be the suggested done
        # status
        cur.execute(
            fetch_query_string("_update-suggested-puzzle-to-done-status.sql"),
            {"puzzle_id": puzzle_id},
        )
        db.commit()
        cur.close()

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

        # 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)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        # Check description
        instance_description = args.get("instance_description", "")

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

        # Check fork
        fork = int(args.get("fork", "0"))
        if fork not in (0, 1):
            abort(400)
        fork = bool(fork == 1)

        # Check permission
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            abort(400)
        if fork:
            # All copies of puzzles are unlisted
            permission = PRIVATE

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

        cur = db.cursor()

        # The user should have
        # 2400 or more dots (points)
        # TODO: this could be configurable per site or for other reasons.
        # userHasEnoughPoints = cur.execute(fetch_query_string("select-minimum-points-for-user.sql"), {'user': user, 'points': 2400}).fetchall()
        # if not userHasEnoughPoints:
        #    abort(400)

        # An available instance slot
        result = cur.execute(
            fetch_query_string("select-available-user-puzzle-slot-for-player.sql"),
            {"player": user},
        ).fetchone()[0]
        userHasAvailablePuzzleInstanceSlot = bool(result)
        if not userHasAvailablePuzzleInstanceSlot:
            cur.close()
            db.commit()
            abort(400)

        # Check if puzzle is valid to be a new puzzle instance
        if not fork:
            # Creating a new puzzle instance
            result = cur.execute(
                fetch_query_string("select-valid-puzzle-for-new-puzzle-instance.sql"),
                {
                    "puzzle_id": source_puzzle_id,
                    "ACTIVE": ACTIVE,
                    "IN_QUEUE": IN_QUEUE,
                    "COMPLETED": COMPLETED,
                    "FROZEN": FROZEN,
                    "REBUILD": REBUILD,
                    "IN_RENDER_QUEUE": IN_RENDER_QUEUE,
                    "RENDERING": RENDERING,
                },
            ).fetchall()
            if not result:
                # Puzzle does not exist or is not a valid puzzle to create instance from.
                cur.close()
                db.commit()
                abort(400)
        else:
            # Creating a copy of existing puzzle pieces (forking)
            result = cur.execute(
                fetch_query_string(
                    "select-valid-puzzle-for-new-puzzle-instance-fork.sql"
                ),
                {
                    "puzzle_id": source_puzzle_id,
                    "ACTIVE": ACTIVE,
                    "IN_QUEUE": IN_QUEUE,
                    "COMPLETED": COMPLETED,
                    "FROZEN": FROZEN,
                },
            ).fetchall()
            if not result:
                # Puzzle does not exist or is not a valid puzzle to create instance from.
                cur.close()
                db.commit()
                abort(400)

        (result, col_names) = rowify(result, cur.description)
        source_puzzle_data = result[0]

        puzzle_id = generate_new_puzzle_id(source_puzzle_data["name"])

        # Create puzzle dir
        if not fork:
            puzzle_dir = os.path.join(
                current_app.config.get("PUZZLE_RESOURCES"), puzzle_id
            )
            os.mkdir(puzzle_dir)

        if not fork:
            d = {
                "puzzle_id": puzzle_id,
                "pieces": pieces,
                "name": source_puzzle_data["name"],
                "link": source_puzzle_data["link"],
                "description": source_puzzle_data["description"]
                if not instance_description
                else instance_description,
                "bg_color": bg_color,
                "owner": user,
                "queue": QUEUE_NEW,
                "status": IN_RENDER_QUEUE,
                "permission": permission,
            }
            cur.execute(
                fetch_query_string("insert_puzzle.sql"), d,
            )
        else:
            d = {
                "puzzle_id": puzzle_id,
                "pieces": source_puzzle_data["pieces"],
                "rows": source_puzzle_data["rows"],
                "cols": source_puzzle_data["cols"],
                "piece_width": source_puzzle_data["piece_width"],
                "mask_width": source_puzzle_data["mask_width"],
                "table_width": source_puzzle_data["table_width"],
                "table_height": source_puzzle_data["table_height"],
                "name": source_puzzle_data["name"],
                "link": source_puzzle_data["link"],
                "description": source_puzzle_data["description"]
                if not instance_description
                else instance_description,
                "bg_color": bg_color,
                "owner": user,
                "queue": QUEUE_NEW,
                "status": MAINTENANCE,
                "permission": permission,  # All copies of puzzles are unlisted
            }
            cur.execute(
                fetch_query_string("insert_puzzle_instance_copy.sql"), d,
            )
        db.commit()

        result = cur.execute(
            fetch_query_string("select-all-from-puzzle-by-puzzle_id.sql"),
            {"puzzle_id": puzzle_id},
        ).fetchall()
        if not result:
            cur.close()
            db.commit()
            abort(500)

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

        classic_variant = cur.execute(
            fetch_query_string("select-puzzle-variant-id-for-slug.sql"),
            {"slug": CLASSIC},
        ).fetchone()[0]
        cur.execute(
            fetch_query_string("insert-puzzle-instance.sql"),
            {
                "original": source_puzzle_data["id"],
                "instance": puzzle,
                "variant": classic_variant,
            },
        )

        cur.execute(
            fetch_query_string("fill-user-puzzle-slot.sql"),
            {"player": user, "puzzle": puzzle},
        )

        db.commit()
        cur.close()

        if not fork:
            job = current_app.createqueue.enqueue_call(
                func="api.jobs.pieceRenderer.render",
                args=([puzzle_data]),
                result_ttl=0,
                timeout="24h",
            )
        else:
            # Copy existing puzzle
            job = current_app.cleanupqueue.enqueue_call(
                func="api.jobs.piece_forker.fork_puzzle_pieces",
                args=([source_puzzle_data, puzzle_data]),
                result_ttl=0,
            )

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
Пример #4
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))
            args["features"] = set(request.form.getlist("features"))

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

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

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

        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", "").strip())[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", "").strip())[:100]

        if not link and not description:
            abort(400)

        puzzle_id = uuid.uuid1().hex

        features = set(args.get("features", []))

        cur = db.cursor()
        d = {
            "puzzle_id": puzzle_id,
            "pieces": pieces,
            "link": link,
            "description": description,
            "bg_color": bg_color,
            "owner": user,
            "status": SUGGESTED,
            "permission": permission,
        }
        cur.execute(
            """insert into Puzzle (
        puzzle_id,
        pieces,
        link,
        description,
        bg_color,
        owner,
        status,
        permission) values
        (:puzzle_id,
        :pieces,
        :link,
        :description,
        :bg_color,
        :owner,
        :status,
        :permission);
        """,
            d,
        )
        db.commit()

        puzzle = rowify(
            cur.execute(
                fetch_query_string("select_puzzle_id_by_puzzle_id.sql"),
                {
                    "puzzle_id": puzzle_id
                },
            ).fetchall(),
            cur.description,
        )[0][0]
        puzzle = puzzle["puzzle"]

        result = cur.execute(
            fetch_query_string("select-puzzle-features-enabled.sql"), {
                "enabled": 1
            }).fetchall()
        if result:
            (puzzle_features, _) = rowify(result, cur.description)
            # Add puzzle features
            for puzzle_feature in puzzle_features:
                if (puzzle_feature["slug"] == "hidden-preview"
                        and "hidden-preview" in features):
                    cur.execute(
                        fetch_query_string(
                            "add-puzzle-feature-to-puzzle-by-id--hidden-preview.sql"
                        ),
                        {
                            "puzzle": puzzle,
                            "puzzle_feature": puzzle_feature["id"]
                        },
                    )
                elif (puzzle_feature["slug"] == "secret-message"
                      and "secret-message" in features):
                    cur.execute(
                        fetch_query_string(
                            "add-puzzle-feature-to-puzzle-by-id--secret-message.sql"
                        ),
                        {
                            "puzzle": puzzle,
                            "puzzle_feature": puzzle_feature["id"],
                            "message": secret_message,
                        },
                    )

        db.commit()
        cur.close()

        # Send a notification email (silent fail if not configured)
        message = """
http://{DOMAIN_NAME}/chill/site/admin/puzzle/suggested/{puzzle_id}/

pieces: {pieces}
bg_color: {bg_color}
owner: {owner}

link: {link}
description: {description}
        """.format(DOMAIN_NAME=current_app.config.get("DOMAIN_NAME"), **d)
        current_app.logger.debug(message)
        if not current_app.config.get("DEBUG", True):
            try:
                send_message(
                    current_app.config.get("EMAIL_MODERATOR"),
                    "Suggested Image",
                    message,
                    current_app.config,
                )
            except Exception as err:
                current_app.logger.warning(
                    "Failed to send notification message for suggested image. email: {email}\n {message}\n error: {err}"
                    .format(
                        err=err,
                        email=current_app.config.get("EMAIL_MODERATOR"),
                        message=message,
                    ))
                pass

        # Redirect to a thank you page (not revealing the puzzle_id)
        return redirect("/chill/site/suggested-puzzle-thank-you/", code=303)
Пример #5
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        batch = []

        user = int(
            current_app.secure_cookie.get("user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("select-user-details-by-id.sql"),
            {
                "id": user
            },
        ).fetchone()
        if not result:
            # Safe guard against when the user is not in the database. Usually
            # happens in development environments when switching and dropping
            # databases happens often.
            user = ANONYMOUS_USER_ID
        cur.close()

        if request.form:
            args.update(request.form.to_dict(flat=True))
            labels = [
                "unlisted", "hidden_preview", "link", "bg_color", "pieces"
            ]
            batch = list(
                map(
                    lambda x: dict(zip(labels, x)),
                    list(zip(*list(map(request.form.getlist, labels)))),
                ))

        for item in batch:
            try:
                pieces = int(
                    item.get("pieces",
                             current_app.config["MINIMUM_PIECE_COUNT"]))
            except ValueError as err:
                abort(400)
            if (not current_app.config["MINIMUM_PIECE_COUNT"] <= pieces <=
                    current_app.config["MAXIMUM_PIECE_COUNT"]):
                abort(400)
            bg_color = check_bg_color(item.get("bg_color", "#808080")[:50])
            permission = PUBLIC if item.get("unlisted",
                                            "false") == "false" else PRIVATE
            description = ""
            link = url_fix(item.get("link", ""))[:1000]
            secret_message = escape(item.get("secret_message", ""))[:1000]
            features = set()
            if item.get("hidden_preview", "false") != "false":
                features.add("hidden-preview")

            puzzle_id = submit_puzzle(
                pieces,
                bg_color,
                user,
                permission,
                description,
                link,
                upload_file=None,
                secret_message=secret_message,
                features=features,
            )

        return redirect("/chill/site/player-puzzle-list/", code=303)
Пример #6
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))
            args["features"] = set(request.form.getlist("features"))

        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 (not current_app.config["MINIMUM_PIECE_COUNT"] <= pieces <=
                current_app.config["MAXIMUM_PIECE_COUNT"]):
            abort(400)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        # All puzzles are public by default, but allow the admin to set
        # permission to PRIVATE.
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        # Get the owner of the suggested puzzle
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("_select-owner-for-suggested-puzzle.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchone()
        if not result:
            cur.close()
            abort(400)
        owner = int(result[0])
        if owner == 0:
            # Safe guard against when the user is not in the database. Usually
            # happens in development environments when switching and dropping
            # databases happens often.
            owner = ANONYMOUS_USER_ID

        features = args.get("features")

        new_puzzle_id = submit_puzzle(
            pieces,
            bg_color,
            owner,
            permission,
            description,
            link,
            upload_file,
            secret_message=secret_message,
            features=features,
        )

        # Update the status of this suggested puzzle to be the suggested done
        # status
        cur.execute(
            fetch_query_string("_update-suggested-puzzle-to-done-status.sql"),
            {"puzzle_id": puzzle_id},
        )
        db.commit()
        cur.close()

        return redirect("/chill/site/admin/puzzle/suggested/", code=303)
Пример #7
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))
            args["features"] = set(request.form.getlist("features"))

        # Only allow valid contributor
        if args.get("contributor",
                    None) != current_app.config.get("NEW_PUZZLE_CONTRIB"):
            abort(403)

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

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        user = int(
            current_app.secure_cookie.get("user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("select-user-details-by-id.sql"),
            {
                "id": user
            },
        ).fetchone()
        if not result:
            # Safe guard against when the user is not in the database. Usually
            # happens in development environments when switching and dropping
            # databases happens often.
            user = ANONYMOUS_USER_ID
        cur.close()

        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        features = set(args.get("features", []))

        puzzle_id = submit_puzzle(
            pieces,
            bg_color,
            user,
            permission,
            description,
            link,
            upload_file,
            secret_message=secret_message,
            features=features,
        )

        # TODO AUTO_APPROVE_PUZZLES only works for non Unsplash photos at the moment.
        if current_app.config["AUTO_APPROVE_PUZZLES"] and not re.search(
                unsplash_url_regex, link):
            cur = db.cursor()
            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]
            cur.close()
            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",
                )

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
Пример #8
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))
            args["features"] = set(request.form.getlist("features"))

        # 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)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        # Check description
        instance_description = escape(args.get("instance_description",
                                               ""))[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

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

        # Check fork
        fork = int(args.get("fork", "0"))
        if fork not in (0, 1):
            abort(400)
        fork = bool(fork == 1)

        # Validate permission
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            abort(400)
        if fork:
            # All copies of puzzles are unlisted
            permission = PRIVATE
        # Note that the permission value is updated to be unlisted later on if
        # the source puzzle is unlisted based on source puzzle data.

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

        cur = db.cursor()

        # The user should have
        # 2400 or more dots (points)
        # TODO: this could be configurable per site or for other reasons.
        # userHasEnoughPoints = cur.execute(fetch_query_string("select-minimum-points-for-user.sql"), {'user': user, 'points': 2400}).fetchall()
        # if not userHasEnoughPoints:
        #    abort(400)

        # An available instance slot
        result = cur.execute(
            fetch_query_string(
                "select-available-user-puzzle-slot-for-player.sql"),
            {
                "player": user
            },
        ).fetchone()[0]
        userHasAvailablePuzzleInstanceSlot = bool(result)
        if not userHasAvailablePuzzleInstanceSlot:
            cur.close()
            db.commit()
            abort(400)

        # Check if puzzle is valid to be a new puzzle instance
        if not fork:
            # Creating a new puzzle instance
            result = cur.execute(
                fetch_query_string(
                    "select-valid-puzzle-for-new-puzzle-instance.sql"),
                {
                    "puzzle_id": source_puzzle_id,
                    "ACTIVE": ACTIVE,
                    "IN_QUEUE": IN_QUEUE,
                    "COMPLETED": COMPLETED,
                    "FROZEN": FROZEN,
                    "REBUILD": REBUILD,
                    "IN_RENDER_QUEUE": IN_RENDER_QUEUE,
                    "RENDERING": RENDERING,
                },
            ).fetchall()
            if not result:
                # Puzzle does not exist or is not a valid puzzle to create instance from.
                cur.close()
                db.commit()
                abort(400)
        else:
            # Creating a copy of existing puzzle pieces (forking)
            result = cur.execute(
                fetch_query_string(
                    "select-valid-puzzle-for-new-puzzle-instance-fork.sql"),
                {
                    "puzzle_id": source_puzzle_id,
                    "ACTIVE": ACTIVE,
                    "IN_QUEUE": IN_QUEUE,
                    "COMPLETED": COMPLETED,
                    "FROZEN": FROZEN,
                },
            ).fetchall()
            if not result:
                # Puzzle does not exist or is not a valid puzzle to create instance from.
                cur.close()
                db.commit()
                abort(400)

        (result, col_names) = rowify(result, cur.description)
        source_puzzle_data = result[0]

        result = cur.execute(
            fetch_query_string("select-puzzle-features-for-puzzle_id.sql"),
            {
                "puzzle_id": source_puzzle_id,
                "enabled": 1
            },
        ).fetchall()
        source_features = set()
        if result:
            (result, _) = rowify(result, cur.description)
            source_features = set(map(lambda x: x["slug"], result))

        # Set the permission of new puzzle to be unlisted if source puzzle is
        # that way. The form should have this field as disabled if the source
        # puzzle is unlisted; which would mean it isn't sent as a parameter.
        if source_puzzle_data["permission"] == PRIVATE:
            permission = PRIVATE

        puzzle_id = generate_new_puzzle_id(source_puzzle_data["name"])

        # Create puzzle dir
        if not fork:
            puzzle_dir = os.path.join(
                current_app.config.get("PUZZLE_RESOURCES"), puzzle_id)
            os.mkdir(puzzle_dir)

        if not fork:
            d = {
                "puzzle_id":
                puzzle_id,
                "pieces":
                pieces,
                "name":
                source_puzzle_data["name"],
                "link":
                source_puzzle_data["link"],
                "description":
                source_puzzle_data["description"]
                if not instance_description else instance_description,
                "bg_color":
                bg_color,
                "owner":
                user,
                "queue":
                QUEUE_NEW,
                "status":
                IN_RENDER_QUEUE,
                "permission":
                permission,
            }
            cur.execute(
                fetch_query_string("insert_puzzle.sql"),
                d,
            )
        else:
            d = {
                "puzzle_id":
                puzzle_id,
                "pieces":
                source_puzzle_data["pieces"],
                "rows":
                source_puzzle_data["rows"],
                "cols":
                source_puzzle_data["cols"],
                "piece_width":
                source_puzzle_data["piece_width"],
                "mask_width":
                source_puzzle_data["mask_width"],
                "table_width":
                source_puzzle_data["table_width"],
                "table_height":
                source_puzzle_data["table_height"],
                "name":
                source_puzzle_data["name"],
                "link":
                source_puzzle_data["link"],
                "description":
                source_puzzle_data["description"]
                if not instance_description else instance_description,
                "bg_color":
                bg_color,
                "owner":
                user,
                "queue":
                QUEUE_NEW,
                "status":
                MAINTENANCE,
                "permission":
                permission,  # All copies of puzzles are unlisted
            }
            cur.execute(
                fetch_query_string("insert_puzzle_instance_copy.sql"),
                d,
            )
        db.commit()

        result = cur.execute(
            fetch_query_string("select_puzzle_id_by_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchall()
        if not result:
            cur.close()
            db.commit()
            current_app.logger.error(
                f"Failed to get puzzle id from select_puzzle_id_by_puzzle_id.sql using {puzzle_id}"
            )
            abort(500)

        puzzle = result[0][0]

        classic_variant = cur.execute(
            fetch_query_string("select-puzzle-variant-id-for-slug.sql"),
            {
                "slug": CLASSIC
            },
        ).fetchone()[0]
        cur.execute(
            fetch_query_string("insert-puzzle-instance.sql"),
            {
                "original": source_puzzle_data["id"],
                "instance": puzzle,
                "variant": classic_variant,
            },
        )

        cur.execute(
            fetch_query_string("fill-user-puzzle-slot.sql"),
            {
                "player": user,
                "puzzle": puzzle
            },
        )

        features = args.get("features")
        result = cur.execute(
            fetch_query_string("select-puzzle-features-enabled.sql"), {
                "enabled": 1
            }).fetchall()
        if result:
            (puzzle_features, _) = rowify(result, cur.description)
            # Add puzzle features
            for puzzle_feature in puzzle_features:
                if puzzle_feature[
                        "slug"] == "hidden-preview" and "hidden-preview" in features.union(
                            source_features):
                    # If source puzzle had hidden-preview then this puzzle will
                    # also.
                    cur.execute(
                        fetch_query_string(
                            "add-puzzle-feature-to-puzzle-by-id--hidden-preview.sql"
                        ),
                        {
                            "puzzle": puzzle,
                            "puzzle_feature": puzzle_feature["id"]
                        },
                    )
                elif (puzzle_feature["slug"] == "secret-message"
                      and "secret-message" in features):
                    cur.execute(
                        fetch_query_string(
                            "add-puzzle-feature-to-puzzle-by-id--secret-message.sql"
                        ),
                        {
                            "puzzle": puzzle,
                            "puzzle_feature": puzzle_feature["id"],
                            "message": secret_message,
                        },
                    )

        result = cur.execute(
            fetch_query_string(
                "select-all-and-preview_full-from-puzzle-by-puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchall()
        if not result:
            cur.close()
            db.commit()
            current_app.logger.error(
                f"Failed to get result from select-all-and-preview_full-from-puzzle-by-puzzle_id.sql using {puzzle_id}"
            )
            abort(500)

        (result, col_names) = rowify(result, cur.description)
        puzzle_data = result[0]

        db.commit()
        cur.close()

        if not fork:
            job = current_app.createqueue.enqueue(
                "api.jobs.pieceRenderer.render",
                [puzzle_data],
                result_ttl=0,
                job_timeout="24h",
            )
        else:
            # Copy existing puzzle
            job = current_app.cleanupqueue.enqueue(
                "api.jobs.piece_forker.fork_puzzle_pieces",
                source_puzzle_data,
                puzzle_data,
                result_ttl=0,
            )

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)