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