Creating a new card by REST API #208

Closed
opened 2026-02-04 17:51:15 +03:00 by OVERLORD · 2 comments
Owner

Originally created by @andreasgerstmayr on GitHub (Jun 24, 2022).

I'm looking for a way to create a new card with the REST API.

Afaics I need the POST /api/boards/:boardId/cards route, with these parameters: ecc45c7a9e/server/api/controllers/cards/create.js (L20-L31)

I can get the the boardId by calling /api/projects and looking at included.boards of the response, but I don't know how to get the listId.

I think the boards/show endpoint would list all lists, but I'm always getting a bad request error: ecc45c7a9e/server/api/controllers/boards/show.js (L23-L26)
Is there a security check missing, or can this conditional be safely removed?

Second question: What is the correct value for the position parameter, if I want to place the new card at the end? Is there an easier way other than requesting all cards, filter them for the current list and then use max(position)+1?

And thanks for this great project, I love the UI/UX, and so far everything is working perfectly :)

Originally created by @andreasgerstmayr on GitHub (Jun 24, 2022). I'm looking for a way to create a new card with the REST API. Afaics I need the `POST /api/boards/:boardId/cards` route, with these parameters: https://github.com/plankanban/planka/blob/ecc45c7a9eb77e0e38bbbb1880a42c72d088fb99/server/api/controllers/cards/create.js#L20-L31 I can get the the `boardId` by calling `/api/projects` and looking at `included.boards` of the response, but I don't know how to get the `listId`. I think the `boards/show` endpoint would list all `lists`, but I'm always getting a bad request error: https://github.com/plankanban/planka/blob/ecc45c7a9eb77e0e38bbbb1880a42c72d088fb99/server/api/controllers/boards/show.js#L23-L26 Is there a security check missing, or can this conditional be safely removed? Second question: What is the correct value for the `position` parameter, if I want to place the new card at the end? Is there an easier way other than requesting all cards, filter them for the current `list` and then use `max(position)+1`? And thanks for this great project, I love the UI/UX, and so far everything is working perfectly :)
Author
Owner

@meltyshev commented on GitHub (Jun 24, 2022):

Hi!

Is there a security check missing, or can this conditional be safely removed?

This endpoint needs to be fixed, I'll try it now. I think I did this check because of the sails.sockets.join at the bottom.

Second question: What is the correct value for the position parameter, if I want to place the new card at the end? Is there an easier way other than requesting all cards, filter them for the current list and then use max(position)+1?

You can find how the position is calculated in client/src/selectors/core.js. To insert somewhere in the middle you need to get the previous card and next card and then use the formula prevPosition + (nextPosition - prevPosition) / 2. If you want to place the card at the end, just get the position for the last card and add 65535 to it.
At some point the positions will be recalculated automatically when the gap between cards will be a very small number, this occurs on the server and then the client side is notified of the changes via socket.

And thanks for this great project, I love the UI/UX, and so far everything is working perfectly :)

🙏

@meltyshev commented on GitHub (Jun 24, 2022): Hi! > Is there a security check missing, or can this conditional be safely removed? This endpoint needs to be fixed, I'll try it now. I think I did this check because of the `sails.sockets.join` at the bottom. > Second question: What is the correct value for the position parameter, if I want to place the new card at the end? Is there an easier way other than requesting all cards, filter them for the current list and then use max(position)+1? You can find how the position is calculated in `client/src/selectors/core.js`. To insert somewhere in the middle you need to get the previous card and next card and then use the formula `prevPosition + (nextPosition - prevPosition) / 2`. If you want to place the card at the end, just get the position for the last card and add 65535 to it. At some point the positions will be recalculated automatically when the gap between cards will be a very small number, this occurs on the server and then the client side is notified of the changes via socket. > And thanks for this great project, I love the UI/UX, and so far everything is working perfectly :) 🙏
Author
Owner

@andreasgerstmayr commented on GitHub (Jun 27, 2022):

Thanks a lot @meltyshev!
I just tried the new version 1.3.2 including 3b3bff1b6b and it works perfectly.

For reference, this is my minimal Python API client (probably there's a better way to write or generate an API client, but it works for my usage - creating new cards. doesn't support card positions yet):

import requests
from typing import Optional, Dict


class Project:
    def __init__(self, id: str, name: str):
        self.id = id
        self.name = name

    def __repr__(self):
        return f"<Project {self.name}>"


class Board:
    def __init__(self, id: str, name: str):
        self.id = id
        self.name = name

    def __repr__(self):
        return f"<Board {self.name}>"


class BoardList:
    def __init__(self, id: str, name: str):
        self.id = id
        self.name = name

    def __repr__(self):
        return f"<List {self.name}>"


class Card:
    def __init__(self, id: str, name: str, description: str):
        self.id = id
        self.name = name
        self.description = description

    def __repr__(self):
        return f"<Card {self.id}, {self.name}, {self.description}>"


class Planka:
    def __init__(self, url: str, username: str, password: str):
        self.url = url
        self.username = username
        self.password = password
        self.token = None

    def authorize(self):
        r = requests.post(
            f"{self.url}/api/access-tokens",
            json={"emailOrUsername": self.username, "password": self.password},
        )
        r.raise_for_status()

        content_type = r.headers["Content-Type"]
        if not content_type.startswith("application/json"):
            raise Exception(
                f"Received '{content_type}' instead of JSON. Is SSO active?"
            )

        data = r.json()
        self.token = data["item"]

    def api_call(self, path: str, json: Optional[Dict] = None):
        if not self.token:
            self.authorize()

        headers = {"Authorization": f"Bearer {self.token}"}
        if json is None:
            r = requests.get(
                f"{self.url}{path}",
                headers=headers,
            )
        else:
            r = requests.post(
                f"{self.url}{path}",
                headers=headers,
                json=json,
            )

        try:
            r.raise_for_status()
        except requests.exceptions.HTTPError as err:
            print("ERROR", r.content)
            raise err
        return r

    def list_projects(self):
        r = self.api_call(f"/api/projects")
        return [Project(p["id"], p["name"]) for p in r.json()["items"]]

    def list_boards(self, project_id):
        r = self.api_call(f"/api/projects/{project_id}")
        return [Board(b["id"], b["name"]) for b in r.json()["included"]["boards"]]

    def list_lists(self, board_id: str):
        r = self.api_call(f"/api/boards/{board_id}")
        return [BoardList(b["id"], b["name"]) for b in r.json()["included"]["lists"]]

    def list_cards(self, board_id: str):
        r = self.api_call(f"/api/boards/{board_id}/cards")
        return [Card(c["id"], c["name"], c["description"]) for c in r.json()["items"]]

    def add_card(
        self,
        board_id: str,
        list_id: str,
        name: str,
        description: Optional[str] = None,
    ):
        r = self.api_call(
            f"/api/boards/{board_id}/cards",
            {
                "listId": list_id,
                "name": name,
                "position": 0,
                "description": description,
            },
        )
        c = r.json()["item"]
        return Card(c["id"], c["name"], c["description"])
@andreasgerstmayr commented on GitHub (Jun 27, 2022): Thanks a lot @meltyshev! I just tried the new version 1.3.2 including 3b3bff1b6bba96dc6c820a1c81fb7ef03bc641d6 and it works perfectly. For reference, this is my minimal Python API client (probably there's a better way to write or generate an API client, but it works for my usage - creating new cards. doesn't support card positions yet): ``` import requests from typing import Optional, Dict class Project: def __init__(self, id: str, name: str): self.id = id self.name = name def __repr__(self): return f"<Project {self.name}>" class Board: def __init__(self, id: str, name: str): self.id = id self.name = name def __repr__(self): return f"<Board {self.name}>" class BoardList: def __init__(self, id: str, name: str): self.id = id self.name = name def __repr__(self): return f"<List {self.name}>" class Card: def __init__(self, id: str, name: str, description: str): self.id = id self.name = name self.description = description def __repr__(self): return f"<Card {self.id}, {self.name}, {self.description}>" class Planka: def __init__(self, url: str, username: str, password: str): self.url = url self.username = username self.password = password self.token = None def authorize(self): r = requests.post( f"{self.url}/api/access-tokens", json={"emailOrUsername": self.username, "password": self.password}, ) r.raise_for_status() content_type = r.headers["Content-Type"] if not content_type.startswith("application/json"): raise Exception( f"Received '{content_type}' instead of JSON. Is SSO active?" ) data = r.json() self.token = data["item"] def api_call(self, path: str, json: Optional[Dict] = None): if not self.token: self.authorize() headers = {"Authorization": f"Bearer {self.token}"} if json is None: r = requests.get( f"{self.url}{path}", headers=headers, ) else: r = requests.post( f"{self.url}{path}", headers=headers, json=json, ) try: r.raise_for_status() except requests.exceptions.HTTPError as err: print("ERROR", r.content) raise err return r def list_projects(self): r = self.api_call(f"/api/projects") return [Project(p["id"], p["name"]) for p in r.json()["items"]] def list_boards(self, project_id): r = self.api_call(f"/api/projects/{project_id}") return [Board(b["id"], b["name"]) for b in r.json()["included"]["boards"]] def list_lists(self, board_id: str): r = self.api_call(f"/api/boards/{board_id}") return [BoardList(b["id"], b["name"]) for b in r.json()["included"]["lists"]] def list_cards(self, board_id: str): r = self.api_call(f"/api/boards/{board_id}/cards") return [Card(c["id"], c["name"], c["description"]) for c in r.json()["items"]] def add_card( self, board_id: str, list_id: str, name: str, description: Optional[str] = None, ): r = self.api_call( f"/api/boards/{board_id}/cards", { "listId": list_id, "name": name, "position": 0, "description": description, }, ) c = r.json()["item"] return Card(c["id"], c["name"], c["description"]) ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/planka#208