コード例 #1
0
ファイル: upload.py プロジェクト: b0r1ngx/puzzle-massive
def submit_puzzle(pieces, bg_color, user, permission, description, link,
                  upload_file):
    """
    Submit a puzzle to be reviewed.  Generates the puzzle_id and original.jpg.
    """
    unsplash_image_thread = None

    puzzle_id = None
    cur = db.cursor()

    unsplash_match = re.search(
        r"^(http://|https://)?unsplash.com/photos/([^/]+)", link)
    if link and unsplash_match:
        if not current_app.config.get("UNSPLASH_APPLICATION_ID"):
            cur.close()
            abort(400)

        d = time.strftime("%Y_%m_%d.%H_%M_%S", time.localtime())
        filename = unsplash_match.group(2)
        u_id = "%s" % (hashlib.sha224(bytes("%s%s" % (filename, d),
                                            "utf-8")).hexdigest()[0:9])
        puzzle_id = "unsplash{filename}-mxyz-{u_id}".format(filename=filename,
                                                            u_id=u_id)

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

        # Download the unsplash image
        unsplash_image_thread = UnsplashPuzzleThread(
            puzzle_id,
            filename,
            description,
            current_app.config_file,
            current_app.config.get("SECURE_COOKIE_SECRET"),
        )
    else:
        if not upload_file:
            cur.close()
            abort(400)
        filename = secure_filename(upload_file.filename)
        filename = filename.lower()

        # Check the filename to see if the extension is allowed
        if os.path.splitext(filename)[1][1:].lower() not in ALLOWED_EXTENSIONS:
            cur.close()
            abort(400)

        puzzle_id = generate_new_puzzle_id(filename)

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

        # Convert the uploaded file to jpg
        upload_file_path = os.path.join(puzzle_dir, filename)
        upload_file.save(upload_file_path)

        # verify the image file format
        identify_format = subprocess.check_output(
            ["identify", "-format", "%m", upload_file_path], encoding="utf-8")
        identify_format = identify_format.lower()
        if identify_format not in ALLOWED_EXTENSIONS:
            os.unlink(upload_file_path)
            os.rmdir(puzzle_dir)
            cur.close()
            abort(400)

        # Abort if imagemagick fails converting the image to jpg
        try:
            subprocess.check_call([
                "convert",
                upload_file_path,
                "-quality",
                "85%",
                "-format",
                "jpg",
                os.path.join(puzzle_dir, "original.jpg"),
            ])
        except subprocess.CalledProcessError:
            os.unlink(upload_file_path)
            os.rmdir(puzzle_dir)
            cur.close()
            abort(400)
        os.unlink(upload_file_path)

        # The preview_full image is only created in the pieceRender process.

    d = {
        "puzzle_id": puzzle_id,
        "pieces": pieces,
        "name": filename,
        "link": link,
        "description": description,
        "bg_color": bg_color,
        "owner": user,
        "queue": QUEUE_NEW,
        "status": NEEDS_MODERATION,
        "permission": permission,
    }
    cur.execute(
        fetch_query_string("insert_puzzle.sql"),
        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"]

    cur.execute(
        fetch_query_string("add-puzzle-file.sql"),
        {
            "puzzle": puzzle,
            "name": "original",
            "url": "/resources/{0}/original.jpg".format(
                puzzle_id),  # Not a public file (only on admin page)
        },
    )

    cur.execute(
        fetch_query_string("add-puzzle-file.sql"),
        {
            "puzzle": puzzle,
            "name": "preview_full",
            "url": "/resources/{0}/preview_full.jpg".format(puzzle_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": puzzle,
            "instance": puzzle,
            "variant": classic_variant
        },
    )

    db.commit()
    cur.close()

    if unsplash_image_thread:
        # Go download the unsplash image and update the db
        unsplash_image_thread.start()

    return puzzle_id
コード例 #2
0
    def setUp(self):
        super().setUp()

        with self.app.app_context():
            cur = self.db.cursor()
            # TODO: create players

            # Create fake source puzzle that will be forked
            (
                fake_source_puzzle_data,
                fake_source_piece_properties,
            ) = self.fabricate_fake_puzzle()
            self.source_puzzle_id = fake_source_puzzle_data.get("puzzle_id")

            result = cur.execute(
                fetch_query_string(
                    "select-valid-puzzle-for-new-puzzle-instance-fork.sql"),
                {
                    "puzzle_id": self.source_puzzle_id,
                    "ACTIVE": ACTIVE,
                    "IN_QUEUE": IN_QUEUE,
                    "COMPLETED": COMPLETED,
                    "FROZEN": FROZEN,
                },
            ).fetchall()
            if not result:
                raise Exception("fake puzzle failed")
            (result, col_names) = rowify(result, cur.description)
            self.source_puzzle_data = result[0]

            self.puzzle_id = generate_new_puzzle_id("fork-puzzle")

            d = {
                "puzzle_id": self.puzzle_id,
                "pieces": self.source_puzzle_data["pieces"],
                "name": self.source_puzzle_data["name"],
                "link": self.source_puzzle_data["link"],
                "description": "forky",
                "bg_color": "#f041EE",
                "owner": 3,
                "queue": 1,
                "status": MAINTENANCE,
                "permission": PRIVATE,
            }
            cur.execute(
                fetch_query_string("insert_puzzle.sql"),
                d,
            )
            db.commit()

            result = cur.execute(
                fetch_query_string("select-all-from-puzzle-by-puzzle_id.sql"),
                {
                    "puzzle_id": self.puzzle_id
                },
            ).fetchall()

            (result, col_names) = rowify(result, cur.description)
            self.puzzle_data = result[0]
            puzzle = self.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": self.source_puzzle_data["id"],
                    "instance": puzzle,
                    "variant": classic_variant,
                },
            )

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

            self.db.commit()
コード例 #3
0
def submit_puzzle(
        pieces,
        bg_color,
        user,
        permission,
        description,
        link,
        upload_file,
        secret_message="",
        features=set(),
):
    """
    Submit a puzzle to be reviewed.  Generates the puzzle_id and original.jpg.
    """

    puzzle_id = None
    filename = ""
    original_slip = uuid.uuid4().hex[:10]
    cur = db.cursor()

    unsplash_match = re.search(unsplash_url_regex, link)
    is_unsplash_link = True if link and unsplash_match else False
    if is_unsplash_link:
        if not current_app.config.get("UNSPLASH_APPLICATION_ID"):
            cur.close()
            abort(400)

        filename = unsplash_match.group(2)
        u_id = uuid.uuid4().hex[:20]
        puzzle_id = f"unsplash-mxyz-{u_id}"

    else:
        if not upload_file:
            cur.close()
            abort(400)
        filename = secure_filename(upload_file.filename)
        filename = filename.lower()

        # Check the filename to see if the extension is allowed
        if os.path.splitext(filename)[1][1:].lower() not in ALLOWED_EXTENSIONS:
            cur.close()
            abort(400)

        puzzle_id = generate_new_puzzle_id(filename)

    CDN_BASE_URL = current_app.config["CDN_BASE_URL"]
    prefix_resources_url = ("" if current_app.config["LOCAL_PUZZLE_RESOURCES"]
                            else CDN_BASE_URL)
    pr = PuzzleResource(puzzle_id,
                        current_app.config,
                        is_local_resource=not bool(prefix_resources_url))

    tmp_dir = tempfile.mkdtemp()
    tmp_puzzle_dir = os.path.join(tmp_dir, puzzle_id)
    os.mkdir(tmp_puzzle_dir)

    if not is_unsplash_link:
        # Convert the uploaded file to jpg
        with tempfile.NamedTemporaryFile() as temp_upload_file:
            upload_file.save(temp_upload_file)

            # verify the image file format
            identify_format = subprocess.check_output(
                ["identify", "-format", "%m", temp_upload_file.name],
                encoding="utf-8")
            identify_format = identify_format.lower()
            if identify_format not in ALLOWED_EXTENSIONS:
                pr.delete()
                cur.close()
                abort(400)

            # Abort if imagemagick fails converting the image to jpg
            try:
                subprocess.check_call([
                    "convert",
                    temp_upload_file.name,
                    "-quality",
                    "85%",
                    "-format",
                    "jpg",
                    os.path.join(tmp_puzzle_dir,
                                 f"original.{original_slip}.jpg"),
                ])
            except subprocess.CalledProcessError:
                pr.delete()
                cur.close()
                abort(400)

    d = {
        "puzzle_id":
        puzzle_id,
        "pieces":
        pieces,
        "name":
        filename,
        "link":
        link,
        "description":
        description,
        "bg_color":
        bg_color,
        "owner":
        user,
        "queue":
        QUEUE_NEW,
        "status":
        NEEDS_MODERATION
        if not current_app.config["AUTO_APPROVE_PUZZLES"] else IN_RENDER_QUEUE,
        "permission":
        permission,
    }
    cur.execute(
        fetch_query_string("insert_puzzle.sql"),
        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"]

    cur.execute(
        fetch_query_string("add-puzzle-file.sql"),
        {
            "puzzle":
            puzzle,
            "name":
            "original",
            "url":
            f"{prefix_resources_url}/resources/{puzzle_id}/original.{original_slip}.jpg",
        },
    )

    if not is_unsplash_link:
        slip = uuid.uuid4().hex[:4]
        preview_full_slip = f"preview_full.{slip}.jpg"
        cur.execute(
            fetch_query_string("add-puzzle-file.sql"),
            {
                "puzzle":
                puzzle,
                "name":
                "preview_full",
                "url":
                f"{prefix_resources_url}/resources/{puzzle_id}/{preview_full_slip}",
            },
        )
        im = Image.open(
            os.path.join(tmp_puzzle_dir,
                         f"original.{original_slip}.jpg")).copy()
        im.thumbnail(size=(384, 384))
        im.save(os.path.join(tmp_puzzle_dir, preview_full_slip))
        im.close()

    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": puzzle,
            "instance": puzzle,
            "variant": classic_variant
        },
    )

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

    pr.put(tmp_puzzle_dir)
    rmtree(tmp_dir)

    if is_unsplash_link:
        original_filename = f"original.{original_slip}.jpg"
        # Go download the unsplash image and update the db
        job = current_app.unsplashqueue.enqueue(
            "api.jobs.unsplash_image.add_photo_to_puzzle",
            puzzle_id,
            filename,
            description,
            original_filename,
            result_ttl=0,
            job_timeout="24h",
        )

    return puzzle_id
コード例 #4
0
ファイル: instance.py プロジェクト: b0r1ngx/puzzle-massive
    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)
コード例 #5
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)