forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into dev/private_bucket_for_attachments
This commit is contained in:
commit
2628890068
@ -36,15 +36,13 @@ jobs:
|
|||||||
- name: Build Plane's Main App
|
- name: Build Plane's Main App
|
||||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd web
|
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build --filter=web
|
||||||
|
|
||||||
- name: Build Plane's Deploy App
|
- name: Build Plane's Deploy App
|
||||||
if: steps.changed-files.outputs.deploy_any_changed == 'true'
|
if: steps.changed-files.outputs.deploy_any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd space
|
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build --filter=space
|
||||||
|
|
||||||
|
|
79
.github/workflows/create-sync-pr.yml
vendored
Normal file
79
.github/workflows/create-sync-pr.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
name: Create PR in Plane EE Repository to sync the changes
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create_pr:
|
||||||
|
# Only run the job when a PR is merged
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check SOURCE_REPO
|
||||||
|
id: check_repo
|
||||||
|
env:
|
||||||
|
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Branch Name
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
run: |
|
||||||
|
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup GH CLI
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
run: |
|
||||||
|
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||||
|
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||||
|
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install gh -y
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||||
|
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||||
|
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||||
|
|
||||||
|
git checkout $SOURCE_BRANCH
|
||||||
|
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||||
|
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||||
|
|
||||||
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
PR_BODY="${{ github.event.pull_request.body }}"
|
||||||
|
|
||||||
|
# Remove double quotes
|
||||||
|
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||||
|
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||||
|
|
||||||
|
# Construct PR_BODY_CONTENT using a here-document
|
||||||
|
PR_BODY_CONTENT=$(cat <<EOF
|
||||||
|
$PR_BODY_CLEANED
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--base $TARGET_BRANCH \
|
||||||
|
--head $SOURCE_BRANCH \
|
||||||
|
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||||
|
--body "$PR_BODY_CONTENT" \
|
||||||
|
--repo $TARGET_REPO
|
@ -39,10 +39,10 @@ jobs:
|
|||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: metaDeploy
|
id: metaSpace
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
with:
|
with:
|
||||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
file: ./space/Dockerfile.space
|
file: ./space/Dockerfile.space
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
tags: ${{ steps.metaSpace.outputs.tags }}
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -16,6 +16,7 @@ node_modules
|
|||||||
|
|
||||||
# Production
|
# Production
|
||||||
/build
|
/build
|
||||||
|
dist
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -73,3 +74,7 @@ pnpm-lock.yaml
|
|||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
|
|
||||||
.npmrc
|
.npmrc
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
## packages
|
||||||
|
dist
|
||||||
|
@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
|
|||||||
|
|
||||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||||
|
|
||||||
- 3rd-party libraries being used and their versions
|
- 3rd-party libraries being used and their versions
|
||||||
- a use-case that fails
|
- a use-case that fails
|
||||||
|
|
||||||
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Node.js version v16.18.0
|
- Node.js version v16.18.0
|
||||||
- Python version 3.8+
|
- Python version 3.8+
|
||||||
- Postgres version v14
|
- Postgres version v14
|
||||||
- Redis version v6.2.7
|
- Redis version v6.2.7
|
||||||
|
|
||||||
### Setup the project
|
### Setup the project
|
||||||
|
|
||||||
@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
|
|||||||
|
|
||||||
The backend is a django project which is kept inside apiserver
|
The backend is a django project which is kept inside apiserver
|
||||||
|
|
||||||
|
1. Clone the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/makeplane/plane
|
||||||
|
cd plane
|
||||||
|
chmod +x setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run setup.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run Docker compose up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run the web app in development mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
## Missing a Feature?
|
## Missing a Feature?
|
||||||
|
|
||||||
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
||||||
@ -39,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
|
|||||||
|
|
||||||
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
||||||
|
|
||||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||||
|
|
||||||
## Need help? Questions and suggestions
|
## Need help? Questions and suggestions
|
||||||
|
|
||||||
@ -48,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
|
|||||||
|
|
||||||
## Ways to contribute
|
## Ways to contribute
|
||||||
|
|
||||||
- Try Plane Cloud and the self hosting platform and give feedback
|
- Try Plane Cloud and the self hosting platform and give feedback
|
||||||
- Add new integrations
|
- Add new integrations
|
||||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||||
- Share your thoughts and suggestions with us
|
- Share your thoughts and suggestions with us
|
||||||
- Help create tutorials and blog posts
|
- Help create tutorials and blog posts
|
||||||
- Request a feature by submitting a proposal
|
- Request a feature by submitting a proposal
|
||||||
- Report a bug
|
- Report a bug
|
||||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||||
|
134
ENV_SETUP.md
Normal file
134
ENV_SETUP.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
Environment variables are distributed in various files. Please refer them carefully.
|
||||||
|
|
||||||
|
## {PROJECT_FOLDER}/.env
|
||||||
|
File is available in the project root folder
|
||||||
|
|
||||||
|
```
|
||||||
|
# Database Settings
|
||||||
|
PGUSER="plane"
|
||||||
|
PGPASSWORD="plane"
|
||||||
|
PGHOST="plane-db"
|
||||||
|
PGDATABASE="plane"
|
||||||
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# Redis Settings
|
||||||
|
REDIS_HOST="plane-redis"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
|
# AWS Settings
|
||||||
|
AWS_REGION=""
|
||||||
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
|
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||||
|
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||||
|
# Changing this requires change in the nginx.conf for uploads if using minio setup
|
||||||
|
AWS_S3_BUCKET_NAME="uploads"
|
||||||
|
# Maximum file upload limit
|
||||||
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
|
# GPT settings
|
||||||
|
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
||||||
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
|
# Settings related to Docker
|
||||||
|
DOCKERIZED=1
|
||||||
|
# set to 1 If using the pre-configured minio setup
|
||||||
|
USE_MINIO=1
|
||||||
|
|
||||||
|
# Nginx Configuration
|
||||||
|
NGINX_PORT=80
|
||||||
|
```
|
||||||
|
|
||||||
|
## {PROJECT_FOLDER}/web/.env.example
|
||||||
|
|
||||||
|
```
|
||||||
|
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||||
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
|
# Public boards deploy URL
|
||||||
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||||
|
```
|
||||||
|
|
||||||
|
## {PROJECT_FOLDER}/spaces/.env.example
|
||||||
|
|
||||||
|
```
|
||||||
|
# Flag to toggle OAuth
|
||||||
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## {PROJECT_FOLDER}/apiserver/.env
|
||||||
|
|
||||||
|
```
|
||||||
|
# Backend
|
||||||
|
# Debug value for api server use it as 0 for production use
|
||||||
|
DEBUG=0
|
||||||
|
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
||||||
|
|
||||||
|
# Error logs
|
||||||
|
SENTRY_DSN=""
|
||||||
|
|
||||||
|
# Database Settings
|
||||||
|
PGUSER="plane"
|
||||||
|
PGPASSWORD="plane"
|
||||||
|
PGHOST="plane-db"
|
||||||
|
PGDATABASE="plane"
|
||||||
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# Redis Settings
|
||||||
|
REDIS_HOST="plane-redis"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
EMAIL_HOST=""
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
|
EMAIL_USE_TLS="1"
|
||||||
|
EMAIL_USE_SSL="0"
|
||||||
|
|
||||||
|
# AWS Settings
|
||||||
|
AWS_REGION=""
|
||||||
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
|
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||||
|
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||||
|
# Changing this requires change in the nginx.conf for uploads if using minio setup
|
||||||
|
AWS_S3_BUCKET_NAME="uploads"
|
||||||
|
# Maximum file upload limit
|
||||||
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
|
# GPT settings
|
||||||
|
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
||||||
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
|
# Github
|
||||||
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
|
||||||
|
# Settings related to Docker
|
||||||
|
DOCKERIZED=1
|
||||||
|
# set to 1 If using the pre-configured minio setup
|
||||||
|
USE_MINIO=1
|
||||||
|
|
||||||
|
# Nginx Configuration
|
||||||
|
NGINX_PORT=80
|
||||||
|
|
||||||
|
# Default Creds
|
||||||
|
DEFAULT_EMAIL="captain@plane.so"
|
||||||
|
DEFAULT_PASSWORD="password123"
|
||||||
|
|
||||||
|
# SignUps
|
||||||
|
ENABLE_SIGNUP="1"
|
||||||
|
|
||||||
|
# Email Redirection URL
|
||||||
|
WEB_URL="http://localhost"
|
||||||
|
```
|
||||||
|
## Updates
|
||||||
|
- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
|
||||||
|
- The naming convention for containers and images has been updated.
|
||||||
|
- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
|
||||||
|
- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys.
|
||||||
|
- The image name for Plane deployment has been changed to plane-space.
|
38
README.md
38
README.md
@ -39,33 +39,35 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
|||||||
|
|
||||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||||
|
|
||||||
## ⚡️ Quick start with Docker Compose
|
## ⚡️ Contributors Quick Start
|
||||||
|
|
||||||
### Docker Compose Setup
|
### Prerequisite
|
||||||
|
|
||||||
- Clone the repository
|
Development system must have docker engine installed and running.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
|
||||||
|
|
||||||
|
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
|
||||||
|
1. Switch to the code folder `cd plane`
|
||||||
|
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
|
||||||
|
1. Open terminal and run `./setup.sh`
|
||||||
|
1. Open the code on VSCode or similar equivalent IDE
|
||||||
|
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
||||||
|
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/makeplane/plane
|
./setup.sh
|
||||||
cd plane
|
|
||||||
chmod +x setup.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Run setup.sh
|
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
||||||
|
|
||||||
```bash
|
Thats it!
|
||||||
./setup.sh http://localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
## 🍙 Self Hosting
|
||||||
|
|
||||||
- Run Docker compose up
|
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Backend
|
# Backend
|
||||||
# Debug value for api server use it as 0 for production use
|
# Debug value for api server use it as 0 for production use
|
||||||
DEBUG=0
|
DEBUG=0
|
||||||
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
DJANGO_SETTINGS_MODULE="plane.settings.production"
|
||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
@ -59,3 +59,14 @@ DEFAULT_PASSWORD="password123"
|
|||||||
|
|
||||||
# SignUps
|
# SignUps
|
||||||
ENABLE_SIGNUP="1"
|
ENABLE_SIGNUP="1"
|
||||||
|
|
||||||
|
|
||||||
|
# Enable Email/Password Signup
|
||||||
|
ENABLE_EMAIL_PASSWORD="1"
|
||||||
|
|
||||||
|
# Enable Magic link Login
|
||||||
|
ENABLE_MAGIC_LINK_LOGIN="0"
|
||||||
|
|
||||||
|
# Email redirections and minio domain settings
|
||||||
|
WEB_URL="http://localhost"
|
||||||
|
|
||||||
|
52
apiserver/Dockerfile.dev
Normal file
52
apiserver/Dockerfile.dev
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
FROM python:3.11.1-alpine3.17 AS backend
|
||||||
|
|
||||||
|
# set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
"bash~=5.2" \
|
||||||
|
"libpq~=15" \
|
||||||
|
"libxslt~=1.1" \
|
||||||
|
"nodejs-current~=19" \
|
||||||
|
"xmlsec~=1.2" \
|
||||||
|
"libffi-dev" \
|
||||||
|
"bash~=5.2" \
|
||||||
|
"g++~=12.2" \
|
||||||
|
"gcc~=12.2" \
|
||||||
|
"cargo~=1.64" \
|
||||||
|
"git~=2" \
|
||||||
|
"make~=4.3" \
|
||||||
|
"postgresql13-dev~=13" \
|
||||||
|
"libc-dev" \
|
||||||
|
"linux-headers"
|
||||||
|
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
COPY requirements.txt ./requirements.txt
|
||||||
|
ADD requirements ./requirements
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt --compile --no-cache-dir
|
||||||
|
|
||||||
|
RUN addgroup -S plane && \
|
||||||
|
adduser -S captain -G plane
|
||||||
|
|
||||||
|
RUN chown captain.plane /code
|
||||||
|
|
||||||
|
USER captain
|
||||||
|
|
||||||
|
# Add in Django deps and generate Django's static files
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||||
|
RUN chmod -R 777 /code
|
||||||
|
|
||||||
|
USER captain
|
||||||
|
|
||||||
|
# Expose container port and run entry point script
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# CMD [ "./bin/takeoff" ]
|
||||||
|
|
@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission):
|
|||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
|
if request.method in SAFE_METHODS:
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
).exists()
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role__in=[Owner, Admin],
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
|
from .user import (
|
||||||
|
UserSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
ChangePasswordSerializer,
|
||||||
|
ResetPasswordSerializer,
|
||||||
|
UserAdminLiteSerializer,
|
||||||
|
UserMeSerializer,
|
||||||
|
UserMeSettingsSerializer,
|
||||||
|
)
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceSerializer,
|
WorkSpaceSerializer,
|
||||||
WorkSpaceMemberSerializer,
|
WorkSpaceMemberSerializer,
|
||||||
@ -8,9 +16,11 @@ from .workspace import (
|
|||||||
WorkspaceLiteSerializer,
|
WorkspaceLiteSerializer,
|
||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
|
WorkspaceMemberMeSerializer,
|
||||||
)
|
)
|
||||||
from .project import (
|
from .project import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
|
ProjectListSerializer,
|
||||||
ProjectDetailSerializer,
|
ProjectDetailSerializer,
|
||||||
ProjectMemberSerializer,
|
ProjectMemberSerializer,
|
||||||
ProjectMemberInviteSerializer,
|
ProjectMemberInviteSerializer,
|
||||||
@ -20,11 +30,16 @@ from .project import (
|
|||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
ProjectPublicMemberSerializer
|
ProjectPublicMemberSerializer,
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
from .cycle import (
|
||||||
|
CycleSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
CycleFavoriteSerializer,
|
||||||
|
CycleWriteSerializer,
|
||||||
|
)
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
IssueCreateSerializer,
|
IssueCreateSerializer,
|
||||||
|
@ -3,3 +3,56 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
class BaseSerializer(serializers.ModelSerializer):
|
class BaseSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
|
class DynamicBaseSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||||
|
# This is done so as not to pass this custom argument up to the superclass.
|
||||||
|
fields = kwargs.pop("fields", None)
|
||||||
|
|
||||||
|
# Call the initialization of the superclass.
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||||
|
if fields is not None:
|
||||||
|
self.fields = self._filter_fields(fields)
|
||||||
|
|
||||||
|
def _filter_fields(self, fields):
|
||||||
|
"""
|
||||||
|
Adjust the serializer's fields based on the provided 'fields' list.
|
||||||
|
|
||||||
|
:param fields: List or dictionary specifying which fields to include in the serializer.
|
||||||
|
:return: The updated fields for the serializer.
|
||||||
|
"""
|
||||||
|
# Check each field_name in the provided fields.
|
||||||
|
for field_name in fields:
|
||||||
|
# If the field is a dictionary (indicating nested fields),
|
||||||
|
# loop through its keys and values.
|
||||||
|
if isinstance(field_name, dict):
|
||||||
|
for key, value in field_name.items():
|
||||||
|
# If the value of this nested field is a list,
|
||||||
|
# perform a recursive filter on it.
|
||||||
|
if isinstance(value, list):
|
||||||
|
self._filter_fields(self.fields[key], value)
|
||||||
|
|
||||||
|
# Create a list to store allowed fields.
|
||||||
|
allowed = []
|
||||||
|
for item in fields:
|
||||||
|
# If the item is a string, it directly represents a field's name.
|
||||||
|
if isinstance(item, str):
|
||||||
|
allowed.append(item)
|
||||||
|
# If the item is a dictionary, it represents a nested field.
|
||||||
|
# Add the key of this dictionary to the allowed list.
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
allowed.append(list(item.keys())[0])
|
||||||
|
|
||||||
|
# Convert the current serializer's fields and the allowed fields to sets.
|
||||||
|
existing = set(self.fields)
|
||||||
|
allowed = set(allowed)
|
||||||
|
|
||||||
|
# Remove fields from the serializer that aren't in the 'allowed' list.
|
||||||
|
for field_name in (existing - allowed):
|
||||||
|
self.fields.pop(field_name)
|
||||||
|
|
||||||
|
return self.fields
|
||||||
|
@ -12,10 +12,14 @@ from .workspace import WorkspaceLiteSerializer
|
|||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
||||||
|
|
||||||
class CycleWriteSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class CycleWriteSerializer(BaseSerializer):
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
|
if (
|
||||||
|
data.get("start_date", None) is not None
|
||||||
|
and data.get("end_date", None) is not None
|
||||||
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -34,7 +38,6 @@ class CycleSerializer(BaseSerializer):
|
|||||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||||
backlog_issues = serializers.IntegerField(read_only=True)
|
backlog_issues = serializers.IntegerField(read_only=True)
|
||||||
assignees = serializers.SerializerMethodField(read_only=True)
|
assignees = serializers.SerializerMethodField(read_only=True)
|
||||||
labels = serializers.SerializerMethodField(read_only=True)
|
|
||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
@ -42,19 +45,24 @@ class CycleSerializer(BaseSerializer):
|
|||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
|
if (
|
||||||
|
data.get("start_date", None) is not None
|
||||||
|
and data.get("end_date", None) is not None
|
||||||
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
def get_assignees(self, obj):
|
||||||
members = [
|
members = [
|
||||||
{
|
{
|
||||||
"avatar": assignee.avatar,
|
"avatar": assignee.avatar,
|
||||||
"first_name": assignee.first_name,
|
|
||||||
"display_name": assignee.display_name,
|
"display_name": assignee.display_name,
|
||||||
"id": assignee.id,
|
"id": assignee.id,
|
||||||
}
|
}
|
||||||
for issue_cycle in obj.issue_cycle.all()
|
for issue_cycle in obj.issue_cycle.prefetch_related(
|
||||||
|
"issue__assignees"
|
||||||
|
).all()
|
||||||
for assignee in issue_cycle.issue.assignees.all()
|
for assignee in issue_cycle.issue.assignees.all()
|
||||||
]
|
]
|
||||||
# Use a set comprehension to return only the unique objects
|
# Use a set comprehension to return only the unique objects
|
||||||
@ -64,24 +72,6 @@ class CycleSerializer(BaseSerializer):
|
|||||||
unique_list = [dict(item) for item in unique_objects]
|
unique_list = [dict(item) for item in unique_objects]
|
||||||
|
|
||||||
return unique_list
|
return unique_list
|
||||||
|
|
||||||
def get_labels(self, obj):
|
|
||||||
labels = [
|
|
||||||
{
|
|
||||||
"name": label.name,
|
|
||||||
"color": label.color,
|
|
||||||
"id": label.id,
|
|
||||||
}
|
|
||||||
for issue_cycle in obj.issue_cycle.all()
|
|
||||||
for label in issue_cycle.issue.labels.all()
|
|
||||||
]
|
|
||||||
# Use a set comprehension to return only the unique objects
|
|
||||||
unique_objects = {frozenset(item.items()) for item in labels}
|
|
||||||
|
|
||||||
# Convert the set back to a list of dictionaries
|
|
||||||
unique_list = [dict(item) for item in unique_objects]
|
|
||||||
|
|
||||||
return unique_list
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
# Django imports
|
|
||||||
from django.db import IntegrityError
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
|
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
|
||||||
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
@ -94,8 +91,33 @@ class ProjectLiteSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectListSerializer(DynamicBaseSerializer):
|
||||||
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
|
total_cycles = serializers.IntegerField(read_only=True)
|
||||||
|
total_modules = serializers.IntegerField(read_only=True)
|
||||||
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
|
sort_order = serializers.FloatField(read_only=True)
|
||||||
|
member_role = serializers.IntegerField(read_only=True)
|
||||||
|
is_deployed = serializers.BooleanField(read_only=True)
|
||||||
|
members = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_members(self, obj):
|
||||||
|
project_members = ProjectMember.objects.filter(project_id=obj.id).values(
|
||||||
|
"id",
|
||||||
|
"member_id",
|
||||||
|
"member__display_name",
|
||||||
|
"member__avatar",
|
||||||
|
)
|
||||||
|
return project_members
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailSerializer(BaseSerializer):
|
class ProjectDetailSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
# workspace = WorkSpaceSerializer(read_only=True)
|
||||||
default_assignee = UserLiteSerializer(read_only=True)
|
default_assignee = UserLiteSerializer(read_only=True)
|
||||||
project_lead = UserLiteSerializer(read_only=True)
|
project_lead = UserLiteSerializer(read_only=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
@ -148,8 +170,6 @@ class ProjectIdentifierSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectFavoriteSerializer(BaseSerializer):
|
class ProjectFavoriteSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectFavorite
|
model = ProjectFavorite
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -178,12 +198,12 @@ class ProjectDeployBoardSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project", "anchor",
|
"project",
|
||||||
|
"anchor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectPublicMember
|
model = ProjectPublicMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
# Module import
|
# Module import
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import User
|
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(BaseSerializer):
|
class UserSerializer(BaseSerializer):
|
||||||
@ -33,6 +33,81 @@ class UserSerializer(BaseSerializer):
|
|||||||
return bool(obj.first_name) or bool(obj.last_name)
|
return bool(obj.first_name) or bool(obj.last_name)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMeSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"avatar",
|
||||||
|
"cover_image",
|
||||||
|
"date_joined",
|
||||||
|
"display_name",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"is_active",
|
||||||
|
"is_bot",
|
||||||
|
"is_email_verified",
|
||||||
|
"is_managed",
|
||||||
|
"is_onboarded",
|
||||||
|
"is_tour_completed",
|
||||||
|
"mobile_number",
|
||||||
|
"role",
|
||||||
|
"onboarding_step",
|
||||||
|
"user_timezone",
|
||||||
|
"username",
|
||||||
|
"theme",
|
||||||
|
"last_workspace_id",
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class UserMeSettingsSerializer(BaseSerializer):
|
||||||
|
workspace = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"email",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
def get_workspace(self, obj):
|
||||||
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=obj.email
|
||||||
|
).count()
|
||||||
|
if obj.last_workspace_id is not None:
|
||||||
|
workspace = Workspace.objects.get(
|
||||||
|
pk=obj.last_workspace_id, workspace_member__member=obj.id
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"last_workspace_id": obj.last_workspace_id,
|
||||||
|
"last_workspace_slug": workspace.slug,
|
||||||
|
"fallback_workspace_id": obj.last_workspace_id,
|
||||||
|
"fallback_workspace_slug": workspace.slug,
|
||||||
|
"invites": workspace_invites,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
fallback_workspace = (
|
||||||
|
Workspace.objects.filter(workspace_member__member_id=obj.id)
|
||||||
|
.order_by("created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"last_workspace_id": None,
|
||||||
|
"last_workspace_slug": None,
|
||||||
|
"fallback_workspace_id": fallback_workspace.id
|
||||||
|
if fallback_workspace is not None
|
||||||
|
else None,
|
||||||
|
"fallback_workspace_slug": fallback_workspace.slug
|
||||||
|
if fallback_workspace is not None
|
||||||
|
else None,
|
||||||
|
"invites": workspace_invites,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserLiteSerializer(BaseSerializer):
|
class UserLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -51,7 +126,6 @@ class UserLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UserAdminLiteSerializer(BaseSerializer):
|
class UserAdminLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -54,6 +54,13 @@ class WorkSpaceMemberSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WorkspaceMember
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
||||||
member = UserAdminLiteSerializer(read_only=True)
|
member = UserAdminLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
50
apiserver/plane/api/urls/__init__.py
Normal file
50
apiserver/plane/api/urls/__init__.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from .analytic import urlpatterns as analytic_urls
|
||||||
|
from .asset import urlpatterns as asset_urls
|
||||||
|
from .authentication import urlpatterns as authentication_urls
|
||||||
|
from .configuration import urlpatterns as configuration_urls
|
||||||
|
from .cycle import urlpatterns as cycle_urls
|
||||||
|
from .estimate import urlpatterns as estimate_urls
|
||||||
|
from .gpt import urlpatterns as gpt_urls
|
||||||
|
from .importer import urlpatterns as importer_urls
|
||||||
|
from .inbox import urlpatterns as inbox_urls
|
||||||
|
from .integration import urlpatterns as integration_urls
|
||||||
|
from .issue import urlpatterns as issue_urls
|
||||||
|
from .module import urlpatterns as module_urls
|
||||||
|
from .notification import urlpatterns as notification_urls
|
||||||
|
from .page import urlpatterns as page_urls
|
||||||
|
from .project import urlpatterns as project_urls
|
||||||
|
from .public_board import urlpatterns as public_board_urls
|
||||||
|
from .release_note import urlpatterns as release_note_urls
|
||||||
|
from .search import urlpatterns as search_urls
|
||||||
|
from .state import urlpatterns as state_urls
|
||||||
|
from .unsplash import urlpatterns as unsplash_urls
|
||||||
|
from .user import urlpatterns as user_urls
|
||||||
|
from .views import urlpatterns as view_urls
|
||||||
|
from .workspace import urlpatterns as workspace_urls
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
*analytic_urls,
|
||||||
|
*asset_urls,
|
||||||
|
*authentication_urls,
|
||||||
|
*configuration_urls,
|
||||||
|
*cycle_urls,
|
||||||
|
*estimate_urls,
|
||||||
|
*gpt_urls,
|
||||||
|
*importer_urls,
|
||||||
|
*inbox_urls,
|
||||||
|
*integration_urls,
|
||||||
|
*issue_urls,
|
||||||
|
*module_urls,
|
||||||
|
*notification_urls,
|
||||||
|
*page_urls,
|
||||||
|
*project_urls,
|
||||||
|
*public_board_urls,
|
||||||
|
*release_note_urls,
|
||||||
|
*search_urls,
|
||||||
|
*state_urls,
|
||||||
|
*unsplash_urls,
|
||||||
|
*user_urls,
|
||||||
|
*view_urls,
|
||||||
|
*workspace_urls,
|
||||||
|
]
|
46
apiserver/plane/api/urls/analytic.py
Normal file
46
apiserver/plane/api/urls/analytic.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
AnalyticsEndpoint,
|
||||||
|
AnalyticViewViewset,
|
||||||
|
SavedAnalyticEndpoint,
|
||||||
|
ExportAnalyticsEndpoint,
|
||||||
|
DefaultAnalyticsEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/analytics/",
|
||||||
|
AnalyticsEndpoint.as_view(),
|
||||||
|
name="plane-analytics",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/analytic-view/",
|
||||||
|
AnalyticViewViewset.as_view({"get": "list", "post": "create"}),
|
||||||
|
name="analytic-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/analytic-view/<uuid:pk>/",
|
||||||
|
AnalyticViewViewset.as_view(
|
||||||
|
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||||
|
),
|
||||||
|
name="analytic-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/saved-analytic-view/<uuid:analytic_id>/",
|
||||||
|
SavedAnalyticEndpoint.as_view(),
|
||||||
|
name="saved-analytic-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/export-analytics/",
|
||||||
|
ExportAnalyticsEndpoint.as_view(),
|
||||||
|
name="export-analytics",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/default-analytics/",
|
||||||
|
DefaultAnalyticsEndpoint.as_view(),
|
||||||
|
name="default-analytics",
|
||||||
|
),
|
||||||
|
]
|
31
apiserver/plane/api/urls/asset.py
Normal file
31
apiserver/plane/api/urls/asset.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
FileAssetEndpoint,
|
||||||
|
UserAssetsEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/file-assets/",
|
||||||
|
FileAssetEndpoint.as_view(),
|
||||||
|
name="file-assets",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/",
|
||||||
|
FileAssetEndpoint.as_view(),
|
||||||
|
name="file-assets",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/file-assets/",
|
||||||
|
UserAssetsEndpoint.as_view(),
|
||||||
|
name="user-file-assets",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/file-assets/<str:asset_key>/",
|
||||||
|
UserAssetsEndpoint.as_view(),
|
||||||
|
name="user-file-assets",
|
||||||
|
),
|
||||||
|
]
|
68
apiserver/plane/api/urls/authentication.py
Normal file
68
apiserver/plane/api/urls/authentication.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
# Authentication
|
||||||
|
SignUpEndpoint,
|
||||||
|
SignInEndpoint,
|
||||||
|
SignOutEndpoint,
|
||||||
|
MagicSignInEndpoint,
|
||||||
|
MagicSignInGenerateEndpoint,
|
||||||
|
OauthEndpoint,
|
||||||
|
## End Authentication
|
||||||
|
# Auth Extended
|
||||||
|
ForgotPasswordEndpoint,
|
||||||
|
VerifyEmailEndpoint,
|
||||||
|
ResetPasswordEndpoint,
|
||||||
|
RequestEmailVerificationEndpoint,
|
||||||
|
ChangePasswordEndpoint,
|
||||||
|
## End Auth Extender
|
||||||
|
# API Tokens
|
||||||
|
ApiTokenEndpoint,
|
||||||
|
## End API Tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Social Auth
|
||||||
|
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||||
|
# Auth
|
||||||
|
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||||
|
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||||
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
|
# Magic Sign In/Up
|
||||||
|
path(
|
||||||
|
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||||
|
),
|
||||||
|
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||||
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
|
# Email verification
|
||||||
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
|
path(
|
||||||
|
"request-email-verify/",
|
||||||
|
RequestEmailVerificationEndpoint.as_view(),
|
||||||
|
name="request-reset-email",
|
||||||
|
),
|
||||||
|
# Password Manipulation
|
||||||
|
path(
|
||||||
|
"users/me/change-password/",
|
||||||
|
ChangePasswordEndpoint.as_view(),
|
||||||
|
name="change-password",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"reset-password/<uidb64>/<token>/",
|
||||||
|
ResetPasswordEndpoint.as_view(),
|
||||||
|
name="password-reset",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"forgot-password/",
|
||||||
|
ForgotPasswordEndpoint.as_view(),
|
||||||
|
name="forgot-password",
|
||||||
|
),
|
||||||
|
# API Tokens
|
||||||
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
|
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
|
## End API Tokens
|
||||||
|
]
|
12
apiserver/plane/api/urls/configuration.py
Normal file
12
apiserver/plane/api/urls/configuration.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import ConfigurationEndpoint
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"configs/",
|
||||||
|
ConfigurationEndpoint.as_view(),
|
||||||
|
name="configuration",
|
||||||
|
),
|
||||||
|
]
|
87
apiserver/plane/api/urls/cycle.py
Normal file
87
apiserver/plane/api/urls/cycle.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
CycleViewSet,
|
||||||
|
CycleIssueViewSet,
|
||||||
|
CycleDateCheckEndpoint,
|
||||||
|
CycleFavoriteViewSet,
|
||||||
|
TransferCycleIssueEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||||
|
CycleViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
|
||||||
|
CycleViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
|
||||||
|
CycleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||||
|
CycleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
|
||||||
|
CycleDateCheckEndpoint.as_view(),
|
||||||
|
name="project-cycle-date",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
|
||||||
|
CycleFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
|
||||||
|
CycleFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
|
||||||
|
TransferCycleIssueEndpoint.as_view(),
|
||||||
|
name="transfer-issues",
|
||||||
|
),
|
||||||
|
]
|
37
apiserver/plane/api/urls/estimate.py
Normal file
37
apiserver/plane/api/urls/estimate.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
ProjectEstimatePointEndpoint,
|
||||||
|
BulkEstimatePointEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
|
||||||
|
ProjectEstimatePointEndpoint.as_view(),
|
||||||
|
name="project-estimate-points",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
|
BulkEstimatePointEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="bulk-create-estimate-points",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
|
||||||
|
BulkEstimatePointEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="bulk-create-estimate-points",
|
||||||
|
),
|
||||||
|
]
|
13
apiserver/plane/api/urls/gpt.py
Normal file
13
apiserver/plane/api/urls/gpt.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import GPTIntegrationEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
|
||||||
|
GPTIntegrationEndpoint.as_view(),
|
||||||
|
name="importer",
|
||||||
|
),
|
||||||
|
]
|
37
apiserver/plane/api/urls/importer.py
Normal file
37
apiserver/plane/api/urls/importer.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
ServiceIssueImportSummaryEndpoint,
|
||||||
|
ImportServiceEndpoint,
|
||||||
|
UpdateServiceImportStatusEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/importers/<str:service>/",
|
||||||
|
ServiceIssueImportSummaryEndpoint.as_view(),
|
||||||
|
name="importer-summary",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/importers/<str:service>/",
|
||||||
|
ImportServiceEndpoint.as_view(),
|
||||||
|
name="importer",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/importers/",
|
||||||
|
ImportServiceEndpoint.as_view(),
|
||||||
|
name="importer",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/importers/<str:service>/<uuid:pk>/",
|
||||||
|
ImportServiceEndpoint.as_view(),
|
||||||
|
name="importer",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/service/<str:service>/importers/<uuid:importer_id>/",
|
||||||
|
UpdateServiceImportStatusEndpoint.as_view(),
|
||||||
|
name="importer-status",
|
||||||
|
),
|
||||||
|
]
|
53
apiserver/plane/api/urls/inbox.py
Normal file
53
apiserver/plane/api/urls/inbox.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
InboxViewSet,
|
||||||
|
InboxIssueViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
||||||
|
InboxViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
|
||||||
|
InboxViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||||
|
InboxIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||||
|
InboxIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
]
|
150
apiserver/plane/api/urls/integration.py
Normal file
150
apiserver/plane/api/urls/integration.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
IntegrationViewSet,
|
||||||
|
WorkspaceIntegrationViewSet,
|
||||||
|
GithubRepositoriesEndpoint,
|
||||||
|
GithubRepositorySyncViewSet,
|
||||||
|
GithubIssueSyncViewSet,
|
||||||
|
GithubCommentSyncViewSet,
|
||||||
|
BulkCreateGithubIssueSyncEndpoint,
|
||||||
|
SlackProjectSyncViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"integrations/",
|
||||||
|
IntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"integrations/<uuid:pk>/",
|
||||||
|
IntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<str:provider>/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/provider/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
# Github Integrations
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<uuid:workspace_integration_id>/github-repositories/",
|
||||||
|
GithubRepositoriesEndpoint.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/",
|
||||||
|
GithubRepositorySyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/<uuid:pk>/",
|
||||||
|
GithubRepositorySyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/",
|
||||||
|
GithubIssueSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/bulk-create-github-issue-sync/",
|
||||||
|
BulkCreateGithubIssueSyncEndpoint.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:pk>/",
|
||||||
|
GithubIssueSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/",
|
||||||
|
GithubCommentSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/<uuid:pk>/",
|
||||||
|
GithubCommentSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
## End Github Integrations
|
||||||
|
# Slack Integration
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
|
||||||
|
SlackProjectSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
|
||||||
|
SlackProjectSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
## End Slack Integration
|
||||||
|
]
|
332
apiserver/plane/api/urls/issue.py
Normal file
332
apiserver/plane/api/urls/issue.py
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
IssueViewSet,
|
||||||
|
LabelViewSet,
|
||||||
|
BulkCreateIssueLabelsEndpoint,
|
||||||
|
BulkDeleteIssuesEndpoint,
|
||||||
|
BulkImportIssuesEndpoint,
|
||||||
|
UserWorkSpaceIssues,
|
||||||
|
SubIssuesEndpoint,
|
||||||
|
IssueLinkViewSet,
|
||||||
|
IssueAttachmentEndpoint,
|
||||||
|
ExportIssuesEndpoint,
|
||||||
|
IssueActivityEndpoint,
|
||||||
|
IssueCommentViewSet,
|
||||||
|
IssueSubscriberViewSet,
|
||||||
|
IssueReactionViewSet,
|
||||||
|
CommentReactionViewSet,
|
||||||
|
IssuePropertyViewSet,
|
||||||
|
IssueArchiveViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
|
IssueDraftViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
||||||
|
IssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
||||||
|
IssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||||
|
LabelViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-labels",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
|
||||||
|
LabelViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-labels",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
|
||||||
|
BulkCreateIssueLabelsEndpoint.as_view(),
|
||||||
|
name="project-bulk-labels",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
|
||||||
|
BulkDeleteIssuesEndpoint.as_view(),
|
||||||
|
name="project-issues-bulk",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-issues/<str:service>/",
|
||||||
|
BulkImportIssuesEndpoint.as_view(),
|
||||||
|
name="project-issues-bulk",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/my-issues/",
|
||||||
|
UserWorkSpaceIssues.as_view(),
|
||||||
|
name="workspace-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
||||||
|
SubIssuesEndpoint.as_view(),
|
||||||
|
name="sub-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
|
||||||
|
IssueLinkViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-links",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
|
||||||
|
IssueLinkViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-links",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
|
||||||
|
IssueAttachmentEndpoint.as_view(),
|
||||||
|
name="project-issue-attachments",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
|
||||||
|
IssueAttachmentEndpoint.as_view(),
|
||||||
|
name="project-issue-attachments",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/export-issues/",
|
||||||
|
ExportIssuesEndpoint.as_view(),
|
||||||
|
name="export-issues",
|
||||||
|
),
|
||||||
|
## End Issues
|
||||||
|
## Issue Activity
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/history/",
|
||||||
|
IssueActivityEndpoint.as_view(),
|
||||||
|
name="project-issue-history",
|
||||||
|
),
|
||||||
|
## Issue Activity
|
||||||
|
## IssueComments
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||||
|
IssueCommentViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||||
|
IssueCommentViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment",
|
||||||
|
),
|
||||||
|
## End IssueComments
|
||||||
|
# Issue Subscribers
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
|
||||||
|
IssueSubscriberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
|
||||||
|
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
|
||||||
|
IssueSubscriberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "subscription_status",
|
||||||
|
"post": "subscribe",
|
||||||
|
"delete": "unsubscribe",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-subscribers",
|
||||||
|
),
|
||||||
|
## End Issue Subscribers
|
||||||
|
# Issue Reactions
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||||
|
IssueReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-reactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||||
|
IssueReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-reactions",
|
||||||
|
),
|
||||||
|
## End Issue Reactions
|
||||||
|
# Comment Reactions
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||||
|
CommentReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment-reactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||||
|
CommentReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment-reactions",
|
||||||
|
),
|
||||||
|
## End Comment Reactions
|
||||||
|
## IssueProperty
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||||
|
IssuePropertyViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-roadmap",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
|
||||||
|
IssuePropertyViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-roadmap",
|
||||||
|
),
|
||||||
|
## IssueProperty Ebd
|
||||||
|
## Issue Archives
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
|
||||||
|
IssueArchiveViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-archive",
|
||||||
|
),
|
||||||
|
## End Issue Archives
|
||||||
|
## Issue Relation
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
## End Issue Relation
|
||||||
|
## Issue Drafts
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||||
|
IssueDraftViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||||
|
IssueDraftViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
]
|
104
apiserver/plane/api/urls/module.py
Normal file
104
apiserver/plane/api/urls/module.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
ModuleViewSet,
|
||||||
|
ModuleIssueViewSet,
|
||||||
|
ModuleLinkViewSet,
|
||||||
|
ModuleFavoriteViewSet,
|
||||||
|
BulkImportModulesEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
|
||||||
|
ModuleViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-modules",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
|
||||||
|
ModuleViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-modules",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
||||||
|
ModuleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-module-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||||
|
ModuleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-module-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
|
||||||
|
ModuleLinkViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-module-links",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
|
||||||
|
ModuleLinkViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-module-links",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
|
||||||
|
ModuleFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-module",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
|
||||||
|
ModuleFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-module",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-modules/<str:service>/",
|
||||||
|
BulkImportModulesEndpoint.as_view(),
|
||||||
|
name="bulk-modules-create",
|
||||||
|
),
|
||||||
|
]
|
66
apiserver/plane/api/urls/notification.py
Normal file
66
apiserver/plane/api/urls/notification.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
NotificationViewSet,
|
||||||
|
UnreadNotificationEndpoint,
|
||||||
|
MarkAllReadNotificationViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "mark_read",
|
||||||
|
"delete": "mark_unread",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
|
||||||
|
NotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "archive",
|
||||||
|
"delete": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/unread/",
|
||||||
|
UnreadNotificationEndpoint.as_view(),
|
||||||
|
name="unread-notifications",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/users/notifications/mark-all-read/",
|
||||||
|
MarkAllReadNotificationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="mark-all-read-notifications",
|
||||||
|
),
|
||||||
|
]
|
79
apiserver/plane/api/urls/page.py
Normal file
79
apiserver/plane/api/urls/page.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
PageViewSet,
|
||||||
|
PageBlockViewSet,
|
||||||
|
PageFavoriteViewSet,
|
||||||
|
CreateIssueFromPageBlockEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
|
||||||
|
PageBlockViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-blocks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
|
||||||
|
PageBlockViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-blocks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
||||||
|
PageFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
|
||||||
|
PageFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
||||||
|
CreateIssueFromPageBlockEndpoint.as_view(),
|
||||||
|
name="page-block-issues",
|
||||||
|
),
|
||||||
|
]
|
144
apiserver/plane/api/urls/project.py
Normal file
144
apiserver/plane/api/urls/project.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
ProjectViewSet,
|
||||||
|
InviteProjectEndpoint,
|
||||||
|
ProjectMemberViewSet,
|
||||||
|
ProjectMemberEndpoint,
|
||||||
|
ProjectMemberInvitationsViewset,
|
||||||
|
ProjectMemberUserEndpoint,
|
||||||
|
AddMemberToProjectEndpoint,
|
||||||
|
ProjectJoinEndpoint,
|
||||||
|
AddTeamToProjectEndpoint,
|
||||||
|
ProjectUserViewsEndpoint,
|
||||||
|
ProjectIdentifierEndpoint,
|
||||||
|
ProjectFavoritesViewSet,
|
||||||
|
LeaveProjectEndpoint,
|
||||||
|
ProjectPublicCoverImagesEndpoint
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/",
|
||||||
|
ProjectViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||||
|
ProjectViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/project-identifiers/",
|
||||||
|
ProjectIdentifierEndpoint.as_view(),
|
||||||
|
name="project-identifiers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
|
||||||
|
InviteProjectEndpoint.as_view(),
|
||||||
|
name="invite-project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||||
|
ProjectMemberViewSet.as_view({"get": "list"}),
|
||||||
|
name="project-member",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
|
||||||
|
ProjectMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
|
||||||
|
ProjectMemberEndpoint.as_view(),
|
||||||
|
name="project-member",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
|
||||||
|
AddMemberToProjectEndpoint.as_view(),
|
||||||
|
name="project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/join/",
|
||||||
|
ProjectJoinEndpoint.as_view(),
|
||||||
|
name="project-join",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
||||||
|
AddTeamToProjectEndpoint.as_view(),
|
||||||
|
name="projects",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
|
||||||
|
ProjectMemberInvitationsViewset.as_view({"get": "list"}),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
||||||
|
ProjectMemberInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
||||||
|
ProjectUserViewsEndpoint.as_view(),
|
||||||
|
name="project-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
|
||||||
|
ProjectMemberUserEndpoint.as_view(),
|
||||||
|
name="project-member-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-favorite-projects/",
|
||||||
|
ProjectFavoritesViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-favorite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
|
||||||
|
ProjectFavoritesViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-favorite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||||
|
LeaveProjectEndpoint.as_view(),
|
||||||
|
name="leave-project",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"project-covers/",
|
||||||
|
ProjectPublicCoverImagesEndpoint.as_view(),
|
||||||
|
name="project-covers",
|
||||||
|
),
|
||||||
|
]
|
151
apiserver/plane/api/urls/public_board.py
Normal file
151
apiserver/plane/api/urls/public_board.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
ProjectDeployBoardViewSet,
|
||||||
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
|
ProjectIssuesPublicEndpoint,
|
||||||
|
IssueRetrievePublicEndpoint,
|
||||||
|
IssueCommentPublicViewSet,
|
||||||
|
IssueReactionPublicViewSet,
|
||||||
|
CommentReactionPublicViewSet,
|
||||||
|
InboxIssuePublicViewSet,
|
||||||
|
IssueVotePublicViewSet,
|
||||||
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||||
|
ProjectDeployBoardViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||||
|
ProjectDeployBoardViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
|
||||||
|
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
|
||||||
|
name="project-deploy-board-settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||||
|
ProjectIssuesPublicEndpoint.as_view(),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||||
|
IssueRetrievePublicEndpoint.as_view(),
|
||||||
|
name="workspace-project-boards",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||||
|
IssueCommentPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-comments-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||||
|
IssueCommentPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-comments-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||||
|
IssueReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||||
|
IssueReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||||
|
CommentReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="comment-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||||
|
CommentReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="comment-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||||
|
InboxIssuePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||||
|
InboxIssuePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
|
||||||
|
IssueVotePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-vote-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/",
|
||||||
|
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||||
|
name="workspace-project-boards",
|
||||||
|
),
|
||||||
|
]
|
13
apiserver/plane/api/urls/release_note.py
Normal file
13
apiserver/plane/api/urls/release_note.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import ReleaseNotesEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"release-notes/",
|
||||||
|
ReleaseNotesEndpoint.as_view(),
|
||||||
|
name="release-notes",
|
||||||
|
),
|
||||||
|
]
|
21
apiserver/plane/api/urls/search.py
Normal file
21
apiserver/plane/api/urls/search.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
GlobalSearchEndpoint,
|
||||||
|
IssueSearchEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/search/",
|
||||||
|
GlobalSearchEndpoint.as_view(),
|
||||||
|
name="global-search",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
|
||||||
|
IssueSearchEndpoint.as_view(),
|
||||||
|
name="project-issue-search",
|
||||||
|
),
|
||||||
|
]
|
30
apiserver/plane/api/urls/state.py
Normal file
30
apiserver/plane/api/urls/state.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import StateViewSet
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
|
||||||
|
StateViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-states",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
|
||||||
|
StateViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-state",
|
||||||
|
),
|
||||||
|
]
|
13
apiserver/plane/api/urls/unsplash.py
Normal file
13
apiserver/plane/api/urls/unsplash.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import UnsplashEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"unsplash/",
|
||||||
|
UnsplashEndpoint.as_view(),
|
||||||
|
name="unsplash",
|
||||||
|
),
|
||||||
|
]
|
113
apiserver/plane/api/urls/user.py
Normal file
113
apiserver/plane/api/urls/user.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
## User
|
||||||
|
UserEndpoint,
|
||||||
|
UpdateUserOnBoardedEndpoint,
|
||||||
|
UpdateUserTourCompletedEndpoint,
|
||||||
|
UserActivityEndpoint,
|
||||||
|
ChangePasswordEndpoint,
|
||||||
|
## End User
|
||||||
|
## Workspaces
|
||||||
|
UserWorkspaceInvitationsEndpoint,
|
||||||
|
UserWorkSpacesEndpoint,
|
||||||
|
JoinWorkspaceEndpoint,
|
||||||
|
UserWorkspaceInvitationsEndpoint,
|
||||||
|
UserWorkspaceInvitationEndpoint,
|
||||||
|
UserActivityGraphEndpoint,
|
||||||
|
UserIssueCompletedGraphEndpoint,
|
||||||
|
UserWorkspaceDashboardEndpoint,
|
||||||
|
UserProjectInvitationsViewset,
|
||||||
|
## End Workspaces
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# User Profile
|
||||||
|
path(
|
||||||
|
"users/me/",
|
||||||
|
UserEndpoint.as_view(
|
||||||
|
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||||
|
),
|
||||||
|
name="users",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/settings/",
|
||||||
|
UserEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve_user_settings",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="users",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/change-password/",
|
||||||
|
ChangePasswordEndpoint.as_view(),
|
||||||
|
name="change-password",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/onboard/",
|
||||||
|
UpdateUserOnBoardedEndpoint.as_view(),
|
||||||
|
name="user-onboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/tour-completed/",
|
||||||
|
UpdateUserTourCompletedEndpoint.as_view(),
|
||||||
|
name="user-tour",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/workspaces/<str:slug>/activities/",
|
||||||
|
UserActivityEndpoint.as_view(),
|
||||||
|
name="user-activities",
|
||||||
|
),
|
||||||
|
# user workspaces
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/",
|
||||||
|
UserWorkSpacesEndpoint.as_view(),
|
||||||
|
name="user-workspace",
|
||||||
|
),
|
||||||
|
# user workspace invitations
|
||||||
|
path(
|
||||||
|
"users/me/invitations/workspaces/",
|
||||||
|
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
||||||
|
name="user-workspace-invitations",
|
||||||
|
),
|
||||||
|
# user workspace invitation
|
||||||
|
path(
|
||||||
|
"users/me/invitations/<uuid:pk>/",
|
||||||
|
UserWorkspaceInvitationEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-workspace-invitation",
|
||||||
|
),
|
||||||
|
# user join workspace
|
||||||
|
# User Graphs
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/<str:slug>/activity-graph/",
|
||||||
|
UserActivityGraphEndpoint.as_view(),
|
||||||
|
name="user-activity-graph",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/<str:slug>/issues-completed-graph/",
|
||||||
|
UserIssueCompletedGraphEndpoint.as_view(),
|
||||||
|
name="completed-graph",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/<str:slug>/dashboard/",
|
||||||
|
UserWorkspaceDashboardEndpoint.as_view(),
|
||||||
|
name="user-workspace-dashboard",
|
||||||
|
),
|
||||||
|
## End User Graph
|
||||||
|
path(
|
||||||
|
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
|
||||||
|
JoinWorkspaceEndpoint.as_view(),
|
||||||
|
name="user-join-workspace",
|
||||||
|
),
|
||||||
|
# user project invitations
|
||||||
|
path(
|
||||||
|
"users/me/invitations/projects/",
|
||||||
|
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
||||||
|
name="user-project-invitations",
|
||||||
|
),
|
||||||
|
]
|
85
apiserver/plane/api/urls/views.py
Normal file
85
apiserver/plane/api/urls/views.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
IssueViewViewSet,
|
||||||
|
GlobalViewViewSet,
|
||||||
|
GlobalViewIssuesViewSet,
|
||||||
|
IssueViewFavoriteViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
|
||||||
|
IssueViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
|
||||||
|
IssueViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/issues/",
|
||||||
|
GlobalViewIssuesViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||||
|
IssueViewFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
|
||||||
|
IssueViewFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-view",
|
||||||
|
),
|
||||||
|
]
|
182
apiserver/plane/api/urls/workspace.py
Normal file
182
apiserver/plane/api/urls/workspace.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
WorkSpaceViewSet,
|
||||||
|
InviteWorkspaceEndpoint,
|
||||||
|
WorkSpaceMemberViewSet,
|
||||||
|
WorkspaceMembersEndpoint,
|
||||||
|
WorkspaceInvitationsViewset,
|
||||||
|
WorkspaceMemberUserEndpoint,
|
||||||
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
|
TeamMemberViewSet,
|
||||||
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
|
WorkspaceThemeViewSet,
|
||||||
|
WorkspaceUserProfileStatsEndpoint,
|
||||||
|
WorkspaceUserActivityEndpoint,
|
||||||
|
WorkspaceUserProfileEndpoint,
|
||||||
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
|
WorkspaceLabelsEndpoint,
|
||||||
|
LeaveWorkspaceEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspace-slug-check/",
|
||||||
|
WorkSpaceAvailabilityCheckEndpoint.as_view(),
|
||||||
|
name="workspace-availability",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/",
|
||||||
|
WorkSpaceViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/",
|
||||||
|
WorkSpaceViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/invite/",
|
||||||
|
InviteWorkspaceEndpoint.as_view(),
|
||||||
|
name="invite-workspace",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/invitations/",
|
||||||
|
WorkspaceInvitationsViewset.as_view({"get": "list"}),
|
||||||
|
name="workspace-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/invitations/<uuid:pk>/",
|
||||||
|
WorkspaceInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/",
|
||||||
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
|
name="workspace-member",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||||
|
WorkSpaceMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-member",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-members/",
|
||||||
|
WorkspaceMembersEndpoint.as_view(),
|
||||||
|
name="workspace-members",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/teams/",
|
||||||
|
TeamMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-team-members",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/teams/<uuid:pk>/",
|
||||||
|
TeamMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-team-members",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/last-visited-workspace/",
|
||||||
|
UserLastProjectWithWorkspaceEndpoint.as_view(),
|
||||||
|
name="workspace-project-details",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-members/me/",
|
||||||
|
WorkspaceMemberUserEndpoint.as_view(),
|
||||||
|
name="workspace-member-details",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-views/",
|
||||||
|
WorkspaceMemberUserViewsEndpoint.as_view(),
|
||||||
|
name="workspace-member-views-details",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-themes/",
|
||||||
|
WorkspaceThemeViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-themes",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
|
||||||
|
WorkspaceThemeViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-themes",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-stats/<uuid:user_id>/",
|
||||||
|
WorkspaceUserProfileStatsEndpoint.as_view(),
|
||||||
|
name="workspace-user-stats",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-activity/<uuid:user_id>/",
|
||||||
|
WorkspaceUserActivityEndpoint.as_view(),
|
||||||
|
name="workspace-user-activity",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
|
||||||
|
WorkspaceUserProfileEndpoint.as_view(),
|
||||||
|
name="workspace-user-profile-page",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-issues/<uuid:user_id>/",
|
||||||
|
WorkspaceUserProfileIssuesEndpoint.as_view(),
|
||||||
|
name="workspace-user-profile-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/labels/",
|
||||||
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
|
name="workspace-labels",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/leave/",
|
||||||
|
LeaveWorkspaceEndpoint.as_view(),
|
||||||
|
name="leave-workspace-members",
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
|
|
||||||
# Create your urls here.
|
# Create your urls here.
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ from plane.api.views import (
|
|||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
LeaveProjectEndpoint,
|
LeaveProjectEndpoint,
|
||||||
|
ProjectPublicCoverImagesEndpoint,
|
||||||
## End Projects
|
## End Projects
|
||||||
# Issues
|
# Issues
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
@ -105,7 +107,6 @@ from plane.api.views import (
|
|||||||
GlobalViewViewSet,
|
GlobalViewViewSet,
|
||||||
GlobalViewIssuesViewSet,
|
GlobalViewIssuesViewSet,
|
||||||
IssueViewViewSet,
|
IssueViewViewSet,
|
||||||
ViewIssuesEndpoint,
|
|
||||||
IssueViewFavoriteViewSet,
|
IssueViewFavoriteViewSet,
|
||||||
## End Views
|
## End Views
|
||||||
# Cycles
|
# Cycles
|
||||||
@ -150,12 +151,11 @@ from plane.api.views import (
|
|||||||
GlobalSearchEndpoint,
|
GlobalSearchEndpoint,
|
||||||
IssueSearchEndpoint,
|
IssueSearchEndpoint,
|
||||||
## End Search
|
## End Search
|
||||||
# Gpt
|
# External
|
||||||
GPTIntegrationEndpoint,
|
GPTIntegrationEndpoint,
|
||||||
## End Gpt
|
|
||||||
# Release Notes
|
|
||||||
ReleaseNotesEndpoint,
|
ReleaseNotesEndpoint,
|
||||||
## End Release Notes
|
UnsplashEndpoint,
|
||||||
|
## End External
|
||||||
# Inbox
|
# Inbox
|
||||||
InboxViewSet,
|
InboxViewSet,
|
||||||
InboxIssueViewSet,
|
InboxIssueViewSet,
|
||||||
@ -186,9 +186,15 @@ from plane.api.views import (
|
|||||||
## Exporter
|
## Exporter
|
||||||
ExportIssuesEndpoint,
|
ExportIssuesEndpoint,
|
||||||
## End Exporter
|
## End Exporter
|
||||||
|
# Configuration
|
||||||
|
ConfigurationEndpoint,
|
||||||
|
## End Configuration
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#TODO: Delete this file
|
||||||
|
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Social Auth
|
# Social Auth
|
||||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||||
@ -201,6 +207,7 @@ urlpatterns = [
|
|||||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||||
),
|
),
|
||||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||||
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
# Email verification
|
# Email verification
|
||||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
path(
|
path(
|
||||||
@ -227,6 +234,15 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="users",
|
name="users",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/me/settings/",
|
||||||
|
UserEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve_user_settings",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="users",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"users/me/change-password/",
|
"users/me/change-password/",
|
||||||
ChangePasswordEndpoint.as_view(),
|
ChangePasswordEndpoint.as_view(),
|
||||||
@ -554,6 +570,7 @@ urlpatterns = [
|
|||||||
"workspaces/<str:slug>/user-favorite-projects/",
|
"workspaces/<str:slug>/user-favorite-projects/",
|
||||||
ProjectFavoritesViewSet.as_view(
|
ProjectFavoritesViewSet.as_view(
|
||||||
{
|
{
|
||||||
|
"get": "list",
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -573,6 +590,11 @@ urlpatterns = [
|
|||||||
LeaveProjectEndpoint.as_view(),
|
LeaveProjectEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"project-covers/",
|
||||||
|
ProjectPublicCoverImagesEndpoint.as_view(),
|
||||||
|
name="project-covers",
|
||||||
|
),
|
||||||
# End Projects
|
# End Projects
|
||||||
# States
|
# States
|
||||||
path(
|
path(
|
||||||
@ -649,11 +671,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-view",
|
name="project-view",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/issues/",
|
|
||||||
ViewIssuesEndpoint.as_view(),
|
|
||||||
name="project-view-issues",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/views/",
|
"workspaces/<str:slug>/views/",
|
||||||
GlobalViewViewSet.as_view(
|
GlobalViewViewSet.as_view(
|
||||||
@ -1446,20 +1463,23 @@ urlpatterns = [
|
|||||||
name="project-issue-search",
|
name="project-issue-search",
|
||||||
),
|
),
|
||||||
## End Search
|
## End Search
|
||||||
# Gpt
|
# External
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
|
||||||
GPTIntegrationEndpoint.as_view(),
|
GPTIntegrationEndpoint.as_view(),
|
||||||
name="importer",
|
name="importer",
|
||||||
),
|
),
|
||||||
## End Gpt
|
|
||||||
# Release Notes
|
|
||||||
path(
|
path(
|
||||||
"release-notes/",
|
"release-notes/",
|
||||||
ReleaseNotesEndpoint.as_view(),
|
ReleaseNotesEndpoint.as_view(),
|
||||||
name="release-notes",
|
name="release-notes",
|
||||||
),
|
),
|
||||||
## End Release Notes
|
path(
|
||||||
|
"unsplash/",
|
||||||
|
UnsplashEndpoint.as_view(),
|
||||||
|
name="release-notes",
|
||||||
|
),
|
||||||
|
## End External
|
||||||
# Inbox
|
# Inbox
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
||||||
@ -1728,4 +1748,11 @@ urlpatterns = [
|
|||||||
name="workspace-project-boards",
|
name="workspace-project-boards",
|
||||||
),
|
),
|
||||||
## End Public Boards
|
## End Public Boards
|
||||||
|
# Configuration
|
||||||
|
path(
|
||||||
|
"configs/",
|
||||||
|
ConfigurationEndpoint.as_view(),
|
||||||
|
name="configuration",
|
||||||
|
),
|
||||||
|
## End Configuration
|
||||||
]
|
]
|
@ -17,6 +17,7 @@ from .project import (
|
|||||||
ProjectMemberEndpoint,
|
ProjectMemberEndpoint,
|
||||||
WorkspaceProjectDeployBoardEndpoint,
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
LeaveProjectEndpoint,
|
LeaveProjectEndpoint,
|
||||||
|
ProjectPublicCoverImagesEndpoint,
|
||||||
)
|
)
|
||||||
from .user import (
|
from .user import (
|
||||||
UserEndpoint,
|
UserEndpoint,
|
||||||
@ -56,7 +57,7 @@ from .workspace import (
|
|||||||
LeaveWorkspaceEndpoint,
|
LeaveWorkspaceEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
@ -147,16 +148,13 @@ from .page import (
|
|||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .gpt import GPTIntegrationEndpoint
|
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .release import ReleaseNotesEndpoint
|
|
||||||
|
|
||||||
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
|
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
|
||||||
|
|
||||||
from .analytic import (
|
from .analytic import (
|
||||||
@ -169,4 +167,6 @@ from .analytic import (
|
|||||||
|
|
||||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||||
|
|
||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter import ExportIssuesEndpoint
|
||||||
|
|
||||||
|
from .config import ConfigurationEndpoint
|
@ -1,10 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import (
|
from django.db.models import Count, Sum, F, Q
|
||||||
Count,
|
|
||||||
Sum,
|
|
||||||
F,
|
|
||||||
Q
|
|
||||||
)
|
|
||||||
from django.db.models.functions import ExtractMonth
|
from django.db.models.functions import ExtractMonth
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
@ -28,82 +23,156 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
x_axis = request.GET.get("x_axis", False)
|
||||||
x_axis = request.GET.get("x_axis", False)
|
y_axis = request.GET.get("y_axis", False)
|
||||||
y_axis = request.GET.get("y_axis", False)
|
segment = request.GET.get("segment", False)
|
||||||
|
|
||||||
if not x_axis or not y_axis:
|
valid_xaxis_segment = [
|
||||||
return Response(
|
"state_id",
|
||||||
{"error": "x-axis and y-axis dimensions are required"},
|
"state__group",
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
"labels__id",
|
||||||
)
|
"assignees__id",
|
||||||
|
"estimate_point",
|
||||||
segment = request.GET.get("segment", False)
|
"issue_cycle__cycle_id",
|
||||||
filters = issue_filters(request.GET, "GET")
|
"issue_module__module_id",
|
||||||
|
"priority",
|
||||||
queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
"start_date",
|
||||||
|
"target_date",
|
||||||
total_issues = queryset.count()
|
"created_at",
|
||||||
distribution = build_graph_plot(
|
"completed_at",
|
||||||
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
]
|
||||||
)
|
|
||||||
|
|
||||||
colors = dict()
|
|
||||||
if x_axis in ["state__name", "state__group"] or segment in [
|
|
||||||
"state__name",
|
|
||||||
"state__group",
|
|
||||||
]:
|
|
||||||
if x_axis in ["state__name", "state__group"]:
|
|
||||||
key = "name" if x_axis == "state__name" else "group"
|
|
||||||
else:
|
|
||||||
key = "name" if segment == "state__name" else "group"
|
|
||||||
|
|
||||||
colors = (
|
|
||||||
State.objects.filter(
|
|
||||||
~Q(name="Triage"),
|
|
||||||
workspace__slug=slug, project_id__in=filters.get("project__in")
|
|
||||||
).values(key, "color")
|
|
||||||
if filters.get("project__in", False)
|
|
||||||
else State.objects.filter(~Q(name="Triage"), workspace__slug=slug).values(key, "color")
|
|
||||||
)
|
|
||||||
|
|
||||||
if x_axis in ["labels__name"] or segment in ["labels__name"]:
|
|
||||||
colors = (
|
|
||||||
Label.objects.filter(
|
|
||||||
workspace__slug=slug, project_id__in=filters.get("project__in")
|
|
||||||
).values("name", "color")
|
|
||||||
if filters.get("project__in", False)
|
|
||||||
else Label.objects.filter(workspace__slug=slug).values(
|
|
||||||
"name", "color"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assignee_details = {}
|
|
||||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
|
||||||
assignee_details = (
|
|
||||||
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
|
|
||||||
.order_by("assignees__id")
|
|
||||||
.distinct("assignees__id")
|
|
||||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
valid_yaxis = [
|
||||||
|
"issue_count",
|
||||||
|
"estimate",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for x-axis and y-axis as thery are required parameters
|
||||||
|
if (
|
||||||
|
not x_axis
|
||||||
|
or not y_axis
|
||||||
|
or not x_axis in valid_xaxis_segment
|
||||||
|
or not y_axis in valid_yaxis
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"total": total_issues,
|
"error": "x-axis and y-axis dimensions are required and the values should be valid"
|
||||||
"distribution": distribution,
|
|
||||||
"extras": {"colors": colors, "assignee_details": assignee_details},
|
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If segment is present it cannot be same as x-axis
|
||||||
|
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Additional filters that need to be applied
|
||||||
|
filters = issue_filters(request.GET, "GET")
|
||||||
|
|
||||||
|
# Get the issues for the workspace with the additional filters applied
|
||||||
|
queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
||||||
|
|
||||||
|
# Get the total issue count
|
||||||
|
total_issues = queryset.count()
|
||||||
|
|
||||||
|
# Build the graph payload
|
||||||
|
distribution = build_graph_plot(
|
||||||
|
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
||||||
|
)
|
||||||
|
|
||||||
|
state_details = {}
|
||||||
|
if x_axis in ["state_id"] or segment in ["state_id"]:
|
||||||
|
state_details = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
)
|
||||||
|
.distinct("state_id")
|
||||||
|
.order_by("state_id")
|
||||||
|
.values("state_id", "state__name", "state__color")
|
||||||
|
)
|
||||||
|
|
||||||
|
label_details = {}
|
||||||
|
if x_axis in ["labels__id"] or segment in ["labels__id"]:
|
||||||
|
label_details = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
workspace__slug=slug, **filters, labels__id__isnull=False
|
||||||
|
)
|
||||||
|
.distinct("labels__id")
|
||||||
|
.order_by("labels__id")
|
||||||
|
.values("labels__id", "labels__color", "labels__name")
|
||||||
|
)
|
||||||
|
|
||||||
|
assignee_details = {}
|
||||||
|
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||||
|
assignee_details = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
||||||
|
)
|
||||||
|
.order_by("assignees__id")
|
||||||
|
.distinct("assignees__id")
|
||||||
|
.values(
|
||||||
|
"assignees__avatar",
|
||||||
|
"assignees__display_name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"assignees__last_name",
|
||||||
|
"assignees__id",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle_details = {}
|
||||||
|
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
|
||||||
|
cycle_details = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
issue_cycle__cycle_id__isnull=False,
|
||||||
|
)
|
||||||
|
.distinct("issue_cycle__cycle_id")
|
||||||
|
.order_by("issue_cycle__cycle_id")
|
||||||
|
.values(
|
||||||
|
"issue_cycle__cycle_id",
|
||||||
|
"issue_cycle__cycle__name",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module_details = {}
|
||||||
|
if x_axis in ["issue_module__module_id"] or segment in [
|
||||||
|
"issue_module__module_id"
|
||||||
|
]:
|
||||||
|
module_details = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
issue_module__module_id__isnull=False,
|
||||||
|
)
|
||||||
|
.distinct("issue_module__module_id")
|
||||||
|
.order_by("issue_module__module_id")
|
||||||
|
.values(
|
||||||
|
"issue_module__module_id",
|
||||||
|
"issue_module__module__name",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"total": total_issues,
|
||||||
|
"distribution": distribution,
|
||||||
|
"extras": {
|
||||||
|
"state_details": state_details,
|
||||||
|
"assignee_details": assignee_details,
|
||||||
|
"label_details": label_details,
|
||||||
|
"cycle_details": cycle_details,
|
||||||
|
"module_details": module_details,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AnalyticViewViewset(BaseViewSet):
|
class AnalyticViewViewset(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -128,45 +197,30 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, analytic_id):
|
def get(self, request, slug, analytic_id):
|
||||||
try:
|
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
|
||||||
analytic_view = AnalyticView.objects.get(
|
|
||||||
pk=analytic_id, workspace__slug=slug
|
|
||||||
)
|
|
||||||
|
|
||||||
filter = analytic_view.query
|
filter = analytic_view.query
|
||||||
queryset = Issue.issue_objects.filter(**filter)
|
queryset = Issue.issue_objects.filter(**filter)
|
||||||
|
|
||||||
x_axis = analytic_view.query_dict.get("x_axis", False)
|
x_axis = analytic_view.query_dict.get("x_axis", False)
|
||||||
y_axis = analytic_view.query_dict.get("y_axis", False)
|
y_axis = analytic_view.query_dict.get("y_axis", False)
|
||||||
|
|
||||||
if not x_axis or not y_axis:
|
if not x_axis or not y_axis:
|
||||||
return Response(
|
|
||||||
{"error": "x-axis and y-axis dimensions are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
segment = request.GET.get("segment", False)
|
|
||||||
distribution = build_graph_plot(
|
|
||||||
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
|
||||||
)
|
|
||||||
total_issues = queryset.count()
|
|
||||||
return Response(
|
return Response(
|
||||||
{"total": total_issues, "distribution": distribution},
|
{"error": "x-axis and y-axis dimensions are required"},
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except AnalyticView.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Analytic View Does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
segment = request.GET.get("segment", False)
|
||||||
|
distribution = build_graph_plot(
|
||||||
|
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
||||||
|
)
|
||||||
|
total_issues = queryset.count()
|
||||||
|
return Response(
|
||||||
|
{"total": total_issues, "distribution": distribution},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportAnalyticsEndpoint(BaseAPIView):
|
class ExportAnalyticsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -174,33 +228,64 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
try:
|
x_axis = request.data.get("x_axis", False)
|
||||||
x_axis = request.data.get("x_axis", False)
|
y_axis = request.data.get("y_axis", False)
|
||||||
y_axis = request.data.get("y_axis", False)
|
segment = request.data.get("segment", False)
|
||||||
|
|
||||||
if not x_axis or not y_axis:
|
valid_xaxis_segment = [
|
||||||
return Response(
|
"state_id",
|
||||||
{"error": "x-axis and y-axis dimensions are required"},
|
"state__group",
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
"labels__id",
|
||||||
)
|
"assignees__id",
|
||||||
|
"estimate_point",
|
||||||
|
"issue_cycle__cycle_id",
|
||||||
|
"issue_module__module_id",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"created_at",
|
||||||
|
"completed_at",
|
||||||
|
]
|
||||||
|
|
||||||
analytic_export_task.delay(
|
valid_yaxis = [
|
||||||
email=request.user.email, data=request.data, slug=slug
|
"issue_count",
|
||||||
)
|
"estimate",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for x-axis and y-axis as thery are required parameters
|
||||||
|
if (
|
||||||
|
not x_axis
|
||||||
|
or not y_axis
|
||||||
|
or not x_axis in valid_xaxis_segment
|
||||||
|
or not y_axis in valid_yaxis
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
|
"error": "x-axis and y-axis dimensions are required and the values should be valid"
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If segment is present it cannot be same as x-axis
|
||||||
|
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
analytic_export_task.delay(
|
||||||
|
email=request.user.email, data=request.data, slug=slug
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DefaultAnalyticsEndpoint(BaseAPIView):
|
class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -208,90 +293,92 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
filters = issue_filters(request.GET, "GET")
|
||||||
filters = issue_filters(request.GET, "GET")
|
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
||||||
|
|
||||||
queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
total_issues = base_issues.count()
|
||||||
|
|
||||||
total_issues = queryset.count()
|
state_groups = base_issues.annotate(state_group=F("state__group"))
|
||||||
|
|
||||||
total_issues_classified = (
|
total_issues_classified = (
|
||||||
queryset.annotate(state_group=F("state__group"))
|
state_groups.values("state_group")
|
||||||
.values("state_group")
|
.annotate(state_count=Count("state_group"))
|
||||||
.annotate(state_count=Count("state_group"))
|
.order_by("state_group")
|
||||||
.order_by("state_group")
|
)
|
||||||
)
|
|
||||||
|
|
||||||
open_issues = queryset.filter(
|
open_issues_groups = ["backlog", "unstarted", "started"]
|
||||||
state__group__in=["backlog", "unstarted", "started"]
|
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
|
||||||
).count()
|
|
||||||
|
|
||||||
open_issues_classified = (
|
open_issues = open_issues_queryset.count()
|
||||||
queryset.filter(state__group__in=["backlog", "unstarted", "started"])
|
open_issues_classified = (
|
||||||
.annotate(state_group=F("state__group"))
|
open_issues_queryset.values("state_group")
|
||||||
.values("state_group")
|
.annotate(state_count=Count("state_group"))
|
||||||
.annotate(state_count=Count("state_group"))
|
.order_by("state_group")
|
||||||
.order_by("state_group")
|
)
|
||||||
)
|
|
||||||
|
|
||||||
issue_completed_month_wise = (
|
issue_completed_month_wise = (
|
||||||
queryset.filter(completed_at__isnull=False)
|
base_issues.filter(completed_at__isnull=False)
|
||||||
.annotate(month=ExtractMonth("completed_at"))
|
.annotate(month=ExtractMonth("completed_at"))
|
||||||
.values("month")
|
.values("month")
|
||||||
.annotate(count=Count("*"))
|
.annotate(count=Count("*"))
|
||||||
.order_by("month")
|
.order_by("month")
|
||||||
)
|
)
|
||||||
most_issue_created_user = (
|
|
||||||
queryset.exclude(created_by=None)
|
|
||||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id")
|
|
||||||
.annotate(count=Count("id"))
|
|
||||||
.order_by("-count")
|
|
||||||
)[:5]
|
|
||||||
|
|
||||||
most_issue_closed_user = (
|
user_details = [
|
||||||
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
|
"created_by__first_name",
|
||||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
"created_by__last_name",
|
||||||
.annotate(count=Count("id"))
|
"created_by__avatar",
|
||||||
.order_by("-count")
|
"created_by__display_name",
|
||||||
)[:5]
|
"created_by__id",
|
||||||
|
]
|
||||||
|
|
||||||
pending_issue_user = (
|
most_issue_created_user = (
|
||||||
queryset.filter(completed_at__isnull=True)
|
base_issues.exclude(created_by=None)
|
||||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
.values(*user_details)
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
.order_by("-count")
|
.order_by("-count")[:5]
|
||||||
)
|
)
|
||||||
|
|
||||||
open_estimate_sum = (
|
user_assignee_details = [
|
||||||
queryset.filter(
|
"assignees__first_name",
|
||||||
state__group__in=["backlog", "unstarted", "started"]
|
"assignees__last_name",
|
||||||
).aggregate(open_estimate_sum=Sum("estimate_point"))
|
"assignees__avatar",
|
||||||
)["open_estimate_sum"]
|
"assignees__display_name",
|
||||||
print(open_estimate_sum)
|
"assignees__id",
|
||||||
|
]
|
||||||
total_estimate_sum = queryset.aggregate(
|
|
||||||
total_estimate_sum=Sum("estimate_point")
|
|
||||||
)["total_estimate_sum"]
|
|
||||||
|
|
||||||
return Response(
|
most_issue_closed_user = (
|
||||||
{
|
base_issues.filter(completed_at__isnull=False)
|
||||||
"total_issues": total_issues,
|
.exclude(assignees=None)
|
||||||
"total_issues_classified": total_issues_classified,
|
.values(*user_assignee_details)
|
||||||
"open_issues": open_issues,
|
.annotate(count=Count("id"))
|
||||||
"open_issues_classified": open_issues_classified,
|
.order_by("-count")[:5]
|
||||||
"issue_completed_month_wise": issue_completed_month_wise,
|
)
|
||||||
"most_issue_created_user": most_issue_created_user,
|
|
||||||
"most_issue_closed_user": most_issue_closed_user,
|
|
||||||
"pending_issue_user": pending_issue_user,
|
|
||||||
"open_estimate_sum": open_estimate_sum,
|
|
||||||
"total_estimate_sum": total_estimate_sum,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
pending_issue_user = (
|
||||||
capture_exception(e)
|
base_issues.filter(completed_at__isnull=True)
|
||||||
return Response(
|
.values(*user_assignee_details)
|
||||||
{"error": "Something went wrong please try again later"},
|
.annotate(count=Count("id"))
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
|
||||||
|
"sum"
|
||||||
|
]
|
||||||
|
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"total_issues": total_issues,
|
||||||
|
"total_issues_classified": total_issues_classified,
|
||||||
|
"open_issues": open_issues,
|
||||||
|
"open_issues_classified": open_issues_classified,
|
||||||
|
"issue_completed_month_wise": issue_completed_month_wise,
|
||||||
|
"most_issue_created_user": most_issue_created_user,
|
||||||
|
"most_issue_closed_user": most_issue_closed_user,
|
||||||
|
"pending_issue_user": pending_issue_user,
|
||||||
|
"open_estimate_sum": open_estimate_sum,
|
||||||
|
"total_estimate_sum": total_estimate_sum,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
@ -14,57 +14,34 @@ from plane.api.serializers import APITokenSerializer
|
|||||||
|
|
||||||
class ApiTokenEndpoint(BaseAPIView):
|
class ApiTokenEndpoint(BaseAPIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
label = request.data.get("label", str(uuid4().hex))
|
||||||
label = request.data.get("label", str(uuid4().hex))
|
workspace = request.data.get("workspace", False)
|
||||||
workspace = request.data.get("workspace", False)
|
|
||||||
|
|
||||||
if not workspace:
|
if not workspace:
|
||||||
return Response(
|
|
||||||
{"error": "Workspace is required"}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
api_token = APIToken.objects.create(
|
|
||||||
label=label, user=request.user, workspace_id=workspace
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = APITokenSerializer(api_token)
|
|
||||||
# Token will be only vissible while creating
|
|
||||||
return Response(
|
return Response(
|
||||||
{"api_token": serializer.data, "token": api_token.token},
|
{"error": "Workspace is required"}, status=status.HTTP_200_OK
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
api_token = APIToken.objects.create(
|
||||||
capture_exception(e)
|
label=label, user=request.user, workspace_id=workspace
|
||||||
return Response(
|
)
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
serializer = APITokenSerializer(api_token)
|
||||||
)
|
# Token will be only vissible while creating
|
||||||
|
return Response(
|
||||||
|
{"api_token": serializer.data, "token": api_token.token},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
try:
|
api_tokens = APIToken.objects.filter(user=request.user)
|
||||||
api_tokens = APIToken.objects.filter(user=request.user)
|
serializer = APITokenSerializer(api_tokens, many=True)
|
||||||
serializer = APITokenSerializer(api_tokens, many=True)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, request, pk):
|
def delete(self, request, pk):
|
||||||
try:
|
api_token = APIToken.objects.get(pk=pk)
|
||||||
api_token = APIToken.objects.get(pk=pk)
|
api_token.delete()
|
||||||
api_token.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except APIToken.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -18,108 +18,58 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, workspace_id, asset_key):
|
def get(self, request, workspace_id, asset_key):
|
||||||
try:
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
files = FileAsset.objects.filter(asset=asset_key)
|
||||||
files = FileAsset.objects.filter(asset=asset_key)
|
if files.exists():
|
||||||
if files.exists():
|
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
else:
|
||||||
else:
|
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
try:
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
# Get the workspace
|
||||||
# Get the workspace
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
serializer.save(workspace_id=workspace.id)
|
||||||
serializer.save(workspace_id=workspace.id)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Workspace.DoesNotExist:
|
|
||||||
return Response({"error": "Workspace does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, request, workspace_id, asset_key):
|
def delete(self, request, workspace_id, asset_key):
|
||||||
try:
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
# Delete the file from storage
|
||||||
# Delete the file from storage
|
file_asset.asset.delete(save=False)
|
||||||
file_asset.asset.delete(save=False)
|
# Delete the file object
|
||||||
# Delete the file object
|
file_asset.delete()
|
||||||
file_asset.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except FileAsset.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserAssetsEndpoint(BaseAPIView):
|
class UserAssetsEndpoint(BaseAPIView):
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
def get(self, request, asset_key):
|
def get(self, request, asset_key):
|
||||||
try:
|
|
||||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(files, context={"request": request})
|
serializer = FileAssetSerializer(files, context={"request": request})
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, request, asset_key):
|
def delete(self, request, asset_key):
|
||||||
try:
|
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
||||||
# Delete the file from storage
|
# Delete the file from storage
|
||||||
file_asset.asset.delete(save=False)
|
file_asset.asset.delete(save=False)
|
||||||
# Delete the file object
|
# Delete the file object
|
||||||
file_asset.delete()
|
file_asset.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
except FileAsset.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -9,7 +9,6 @@ from django.utils.encoding import (
|
|||||||
DjangoUnicodeDecodeError,
|
DjangoUnicodeDecodeError,
|
||||||
)
|
)
|
||||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
## Third Party Imports
|
## Third Party Imports
|
||||||
@ -128,32 +127,25 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
class ChangePasswordEndpoint(BaseAPIView):
|
class ChangePasswordEndpoint(BaseAPIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
serializer = ChangePasswordSerializer(data=request.data)
|
||||||
serializer = ChangePasswordSerializer(data=request.data)
|
|
||||||
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
# Check old password
|
# Check old password
|
||||||
if not user.object.check_password(serializer.data.get("old_password")):
|
if not user.object.check_password(serializer.data.get("old_password")):
|
||||||
return Response(
|
return Response(
|
||||||
{"old_password": ["Wrong password."]},
|
{"old_password": ["Wrong password."]},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# set_password also hashes the password that the user will get
|
# set_password also hashes the password that the user will get
|
||||||
self.object.set_password(serializer.data.get("new_password"))
|
self.object.set_password(serializer.data.get("new_password"))
|
||||||
self.object.save()
|
self.object.save()
|
||||||
response = {
|
response = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"code": status.HTTP_200_OK,
|
"code": status.HTTP_200_OK,
|
||||||
"message": "Password updated successfully",
|
"message": "Password updated successfully",
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -40,229 +40,194 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
if not settings.ENABLE_SIGNUP:
|
||||||
if not settings.ENABLE_SIGNUP:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "New account creation is disabled. Please contact your site administrator"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
|
||||||
password = request.data.get("password", False)
|
|
||||||
|
|
||||||
## Raise exception if any of the above are missing
|
|
||||||
if not email or not password:
|
|
||||||
return Response(
|
|
||||||
{"error": "Both email and password are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
email = email.strip().lower()
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_email(email)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Please provide a valid email address."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if the user already exists
|
|
||||||
if User.objects.filter(email=email).exists():
|
|
||||||
return Response(
|
|
||||||
{"error": "User with this email already exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User.objects.create(email=email, username=uuid.uuid4().hex)
|
|
||||||
user.set_password(password)
|
|
||||||
|
|
||||||
# settings last actives for the user
|
|
||||||
user.last_active = timezone.now()
|
|
||||||
user.last_login_time = timezone.now()
|
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
|
||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
|
||||||
user.token_updated_at = timezone.now()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
serialized_user = UserSerializer(user).data
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
"user": serialized_user,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send Analytics
|
|
||||||
if settings.ANALYTICS_BASE_API:
|
|
||||||
_ = requests.post(
|
|
||||||
settings.ANALYTICS_BASE_API,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "email",
|
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_UP",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{
|
||||||
|
"error": "New account creation is disabled. Please contact your site administrator"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
email = request.data.get("email", False)
|
||||||
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
|
## Raise exception if any of the above are missing
|
||||||
|
if not email or not password:
|
||||||
|
return Response(
|
||||||
|
{"error": "Both email and password are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = email.strip().lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Please provide a valid email address."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user already exists
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
return Response(
|
||||||
|
{"error": "User with this email already exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.create(email=email, username=uuid.uuid4().hex)
|
||||||
|
user.set_password(password)
|
||||||
|
|
||||||
|
# settings last actives for the user
|
||||||
|
user.last_active = timezone.now()
|
||||||
|
user.last_login_time = timezone.now()
|
||||||
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
|
user.token_updated_at = timezone.now()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Analytics
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SignInEndpoint(BaseAPIView):
|
class SignInEndpoint(BaseAPIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
email = request.data.get("email", False)
|
||||||
email = request.data.get("email", False)
|
password = request.data.get("password", False)
|
||||||
password = request.data.get("password", False)
|
|
||||||
|
|
||||||
## Raise exception if any of the above are missing
|
## Raise exception if any of the above are missing
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
return Response(
|
|
||||||
{"error": "Both email and password are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
email = email.strip().lower()
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_email(email)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Please provide a valid email address."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User.objects.filter(email=email).first()
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
|
||||||
},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sign up Process
|
|
||||||
if not user.check_password(password):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
|
||||||
},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
if not user.is_active:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
|
||||||
},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
serialized_user = UserSerializer(user).data
|
|
||||||
|
|
||||||
# settings last active for the user
|
|
||||||
user.last_active = timezone.now()
|
|
||||||
user.last_login_time = timezone.now()
|
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
|
||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
|
||||||
user.token_updated_at = timezone.now()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
|
||||||
# Send Analytics
|
|
||||||
if settings.ANALYTICS_BASE_API:
|
|
||||||
_ = requests.post(
|
|
||||||
settings.ANALYTICS_BASE_API,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "email",
|
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_IN",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
data = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
"user": serialized_user,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Both email and password are required"},
|
||||||
"error": "Something went wrong. Please try again later or contact the support team."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
email = email.strip().lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Please provide a valid email address."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sign up Process
|
||||||
|
if not user.check_password(password):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if not user.is_active:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# settings last active for the user
|
||||||
|
user.last_active = timezone.now()
|
||||||
|
user.last_login_time = timezone.now()
|
||||||
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
|
user.token_updated_at = timezone.now()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
# Send Analytics
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SignOutEndpoint(BaseAPIView):
|
class SignOutEndpoint(BaseAPIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
refresh_token = request.data.get("refresh_token", False)
|
||||||
refresh_token = request.data.get("refresh_token", False)
|
|
||||||
|
|
||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
capture_message("No refresh token provided")
|
capture_message("No refresh token provided")
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Something went wrong. Please try again later or contact the support team."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
|
||||||
|
|
||||||
user.last_logout_time = timezone.now()
|
|
||||||
user.last_logout_ip = request.META.get("REMOTE_ADDR")
|
|
||||||
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
token = RefreshToken(refresh_token)
|
|
||||||
token.blacklist()
|
|
||||||
return Response({"message": "success"}, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "No refresh token provided"},
|
||||||
"error": "Something went wrong. Please try again later or contact the support team."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user = User.objects.get(pk=request.user.id)
|
||||||
|
|
||||||
|
user.last_logout_time = timezone.now()
|
||||||
|
user.last_logout_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
token.blacklist()
|
||||||
|
return Response({"message": "success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class MagicSignInGenerateEndpoint(BaseAPIView):
|
class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -270,74 +235,62 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
email = request.data.get("email", False)
|
||||||
email = request.data.get("email", False)
|
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
|
return Response(
|
||||||
|
{"error": "Please provide a valid email address"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
email = email.strip().lower()
|
||||||
|
validate_email(email)
|
||||||
|
|
||||||
|
## Generate a random token
|
||||||
|
token = (
|
||||||
|
"".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||||||
|
+ "-"
|
||||||
|
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||||||
|
+ "-"
|
||||||
|
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||||||
|
)
|
||||||
|
|
||||||
|
ri = redis_instance()
|
||||||
|
|
||||||
|
key = "magic_" + str(email)
|
||||||
|
|
||||||
|
# Check if the key already exists in python
|
||||||
|
if ri.exists(key):
|
||||||
|
data = json.loads(ri.get(key))
|
||||||
|
|
||||||
|
current_attempt = data["current_attempt"] + 1
|
||||||
|
|
||||||
|
if data["current_attempt"] > 2:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Please provide a valid email address"},
|
{"error": "Max attempts exhausted. Please try again later."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clean up
|
value = {
|
||||||
email = email.strip().lower()
|
"current_attempt": current_attempt,
|
||||||
validate_email(email)
|
"email": email,
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
expiry = 600
|
||||||
|
|
||||||
## Generate a random token
|
ri.set(key, json.dumps(value), ex=expiry)
|
||||||
token = (
|
|
||||||
"".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
|
||||||
+ "-"
|
|
||||||
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
|
||||||
+ "-"
|
|
||||||
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
|
||||||
)
|
|
||||||
|
|
||||||
ri = redis_instance()
|
else:
|
||||||
|
value = {"current_attempt": 0, "email": email, "token": token}
|
||||||
|
expiry = 600
|
||||||
|
|
||||||
key = "magic_" + str(email)
|
ri.set(key, json.dumps(value), ex=expiry)
|
||||||
|
|
||||||
# Check if the key already exists in python
|
current_site = settings.WEB_URL
|
||||||
if ri.exists(key):
|
magic_link.delay(email, key, token, current_site)
|
||||||
data = json.loads(ri.get(key))
|
|
||||||
|
|
||||||
current_attempt = data["current_attempt"] + 1
|
return Response({"key": key}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
if data["current_attempt"] > 2:
|
|
||||||
return Response(
|
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
value = {
|
|
||||||
"current_attempt": current_attempt,
|
|
||||||
"email": email,
|
|
||||||
"token": token,
|
|
||||||
}
|
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
|
||||||
|
|
||||||
else:
|
|
||||||
value = {"current_attempt": 0, "email": email, "token": token}
|
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
|
||||||
|
|
||||||
current_site = settings.WEB_URL
|
|
||||||
magic_link.delay(email, key, token, current_site)
|
|
||||||
|
|
||||||
return Response({"key": key}, status=status.HTTP_200_OK)
|
|
||||||
except ValidationError:
|
|
||||||
return Response(
|
|
||||||
{"error": "Please provide a valid email address."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MagicSignInEndpoint(BaseAPIView):
|
class MagicSignInEndpoint(BaseAPIView):
|
||||||
@ -346,113 +299,99 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
user_token = request.data.get("token", "").strip()
|
||||||
user_token = request.data.get("token", "").strip()
|
key = request.data.get("key", False).strip().lower()
|
||||||
key = request.data.get("key", False).strip().lower()
|
|
||||||
|
|
||||||
if not key or user_token == "":
|
if not key or user_token == "":
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "User token and key are required"},
|
{"error": "User token and key are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
ri = redis_instance()
|
ri = redis_instance()
|
||||||
|
|
||||||
if ri.exists(key):
|
if ri.exists(key):
|
||||||
data = json.loads(ri.get(key))
|
data = json.loads(ri.get(key))
|
||||||
|
|
||||||
token = data["token"]
|
token = data["token"]
|
||||||
email = data["email"]
|
email = data["email"]
|
||||||
|
|
||||||
if str(token) == str(user_token):
|
if str(token) == str(user_token):
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
user = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
# Send event to Jitsu for tracking
|
# Send event to Jitsu for tracking
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
settings.ANALYTICS_BASE_API,
|
settings.ANALYTICS_BASE_API,
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "code",
|
||||||
},
|
},
|
||||||
json={
|
"user": {"email": email, "id": str(user.id)},
|
||||||
"event_id": uuid.uuid4().hex,
|
"device_ctx": {
|
||||||
"event_data": {
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
"medium": "code",
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get(
|
|
||||||
"HTTP_USER_AGENT"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_IN",
|
|
||||||
},
|
},
|
||||||
)
|
"event_type": "SIGN_IN",
|
||||||
else:
|
},
|
||||||
user = User.objects.create(
|
|
||||||
email=email,
|
|
||||||
username=uuid.uuid4().hex,
|
|
||||||
password=make_password(uuid.uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
)
|
)
|
||||||
# Send event to Jitsu for tracking
|
|
||||||
if settings.ANALYTICS_BASE_API:
|
|
||||||
_ = requests.post(
|
|
||||||
settings.ANALYTICS_BASE_API,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "code",
|
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get(
|
|
||||||
"HTTP_USER_AGENT"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_UP",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
user.last_active = timezone.now()
|
|
||||||
user.last_login_time = timezone.now()
|
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
|
||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
|
||||||
user.token_updated_at = timezone.now()
|
|
||||||
user.save()
|
|
||||||
serialized_user = UserSerializer(user).data
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
|
||||||
data = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
"user": serialized_user,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
user = User.objects.create(
|
||||||
{"error": "Your login code was incorrect. Please try again."},
|
email=email,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
username=uuid.uuid4().hex,
|
||||||
|
password=make_password(uuid.uuid4().hex),
|
||||||
|
is_password_autoset=True,
|
||||||
)
|
)
|
||||||
|
# Send event to Jitsu for tracking
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "code",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
user.last_active = timezone.now()
|
||||||
|
user.last_login_time = timezone.now()
|
||||||
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
|
user.token_updated_at = timezone.now()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The magic code/link has expired please try again"},
|
{"error": "Your login code was incorrect. Please try again."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
else:
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "The magic code/link has expired please try again"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
@ -5,10 +5,14 @@ import zoneinfo
|
|||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
# Third part imports
|
from django.db import IntegrityError
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
|
|
||||||
|
# Third part imports
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.filters import SearchFilter
|
||||||
@ -33,8 +37,6 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
@ -58,17 +60,49 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def handle_exception(self, exc):
|
||||||
|
"""
|
||||||
|
Handle any exception that occurs, by returning an appropriate response,
|
||||||
|
or re-raising the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = super().handle_exception(exc)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, IntegrityError):
|
||||||
|
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if isinstance(e, ValidationError):
|
||||||
|
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
|
model_name = str(exc).split(" matching query does not exist.")[0]
|
||||||
|
return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if isinstance(e, KeyError):
|
||||||
|
capture_exception(e)
|
||||||
|
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
capture_exception(e)
|
||||||
|
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
try:
|
||||||
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
response = self.handle_exception(exc)
|
||||||
|
return exc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def workspace_slug(self):
|
def workspace_slug(self):
|
||||||
@ -104,16 +138,48 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(self, exc):
|
||||||
|
"""
|
||||||
|
Handle any exception that occurs, by returning an appropriate response,
|
||||||
|
or re-raising the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = super().handle_exception(exc)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, IntegrityError):
|
||||||
|
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if isinstance(e, ValidationError):
|
||||||
|
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
|
model_name = str(exc).split(" matching query does not exist.")[0]
|
||||||
|
return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if isinstance(e, KeyError):
|
||||||
|
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
capture_exception(e)
|
||||||
|
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
try:
|
||||||
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
response = self.handle_exception(exc)
|
||||||
|
return exc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def workspace_slug(self):
|
def workspace_slug(self):
|
||||||
|
33
apiserver/plane/api/views/config.py
Normal file
33
apiserver/plane/api/views/config.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
data = {}
|
||||||
|
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
|
||||||
|
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
|
||||||
|
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
|
||||||
|
data["magic_login"] = (
|
||||||
|
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
|
||||||
|
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
|
||||||
|
data["email_password_login"] = (
|
||||||
|
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
|
||||||
|
)
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,3 @@
|
|||||||
# Django imports
|
|
||||||
from django.db import IntegrityError
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -23,7 +20,6 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
try:
|
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
if project.estimate_id is not None:
|
if project.estimate_id is not None:
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
@ -34,12 +30,6 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
|||||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response([], status=status.HTTP_200_OK)
|
return Response([], status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BulkEstimatePointEndpoint(BaseViewSet):
|
class BulkEstimatePointEndpoint(BaseViewSet):
|
||||||
@ -50,204 +40,139 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
serializer_class = EstimateSerializer
|
serializer_class = EstimateSerializer
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
estimates = Estimate.objects.filter(
|
||||||
estimates = Estimate.objects.filter(
|
workspace__slug=slug, project_id=project_id
|
||||||
workspace__slug=slug, project_id=project_id
|
).prefetch_related("points").select_related("workspace", "project")
|
||||||
).prefetch_related("points").select_related("workspace", "project")
|
serializer = EstimateReadSerializer(estimates, many=True)
|
||||||
serializer = EstimateReadSerializer(estimates, many=True)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
if not request.data.get("estimate", False):
|
||||||
if not request.data.get("estimate", False):
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate_points = request.data.get("estimate_points", [])
|
|
||||||
|
|
||||||
if not len(estimate_points) or len(estimate_points) > 8:
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate points are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
|
||||||
if not estimate_serializer.is_valid():
|
|
||||||
return Response(
|
|
||||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
estimate = estimate_serializer.save(project_id=project_id)
|
|
||||||
except IntegrityError:
|
|
||||||
return Response(
|
|
||||||
{"errror": "Estimate with the name already exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
estimate_points = EstimatePoint.objects.bulk_create(
|
|
||||||
[
|
|
||||||
EstimatePoint(
|
|
||||||
estimate=estimate,
|
|
||||||
key=estimate_point.get("key", 0),
|
|
||||||
value=estimate_point.get("value", ""),
|
|
||||||
description=estimate_point.get("description", ""),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=estimate.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
for estimate_point in estimate_points
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate_point_serializer = EstimatePointSerializer(
|
|
||||||
estimate_points, many=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Estimate is required"},
|
||||||
"estimate": estimate_serializer.data,
|
|
||||||
"estimate_points": estimate_point_serializer.data,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Estimate.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
estimate_points = request.data.get("estimate_points", [])
|
||||||
|
|
||||||
|
if not len(estimate_points) or len(estimate_points) > 8:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Estimate points are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
||||||
|
if not estimate_serializer.is_valid():
|
||||||
|
return Response(
|
||||||
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
estimate = estimate_serializer.save(project_id=project_id)
|
||||||
|
estimate_points = EstimatePoint.objects.bulk_create(
|
||||||
|
[
|
||||||
|
EstimatePoint(
|
||||||
|
estimate=estimate,
|
||||||
|
key=estimate_point.get("key", 0),
|
||||||
|
value=estimate_point.get("value", ""),
|
||||||
|
description=estimate_point.get("description", ""),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=estimate.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for estimate_point in estimate_points
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
estimate_point_serializer = EstimatePointSerializer(
|
||||||
|
estimate_points, many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"estimate": estimate_serializer.data,
|
||||||
|
"estimate_points": estimate_point_serializer.data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, estimate_id):
|
def retrieve(self, request, slug, project_id, estimate_id):
|
||||||
try:
|
estimate = Estimate.objects.get(
|
||||||
estimate = Estimate.objects.get(
|
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||||
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
)
|
||||||
)
|
serializer = EstimateReadSerializer(estimate)
|
||||||
serializer = EstimateReadSerializer(estimate)
|
return Response(
|
||||||
return Response(
|
serializer.data,
|
||||||
serializer.data,
|
status=status.HTTP_200_OK,
|
||||||
status=status.HTTP_200_OK,
|
)
|
||||||
)
|
|
||||||
except Estimate.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, estimate_id):
|
def partial_update(self, request, slug, project_id, estimate_id):
|
||||||
try:
|
if not request.data.get("estimate", False):
|
||||||
if not request.data.get("estimate", False):
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not len(request.data.get("estimate_points", [])):
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate points are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate = Estimate.objects.get(pk=estimate_id)
|
|
||||||
|
|
||||||
estimate_serializer = EstimateSerializer(
|
|
||||||
estimate, data=request.data.get("estimate"), partial=True
|
|
||||||
)
|
|
||||||
if not estimate_serializer.is_valid():
|
|
||||||
return Response(
|
|
||||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
estimate = estimate_serializer.save()
|
|
||||||
except IntegrityError:
|
|
||||||
return Response(
|
|
||||||
{"errror": "Estimate with the name already exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate_points_data = request.data.get("estimate_points", [])
|
|
||||||
|
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
|
||||||
pk__in=[
|
|
||||||
estimate_point.get("id") for estimate_point in estimate_points_data
|
|
||||||
],
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
estimate_id=estimate_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_estimate_points = []
|
|
||||||
for estimate_point in estimate_points:
|
|
||||||
# Find the data for that estimate point
|
|
||||||
estimate_point_data = [
|
|
||||||
point
|
|
||||||
for point in estimate_points_data
|
|
||||||
if point.get("id") == str(estimate_point.id)
|
|
||||||
]
|
|
||||||
if len(estimate_point_data):
|
|
||||||
estimate_point.value = estimate_point_data[0].get(
|
|
||||||
"value", estimate_point.value
|
|
||||||
)
|
|
||||||
updated_estimate_points.append(estimate_point)
|
|
||||||
|
|
||||||
try:
|
|
||||||
EstimatePoint.objects.bulk_update(
|
|
||||||
updated_estimate_points, ["value"], batch_size=10,
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Values need to be unique for each key"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Estimate is required"},
|
||||||
"estimate": estimate_serializer.data,
|
|
||||||
"estimate_points": estimate_point_serializer.data,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Estimate.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not len(request.data.get("estimate_points", [])):
|
||||||
|
return Response(
|
||||||
|
{"error": "Estimate points are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
estimate = Estimate.objects.get(pk=estimate_id)
|
||||||
|
|
||||||
|
estimate_serializer = EstimateSerializer(
|
||||||
|
estimate, data=request.data.get("estimate"), partial=True
|
||||||
|
)
|
||||||
|
if not estimate_serializer.is_valid():
|
||||||
|
return Response(
|
||||||
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
estimate = estimate_serializer.save()
|
||||||
|
|
||||||
|
estimate_points_data = request.data.get("estimate_points", [])
|
||||||
|
|
||||||
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
|
pk__in=[
|
||||||
|
estimate_point.get("id") for estimate_point in estimate_points_data
|
||||||
|
],
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
estimate_id=estimate_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_estimate_points = []
|
||||||
|
for estimate_point in estimate_points:
|
||||||
|
# Find the data for that estimate point
|
||||||
|
estimate_point_data = [
|
||||||
|
point
|
||||||
|
for point in estimate_points_data
|
||||||
|
if point.get("id") == str(estimate_point.id)
|
||||||
|
]
|
||||||
|
if len(estimate_point_data):
|
||||||
|
estimate_point.value = estimate_point_data[0].get(
|
||||||
|
"value", estimate_point.value
|
||||||
|
)
|
||||||
|
updated_estimate_points.append(estimate_point)
|
||||||
|
|
||||||
|
EstimatePoint.objects.bulk_update(
|
||||||
|
updated_estimate_points, ["value"], batch_size=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"estimate": estimate_serializer.data,
|
||||||
|
"estimate_points": estimate_point_serializer.data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, estimate_id):
|
def destroy(self, request, slug, project_id, estimate_id):
|
||||||
try:
|
estimate = Estimate.objects.get(
|
||||||
estimate = Estimate.objects.get(
|
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||||
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
)
|
||||||
)
|
estimate.delete()
|
||||||
estimate.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -20,81 +20,62 @@ class ExportIssuesEndpoint(BaseAPIView):
|
|||||||
serializer_class = ExporterHistorySerializer
|
serializer_class = ExporterHistorySerializer
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
try:
|
# Get the workspace
|
||||||
# Get the workspace
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
|
||||||
|
provider = request.data.get("provider", False)
|
||||||
provider = request.data.get("provider", False)
|
multiple = request.data.get("multiple", False)
|
||||||
multiple = request.data.get("multiple", False)
|
project_ids = request.data.get("project", [])
|
||||||
project_ids = request.data.get("project", [])
|
|
||||||
|
if provider in ["csv", "xlsx", "json"]:
|
||||||
if provider in ["csv", "xlsx", "json"]:
|
if not project_ids:
|
||||||
if not project_ids:
|
project_ids = Project.objects.filter(
|
||||||
project_ids = Project.objects.filter(
|
workspace__slug=slug
|
||||||
workspace__slug=slug
|
).values_list("id", flat=True)
|
||||||
).values_list("id", flat=True)
|
project_ids = [str(project_id) for project_id in project_ids]
|
||||||
project_ids = [str(project_id) for project_id in project_ids]
|
|
||||||
|
|
||||||
exporter = ExporterHistory.objects.create(
|
exporter = ExporterHistory.objects.create(
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
project=project_ids,
|
project=project_ids,
|
||||||
initiated_by=request.user,
|
initiated_by=request.user,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
)
|
)
|
||||||
|
|
||||||
issue_export_task.delay(
|
issue_export_task.delay(
|
||||||
provider=exporter.provider,
|
provider=exporter.provider,
|
||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
project_ids=project_ids,
|
project_ids=project_ids,
|
||||||
token_id=exporter.token,
|
token_id=exporter.token,
|
||||||
multiple=multiple,
|
multiple=multiple,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"message": f"Once the export is ready you will be able to download it"
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": f"Provider '{provider}' not found."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Workspace.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{
|
||||||
|
"message": f"Once the export is ready you will be able to download it"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": f"Provider '{provider}' not found."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
exporter_history = ExporterHistory.objects.filter(
|
||||||
exporter_history = ExporterHistory.objects.filter(
|
workspace__slug=slug
|
||||||
workspace__slug=slug
|
).select_related("workspace","initiated_by")
|
||||||
).select_related("workspace","initiated_by")
|
|
||||||
|
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=exporter_history,
|
queryset=exporter_history,
|
||||||
on_results=lambda exporter_history: ExporterHistorySerializer(
|
on_results=lambda exporter_history: ExporterHistorySerializer(
|
||||||
exporter_history, many=True
|
exporter_history, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
|
||||||
{"error": "per_page and cursor are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "per_page and cursor are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
92
apiserver/plane/api/views/external.py
Normal file
92
apiserver/plane/api/views/external.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Python imports
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import openai
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseAPIView
|
||||||
|
from plane.api.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import Workspace, Project
|
||||||
|
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
||||||
|
from plane.utils.integrations.github import get_release_notes
|
||||||
|
|
||||||
|
|
||||||
|
class GPTIntegrationEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
|
||||||
|
return Response(
|
||||||
|
{"error": "OpenAI API key and engine is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = request.data.get("prompt", False)
|
||||||
|
task = request.data.get("task", False)
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
return Response(
|
||||||
|
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
final_text = task + "\n" + prompt
|
||||||
|
|
||||||
|
openai.api_key = settings.OPENAI_API_KEY
|
||||||
|
response = openai.ChatCompletion.create(
|
||||||
|
model=settings.GPT_ENGINE,
|
||||||
|
messages=[{"role": "user", "content": final_text}],
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
text = response.choices[0].message.content.strip()
|
||||||
|
text_html = text.replace("\n", "<br/>")
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"response": text,
|
||||||
|
"response_html": text_html,
|
||||||
|
"project_detail": ProjectLiteSerializer(project).data,
|
||||||
|
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseNotesEndpoint(BaseAPIView):
|
||||||
|
def get(self, request):
|
||||||
|
release_notes = get_release_notes()
|
||||||
|
return Response(release_notes, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsplashEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
query = request.GET.get("query", False)
|
||||||
|
page = request.GET.get("page", 1)
|
||||||
|
per_page = request.GET.get("per_page", 20)
|
||||||
|
|
||||||
|
url = (
|
||||||
|
f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
|
||||||
|
if query
|
||||||
|
else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = requests.get(url=url, headers=headers)
|
||||||
|
return Response(resp.json(), status=status.HTTP_200_OK)
|
@ -1,75 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
import openai
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Django imports
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from .base import BaseAPIView
|
|
||||||
from plane.api.permissions import ProjectEntityPermission
|
|
||||||
from plane.db.models import Workspace, Project
|
|
||||||
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class GPTIntegrationEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
|
|
||||||
return Response(
|
|
||||||
{"error": "OpenAI API key and engine is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt = request.data.get("prompt", False)
|
|
||||||
task = request.data.get("task", False)
|
|
||||||
|
|
||||||
if not task:
|
|
||||||
return Response(
|
|
||||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
final_text = task + "\n" + prompt
|
|
||||||
|
|
||||||
openai.api_key = settings.OPENAI_API_KEY
|
|
||||||
response = openai.ChatCompletion.create(
|
|
||||||
model=settings.GPT_ENGINE,
|
|
||||||
messages=[{"role": "user", "content": final_text}],
|
|
||||||
temperature=0.7,
|
|
||||||
max_tokens=1024,
|
|
||||||
)
|
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
|
||||||
project = Project.objects.get(pk=project_id)
|
|
||||||
|
|
||||||
text = response.choices[0].message.content.strip()
|
|
||||||
text_html = text.replace("\n", "<br/>")
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"response": text,
|
|
||||||
"response_html": text_html,
|
|
||||||
"project_detail": ProjectLiteSerializer(project).data,
|
|
||||||
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace or Project Does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
@ -44,559 +44,479 @@ from plane.utils.html_processor import strip_tags
|
|||||||
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||||
|
|
||||||
def get(self, request, slug, service):
|
def get(self, request, slug, service):
|
||||||
try:
|
if service == "github":
|
||||||
if service == "github":
|
owner = request.GET.get("owner", False)
|
||||||
owner = request.GET.get("owner", False)
|
repo = request.GET.get("repo", False)
|
||||||
repo = request.GET.get("repo", False)
|
|
||||||
|
|
||||||
if not owner or not repo:
|
if not owner or not repo:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Owner and repo are required"},
|
{"error": "Owner and repo are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
|
||||||
|
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
|
||||||
integration__provider="github", workspace__slug=slug
|
|
||||||
)
|
)
|
||||||
|
|
||||||
access_tokens_url = workspace_integration.metadata.get(
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
"access_tokens_url", False
|
integration__provider="github", workspace__slug=slug
|
||||||
)
|
)
|
||||||
|
|
||||||
if not access_tokens_url:
|
access_tokens_url = workspace_integration.metadata.get(
|
||||||
return Response(
|
"access_tokens_url", False
|
||||||
{
|
)
|
||||||
"error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
issue_count, labels, collaborators = get_github_repo_details(
|
if not access_tokens_url:
|
||||||
access_tokens_url, owner, repo
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"issue_count": issue_count,
|
"error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
|
||||||
"labels": labels,
|
|
||||||
"collaborators": collaborators,
|
|
||||||
},
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_count, labels, collaborators = get_github_repo_details(
|
||||||
|
access_tokens_url, owner, repo
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issue_count": issue_count,
|
||||||
|
"labels": labels,
|
||||||
|
"collaborators": collaborators,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if service == "jira":
|
||||||
|
# Check for all the keys
|
||||||
|
params = {
|
||||||
|
"project_key": "Project key is required",
|
||||||
|
"api_token": "API token is required",
|
||||||
|
"email": "Email is required",
|
||||||
|
"cloud_hostname": "Cloud hostname is required",
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, error_message in params.items():
|
||||||
|
if not request.GET.get(key, False):
|
||||||
|
return Response(
|
||||||
|
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
project_key = request.GET.get("project_key", "")
|
||||||
|
api_token = request.GET.get("api_token", "")
|
||||||
|
email = request.GET.get("email", "")
|
||||||
|
cloud_hostname = request.GET.get("cloud_hostname", "")
|
||||||
|
|
||||||
|
response = jira_project_issue_summary(
|
||||||
|
email, api_token, project_key, cloud_hostname
|
||||||
|
)
|
||||||
|
if "error" in response:
|
||||||
|
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
response,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
return Response(
|
||||||
if service == "jira":
|
{"error": "Service not supported yet"},
|
||||||
# Check for all the keys
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
params = {
|
)
|
||||||
"project_key": "Project key is required",
|
|
||||||
"api_token": "API token is required",
|
|
||||||
"email": "Email is required",
|
|
||||||
"cloud_hostname": "Cloud hostname is required",
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, error_message in params.items():
|
|
||||||
if not request.GET.get(key, False):
|
|
||||||
return Response(
|
|
||||||
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
project_key = request.GET.get("project_key", "")
|
|
||||||
api_token = request.GET.get("api_token", "")
|
|
||||||
email = request.GET.get("email", "")
|
|
||||||
cloud_hostname = request.GET.get("cloud_hostname", "")
|
|
||||||
|
|
||||||
response = jira_project_issue_summary(
|
|
||||||
email, api_token, project_key, cloud_hostname
|
|
||||||
)
|
|
||||||
if "error" in response:
|
|
||||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
response,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"error": "Service not supported yet"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except WorkspaceIntegration.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Requested integration was not installed in the workspace"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImportServiceEndpoint(BaseAPIView):
|
class ImportServiceEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, service):
|
def post(self, request, slug, service):
|
||||||
try:
|
project_id = request.data.get("project_id", False)
|
||||||
project_id = request.data.get("project_id", False)
|
|
||||||
|
|
||||||
if not project_id:
|
if not project_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project ID is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
if service == "github":
|
||||||
|
data = request.data.get("data", False)
|
||||||
|
metadata = request.data.get("metadata", False)
|
||||||
|
config = request.data.get("config", False)
|
||||||
|
if not data or not metadata or not config:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project ID is required"},
|
{"error": "Data, config and metadata are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
api_token = APIToken.objects.filter(
|
||||||
|
user=request.user, workspace=workspace
|
||||||
if service == "github":
|
).first()
|
||||||
data = request.data.get("data", False)
|
if api_token is None:
|
||||||
metadata = request.data.get("metadata", False)
|
api_token = APIToken.objects.create(
|
||||||
config = request.data.get("config", False)
|
user=request.user,
|
||||||
if not data or not metadata or not config:
|
label="Importer",
|
||||||
return Response(
|
workspace=workspace,
|
||||||
{"error": "Data, config and metadata are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
api_token = APIToken.objects.filter(
|
|
||||||
user=request.user, workspace=workspace
|
|
||||||
).first()
|
|
||||||
if api_token is None:
|
|
||||||
api_token = APIToken.objects.create(
|
|
||||||
user=request.user,
|
|
||||||
label="Importer",
|
|
||||||
workspace=workspace,
|
|
||||||
)
|
|
||||||
|
|
||||||
importer = Importer.objects.create(
|
|
||||||
service=service,
|
|
||||||
project_id=project_id,
|
|
||||||
status="queued",
|
|
||||||
initiated_by=request.user,
|
|
||||||
data=data,
|
|
||||||
metadata=metadata,
|
|
||||||
token=api_token,
|
|
||||||
config=config,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
service_importer.delay(service, importer.id)
|
importer = Importer.objects.create(
|
||||||
serializer = ImporterSerializer(importer)
|
service=service,
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
project_id=project_id,
|
||||||
|
status="queued",
|
||||||
|
initiated_by=request.user,
|
||||||
|
data=data,
|
||||||
|
metadata=metadata,
|
||||||
|
token=api_token,
|
||||||
|
config=config,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
if service == "jira":
|
service_importer.delay(service, importer.id)
|
||||||
data = request.data.get("data", False)
|
serializer = ImporterSerializer(importer)
|
||||||
metadata = request.data.get("metadata", False)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
config = request.data.get("config", False)
|
|
||||||
if not data or not metadata:
|
|
||||||
return Response(
|
|
||||||
{"error": "Data, config and metadata are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
api_token = APIToken.objects.filter(
|
|
||||||
user=request.user, workspace=workspace
|
|
||||||
).first()
|
|
||||||
if api_token is None:
|
|
||||||
api_token = APIToken.objects.create(
|
|
||||||
user=request.user,
|
|
||||||
label="Importer",
|
|
||||||
workspace=workspace,
|
|
||||||
)
|
|
||||||
|
|
||||||
importer = Importer.objects.create(
|
if service == "jira":
|
||||||
service=service,
|
data = request.data.get("data", False)
|
||||||
project_id=project_id,
|
metadata = request.data.get("metadata", False)
|
||||||
status="queued",
|
config = request.data.get("config", False)
|
||||||
initiated_by=request.user,
|
if not data or not metadata:
|
||||||
data=data,
|
return Response(
|
||||||
metadata=metadata,
|
{"error": "Data, config and metadata are required"},
|
||||||
token=api_token,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
config=config,
|
)
|
||||||
created_by=request.user,
|
api_token = APIToken.objects.filter(
|
||||||
updated_by=request.user,
|
user=request.user, workspace=workspace
|
||||||
|
).first()
|
||||||
|
if api_token is None:
|
||||||
|
api_token = APIToken.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
label="Importer",
|
||||||
|
workspace=workspace,
|
||||||
)
|
)
|
||||||
|
|
||||||
service_importer.delay(service, importer.id)
|
importer = Importer.objects.create(
|
||||||
serializer = ImporterSerializer(importer)
|
service=service,
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
project_id=project_id,
|
||||||
|
status="queued",
|
||||||
|
initiated_by=request.user,
|
||||||
|
data=data,
|
||||||
|
metadata=metadata,
|
||||||
|
token=api_token,
|
||||||
|
config=config,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
service_importer.delay(service, importer.id)
|
||||||
{"error": "Servivce not supported yet"},
|
serializer = ImporterSerializer(importer)
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
)
|
|
||||||
except (
|
return Response(
|
||||||
Workspace.DoesNotExist,
|
{"error": "Servivce not supported yet"},
|
||||||
WorkspaceIntegration.DoesNotExist,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
Project.DoesNotExist,
|
)
|
||||||
) as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace Integration or Project does not exist"},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
imports = (
|
||||||
imports = (
|
Importer.objects.filter(workspace__slug=slug)
|
||||||
Importer.objects.filter(workspace__slug=slug)
|
.order_by("-created_at")
|
||||||
.order_by("-created_at")
|
.select_related("initiated_by", "project", "workspace")
|
||||||
.select_related("initiated_by", "project", "workspace")
|
)
|
||||||
)
|
serializer = ImporterSerializer(imports, many=True)
|
||||||
serializer = ImporterSerializer(imports, many=True)
|
return Response(serializer.data)
|
||||||
return Response(serializer.data)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, request, slug, service, pk):
|
def delete(self, request, slug, service, pk):
|
||||||
try:
|
importer = Importer.objects.get(
|
||||||
importer = Importer.objects.get(
|
pk=pk, service=service, workspace__slug=slug
|
||||||
pk=pk, service=service, workspace__slug=slug
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if importer.imported_data is not None:
|
if importer.imported_data is not None:
|
||||||
# Delete all imported Issues
|
# Delete all imported Issues
|
||||||
imported_issues = importer.imported_data.get("issues", [])
|
imported_issues = importer.imported_data.get("issues", [])
|
||||||
Issue.issue_objects.filter(id__in=imported_issues).delete()
|
Issue.issue_objects.filter(id__in=imported_issues).delete()
|
||||||
|
|
||||||
# Delete all imported Labels
|
# Delete all imported Labels
|
||||||
imported_labels = importer.imported_data.get("labels", [])
|
imported_labels = importer.imported_data.get("labels", [])
|
||||||
Label.objects.filter(id__in=imported_labels).delete()
|
Label.objects.filter(id__in=imported_labels).delete()
|
||||||
|
|
||||||
if importer.service == "jira":
|
if importer.service == "jira":
|
||||||
imported_modules = importer.imported_data.get("modules", [])
|
imported_modules = importer.imported_data.get("modules", [])
|
||||||
Module.objects.filter(id__in=imported_modules).delete()
|
Module.objects.filter(id__in=imported_modules).delete()
|
||||||
importer.delete()
|
importer.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def patch(self, request, slug, service, pk):
|
def patch(self, request, slug, service, pk):
|
||||||
try:
|
importer = Importer.objects.get(
|
||||||
importer = Importer.objects.get(
|
pk=pk, service=service, workspace__slug=slug
|
||||||
pk=pk, service=service, workspace__slug=slug
|
)
|
||||||
)
|
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
||||||
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save()
|
||||||
serializer.save()
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Importer.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateServiceImportStatusEndpoint(BaseAPIView):
|
class UpdateServiceImportStatusEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id, service, importer_id):
|
def post(self, request, slug, project_id, service, importer_id):
|
||||||
try:
|
importer = Importer.objects.get(
|
||||||
importer = Importer.objects.get(
|
pk=importer_id,
|
||||||
pk=importer_id,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
service=service,
|
||||||
service=service,
|
)
|
||||||
)
|
importer.status = request.data.get("status", "processing")
|
||||||
importer.status = request.data.get("status", "processing")
|
importer.save()
|
||||||
importer.save()
|
return Response(status.HTTP_200_OK)
|
||||||
return Response(status.HTTP_200_OK)
|
|
||||||
except Importer.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Importer does not exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BulkImportIssuesEndpoint(BaseAPIView):
|
class BulkImportIssuesEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id, service):
|
def post(self, request, slug, project_id, service):
|
||||||
try:
|
# Get the project
|
||||||
# Get the project
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
||||||
|
|
||||||
# Get the default state
|
# Get the default state
|
||||||
|
default_state = State.objects.filter(
|
||||||
|
~Q(name="Triage"), project_id=project_id, default=True
|
||||||
|
).first()
|
||||||
|
# if there is no default state assign any random state
|
||||||
|
if default_state is None:
|
||||||
default_state = State.objects.filter(
|
default_state = State.objects.filter(
|
||||||
~Q(name="Triage"), project_id=project_id, default=True
|
~Q(name="Triage"), project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
# if there is no default state assign any random state
|
|
||||||
if default_state is None:
|
|
||||||
default_state = State.objects.filter(
|
|
||||||
~Q(name="Triage"), project_id=project_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
# Get the maximum sequence_id
|
# Get the maximum sequence_id
|
||||||
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
|
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
|
||||||
largest=Max("sequence")
|
largest=Max("sequence")
|
||||||
)["largest"]
|
)["largest"]
|
||||||
|
|
||||||
last_id = 1 if last_id is None else last_id + 1
|
last_id = 1 if last_id is None else last_id + 1
|
||||||
|
|
||||||
# Get the maximum sort order
|
# Get the maximum sort order
|
||||||
largest_sort_order = Issue.objects.filter(
|
largest_sort_order = Issue.objects.filter(
|
||||||
project_id=project_id, state=default_state
|
project_id=project_id, state=default_state
|
||||||
).aggregate(largest=Max("sort_order"))["largest"]
|
).aggregate(largest=Max("sort_order"))["largest"]
|
||||||
|
|
||||||
largest_sort_order = (
|
largest_sort_order = (
|
||||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the issues_data
|
# Get the issues_data
|
||||||
issues_data = request.data.get("issues_data", [])
|
issues_data = request.data.get("issues_data", [])
|
||||||
|
|
||||||
if not len(issues_data):
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue data is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
bulk_issues = []
|
|
||||||
for issue_data in issues_data:
|
|
||||||
bulk_issues.append(
|
|
||||||
Issue(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
state_id=issue_data.get("state")
|
|
||||||
if issue_data.get("state", False)
|
|
||||||
else default_state.id,
|
|
||||||
name=issue_data.get("name", "Issue Created through Bulk"),
|
|
||||||
description_html=issue_data.get("description_html", "<p></p>"),
|
|
||||||
description_stripped=(
|
|
||||||
None
|
|
||||||
if (
|
|
||||||
issue_data.get("description_html") == ""
|
|
||||||
or issue_data.get("description_html") is None
|
|
||||||
)
|
|
||||||
else strip_tags(issue_data.get("description_html"))
|
|
||||||
),
|
|
||||||
sequence_id=last_id,
|
|
||||||
sort_order=largest_sort_order,
|
|
||||||
start_date=issue_data.get("start_date", None),
|
|
||||||
target_date=issue_data.get("target_date", None),
|
|
||||||
priority=issue_data.get("priority", "none"),
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
largest_sort_order = largest_sort_order + 10000
|
|
||||||
last_id = last_id + 1
|
|
||||||
|
|
||||||
issues = Issue.objects.bulk_create(
|
|
||||||
bulk_issues,
|
|
||||||
batch_size=100,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sequences
|
|
||||||
_ = IssueSequence.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueSequence(
|
|
||||||
issue=issue,
|
|
||||||
sequence=issue.sequence_id,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
)
|
|
||||||
for issue in issues
|
|
||||||
],
|
|
||||||
batch_size=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attach Labels
|
|
||||||
bulk_issue_labels = []
|
|
||||||
for issue, issue_data in zip(issues, issues_data):
|
|
||||||
labels_list = issue_data.get("labels_list", [])
|
|
||||||
bulk_issue_labels = bulk_issue_labels + [
|
|
||||||
IssueLabel(
|
|
||||||
issue=issue,
|
|
||||||
label_id=label_id,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for label_id in labels_list
|
|
||||||
]
|
|
||||||
|
|
||||||
_ = IssueLabel.objects.bulk_create(
|
|
||||||
bulk_issue_labels, batch_size=100, ignore_conflicts=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attach Assignees
|
|
||||||
bulk_issue_assignees = []
|
|
||||||
for issue, issue_data in zip(issues, issues_data):
|
|
||||||
assignees_list = issue_data.get("assignees_list", [])
|
|
||||||
bulk_issue_assignees = bulk_issue_assignees + [
|
|
||||||
IssueAssignee(
|
|
||||||
issue=issue,
|
|
||||||
assignee_id=assignee_id,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for assignee_id in assignees_list
|
|
||||||
]
|
|
||||||
|
|
||||||
_ = IssueAssignee.objects.bulk_create(
|
|
||||||
bulk_issue_assignees, batch_size=100, ignore_conflicts=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track the issue activities
|
|
||||||
IssueActivity.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueActivity(
|
|
||||||
issue=issue,
|
|
||||||
actor=request.user,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
comment=f"imported the issue from {service}",
|
|
||||||
verb="created",
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for issue in issues
|
|
||||||
],
|
|
||||||
batch_size=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create Comments
|
|
||||||
bulk_issue_comments = []
|
|
||||||
for issue, issue_data in zip(issues, issues_data):
|
|
||||||
comments_list = issue_data.get("comments_list", [])
|
|
||||||
bulk_issue_comments = bulk_issue_comments + [
|
|
||||||
IssueComment(
|
|
||||||
issue=issue,
|
|
||||||
comment_html=comment.get("comment_html", "<p></p>"),
|
|
||||||
actor=request.user,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for comment in comments_list
|
|
||||||
]
|
|
||||||
|
|
||||||
_ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
|
|
||||||
|
|
||||||
# Attach Links
|
|
||||||
_ = IssueLink.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueLink(
|
|
||||||
issue=issue,
|
|
||||||
url=issue_data.get("link", {}).get("url", "https://github.com"),
|
|
||||||
title=issue_data.get("link", {}).get("title", "Original Issue"),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for issue, issue_data in zip(issues, issues_data)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if not len(issues_data):
|
||||||
return Response(
|
return Response(
|
||||||
{"issues": IssueFlatSerializer(issues, many=True).data},
|
{"error": "Issue data is required"},
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
except Project.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Issues
|
||||||
|
bulk_issues = []
|
||||||
|
for issue_data in issues_data:
|
||||||
|
bulk_issues.append(
|
||||||
|
Issue(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
state_id=issue_data.get("state")
|
||||||
|
if issue_data.get("state", False)
|
||||||
|
else default_state.id,
|
||||||
|
name=issue_data.get("name", "Issue Created through Bulk"),
|
||||||
|
description_html=issue_data.get("description_html", "<p></p>"),
|
||||||
|
description_stripped=(
|
||||||
|
None
|
||||||
|
if (
|
||||||
|
issue_data.get("description_html") == ""
|
||||||
|
or issue_data.get("description_html") is None
|
||||||
|
)
|
||||||
|
else strip_tags(issue_data.get("description_html"))
|
||||||
|
),
|
||||||
|
sequence_id=last_id,
|
||||||
|
sort_order=largest_sort_order,
|
||||||
|
start_date=issue_data.get("start_date", None),
|
||||||
|
target_date=issue_data.get("target_date", None),
|
||||||
|
priority=issue_data.get("priority", "none"),
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
largest_sort_order = largest_sort_order + 10000
|
||||||
|
last_id = last_id + 1
|
||||||
|
|
||||||
|
issues = Issue.objects.bulk_create(
|
||||||
|
bulk_issues,
|
||||||
|
batch_size=100,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sequences
|
||||||
|
_ = IssueSequence.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueSequence(
|
||||||
|
issue=issue,
|
||||||
|
sequence=issue.sequence_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach Labels
|
||||||
|
bulk_issue_labels = []
|
||||||
|
for issue, issue_data in zip(issues, issues_data):
|
||||||
|
labels_list = issue_data.get("labels_list", [])
|
||||||
|
bulk_issue_labels = bulk_issue_labels + [
|
||||||
|
IssueLabel(
|
||||||
|
issue=issue,
|
||||||
|
label_id=label_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for label_id in labels_list
|
||||||
|
]
|
||||||
|
|
||||||
|
_ = IssueLabel.objects.bulk_create(
|
||||||
|
bulk_issue_labels, batch_size=100, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach Assignees
|
||||||
|
bulk_issue_assignees = []
|
||||||
|
for issue, issue_data in zip(issues, issues_data):
|
||||||
|
assignees_list = issue_data.get("assignees_list", [])
|
||||||
|
bulk_issue_assignees = bulk_issue_assignees + [
|
||||||
|
IssueAssignee(
|
||||||
|
issue=issue,
|
||||||
|
assignee_id=assignee_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for assignee_id in assignees_list
|
||||||
|
]
|
||||||
|
|
||||||
|
_ = IssueAssignee.objects.bulk_create(
|
||||||
|
bulk_issue_assignees, batch_size=100, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track the issue activities
|
||||||
|
IssueActivity.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueActivity(
|
||||||
|
issue=issue,
|
||||||
|
actor=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
comment=f"imported the issue from {service}",
|
||||||
|
verb="created",
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Comments
|
||||||
|
bulk_issue_comments = []
|
||||||
|
for issue, issue_data in zip(issues, issues_data):
|
||||||
|
comments_list = issue_data.get("comments_list", [])
|
||||||
|
bulk_issue_comments = bulk_issue_comments + [
|
||||||
|
IssueComment(
|
||||||
|
issue=issue,
|
||||||
|
comment_html=comment.get("comment_html", "<p></p>"),
|
||||||
|
actor=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for comment in comments_list
|
||||||
|
]
|
||||||
|
|
||||||
|
_ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
|
||||||
|
|
||||||
|
# Attach Links
|
||||||
|
_ = IssueLink.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueLink(
|
||||||
|
issue=issue,
|
||||||
|
url=issue_data.get("link", {}).get("url", "https://github.com"),
|
||||||
|
title=issue_data.get("link", {}).get("title", "Original Issue"),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for issue, issue_data in zip(issues, issues_data)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"issues": IssueFlatSerializer(issues, many=True).data},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkImportModulesEndpoint(BaseAPIView):
|
class BulkImportModulesEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id, service):
|
def post(self, request, slug, project_id, service):
|
||||||
try:
|
modules_data = request.data.get("modules_data", [])
|
||||||
modules_data = request.data.get("modules_data", [])
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
||||||
|
|
||||||
modules = Module.objects.bulk_create(
|
modules = Module.objects.bulk_create(
|
||||||
|
[
|
||||||
|
Module(
|
||||||
|
name=module.get("name", uuid.uuid4().hex),
|
||||||
|
description=module.get("description", ""),
|
||||||
|
start_date=module.get("start_date", None),
|
||||||
|
target_date=module.get("target_date", None),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for module in modules_data
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
||||||
|
|
||||||
|
if len(modules) == len(modules_data):
|
||||||
|
_ = ModuleLink.objects.bulk_create(
|
||||||
[
|
[
|
||||||
Module(
|
ModuleLink(
|
||||||
name=module.get("name", uuid.uuid4().hex),
|
module=module,
|
||||||
description=module.get("description", ""),
|
url=module_data.get("link", {}).get(
|
||||||
start_date=module.get("start_date", None),
|
"url", "https://plane.so"
|
||||||
target_date=module.get("target_date", None),
|
),
|
||||||
|
title=module_data.get("link", {}).get(
|
||||||
|
"title", "Original Issue"
|
||||||
|
),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
for module in modules_data
|
for module, module_data in zip(modules, modules_data)
|
||||||
],
|
],
|
||||||
batch_size=100,
|
batch_size=100,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
bulk_module_issues = []
|
||||||
|
for module, module_data in zip(modules, modules_data):
|
||||||
|
module_issues_list = module_data.get("module_issues_list", [])
|
||||||
|
bulk_module_issues = bulk_module_issues + [
|
||||||
|
ModuleIssue(
|
||||||
|
issue_id=issue,
|
||||||
|
module=module,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for issue in module_issues_list
|
||||||
|
]
|
||||||
|
|
||||||
if len(modules) == len(modules_data):
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
_ = ModuleLink.objects.bulk_create(
|
bulk_module_issues, batch_size=100, ignore_conflicts=True
|
||||||
[
|
|
||||||
ModuleLink(
|
|
||||||
module=module,
|
|
||||||
url=module_data.get("link", {}).get(
|
|
||||||
"url", "https://plane.so"
|
|
||||||
),
|
|
||||||
title=module_data.get("link", {}).get(
|
|
||||||
"title", "Original Issue"
|
|
||||||
),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for module, module_data in zip(modules, modules_data)
|
|
||||||
],
|
|
||||||
batch_size=100,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
bulk_module_issues = []
|
|
||||||
for module, module_data in zip(modules, modules_data):
|
|
||||||
module_issues_list = module_data.get("module_issues_list", [])
|
|
||||||
bulk_module_issues = bulk_module_issues + [
|
|
||||||
ModuleIssue(
|
|
||||||
issue_id=issue,
|
|
||||||
module=module,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for issue in module_issues_list
|
|
||||||
]
|
|
||||||
|
|
||||||
_ = ModuleIssue.objects.bulk_create(
|
|
||||||
bulk_module_issues, batch_size=100, ignore_conflicts=True
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = ModuleSerializer(modules, many=True)
|
|
||||||
return Response(
|
|
||||||
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"message": "Modules created but issues could not be imported"},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Project.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
serializer = ModuleSerializer(modules, many=True)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"message": "Modules created but issues could not be imported"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
@ -64,24 +64,17 @@ class InboxViewSet(BaseViewSet):
|
|||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
try:
|
inbox = Inbox.objects.get(
|
||||||
inbox = Inbox.objects.get(
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
)
|
||||||
)
|
# Handle default inbox delete
|
||||||
# Handle default inbox delete
|
if inbox.is_default:
|
||||||
if inbox.is_default:
|
|
||||||
return Response(
|
|
||||||
{"error": "You cannot delete the default inbox"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
inbox.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wronf please try again later"},
|
{"error": "You cannot delete the default inbox"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
inbox.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class InboxIssueViewSet(BaseViewSet):
|
class InboxIssueViewSet(BaseViewSet):
|
||||||
@ -110,276 +103,239 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug, project_id, inbox_id):
|
def list(self, request, slug, project_id, inbox_id):
|
||||||
try:
|
filters = issue_filters(request.query_params, "GET")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
issues = (
|
||||||
issues = (
|
Issue.objects.filter(
|
||||||
Issue.objects.filter(
|
issue_inbox__inbox_id=inbox_id,
|
||||||
issue_inbox__inbox_id=inbox_id,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(bridge_id=F("issue_inbox__id"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels")
|
||||||
|
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.order_by()
|
||||||
.annotate(bridge_id=F("issue_inbox__id"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.values("count")
|
||||||
.prefetch_related("assignees", "labels")
|
)
|
||||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
.prefetch_related(
|
||||||
.annotate(
|
Prefetch(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
"issue_inbox",
|
||||||
.order_by()
|
queryset=InboxIssue.objects.only(
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
"status", "duplicate_to", "snoozed_till", "source"
|
||||||
.values("count")
|
),
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_inbox",
|
|
||||||
queryset=InboxIssue.objects.only(
|
|
||||||
"status", "duplicate_to", "snoozed_till", "source"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
)
|
||||||
return Response(
|
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||||
issues_data,
|
return Response(
|
||||||
status=status.HTTP_200_OK,
|
issues_data,
|
||||||
)
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
try:
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
|
||||||
return Response(
|
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for valid priority
|
|
||||||
if not request.data.get("issue", {}).get("priority", "none") in [
|
|
||||||
"low",
|
|
||||||
"medium",
|
|
||||||
"high",
|
|
||||||
"urgent",
|
|
||||||
"none",
|
|
||||||
]:
|
|
||||||
return Response(
|
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create or get state
|
|
||||||
state, _ = State.objects.get_or_create(
|
|
||||||
name="Triage",
|
|
||||||
group="backlog",
|
|
||||||
description="Default state for managing all Inbox Issues",
|
|
||||||
project_id=project_id,
|
|
||||||
color="#ff7700",
|
|
||||||
)
|
|
||||||
|
|
||||||
# create an issue
|
|
||||||
issue = Issue.objects.create(
|
|
||||||
name=request.data.get("issue", {}).get("name"),
|
|
||||||
description=request.data.get("issue", {}).get("description", {}),
|
|
||||||
description_html=request.data.get("issue", {}).get(
|
|
||||||
"description_html", "<p></p>"
|
|
||||||
),
|
|
||||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
|
||||||
project_id=project_id,
|
|
||||||
state=state,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create an Issue Activity
|
|
||||||
issue_activity.delay(
|
|
||||||
type="issue.activity.created",
|
|
||||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=str(issue.id),
|
|
||||||
project_id=str(project_id),
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp())
|
|
||||||
)
|
|
||||||
# create an inbox issue
|
|
||||||
InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox_id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue=issue,
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for valid priority
|
||||||
|
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"urgent",
|
||||||
|
"none",
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or get state
|
||||||
|
state, _ = State.objects.get_or_create(
|
||||||
|
name="Triage",
|
||||||
|
group="backlog",
|
||||||
|
description="Default state for managing all Inbox Issues",
|
||||||
|
project_id=project_id,
|
||||||
|
color="#ff7700",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create an issue
|
||||||
|
issue = Issue.objects.create(
|
||||||
|
name=request.data.get("issue", {}).get("name"),
|
||||||
|
description=request.data.get("issue", {}).get("description", {}),
|
||||||
|
description_html=request.data.get("issue", {}).get(
|
||||||
|
"description_html", "<p></p>"
|
||||||
|
),
|
||||||
|
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||||
|
project_id=project_id,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an Issue Activity
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.created",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
# create an inbox issue
|
||||||
|
InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox_id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue=issue,
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
inbox_issue = InboxIssue.objects.get(
|
||||||
inbox_issue = InboxIssue.objects.get(
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
)
|
||||||
|
# Get the project member
|
||||||
|
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
||||||
|
# Only project members admins and created_by users can access this endpoint
|
||||||
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
|
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Get issue data
|
||||||
|
issue_data = request.data.pop("issue", False)
|
||||||
|
|
||||||
|
if bool(issue_data):
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Only allow guests and viewers to edit name and description
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
if project_member.role <= 10:
|
||||||
# Only project members admins and created_by users can access this endpoint
|
# viewers and guests since only viewers and guests
|
||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
issue_data = {
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
"name": issue_data.get("name", issue.name),
|
||||||
|
"description_html": issue_data.get("description_html", issue.description_html),
|
||||||
|
"description": issue_data.get("description", issue.description)
|
||||||
|
}
|
||||||
|
|
||||||
# Get issue data
|
issue_serializer = IssueCreateSerializer(
|
||||||
issue_data = request.data.pop("issue", False)
|
issue, data=issue_data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
if bool(issue_data):
|
if issue_serializer.is_valid():
|
||||||
issue = Issue.objects.get(
|
current_instance = issue
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
# Log all the updates
|
||||||
)
|
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||||
# Only allow guests and viewers to edit name and description
|
if issue is not None:
|
||||||
if project_member.role <= 10:
|
issue_activity.delay(
|
||||||
# viewers and guests since only viewers and guests
|
type="issue.activity.updated",
|
||||||
issue_data = {
|
requested_data=requested_data,
|
||||||
"name": issue_data.get("name", issue.name),
|
actor_id=str(request.user.id),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
issue_id=str(issue.id),
|
||||||
"description": issue_data.get("description", issue.description)
|
project_id=str(project_id),
|
||||||
}
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(current_instance).data,
|
||||||
issue_serializer = IssueCreateSerializer(
|
cls=DjangoJSONEncoder,
|
||||||
issue, data=issue_data, partial=True
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
issue_serializer.save()
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
if issue_serializer.is_valid():
|
# Only project admins and members can edit inbox issue attributes
|
||||||
current_instance = issue
|
if project_member.role > 10:
|
||||||
# Log all the updates
|
serializer = InboxIssueSerializer(
|
||||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
inbox_issue, data=request.data, partial=True
|
||||||
if issue is not None:
|
)
|
||||||
issue_activity.delay(
|
|
||||||
type="issue.activity.updated",
|
if serializer.is_valid():
|
||||||
requested_data=requested_data,
|
serializer.save()
|
||||||
actor_id=str(request.user.id),
|
# Update the issue state if the issue is rejected or marked as duplicate
|
||||||
issue_id=str(issue.id),
|
if serializer.data["status"] in [-1, 2]:
|
||||||
project_id=str(project_id),
|
issue = Issue.objects.get(
|
||||||
current_instance=json.dumps(
|
pk=inbox_issue.issue_id,
|
||||||
IssueSerializer(current_instance).data,
|
workspace__slug=slug,
|
||||||
cls=DjangoJSONEncoder,
|
project_id=project_id,
|
||||||
),
|
)
|
||||||
epoch=int(timezone.now().timestamp())
|
state = State.objects.filter(
|
||||||
)
|
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||||
issue_serializer.save()
|
).first()
|
||||||
else:
|
if state is not None:
|
||||||
return Response(
|
issue.state = state
|
||||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
issue.save()
|
||||||
|
|
||||||
|
# Update the issue state if it is accepted
|
||||||
|
if serializer.data["status"] in [1]:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
pk=inbox_issue.issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only project admins and members can edit inbox issue attributes
|
# Update the issue state only if it is in triage state
|
||||||
if project_member.role > 10:
|
if issue.state.name == "Triage":
|
||||||
serializer = InboxIssueSerializer(
|
# Move to default state
|
||||||
inbox_issue, data=request.data, partial=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
# Update the issue state if the issue is rejected or marked as duplicate
|
|
||||||
if serializer.data["status"] in [-1, 2]:
|
|
||||||
issue = Issue.objects.get(
|
|
||||||
pk=inbox_issue.issue_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id, default=True
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
# Update the issue state if it is accepted
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
if serializer.data["status"] in [1]:
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
issue = Issue.objects.get(
|
else:
|
||||||
pk=inbox_issue.issue_id,
|
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update the issue state only if it is in triage state
|
|
||||||
if issue.state.name == "Triage":
|
|
||||||
# Move to default state
|
|
||||||
state = State.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, default=True
|
|
||||||
).first()
|
|
||||||
if state is not None:
|
|
||||||
issue.state = state
|
|
||||||
issue.save()
|
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
|
|
||||||
except InboxIssue.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Inbox Issue does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
inbox_issue = InboxIssue.objects.get(
|
||||||
inbox_issue = InboxIssue.objects.get(
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
)
|
||||||
)
|
issue = Issue.objects.get(
|
||||||
issue = Issue.objects.get(
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
)
|
||||||
)
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
inbox_issue = InboxIssue.objects.get(
|
||||||
inbox_issue = InboxIssue.objects.get(
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
)
|
||||||
)
|
# Get the project member
|
||||||
# Get the project member
|
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
|
||||||
|
|
||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
inbox_issue.delete()
|
# Check the issue status
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
except InboxIssue.DoesNotExist:
|
# Delete the issue also
|
||||||
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
|
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
inbox_issue.delete()
|
||||||
return Response(
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InboxIssuePublicViewSet(BaseViewSet):
|
class InboxIssuePublicViewSet(BaseViewSet):
|
||||||
@ -408,242 +364,197 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return InboxIssue.objects.none()
|
return InboxIssue.objects.none()
|
||||||
|
|
||||||
def list(self, request, slug, project_id, inbox_id):
|
def list(self, request, slug, project_id, inbox_id):
|
||||||
try:
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
if project_deploy_board.inbox is None:
|
||||||
if project_deploy_board.inbox is None:
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_inbox__inbox_id=inbox_id,
|
issue_inbox__inbox_id=inbox_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(bridge_id=F("issue_inbox__id"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels")
|
||||||
|
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.order_by()
|
||||||
.annotate(bridge_id=F("issue_inbox__id"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.values("count")
|
||||||
.prefetch_related("assignees", "labels")
|
)
|
||||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
.prefetch_related(
|
||||||
.annotate(
|
Prefetch(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
"issue_inbox",
|
||||||
.order_by()
|
queryset=InboxIssue.objects.only(
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
"status", "duplicate_to", "snoozed_till", "source"
|
||||||
.values("count")
|
),
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_inbox",
|
|
||||||
queryset=InboxIssue.objects.only(
|
|
||||||
"status", "duplicate_to", "snoozed_till", "source"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
)
|
||||||
return Response(
|
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||||
issues_data,
|
return Response(
|
||||||
status=status.HTTP_200_OK,
|
issues_data,
|
||||||
)
|
status=status.HTTP_200_OK,
|
||||||
except ProjectDeployBoard.DoesNotExist:
|
)
|
||||||
return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
try:
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
if project_deploy_board.inbox is None:
|
||||||
if project_deploy_board.inbox is None:
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for valid priority
|
|
||||||
if not request.data.get("issue", {}).get("priority", "none") in [
|
|
||||||
"low",
|
|
||||||
"medium",
|
|
||||||
"high",
|
|
||||||
"urgent",
|
|
||||||
"none",
|
|
||||||
]:
|
|
||||||
return Response(
|
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create or get state
|
|
||||||
state, _ = State.objects.get_or_create(
|
|
||||||
name="Triage",
|
|
||||||
group="backlog",
|
|
||||||
description="Default state for managing all Inbox Issues",
|
|
||||||
project_id=project_id,
|
|
||||||
color="#ff7700",
|
|
||||||
)
|
|
||||||
|
|
||||||
# create an issue
|
|
||||||
issue = Issue.objects.create(
|
|
||||||
name=request.data.get("issue", {}).get("name"),
|
|
||||||
description=request.data.get("issue", {}).get("description", {}),
|
|
||||||
description_html=request.data.get("issue", {}).get(
|
|
||||||
"description_html", "<p></p>"
|
|
||||||
),
|
|
||||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
|
||||||
project_id=project_id,
|
|
||||||
state=state,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create an Issue Activity
|
|
||||||
issue_activity.delay(
|
|
||||||
type="issue.activity.created",
|
|
||||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=str(issue.id),
|
|
||||||
project_id=str(project_id),
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp())
|
|
||||||
)
|
|
||||||
# create an inbox issue
|
|
||||||
InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox_id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue=issue,
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for valid priority
|
||||||
|
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"urgent",
|
||||||
|
"none",
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or get state
|
||||||
|
state, _ = State.objects.get_or_create(
|
||||||
|
name="Triage",
|
||||||
|
group="backlog",
|
||||||
|
description="Default state for managing all Inbox Issues",
|
||||||
|
project_id=project_id,
|
||||||
|
color="#ff7700",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create an issue
|
||||||
|
issue = Issue.objects.create(
|
||||||
|
name=request.data.get("issue", {}).get("name"),
|
||||||
|
description=request.data.get("issue", {}).get("description", {}),
|
||||||
|
description_html=request.data.get("issue", {}).get(
|
||||||
|
"description_html", "<p></p>"
|
||||||
|
),
|
||||||
|
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||||
|
project_id=project_id,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an Issue Activity
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.created",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
# create an inbox issue
|
||||||
|
InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox_id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue=issue,
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
if project_deploy_board.inbox is None:
|
||||||
if project_deploy_board.inbox is None:
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
|
|
||||||
|
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# viewers and guests since only viewers and guests
|
# viewers and guests since only viewers and guests
|
||||||
issue_data = {
|
issue_data = {
|
||||||
"name": issue_data.get("name", issue.name),
|
"name": issue_data.get("name", issue.name),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
"description_html": issue_data.get("description_html", issue.description_html),
|
||||||
"description": issue_data.get("description", issue.description)
|
"description": issue_data.get("description", issue.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(
|
||||||
issue, data=issue_data, partial=True
|
issue, data=issue_data, partial=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if issue_serializer.is_valid():
|
if issue_serializer.is_valid():
|
||||||
current_instance = issue
|
current_instance = issue
|
||||||
# Log all the updates
|
# Log all the updates
|
||||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||||
if issue is not None:
|
if issue is not None:
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
requested_data=requested_data,
|
requested_data=requested_data,
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except InboxIssue.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Inbox Issue does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
if project_deploy_board.inbox is None:
|
||||||
if project_deploy_board.inbox is None:
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
inbox_issue = InboxIssue.objects.get(
|
||||||
inbox_issue = InboxIssue.objects.get(
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
)
|
||||||
)
|
issue = Issue.objects.get(
|
||||||
issue = Issue.objects.get(
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
)
|
||||||
)
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||||
try:
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
if project_deploy_board.inbox is None:
|
||||||
if project_deploy_board.inbox is None:
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
inbox_issue.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except InboxIssue.DoesNotExist:
|
|
||||||
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
inbox_issue.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
@ -33,66 +32,40 @@ class IntegrationViewSet(BaseViewSet):
|
|||||||
model = Integration
|
model = Integration
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
try:
|
serializer = IntegrationSerializer(data=request.data)
|
||||||
serializer = IntegrationSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save()
|
||||||
serializer.save()
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def partial_update(self, request, pk):
|
def partial_update(self, request, pk):
|
||||||
try:
|
integration = Integration.objects.get(pk=pk)
|
||||||
integration = Integration.objects.get(pk=pk)
|
if integration.verified:
|
||||||
if integration.verified:
|
|
||||||
return Response(
|
|
||||||
{"error": "Verified integrations cannot be updated"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = IntegrationSerializer(
|
|
||||||
integration, data=request.data, partial=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
except Integration.DoesNotExist:
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Integration Does not exist"},
|
{"error": "Verified integrations cannot be updated"},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, pk):
|
serializer = IntegrationSerializer(
|
||||||
try:
|
integration, data=request.data, partial=True
|
||||||
integration = Integration.objects.get(pk=pk)
|
)
|
||||||
if integration.verified:
|
|
||||||
return Response(
|
|
||||||
{"error": "Verified integrations cannot be updated"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
integration.delete()
|
if serializer.is_valid():
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
serializer.save()
|
||||||
except Integration.DoesNotExist:
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def destroy(self, request, pk):
|
||||||
|
integration = Integration.objects.get(pk=pk)
|
||||||
|
if integration.verified:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Integration Does not exist"},
|
{"error": "Verified integrations cannot be updated"},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
integration.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceIntegrationViewSet(BaseViewSet):
|
class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||||
serializer_class = WorkspaceIntegrationSerializer
|
serializer_class = WorkspaceIntegrationSerializer
|
||||||
@ -111,119 +84,81 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, provider):
|
def create(self, request, slug, provider):
|
||||||
try:
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
integration = Integration.objects.get(provider=provider)
|
||||||
integration = Integration.objects.get(provider=provider)
|
config = {}
|
||||||
config = {}
|
if provider == "github":
|
||||||
if provider == "github":
|
installation_id = request.data.get("installation_id", None)
|
||||||
installation_id = request.data.get("installation_id", None)
|
if not installation_id:
|
||||||
if not installation_id:
|
|
||||||
return Response(
|
|
||||||
{"error": "Installation ID is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
metadata = get_github_metadata(installation_id)
|
|
||||||
config = {"installation_id": installation_id}
|
|
||||||
|
|
||||||
if provider == "slack":
|
|
||||||
metadata = request.data.get("metadata", {})
|
|
||||||
access_token = metadata.get("access_token", False)
|
|
||||||
team_id = metadata.get("team", {}).get("id", False)
|
|
||||||
if not metadata or not access_token or not team_id:
|
|
||||||
return Response(
|
|
||||||
{"error": "Access token and team id is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
config = {"team_id": team_id, "access_token": access_token}
|
|
||||||
|
|
||||||
# Create a bot user
|
|
||||||
bot_user = User.objects.create(
|
|
||||||
email=f"{uuid.uuid4().hex}@plane.so",
|
|
||||||
username=uuid.uuid4().hex,
|
|
||||||
password=make_password(uuid.uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
is_bot=True,
|
|
||||||
first_name=integration.title,
|
|
||||||
avatar=integration.avatar_url
|
|
||||||
if integration.avatar_url is not None
|
|
||||||
else "",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create an API Token for the bot user
|
|
||||||
api_token = APIToken.objects.create(
|
|
||||||
user=bot_user,
|
|
||||||
user_type=1, # bot user
|
|
||||||
workspace=workspace,
|
|
||||||
)
|
|
||||||
|
|
||||||
workspace_integration = WorkspaceIntegration.objects.create(
|
|
||||||
workspace=workspace,
|
|
||||||
integration=integration,
|
|
||||||
actor=bot_user,
|
|
||||||
api_token=api_token,
|
|
||||||
metadata=metadata,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add bot user as a member of workspace
|
|
||||||
_ = WorkspaceMember.objects.create(
|
|
||||||
workspace=workspace_integration.workspace,
|
|
||||||
member=bot_user,
|
|
||||||
role=20,
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
WorkspaceIntegrationSerializer(workspace_integration).data,
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
|
||||||
if "already exists" in str(e):
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Integration is already active in the workspace"},
|
{"error": "Installation ID is required"},
|
||||||
status=status.HTTP_410_GONE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
except (Workspace.DoesNotExist, Integration.DoesNotExist) as e:
|
metadata = get_github_metadata(installation_id)
|
||||||
capture_exception(e)
|
config = {"installation_id": installation_id}
|
||||||
return Response(
|
|
||||||
{"error": "Workspace or Integration not found"},
|
if provider == "slack":
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
metadata = request.data.get("metadata", {})
|
||||||
)
|
access_token = metadata.get("access_token", False)
|
||||||
except Exception as e:
|
team_id = metadata.get("team", {}).get("id", False)
|
||||||
capture_exception(e)
|
if not metadata or not access_token or not team_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Access token and team id is required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
config = {"team_id": team_id, "access_token": access_token}
|
||||||
|
|
||||||
|
# Create a bot user
|
||||||
|
bot_user = User.objects.create(
|
||||||
|
email=f"{uuid.uuid4().hex}@plane.so",
|
||||||
|
username=uuid.uuid4().hex,
|
||||||
|
password=make_password(uuid.uuid4().hex),
|
||||||
|
is_password_autoset=True,
|
||||||
|
is_bot=True,
|
||||||
|
first_name=integration.title,
|
||||||
|
avatar=integration.avatar_url
|
||||||
|
if integration.avatar_url is not None
|
||||||
|
else "",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an API Token for the bot user
|
||||||
|
api_token = APIToken.objects.create(
|
||||||
|
user=bot_user,
|
||||||
|
user_type=1, # bot user
|
||||||
|
workspace=workspace,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.create(
|
||||||
|
workspace=workspace,
|
||||||
|
integration=integration,
|
||||||
|
actor=bot_user,
|
||||||
|
api_token=api_token,
|
||||||
|
metadata=metadata,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add bot user as a member of workspace
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace=workspace_integration.workspace,
|
||||||
|
member=bot_user,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
WorkspaceIntegrationSerializer(workspace_integration).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, pk):
|
def destroy(self, request, slug, pk):
|
||||||
try:
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
pk=pk, workspace__slug=slug
|
||||||
pk=pk, workspace__slug=slug
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if workspace_integration.integration.provider == "github":
|
if workspace_integration.integration.provider == "github":
|
||||||
installation_id = workspace_integration.config.get(
|
installation_id = workspace_integration.config.get(
|
||||||
"installation_id", False
|
"installation_id", False
|
||||||
)
|
|
||||||
if installation_id:
|
|
||||||
delete_github_installation(installation_id=installation_id)
|
|
||||||
|
|
||||||
workspace_integration.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
except WorkspaceIntegration.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace Integration Does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
if installation_id:
|
||||||
|
delete_github_installation(installation_id=installation_id)
|
||||||
|
|
||||||
|
workspace_integration.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -30,31 +30,25 @@ class GithubRepositoriesEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, workspace_integration_id):
|
def get(self, request, slug, workspace_integration_id):
|
||||||
try:
|
page = request.GET.get("page", 1)
|
||||||
page = request.GET.get("page", 1)
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
workspace__slug=slug, pk=workspace_integration_id
|
||||||
workspace__slug=slug, pk=workspace_integration_id
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if workspace_integration.integration.provider != "github":
|
if workspace_integration.integration.provider != "github":
|
||||||
return Response(
|
|
||||||
{"error": "Not a github integration"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
|
|
||||||
repositories_url = (
|
|
||||||
workspace_integration.metadata["repositories_url"]
|
|
||||||
+ f"?per_page=100&page={page}"
|
|
||||||
)
|
|
||||||
repositories = get_github_repos(access_tokens_url, repositories_url)
|
|
||||||
return Response(repositories, status=status.HTTP_200_OK)
|
|
||||||
except WorkspaceIntegration.DoesNotExist:
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace Integration Does not exists"},
|
{"error": "Not a github integration"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
|
||||||
|
repositories_url = (
|
||||||
|
workspace_integration.metadata["repositories_url"]
|
||||||
|
+ f"?per_page=100&page={page}"
|
||||||
|
)
|
||||||
|
repositories = get_github_repos(access_tokens_url, repositories_url)
|
||||||
|
return Response(repositories, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class GithubRepositorySyncViewSet(BaseViewSet):
|
class GithubRepositorySyncViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -76,89 +70,76 @@ class GithubRepositorySyncViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, workspace_integration_id):
|
def create(self, request, slug, project_id, workspace_integration_id):
|
||||||
try:
|
name = request.data.get("name", False)
|
||||||
name = request.data.get("name", False)
|
url = request.data.get("url", False)
|
||||||
url = request.data.get("url", False)
|
config = request.data.get("config", {})
|
||||||
config = request.data.get("config", {})
|
repository_id = request.data.get("repository_id", False)
|
||||||
repository_id = request.data.get("repository_id", False)
|
owner = request.data.get("owner", False)
|
||||||
owner = request.data.get("owner", False)
|
|
||||||
|
|
||||||
if not name or not url or not repository_id or not owner:
|
if not name or not url or not repository_id or not owner:
|
||||||
return Response(
|
|
||||||
{"error": "Name, url, repository_id and owner are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the workspace integration
|
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
|
||||||
pk=workspace_integration_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete the old repository object
|
|
||||||
GithubRepositorySync.objects.filter(
|
|
||||||
project_id=project_id, workspace__slug=slug
|
|
||||||
).delete()
|
|
||||||
GithubRepository.objects.filter(
|
|
||||||
project_id=project_id, workspace__slug=slug
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# Create repository
|
|
||||||
repo = GithubRepository.objects.create(
|
|
||||||
name=name,
|
|
||||||
url=url,
|
|
||||||
config=config,
|
|
||||||
repository_id=repository_id,
|
|
||||||
owner=owner,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a Label for github
|
|
||||||
label = Label.objects.filter(
|
|
||||||
name="GitHub",
|
|
||||||
project_id=project_id,
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if label is None:
|
|
||||||
label = Label.objects.create(
|
|
||||||
name="GitHub",
|
|
||||||
project_id=project_id,
|
|
||||||
description="Label to sync Plane issues with GitHub issues",
|
|
||||||
color="#003773",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create repo sync
|
|
||||||
repo_sync = GithubRepositorySync.objects.create(
|
|
||||||
repository=repo,
|
|
||||||
workspace_integration=workspace_integration,
|
|
||||||
actor=workspace_integration.actor,
|
|
||||||
credentials=request.data.get("credentials", {}),
|
|
||||||
project_id=project_id,
|
|
||||||
label=label,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add bot as a member in the project
|
|
||||||
_ = ProjectMember.objects.get_or_create(
|
|
||||||
member=workspace_integration.actor, role=20, project_id=project_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return Response
|
|
||||||
return Response(
|
return Response(
|
||||||
GithubRepositorySyncSerializer(repo_sync).data,
|
{"error": "Name, url, repository_id and owner are required"},
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
except WorkspaceIntegration.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace Integration does not exist"},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get the workspace integration
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
|
pk=workspace_integration_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete the old repository object
|
||||||
|
GithubRepositorySync.objects.filter(
|
||||||
|
project_id=project_id, workspace__slug=slug
|
||||||
|
).delete()
|
||||||
|
GithubRepository.objects.filter(
|
||||||
|
project_id=project_id, workspace__slug=slug
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
# Create repository
|
||||||
|
repo = GithubRepository.objects.create(
|
||||||
|
name=name,
|
||||||
|
url=url,
|
||||||
|
config=config,
|
||||||
|
repository_id=repository_id,
|
||||||
|
owner=owner,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a Label for github
|
||||||
|
label = Label.objects.filter(
|
||||||
|
name="GitHub",
|
||||||
|
project_id=project_id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if label is None:
|
||||||
|
label = Label.objects.create(
|
||||||
|
name="GitHub",
|
||||||
|
project_id=project_id,
|
||||||
|
description="Label to sync Plane issues with GitHub issues",
|
||||||
|
color="#003773",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create repo sync
|
||||||
|
repo_sync = GithubRepositorySync.objects.create(
|
||||||
|
repository=repo,
|
||||||
|
workspace_integration=workspace_integration,
|
||||||
|
actor=workspace_integration.actor,
|
||||||
|
credentials=request.data.get("credentials", {}),
|
||||||
|
project_id=project_id,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add bot as a member in the project
|
||||||
|
_ = ProjectMember.objects.get_or_create(
|
||||||
|
member=workspace_integration.actor, role=20, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return Response
|
||||||
|
return Response(
|
||||||
|
GithubRepositorySyncSerializer(repo_sync).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GithubIssueSyncViewSet(BaseViewSet):
|
class GithubIssueSyncViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -177,42 +158,30 @@ class GithubIssueSyncViewSet(BaseViewSet):
|
|||||||
|
|
||||||
class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
|
class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id, repo_sync_id):
|
def post(self, request, slug, project_id, repo_sync_id):
|
||||||
try:
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
||||||
|
|
||||||
github_issue_syncs = request.data.get("github_issue_syncs", [])
|
github_issue_syncs = request.data.get("github_issue_syncs", [])
|
||||||
github_issue_syncs = GithubIssueSync.objects.bulk_create(
|
github_issue_syncs = GithubIssueSync.objects.bulk_create(
|
||||||
[
|
[
|
||||||
GithubIssueSync(
|
GithubIssueSync(
|
||||||
issue_id=github_issue_sync.get("issue"),
|
issue_id=github_issue_sync.get("issue"),
|
||||||
repo_issue_id=github_issue_sync.get("repo_issue_id"),
|
repo_issue_id=github_issue_sync.get("repo_issue_id"),
|
||||||
issue_url=github_issue_sync.get("issue_url"),
|
issue_url=github_issue_sync.get("issue_url"),
|
||||||
github_issue_id=github_issue_sync.get("github_issue_id"),
|
github_issue_id=github_issue_sync.get("github_issue_id"),
|
||||||
repository_sync_id=repo_sync_id,
|
repository_sync_id=repo_sync_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
updated_by=request.user,
|
updated_by=request.user,
|
||||||
)
|
)
|
||||||
for github_issue_sync in github_issue_syncs
|
for github_issue_sync in github_issue_syncs
|
||||||
],
|
],
|
||||||
batch_size=100,
|
batch_size=100,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True)
|
serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
except Project.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Project does not exist"},
|
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GithubCommentSyncViewSet(BaseViewSet):
|
class GithubCommentSyncViewSet(BaseViewSet):
|
||||||
|
@ -32,42 +32,25 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, workspace_integration_id):
|
def create(self, request, slug, project_id, workspace_integration_id):
|
||||||
try:
|
serializer = SlackProjectSyncSerializer(data=request.data)
|
||||||
serializer = SlackProjectSyncSerializer(data=request.data)
|
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
|
workspace__slug=slug, pk=workspace_integration_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_integration_id=workspace_integration_id,
|
||||||
|
)
|
||||||
|
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
workspace__slug=slug, pk=workspace_integration_id
|
pk=workspace_integration_id, workspace__slug=slug
|
||||||
)
|
)
|
||||||
|
|
||||||
if serializer.is_valid():
|
_ = ProjectMember.objects.get_or_create(
|
||||||
serializer.save(
|
member=workspace_integration.actor, role=20, project_id=project_id
|
||||||
project_id=project_id,
|
|
||||||
workspace_integration_id=workspace_integration_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
workspace_integration = WorkspaceIntegration.objects.get(
|
|
||||||
pk=workspace_integration_id, workspace__slug=slug
|
|
||||||
)
|
|
||||||
|
|
||||||
_ = ProjectMember.objects.get_or_create(
|
|
||||||
member=workspace_integration.actor, role=20, project_id=project_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except IntegrityError:
|
|
||||||
return Response(
|
|
||||||
{"error": "Slack is already enabled for the project"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except WorkspaceIntegration.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace Integration does not exist"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -141,173 +141,144 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.order_by(order_by, "name")
|
.order_by(order_by, "name")
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def create(self, request, slug, project_id):
|
||||||
module_issues = list(
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
|
serializer = ModuleWriteSerializer(
|
||||||
"issue", flat=True
|
data=request.data, context={"project": project}
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
|
queryset = self.get_queryset().get(pk=pk)
|
||||||
|
|
||||||
|
assignee_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_module__module_id=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.annotate(first_name=F("assignees__first_name"))
|
||||||
|
.annotate(last_name=F("assignees__last_name"))
|
||||||
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
|
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("first_name", "last_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
label_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_module__module_id=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.annotate(label_name=F("labels__name"))
|
||||||
|
.annotate(color=F("labels__color"))
|
||||||
|
.annotate(label_id=F("labels__id"))
|
||||||
|
.values("label_name", "color", "label_id")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("label_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
data = ModuleSerializer(queryset).data
|
||||||
|
data["distribution"] = {
|
||||||
|
"assignees": assignee_distribution,
|
||||||
|
"labels": label_distribution,
|
||||||
|
"completion_chart": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if queryset.start_date and queryset.target_date:
|
||||||
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
|
queryset=queryset, slug=slug, project_id=project_id, module_id=pk
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, pk):
|
||||||
|
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
|
module_issues = list(
|
||||||
|
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||||
|
)
|
||||||
|
module.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(
|
||||||
{
|
{
|
||||||
"module_id": str(self.kwargs.get("pk")),
|
"module_id": str(pk),
|
||||||
"issues": [str(issue_id) for issue_id in module_issues],
|
"issues": [str(issue_id) for issue_id in module_issues],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(pk),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return super().perform_destroy(instance)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
|
||||||
serializer = ModuleWriteSerializer(
|
|
||||||
data=request.data, context={"project": project}
|
|
||||||
)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
except Project.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
|
||||||
if "already exists" in str(e):
|
|
||||||
return Response(
|
|
||||||
{"name": "The module name is already taken"},
|
|
||||||
status=status.HTTP_410_GONE,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
|
||||||
try:
|
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
|
||||||
|
|
||||||
assignee_distribution = (
|
|
||||||
Issue.objects.filter(
|
|
||||||
issue_module__module_id=pk,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
.annotate(first_name=F("assignees__first_name"))
|
|
||||||
.annotate(last_name=F("assignees__last_name"))
|
|
||||||
.annotate(assignee_id=F("assignees__id"))
|
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
|
||||||
.values(
|
|
||||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
total_issues=Count(
|
|
||||||
"assignee_id",
|
|
||||||
filter=Q(
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"assignee_id",
|
|
||||||
filter=Q(
|
|
||||||
completed_at__isnull=False,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
pending_issues=Count(
|
|
||||||
"assignee_id",
|
|
||||||
filter=Q(
|
|
||||||
completed_at__isnull=True,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("first_name", "last_name")
|
|
||||||
)
|
|
||||||
|
|
||||||
label_distribution = (
|
|
||||||
Issue.objects.filter(
|
|
||||||
issue_module__module_id=pk,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
.annotate(label_name=F("labels__name"))
|
|
||||||
.annotate(color=F("labels__color"))
|
|
||||||
.annotate(label_id=F("labels__id"))
|
|
||||||
.values("label_name", "color", "label_id")
|
|
||||||
.annotate(
|
|
||||||
total_issues=Count(
|
|
||||||
"label_id",
|
|
||||||
filter=Q(
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"label_id",
|
|
||||||
filter=Q(
|
|
||||||
completed_at__isnull=False,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
pending_issues=Count(
|
|
||||||
"label_id",
|
|
||||||
filter=Q(
|
|
||||||
completed_at__isnull=True,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("label_name")
|
|
||||||
)
|
|
||||||
|
|
||||||
data = ModuleSerializer(queryset).data
|
|
||||||
data["distribution"] = {
|
|
||||||
"assignees": assignee_distribution,
|
|
||||||
"labels": label_distribution,
|
|
||||||
"completion_chart": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if queryset.start_date and queryset.target_date:
|
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
|
||||||
queryset=queryset, slug=slug, project_id=project_id, module_id=pk
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueViewSet(BaseViewSet):
|
class ModuleIssueViewSet(BaseViewSet):
|
||||||
@ -329,23 +300,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
module_id=self.kwargs.get("module_id"),
|
module_id=self.kwargs.get("module_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
issue_activity.delay(
|
|
||||||
type="module.activity.deleted",
|
|
||||||
requested_data=json.dumps(
|
|
||||||
{
|
|
||||||
"module_id": str(self.kwargs.get("module_id")),
|
|
||||||
"issues": [str(instance.issue_id)],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actor_id=str(self.request.user.id),
|
|
||||||
issue_id=str(self.kwargs.get("pk", None)),
|
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp())
|
|
||||||
)
|
|
||||||
return super().perform_destroy(instance)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
@ -371,162 +325,163 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id, module_id):
|
def list(self, request, slug, project_id, module_id):
|
||||||
try:
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
group_by = request.GET.get("group_by", False)
|
||||||
group_by = request.GET.get("group_by", False)
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
sub_group_by = request.GET.get("sub_group_by", False)
|
filters = issue_filters(request.query_params, "GET")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
issues = (
|
||||||
issues = (
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
.annotate(
|
||||||
.annotate(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
.order_by()
|
||||||
.order_by()
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.values("count")
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(bridge_id=F("issue_module__id"))
|
|
||||||
.filter(project_id=project_id)
|
|
||||||
.filter(workspace__slug=slug)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.order_by(order_by)
|
|
||||||
.filter(**filters)
|
|
||||||
.annotate(
|
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.annotate(bridge_id=F("issue_module__id"))
|
||||||
issues_data = IssueStateSerializer(issues, many=True).data
|
.filter(project_id=project_id)
|
||||||
|
.filter(workspace__slug=slug)
|
||||||
if sub_group_by and sub_group_by == group_by:
|
.select_related("project")
|
||||||
return Response(
|
.select_related("workspace")
|
||||||
{"error": "Group by and sub group by cannot be same"},
|
.select_related("state")
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
.select_related("parent")
|
||||||
)
|
.prefetch_related("assignees")
|
||||||
|
.prefetch_related("labels")
|
||||||
if group_by:
|
.order_by(order_by)
|
||||||
return Response(
|
.filter(**filters)
|
||||||
group_results(issues_data, group_by, sub_group_by),
|
.annotate(
|
||||||
status=status.HTTP_200_OK,
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
)
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
return Response(
|
.values("count")
|
||||||
issues_data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
.annotate(
|
||||||
capture_exception(e)
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
issues_data = IssueStateSerializer(issues, many=True).data
|
||||||
|
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues_data, group_by, sub_group_by),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
issues_data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, module_id):
|
def create(self, request, slug, project_id, module_id):
|
||||||
try:
|
issues = request.data.get("issues", [])
|
||||||
issues = request.data.get("issues", [])
|
if not len(issues):
|
||||||
if not len(issues):
|
return Response(
|
||||||
return Response(
|
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
module = Module.objects.get(
|
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
|
||||||
)
|
)
|
||||||
|
module = Module.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||||
|
)
|
||||||
|
|
||||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
||||||
|
|
||||||
update_module_issue_activity = []
|
update_module_issue_activity = []
|
||||||
records_to_update = []
|
records_to_update = []
|
||||||
record_to_create = []
|
record_to_create = []
|
||||||
|
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
module_issue = [
|
module_issue = [
|
||||||
module_issue
|
module_issue
|
||||||
for module_issue in module_issues
|
for module_issue in module_issues
|
||||||
if str(module_issue.issue_id) in issues
|
if str(module_issue.issue_id) in issues
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(module_issue):
|
if len(module_issue):
|
||||||
if module_issue[0].module_id != module_id:
|
if module_issue[0].module_id != module_id:
|
||||||
update_module_issue_activity.append(
|
update_module_issue_activity.append(
|
||||||
{
|
{
|
||||||
"old_module_id": str(module_issue[0].module_id),
|
"old_module_id": str(module_issue[0].module_id),
|
||||||
"new_module_id": str(module_id),
|
"new_module_id": str(module_id),
|
||||||
"issue_id": str(module_issue[0].issue_id),
|
"issue_id": str(module_issue[0].issue_id),
|
||||||
}
|
}
|
||||||
)
|
|
||||||
module_issue[0].module_id = module_id
|
|
||||||
records_to_update.append(module_issue[0])
|
|
||||||
else:
|
|
||||||
record_to_create.append(
|
|
||||||
ModuleIssue(
|
|
||||||
module=module,
|
|
||||||
issue_id=issue,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace=module.workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
module_issue[0].module_id = module_id
|
||||||
|
records_to_update.append(module_issue[0])
|
||||||
|
else:
|
||||||
|
record_to_create.append(
|
||||||
|
ModuleIssue(
|
||||||
|
module=module,
|
||||||
|
issue_id=issue,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace=module.workspace,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModuleIssue.objects.bulk_create(
|
ModuleIssue.objects.bulk_create(
|
||||||
record_to_create,
|
record_to_create,
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
ModuleIssue.objects.bulk_update(
|
ModuleIssue.objects.bulk_update(
|
||||||
records_to_update,
|
records_to_update,
|
||||||
["module"],
|
["module"],
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Capture Issue Activity
|
# Capture Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.created",
|
type="module.activity.created",
|
||||||
requested_data=json.dumps({"modules_list": issues}),
|
requested_data=json.dumps({"modules_list": issues}),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
{
|
{
|
||||||
"updated_module_issues": update_module_issue_activity,
|
"updated_module_issues": update_module_issue_activity,
|
||||||
"created_module_issues": serializers.serialize(
|
"created_module_issues": serializers.serialize(
|
||||||
"json", record_to_create
|
"json", record_to_create
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
ModuleIssueSerializer(self.get_queryset(), many=True).data,
|
ModuleIssueSerializer(self.get_queryset(), many=True).data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Module.DoesNotExist:
|
|
||||||
return Response(
|
def destroy(self, request, slug, project_id, module_id, pk):
|
||||||
{"error": "Module Does not exists"}, status=status.HTTP_400_BAD_REQUEST
|
module_issue = ModuleIssue.objects.get(
|
||||||
)
|
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||||
except Exception as e:
|
)
|
||||||
capture_exception(e)
|
module_issue.delete()
|
||||||
return Response(
|
issue_activity.delay(
|
||||||
{"error": "Something went wrong please try again later"},
|
type="module.activity.deleted",
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
requested_data=json.dumps(
|
||||||
)
|
{
|
||||||
|
"module_id": str(module_id),
|
||||||
|
"issues": [str(module_issue.issue_id)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(pk),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class ModuleLinkViewSet(BaseViewSet):
|
class ModuleLinkViewSet(BaseViewSet):
|
||||||
@ -570,49 +525,18 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
serializer = ModuleFavoriteSerializer(data=request.data)
|
||||||
serializer = ModuleFavoriteSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save(user=request.user, project_id=project_id)
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except IntegrityError as e:
|
|
||||||
if "already exists" in str(e):
|
|
||||||
return Response(
|
|
||||||
{"error": "The module is already added to favorites"},
|
|
||||||
status=status.HTTP_410_GONE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id):
|
def destroy(self, request, slug, project_id, module_id):
|
||||||
try:
|
module_favorite = ModuleFavorite.objects.get(
|
||||||
module_favorite = ModuleFavorite.objects.get(
|
project=project_id,
|
||||||
project=project_id,
|
user=request.user,
|
||||||
user=request.user,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
module_id=module_id,
|
||||||
module_id=module_id,
|
)
|
||||||
)
|
module_favorite.delete()
|
||||||
module_favorite.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except ModuleFavorite.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Module is not in favorites"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -36,328 +36,239 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
try:
|
# Get query parameters
|
||||||
snoozed = request.GET.get("snoozed", "false")
|
snoozed = request.GET.get("snoozed", "false")
|
||||||
archived = request.GET.get("archived", "false")
|
archived = request.GET.get("archived", "false")
|
||||||
read = request.GET.get("read", "true")
|
read = request.GET.get("read", "true")
|
||||||
|
type = request.GET.get("type", "all")
|
||||||
|
|
||||||
# Filter type
|
notifications = (
|
||||||
type = request.GET.get("type", "all")
|
Notification.objects.filter(
|
||||||
|
workspace__slug=slug, receiver_id=request.user.id
|
||||||
notifications = (
|
|
||||||
Notification.objects.filter(
|
|
||||||
workspace__slug=slug, receiver_id=request.user.id
|
|
||||||
)
|
|
||||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
|
||||||
.order_by("snoozed_till", "-created_at")
|
|
||||||
)
|
)
|
||||||
|
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||||
|
.order_by("snoozed_till", "-created_at")
|
||||||
|
)
|
||||||
|
|
||||||
# Filter for snoozed notifications
|
# Filters based on query parameters
|
||||||
if snoozed == "false":
|
snoozed_filters = {
|
||||||
notifications = notifications.filter(
|
"true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False),
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
"false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||||
)
|
}
|
||||||
|
|
||||||
if snoozed == "true":
|
notifications = notifications.filter(snoozed_filters[snoozed])
|
||||||
notifications = notifications.filter(
|
|
||||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
if read == "false":
|
archived_filters = {
|
||||||
notifications = notifications.filter(read_at__isnull=True)
|
"true": Q(archived_at__isnull=False),
|
||||||
|
"false": Q(archived_at__isnull=True),
|
||||||
|
}
|
||||||
|
|
||||||
# Filter for archived or unarchive
|
notifications = notifications.filter(archived_filters[archived])
|
||||||
if archived == "false":
|
|
||||||
notifications = notifications.filter(archived_at__isnull=True)
|
|
||||||
|
|
||||||
if archived == "true":
|
if read == "false":
|
||||||
notifications = notifications.filter(archived_at__isnull=False)
|
notifications = notifications.filter(read_at__isnull=True)
|
||||||
|
|
||||||
# Subscribed issues
|
# Subscribed issues
|
||||||
if type == "watching":
|
if type == "watching":
|
||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
# Assigned Issues
|
||||||
|
if type == "assigned":
|
||||||
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
# Created issues
|
||||||
|
if type == "created":
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug, member=request.user, role__lt=15
|
||||||
|
).exists():
|
||||||
|
notifications = Notification.objects.none()
|
||||||
|
else:
|
||||||
|
issue_ids = Issue.objects.filter(
|
||||||
|
workspace__slug=slug, created_by=request.user
|
||||||
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
# Assigned Issues
|
# Pagination
|
||||||
if type == "assigned":
|
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
return self.paginate(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
request=request,
|
||||||
).values_list("issue_id", flat=True)
|
queryset=(notifications),
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
on_results=lambda notifications: NotificationSerializer(
|
||||||
|
notifications, many=True
|
||||||
# Created issues
|
).data,
|
||||||
if type == "created":
|
|
||||||
if WorkspaceMember.objects.filter(
|
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
|
||||||
).exists():
|
|
||||||
notifications = Notification.objects.none()
|
|
||||||
else:
|
|
||||||
issue_ids = Issue.objects.filter(
|
|
||||||
workspace__slug=slug, created_by=request.user
|
|
||||||
).values_list("pk", flat=True)
|
|
||||||
notifications = notifications.filter(
|
|
||||||
entity_identifier__in=issue_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
# Pagination
|
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
|
||||||
return self.paginate(
|
|
||||||
request=request,
|
|
||||||
queryset=(notifications),
|
|
||||||
on_results=lambda notifications: NotificationSerializer(
|
|
||||||
notifications, many=True
|
|
||||||
).data,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = NotificationSerializer(notifications, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
serializer = NotificationSerializer(notifications, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, pk):
|
def partial_update(self, request, slug, pk):
|
||||||
try:
|
notification = Notification.objects.get(
|
||||||
notification = Notification.objects.get(
|
workspace__slug=slug, pk=pk, receiver=request.user
|
||||||
workspace__slug=slug, pk=pk, receiver=request.user
|
)
|
||||||
)
|
# Only read_at and snoozed_till can be updated
|
||||||
# Only read_at and snoozed_till can be updated
|
notification_data = {
|
||||||
notification_data = {
|
"snoozed_till": request.data.get("snoozed_till", None),
|
||||||
"snoozed_till": request.data.get("snoozed_till", None),
|
}
|
||||||
}
|
serializer = NotificationSerializer(
|
||||||
serializer = NotificationSerializer(
|
notification, data=notification_data, partial=True
|
||||||
notification, data=notification_data, partial=True
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except Notification.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Notification does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def mark_read(self, request, slug, pk):
|
def mark_read(self, request, slug, pk):
|
||||||
try:
|
notification = Notification.objects.get(
|
||||||
notification = Notification.objects.get(
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
)
|
||||||
)
|
notification.read_at = timezone.now()
|
||||||
notification.read_at = timezone.now()
|
notification.save()
|
||||||
notification.save()
|
serializer = NotificationSerializer(notification)
|
||||||
serializer = NotificationSerializer(notification)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Notification.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Notification does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def mark_unread(self, request, slug, pk):
|
def mark_unread(self, request, slug, pk):
|
||||||
try:
|
notification = Notification.objects.get(
|
||||||
notification = Notification.objects.get(
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
)
|
||||||
)
|
notification.read_at = None
|
||||||
notification.read_at = None
|
notification.save()
|
||||||
notification.save()
|
serializer = NotificationSerializer(notification)
|
||||||
serializer = NotificationSerializer(notification)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Notification.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Notification does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def archive(self, request, slug, pk):
|
def archive(self, request, slug, pk):
|
||||||
try:
|
notification = Notification.objects.get(
|
||||||
notification = Notification.objects.get(
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
)
|
||||||
)
|
notification.archived_at = timezone.now()
|
||||||
notification.archived_at = timezone.now()
|
notification.save()
|
||||||
notification.save()
|
serializer = NotificationSerializer(notification)
|
||||||
serializer = NotificationSerializer(notification)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Notification.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Notification does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def unarchive(self, request, slug, pk):
|
def unarchive(self, request, slug, pk):
|
||||||
try:
|
notification = Notification.objects.get(
|
||||||
notification = Notification.objects.get(
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
)
|
||||||
)
|
notification.archived_at = None
|
||||||
notification.archived_at = None
|
notification.save()
|
||||||
notification.save()
|
serializer = NotificationSerializer(notification)
|
||||||
serializer = NotificationSerializer(notification)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Notification.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Notification does not exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnreadNotificationEndpoint(BaseAPIView):
|
class UnreadNotificationEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
# Watching Issues Count
|
||||||
# Watching Issues Count
|
watching_issues_count = Notification.objects.filter(
|
||||||
watching_issues_count = Notification.objects.filter(
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
receiver_id=request.user.id,
|
||||||
receiver_id=request.user.id,
|
read_at__isnull=True,
|
||||||
read_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
archived_at__isnull=True,
|
entity_identifier__in=IssueSubscriber.objects.filter(
|
||||||
entity_identifier__in=IssueSubscriber.objects.filter(
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
).values_list("issue_id", flat=True),
|
||||||
).values_list("issue_id", flat=True),
|
).count()
|
||||||
).count()
|
|
||||||
|
|
||||||
# My Issues Count
|
# My Issues Count
|
||||||
my_issues_count = Notification.objects.filter(
|
my_issues_count = Notification.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
receiver_id=request.user.id,
|
receiver_id=request.user.id,
|
||||||
read_at__isnull=True,
|
read_at__isnull=True,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
entity_identifier__in=IssueAssignee.objects.filter(
|
entity_identifier__in=IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True),
|
).values_list("issue_id", flat=True),
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# Created Issues Count
|
# Created Issues Count
|
||||||
created_issues_count = Notification.objects.filter(
|
created_issues_count = Notification.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
receiver_id=request.user.id,
|
receiver_id=request.user.id,
|
||||||
read_at__isnull=True,
|
read_at__isnull=True,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
entity_identifier__in=Issue.objects.filter(
|
entity_identifier__in=Issue.objects.filter(
|
||||||
workspace__slug=slug, created_by=request.user
|
workspace__slug=slug, created_by=request.user
|
||||||
).values_list("pk", flat=True),
|
).values_list("pk", flat=True),
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"watching_issues": watching_issues_count,
|
"watching_issues": watching_issues_count,
|
||||||
"my_issues": my_issues_count,
|
"my_issues": my_issues_count,
|
||||||
"created_issues": created_issues_count,
|
"created_issues": created_issues_count,
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
try:
|
snoozed = request.data.get("snoozed", False)
|
||||||
snoozed = request.data.get("snoozed", False)
|
archived = request.data.get("archived", False)
|
||||||
archived = request.data.get("archived", False)
|
type = request.data.get("type", "all")
|
||||||
type = request.data.get("type", "all")
|
|
||||||
|
|
||||||
notifications = (
|
notifications = (
|
||||||
Notification.objects.filter(
|
Notification.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
receiver_id=request.user.id,
|
receiver_id=request.user.id,
|
||||||
read_at__isnull=True,
|
read_at__isnull=True,
|
||||||
)
|
)
|
||||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||||
.order_by("snoozed_till", "-created_at")
|
.order_by("snoozed_till", "-created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter for snoozed notifications
|
||||||
|
if snoozed:
|
||||||
|
notifications = notifications.filter(
|
||||||
|
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
notifications = notifications.filter(
|
||||||
|
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter for snoozed notifications
|
# Filter for archived or unarchive
|
||||||
if snoozed:
|
if archived:
|
||||||
notifications = notifications.filter(
|
notifications = notifications.filter(archived_at__isnull=False)
|
||||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
else:
|
||||||
)
|
notifications = notifications.filter(archived_at__isnull=True)
|
||||||
else:
|
|
||||||
notifications = notifications.filter(
|
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter for archived or unarchive
|
# Subscribed issues
|
||||||
if archived:
|
if type == "watching":
|
||||||
notifications = notifications.filter(archived_at__isnull=False)
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
else:
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
notifications = notifications.filter(archived_at__isnull=True)
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
# Subscribed issues
|
# Assigned Issues
|
||||||
if type == "watching":
|
if type == "assigned":
|
||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
|
# Created issues
|
||||||
|
if type == "created":
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug, member=request.user, role__lt=15
|
||||||
|
).exists():
|
||||||
|
notifications = Notification.objects.none()
|
||||||
|
else:
|
||||||
|
issue_ids = Issue.objects.filter(
|
||||||
|
workspace__slug=slug, created_by=request.user
|
||||||
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||||
|
|
||||||
# Assigned Issues
|
updated_notifications = []
|
||||||
if type == "assigned":
|
for notification in notifications:
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
notification.read_at = timezone.now()
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
updated_notifications.append(notification)
|
||||||
).values_list("issue_id", flat=True)
|
Notification.objects.bulk_update(
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
updated_notifications, ["read_at"], batch_size=100
|
||||||
|
)
|
||||||
# Created issues
|
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||||
if type == "created":
|
|
||||||
if WorkspaceMember.objects.filter(
|
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
|
||||||
).exists():
|
|
||||||
notifications = Notification.objects.none()
|
|
||||||
else:
|
|
||||||
issue_ids = Issue.objects.filter(
|
|
||||||
workspace__slug=slug, created_by=request.user
|
|
||||||
).values_list("pk", flat=True)
|
|
||||||
notifications = notifications.filter(
|
|
||||||
entity_identifier__in=issue_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_notifications = []
|
|
||||||
for notification in notifications:
|
|
||||||
notification.read_at = timezone.now()
|
|
||||||
updated_notifications.append(notification)
|
|
||||||
Notification.objects.bulk_update(
|
|
||||||
updated_notifications, ["read_at"], batch_size=100
|
|
||||||
)
|
|
||||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -15,6 +15,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# sso authentication
|
# sso authentication
|
||||||
from google.oauth2 import id_token
|
from google.oauth2 import id_token
|
||||||
from google.auth.transport import requests as google_auth_request
|
from google.auth.transport import requests as google_auth_request
|
||||||
@ -186,14 +187,11 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.is_email_verified = email_verified
|
user.is_email_verified = email_verified
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
serialized_user = UserSerializer(user).data
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
"user": serialized_user,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SocialLoginConnection.objects.update_or_create(
|
SocialLoginConnection.objects.update_or_create(
|
||||||
@ -264,14 +262,11 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
serialized_user = UserSerializer(user).data
|
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
"user": serialized_user,
|
|
||||||
"permissions": [],
|
|
||||||
}
|
}
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
@ -304,11 +299,3 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Something went wrong. Please try again later or contact the support team."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
from datetime import timedelta, datetime, date
|
from datetime import timedelta, datetime, date
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.db.models import Exists, OuterRef, Q, Prefetch
|
from django.db.models import Exists, OuterRef, Q, Prefetch
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -78,104 +77,82 @@ class PageViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
serializer = PageSerializer(
|
||||||
serializer = PageSerializer(
|
data=request.data,
|
||||||
data=request.data,
|
context={"project_id": project_id, "owned_by_id": request.user.id},
|
||||||
context={"project_id": project_id, "owned_by_id": request.user.id},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
try:
|
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
# Only update access if the page owner is the requesting user
|
||||||
# Only update access if the page owner is the requesting user
|
if (
|
||||||
if (
|
page.access != request.data.get("access", page.access)
|
||||||
page.access != request.data.get("access", page.access)
|
and page.owned_by_id != request.user.id
|
||||||
and page.owned_by_id != request.user.id
|
):
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Access cannot be updated since this page is owned by someone else"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
serializer = PageSerializer(page, data=request.data, partial=True)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Page.DoesNotExist:
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
{
|
||||||
)
|
"error": "Access cannot be updated since this page is owned by someone else"
|
||||||
except Exception as e:
|
},
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
serializer = PageSerializer(page, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
queryset = self.get_queryset()
|
||||||
queryset = self.get_queryset()
|
page_view = request.GET.get("page_view", False)
|
||||||
page_view = request.GET.get("page_view", False)
|
|
||||||
|
|
||||||
if not page_view:
|
if not page_view:
|
||||||
return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# All Pages
|
# All Pages
|
||||||
if page_view == "all":
|
if page_view == "all":
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# Recent pages
|
# Recent pages
|
||||||
if page_view == "recent":
|
if page_view == "recent":
|
||||||
current_time = date.today()
|
current_time = date.today()
|
||||||
day_before = current_time - timedelta(days=1)
|
day_before = current_time - timedelta(days=1)
|
||||||
todays_pages = queryset.filter(updated_at__date=date.today())
|
todays_pages = queryset.filter(updated_at__date=date.today())
|
||||||
yesterdays_pages = queryset.filter(updated_at__date=day_before)
|
yesterdays_pages = queryset.filter(updated_at__date=day_before)
|
||||||
earlier_this_week = queryset.filter( updated_at__date__range=(
|
earlier_this_week = queryset.filter( updated_at__date__range=(
|
||||||
(timezone.now() - timedelta(days=7)),
|
(timezone.now() - timedelta(days=7)),
|
||||||
(timezone.now() - timedelta(days=2)),
|
(timezone.now() - timedelta(days=2)),
|
||||||
))
|
))
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"today": PageSerializer(todays_pages, many=True).data,
|
"today": PageSerializer(todays_pages, many=True).data,
|
||||||
"yesterday": PageSerializer(yesterdays_pages, many=True).data,
|
"yesterday": PageSerializer(yesterdays_pages, many=True).data,
|
||||||
"earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
|
"earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Favorite Pages
|
# Favorite Pages
|
||||||
if page_view == "favorite":
|
if page_view == "favorite":
|
||||||
queryset = queryset.filter(is_favorite=True)
|
queryset = queryset.filter(is_favorite=True)
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# My pages
|
# My pages
|
||||||
if page_view == "created_by_me":
|
if page_view == "created_by_me":
|
||||||
queryset = queryset.filter(owned_by=request.user)
|
queryset = queryset.filter(owned_by=request.user)
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# Created by other Pages
|
# Created by other Pages
|
||||||
if page_view == "created_by_other":
|
if page_view == "created_by_other":
|
||||||
queryset = queryset.filter(~Q(owned_by=request.user), access=0)
|
queryset = queryset.filter(~Q(owned_by=request.user), access=0)
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
class PageBlockViewSet(BaseViewSet):
|
class PageBlockViewSet(BaseViewSet):
|
||||||
serializer_class = PageBlockSerializer
|
serializer_class = PageBlockSerializer
|
||||||
@ -225,53 +202,21 @@ class PageFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
serializer = PageFavoriteSerializer(data=request.data)
|
||||||
serializer = PageFavoriteSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save(user=request.user, project_id=project_id)
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except IntegrityError as e:
|
|
||||||
if "already exists" in str(e):
|
|
||||||
return Response(
|
|
||||||
{"error": "The page is already added to favorites"},
|
|
||||||
status=status.HTTP_410_GONE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, page_id):
|
def destroy(self, request, slug, project_id, page_id):
|
||||||
try:
|
page_favorite = PageFavorite.objects.get(
|
||||||
page_favorite = PageFavorite.objects.get(
|
project=project_id,
|
||||||
project=project_id,
|
user=request.user,
|
||||||
user=request.user,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
page_id=page_id,
|
||||||
page_id=page_id,
|
)
|
||||||
)
|
page_favorite.delete()
|
||||||
page_favorite.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except PageFavorite.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Page is not in favorites"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -279,43 +224,32 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id, page_id, page_block_id):
|
def post(self, request, slug, project_id, page_id, page_block_id):
|
||||||
try:
|
page_block = PageBlock.objects.get(
|
||||||
page_block = PageBlock.objects.get(
|
pk=page_block_id,
|
||||||
pk=page_block_id,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
page_id=page_id,
|
||||||
page_id=page_id,
|
)
|
||||||
)
|
issue = Issue.objects.create(
|
||||||
issue = Issue.objects.create(
|
name=page_block.name,
|
||||||
name=page_block.name,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
description=page_block.description,
|
||||||
description=page_block.description,
|
description_html=page_block.description_html,
|
||||||
description_html=page_block.description_html,
|
description_stripped=page_block.description_stripped,
|
||||||
description_stripped=page_block.description_stripped,
|
)
|
||||||
)
|
_ = IssueAssignee.objects.create(
|
||||||
_ = IssueAssignee.objects.create(
|
issue=issue, assignee=request.user, project_id=project_id
|
||||||
issue=issue, assignee=request.user, project_id=project_id
|
)
|
||||||
)
|
|
||||||
|
|
||||||
_ = IssueActivity.objects.create(
|
_ = IssueActivity.objects.create(
|
||||||
issue=issue,
|
issue=issue,
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
comment=f"created the issue from {page_block.name} block",
|
comment=f"created the issue from {page_block.name} block",
|
||||||
verb="created",
|
verb="created",
|
||||||
)
|
)
|
||||||
|
|
||||||
page_block.issue = issue
|
page_block.issue = issue
|
||||||
page_block.save()
|
page_block.save()
|
||||||
|
|
||||||
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
except PageBlock.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Page Block does not exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
|||||||
# Third party imports
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from .base import BaseAPIView
|
|
||||||
from plane.utils.integrations.github import get_release_notes
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseNotesEndpoint(BaseAPIView):
|
|
||||||
def get(self, request):
|
|
||||||
try:
|
|
||||||
release_notes = get_release_notes()
|
|
||||||
return Response(release_notes, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
@ -168,126 +168,107 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
query = request.query_params.get("search", False)
|
||||||
query = request.query_params.get("search", False)
|
workspace_search = request.query_params.get("workspace_search", "false")
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
project_id = request.query_params.get("project_id", False)
|
||||||
project_id = request.query_params.get("project_id", False)
|
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"results": {
|
|
||||||
"workspace": [],
|
|
||||||
"project": [],
|
|
||||||
"issue": [],
|
|
||||||
"cycle": [],
|
|
||||||
"module": [],
|
|
||||||
"issue_view": [],
|
|
||||||
"page": [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
MODELS_MAPPER = {
|
|
||||||
"workspace": self.filter_workspaces,
|
|
||||||
"project": self.filter_projects,
|
|
||||||
"issue": self.filter_issues,
|
|
||||||
"cycle": self.filter_cycles,
|
|
||||||
"module": self.filter_modules,
|
|
||||||
"issue_view": self.filter_views,
|
|
||||||
"page": self.filter_pages,
|
|
||||||
}
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for model in MODELS_MAPPER.keys():
|
|
||||||
func = MODELS_MAPPER.get(model, None)
|
|
||||||
results[model] = func(query, slug, project_id, workspace_search)
|
|
||||||
return Response({"results": results}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
"results": {
|
||||||
|
"workspace": [],
|
||||||
|
"project": [],
|
||||||
|
"issue": [],
|
||||||
|
"cycle": [],
|
||||||
|
"module": [],
|
||||||
|
"issue_view": [],
|
||||||
|
"page": [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MODELS_MAPPER = {
|
||||||
|
"workspace": self.filter_workspaces,
|
||||||
|
"project": self.filter_projects,
|
||||||
|
"issue": self.filter_issues,
|
||||||
|
"cycle": self.filter_cycles,
|
||||||
|
"module": self.filter_modules,
|
||||||
|
"issue_view": self.filter_views,
|
||||||
|
"page": self.filter_pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for model in MODELS_MAPPER.keys():
|
||||||
|
func = MODELS_MAPPER.get(model, None)
|
||||||
|
results[model] = func(query, slug, project_id, workspace_search)
|
||||||
|
return Response({"results": results}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueSearchEndpoint(BaseAPIView):
|
class IssueSearchEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
try:
|
query = request.query_params.get("search", False)
|
||||||
query = request.query_params.get("search", False)
|
workspace_search = request.query_params.get("workspace_search", "false")
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
parent = request.query_params.get("parent", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
issue_relation = request.query_params.get("issue_relation", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
module = request.query_params.get("module", "false")
|
||||||
module = request.query_params.get("module", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
|
||||||
|
|
||||||
issue_id = request.query_params.get("issue_id", False)
|
issue_id = request.query_params.get("issue_id", False)
|
||||||
|
|
||||||
issues = Issue.issue_objects.filter(
|
issues = Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project__project_projectmember__member=self.request.user,
|
project__project_projectmember__member=self.request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if workspace_search == "false":
|
if workspace_search == "false":
|
||||||
issues = issues.filter(project_id=project_id)
|
issues = issues.filter(project_id=project_id)
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
issues = search_issues(query, issues)
|
issues = search_issues(query, issues)
|
||||||
|
|
||||||
if parent == "true" and issue_id:
|
if parent == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
||||||
).exclude(
|
).exclude(
|
||||||
pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
|
pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
|
||||||
"parent_id", flat=True
|
"parent_id", flat=True
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if issue_relation == "true" and issue_id:
|
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
|
||||||
issues = issues.filter(
|
|
||||||
~Q(pk=issue_id),
|
|
||||||
~Q(issue_related__issue=issue),
|
|
||||||
~Q(issue_relation__related_issue=issue),
|
|
||||||
)
|
|
||||||
if sub_issue == "true" and issue_id:
|
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
|
||||||
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
|
||||||
if issue.parent:
|
|
||||||
issues = issues.filter(~Q(pk=issue.parent_id))
|
|
||||||
|
|
||||||
if cycle == "true":
|
|
||||||
issues = issues.exclude(issue_cycle__isnull=False)
|
|
||||||
|
|
||||||
if module == "true":
|
|
||||||
issues = issues.exclude(issue_module__isnull=False)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
issues.values(
|
|
||||||
"name",
|
|
||||||
"id",
|
|
||||||
"sequence_id",
|
|
||||||
"project__name",
|
|
||||||
"project__identifier",
|
|
||||||
"project_id",
|
|
||||||
"workspace__slug",
|
|
||||||
"state__name",
|
|
||||||
"state__group",
|
|
||||||
"state__color",
|
|
||||||
),
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
)
|
||||||
except Issue.DoesNotExist:
|
if issue_relation == "true" and issue_id:
|
||||||
return Response(
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
issues = issues.filter(
|
||||||
)
|
~Q(pk=issue_id),
|
||||||
except Exception as e:
|
~Q(issue_related__issue=issue),
|
||||||
print(e)
|
~Q(issue_relation__related_issue=issue),
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
if sub_issue == "true" and issue_id:
|
||||||
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
||||||
|
if issue.parent:
|
||||||
|
issues = issues.filter(~Q(pk=issue.parent_id))
|
||||||
|
|
||||||
|
if cycle == "true":
|
||||||
|
issues = issues.exclude(issue_cycle__isnull=False)
|
||||||
|
|
||||||
|
if module == "true":
|
||||||
|
issues = issues.exclude(issue_module__isnull=False)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
issues.values(
|
||||||
|
"name",
|
||||||
|
"id",
|
||||||
|
"sequence_id",
|
||||||
|
"project__name",
|
||||||
|
"project__identifier",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"state__color",
|
||||||
|
),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
@ -41,67 +40,45 @@ class StateViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
serializer = StateSerializer(data=request.data)
|
||||||
serializer = StateSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save(project_id=project_id)
|
||||||
serializer.save(project_id=project_id)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except IntegrityError:
|
|
||||||
return Response(
|
|
||||||
{"error": "State with the name already exists"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
state_dict = dict()
|
||||||
state_dict = dict()
|
states = StateSerializer(self.get_queryset(), many=True).data
|
||||||
states = StateSerializer(self.get_queryset(), many=True).data
|
|
||||||
|
|
||||||
for key, value in groupby(
|
for key, value in groupby(
|
||||||
sorted(states, key=lambda state: state["group"]),
|
sorted(states, key=lambda state: state["group"]),
|
||||||
lambda state: state.get("group"),
|
lambda state: state.get("group"),
|
||||||
):
|
):
|
||||||
state_dict[str(key)] = list(value)
|
state_dict[str(key)] = list(value)
|
||||||
|
|
||||||
return Response(state_dict, status=status.HTTP_200_OK)
|
return Response(state_dict, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
def destroy(self, request, slug, project_id, pk):
|
||||||
|
state = State.objects.get(
|
||||||
|
~Q(name="Triage"),
|
||||||
|
pk=pk, project_id=project_id, workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
if state.default:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Default state cannot be deleted"}, status=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for any issues in the state
|
||||||
|
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||||
|
|
||||||
|
if issue_exist:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
state.delete()
|
||||||
try:
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
state = State.objects.get(
|
|
||||||
~Q(name="Triage"),
|
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
if state.default:
|
|
||||||
return Response(
|
|
||||||
{"error": "Default state cannot be deleted"}, status=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for any issues in the state
|
|
||||||
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
|
||||||
|
|
||||||
if issue_exist:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "The state is not empty, only empty states can be deleted"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
state.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except State.DoesNotExist:
|
|
||||||
return Response({"error": "State does not exists"}, status=status.HTTP_404)
|
|
||||||
|
@ -8,6 +8,8 @@ from sentry_sdk import capture_exception
|
|||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
UserSerializer,
|
UserSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
|
UserMeSerializer,
|
||||||
|
UserMeSettingsSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||||
@ -30,129 +32,43 @@ class UserEndpoint(BaseViewSet):
|
|||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
def retrieve(self, request):
|
def retrieve(self, request):
|
||||||
try:
|
serialized_data = UserMeSerializer(request.user).data
|
||||||
workspace = Workspace.objects.get(
|
return Response(
|
||||||
pk=request.user.last_workspace_id, workspace_member__member=request.user
|
serialized_data,
|
||||||
)
|
status=status.HTTP_200_OK,
|
||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
)
|
||||||
email=request.user.email
|
|
||||||
).count()
|
|
||||||
assigned_issues = Issue.issue_objects.filter(
|
|
||||||
assignees__in=[request.user]
|
|
||||||
).count()
|
|
||||||
|
|
||||||
serialized_data = UserSerializer(request.user).data
|
def retrieve_user_settings(self, request):
|
||||||
serialized_data["workspace"] = {
|
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||||
"last_workspace_id": request.user.last_workspace_id,
|
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||||
"last_workspace_slug": workspace.slug,
|
|
||||||
"fallback_workspace_id": request.user.last_workspace_id,
|
|
||||||
"fallback_workspace_slug": workspace.slug,
|
|
||||||
"invites": workspace_invites,
|
|
||||||
}
|
|
||||||
serialized_data.setdefault("issues", {})[
|
|
||||||
"assigned_issues"
|
|
||||||
] = assigned_issues
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
serialized_data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Workspace.DoesNotExist:
|
|
||||||
# This exception will be hit even when the `last_workspace_id` is None
|
|
||||||
|
|
||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
|
||||||
email=request.user.email
|
|
||||||
).count()
|
|
||||||
assigned_issues = Issue.issue_objects.filter(
|
|
||||||
assignees__in=[request.user]
|
|
||||||
).count()
|
|
||||||
|
|
||||||
fallback_workspace = (
|
|
||||||
Workspace.objects.filter(workspace_member__member=request.user)
|
|
||||||
.order_by("created_at")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
serialized_data = UserSerializer(request.user).data
|
|
||||||
|
|
||||||
serialized_data["workspace"] = {
|
|
||||||
"last_workspace_id": None,
|
|
||||||
"last_workspace_slug": None,
|
|
||||||
"fallback_workspace_id": fallback_workspace.id
|
|
||||||
if fallback_workspace is not None
|
|
||||||
else None,
|
|
||||||
"fallback_workspace_slug": fallback_workspace.slug
|
|
||||||
if fallback_workspace is not None
|
|
||||||
else None,
|
|
||||||
"invites": workspace_invites,
|
|
||||||
}
|
|
||||||
serialized_data.setdefault("issues", {})[
|
|
||||||
"assigned_issues"
|
|
||||||
] = assigned_issues
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
serialized_data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
try:
|
user = User.objects.get(pk=request.user.id)
|
||||||
user = User.objects.get(pk=request.user.id)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.save()
|
||||||
user.save()
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
return Response(
|
|
||||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
try:
|
user = User.objects.get(pk=request.user.id)
|
||||||
user = User.objects.get(pk=request.user.id)
|
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
user.save()
|
||||||
user.save()
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
return Response(
|
|
||||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
try:
|
queryset = IssueActivity.objects.filter(
|
||||||
queryset = IssueActivity.objects.filter(
|
actor=request.user, workspace__slug=slug
|
||||||
actor=request.user, workspace__slug=slug
|
).select_related("actor", "workspace", "issue", "project")
|
||||||
).select_related("actor", "workspace", "issue", "project")
|
|
||||||
|
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=queryset,
|
queryset=queryset,
|
||||||
on_results=lambda issue_activities: IssueActivitySerializer(
|
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||||
issue_activities, many=True
|
issue_activities, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
@ -13,7 +13,6 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.db.models import Prefetch, OuterRef, Exists
|
from django.db.models import Prefetch, OuterRef, Exists
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
@ -61,7 +60,7 @@ class GlobalViewViewSet(BaseViewSet):
|
|||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.order_by("-created_at")
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -97,119 +96,111 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
try:
|
filters = issue_filters(request.query_params, "GET")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
# Priority Ordering
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
issue=OuterRef("id")
|
||||||
priority_order = (
|
|
||||||
priority_order
|
|
||||||
if order_by_param == "priority"
|
|
||||||
else priority_order[::-1]
|
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
.order_by()
|
||||||
priority_order=Case(
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
*[
|
.values("count")
|
||||||
When(priority=p, then=Value(i))
|
)
|
||||||
for i, p in enumerate(priority_order)
|
)
|
||||||
],
|
|
||||||
output_field=CharField(),
|
|
||||||
)
|
|
||||||
).order_by("priority_order")
|
|
||||||
|
|
||||||
# State Ordering
|
# Priority Ordering
|
||||||
elif order_by_param in [
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
"state__name",
|
priority_order = (
|
||||||
"state__group",
|
priority_order
|
||||||
"-state__name",
|
if order_by_param == "priority"
|
||||||
"-state__group",
|
else priority_order[::-1]
|
||||||
]:
|
)
|
||||||
state_order = (
|
issue_queryset = issue_queryset.annotate(
|
||||||
state_order
|
priority_order=Case(
|
||||||
if order_by_param in ["state__name", "state__group"]
|
*[
|
||||||
else state_order[::-1]
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
).order_by("priority_order")
|
||||||
state_order=Case(
|
|
||||||
*[
|
# State Ordering
|
||||||
When(state__group=state_group, then=Value(i))
|
elif order_by_param in [
|
||||||
for i, state_group in enumerate(state_order)
|
"state__name",
|
||||||
],
|
"state__group",
|
||||||
default=Value(len(state_order)),
|
"-state__name",
|
||||||
output_field=CharField(),
|
"-state__group",
|
||||||
)
|
]:
|
||||||
).order_by("state_order")
|
state_order = (
|
||||||
# assignee and label ordering
|
state_order
|
||||||
elif order_by_param in [
|
if order_by_param in ["state__name", "state__group"]
|
||||||
"labels__name",
|
else state_order[::-1]
|
||||||
"-labels__name",
|
)
|
||||||
"assignees__first_name",
|
issue_queryset = issue_queryset.annotate(
|
||||||
"-assignees__first_name",
|
state_order=Case(
|
||||||
]:
|
*[
|
||||||
issue_queryset = issue_queryset.annotate(
|
When(state__group=state_group, then=Value(i))
|
||||||
max_values=Max(
|
for i, state_group in enumerate(state_order)
|
||||||
order_by_param[1::]
|
],
|
||||||
if order_by_param.startswith("-")
|
default=Value(len(state_order)),
|
||||||
else order_by_param
|
output_field=CharField(),
|
||||||
)
|
|
||||||
).order_by(
|
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
|
||||||
)
|
)
|
||||||
else:
|
).order_by("state_order")
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
## Grouping the results
|
"assignees__first_name",
|
||||||
group_by = request.GET.get("group_by", False)
|
"-assignees__first_name",
|
||||||
sub_group_by = request.GET.get("sub_group_by", False)
|
]:
|
||||||
if sub_group_by and sub_group_by == group_by:
|
issue_queryset = issue_queryset.annotate(
|
||||||
return Response(
|
max_values=Max(
|
||||||
{"error": "Group by and sub group by cannot be same"},
|
order_by_param[1::]
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
if order_by_param.startswith("-")
|
||||||
)
|
else order_by_param
|
||||||
|
|
||||||
if group_by:
|
|
||||||
return Response(
|
|
||||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
|
||||||
)
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||||
|
|
||||||
except Exception as e:
|
## Grouping the results
|
||||||
capture_exception(e)
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
@ -243,51 +234,6 @@ class IssueViewViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ViewIssuesEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id, view_id):
|
|
||||||
try:
|
|
||||||
view = IssueView.objects.get(pk=view_id)
|
|
||||||
queries = view.query
|
|
||||||
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
|
|
||||||
issues = (
|
|
||||||
Issue.issue_objects.filter(
|
|
||||||
**queries, project_id=project_id, workspace__slug=slug
|
|
||||||
)
|
|
||||||
.filter(**filters)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_reactions",
|
|
||||||
queryset=IssueReaction.objects.select_related("actor"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = IssueLiteSerializer(issues, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except IssueView.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewFavoriteSerializer
|
serializer_class = IssueViewFavoriteSerializer
|
||||||
model = IssueViewFavorite
|
model = IssueViewFavorite
|
||||||
@ -302,49 +248,18 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
serializer = IssueViewFavoriteSerializer(data=request.data)
|
||||||
serializer = IssueViewFavoriteSerializer(data=request.data)
|
if serializer.is_valid():
|
||||||
if serializer.is_valid():
|
serializer.save(user=request.user, project_id=project_id)
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except IntegrityError as e:
|
|
||||||
if "already exists" in str(e):
|
|
||||||
return Response(
|
|
||||||
{"error": "The view is already added to favorites"},
|
|
||||||
status=status.HTTP_410_GONE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, view_id):
|
def destroy(self, request, slug, project_id, view_id):
|
||||||
try:
|
view_favourite = IssueViewFavorite.objects.get(
|
||||||
view_favourite = IssueViewFavorite.objects.get(
|
project=project_id,
|
||||||
project=project_id,
|
user=request.user,
|
||||||
user=request.user,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
view_id=view_id,
|
||||||
view_id=view_id,
|
)
|
||||||
)
|
view_favourite.delete()
|
||||||
view_favourite.delete()
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
except IssueViewFavorite.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{"error": "View is not in favorites"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -20,8 +20,8 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
row_mapping = {
|
row_mapping = {
|
||||||
"state__name": "State",
|
"state__name": "State",
|
||||||
"state__group": "State Group",
|
"state__group": "State Group",
|
||||||
"labels__name": "Label",
|
"labels__id": "Label",
|
||||||
"assignees__display_name": "Assignee Name",
|
"assignees__id": "Assignee Name",
|
||||||
"start_date": "Start Date",
|
"start_date": "Start Date",
|
||||||
"target_date": "Due Date",
|
"target_date": "Due Date",
|
||||||
"completed_at": "Completed At",
|
"completed_at": "Completed At",
|
||||||
@ -29,8 +29,321 @@ row_mapping = {
|
|||||||
"issue_count": "Issue Count",
|
"issue_count": "Issue Count",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"estimate": "Estimate",
|
"estimate": "Estimate",
|
||||||
|
"issue_cycle__cycle_id": "Cycle",
|
||||||
|
"issue_module__module_id": "Module"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ASSIGNEE_ID = "assignees__id"
|
||||||
|
LABEL_ID = "labels__id"
|
||||||
|
STATE_ID = "state_id"
|
||||||
|
CYCLE_ID = "issue_cycle__cycle_id"
|
||||||
|
MODULE_ID = "issue_module__module_id"
|
||||||
|
|
||||||
|
|
||||||
|
def send_export_email(email, slug, csv_buffer):
|
||||||
|
"""Helper function to send export email."""
|
||||||
|
subject = "Your Export is ready"
|
||||||
|
html_content = render_to_string("emails/exports/analytics.html", {})
|
||||||
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
|
csv_buffer.seek(0)
|
||||||
|
msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_FROM, [email])
|
||||||
|
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
||||||
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_assignee_details(slug, filters):
|
||||||
|
"""Fetch assignee details if required."""
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
||||||
|
)
|
||||||
|
.distinct("assignees__id")
|
||||||
|
.order_by("assignees__id")
|
||||||
|
.values(
|
||||||
|
"assignees__avatar",
|
||||||
|
"assignees__display_name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"assignees__last_name",
|
||||||
|
"assignees__id",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_label_details(slug, filters):
|
||||||
|
"""Fetch label details if required"""
|
||||||
|
return (
|
||||||
|
Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False)
|
||||||
|
.distinct("labels__id")
|
||||||
|
.order_by("labels__id")
|
||||||
|
.values("labels__id", "labels__color", "labels__name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_state_details(slug, filters):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
)
|
||||||
|
.distinct("state_id")
|
||||||
|
.order_by("state_id")
|
||||||
|
.values("state_id", "state__name", "state__color")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_details(slug, filters):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
issue_module__module_id__isnull=False,
|
||||||
|
)
|
||||||
|
.distinct("issue_module__module_id")
|
||||||
|
.order_by("issue_module__module_id")
|
||||||
|
.values(
|
||||||
|
"issue_module__module_id",
|
||||||
|
"issue_module__module__name",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cycle_details(slug, filters):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
issue_cycle__cycle_id__isnull=False,
|
||||||
|
)
|
||||||
|
.distinct("issue_cycle__cycle_id")
|
||||||
|
.order_by("issue_cycle__cycle_id")
|
||||||
|
.values(
|
||||||
|
"issue_cycle__cycle_id",
|
||||||
|
"issue_cycle__cycle__name",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csv_from_rows(rows):
|
||||||
|
"""Generate CSV buffer from rows."""
|
||||||
|
csv_buffer = io.StringIO()
|
||||||
|
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||||
|
[writer.writerow(row) for row in rows]
|
||||||
|
return csv_buffer
|
||||||
|
|
||||||
|
|
||||||
|
def generate_segmented_rows(
|
||||||
|
distribution,
|
||||||
|
x_axis,
|
||||||
|
y_axis,
|
||||||
|
segment,
|
||||||
|
key,
|
||||||
|
assignee_details,
|
||||||
|
label_details,
|
||||||
|
state_details,
|
||||||
|
cycle_details,
|
||||||
|
module_details,
|
||||||
|
):
|
||||||
|
segment_zero = list(
|
||||||
|
set(
|
||||||
|
item.get("segment") for sublist in distribution.values() for item in sublist
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
segmented = segment
|
||||||
|
|
||||||
|
row_zero = [
|
||||||
|
row_mapping.get(x_axis, "X-Axis"),
|
||||||
|
row_mapping.get(y_axis, "Y-Axis"),
|
||||||
|
] + segment_zero
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for item, data in distribution.items():
|
||||||
|
generated_row = [
|
||||||
|
item,
|
||||||
|
sum(obj.get(key) for obj in data if obj.get(key) is not None),
|
||||||
|
]
|
||||||
|
|
||||||
|
for segment in segment_zero:
|
||||||
|
value = next((x.get(key) for x in data if x.get("segment") == segment), "0")
|
||||||
|
generated_row.append(value)
|
||||||
|
|
||||||
|
if x_axis == ASSIGNEE_ID:
|
||||||
|
assignee = next(
|
||||||
|
(
|
||||||
|
user
|
||||||
|
for user in assignee_details
|
||||||
|
if str(user[ASSIGNEE_ID]) == str(item)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if assignee:
|
||||||
|
generated_row[
|
||||||
|
0
|
||||||
|
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
|
||||||
|
|
||||||
|
if x_axis == LABEL_ID:
|
||||||
|
label = next(
|
||||||
|
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if label:
|
||||||
|
generated_row[0] = f"{label['labels__name']}"
|
||||||
|
|
||||||
|
if x_axis == STATE_ID:
|
||||||
|
state = next(
|
||||||
|
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if state:
|
||||||
|
generated_row[0] = f"{state['state__name']}"
|
||||||
|
|
||||||
|
if x_axis == CYCLE_ID:
|
||||||
|
cycle = next(
|
||||||
|
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if cycle:
|
||||||
|
generated_row[0] = f"{cycle['issue_cycle__cycle__name']}"
|
||||||
|
|
||||||
|
if x_axis == MODULE_ID:
|
||||||
|
module = next(
|
||||||
|
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if module:
|
||||||
|
generated_row[0] = f"{module['issue_module__module__name']}"
|
||||||
|
|
||||||
|
rows.append(tuple(generated_row))
|
||||||
|
|
||||||
|
if segmented == ASSIGNEE_ID:
|
||||||
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
|
assignee = next(
|
||||||
|
(
|
||||||
|
user
|
||||||
|
for user in assignee_details
|
||||||
|
if str(user[ASSIGNEE_ID]) == str(segm)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if assignee:
|
||||||
|
row_zero[
|
||||||
|
index + 2
|
||||||
|
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
|
||||||
|
|
||||||
|
if segmented == LABEL_ID:
|
||||||
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
|
label = next(
|
||||||
|
(lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if label:
|
||||||
|
row_zero[index + 2] = label["labels__name"]
|
||||||
|
|
||||||
|
if segmented == STATE_ID:
|
||||||
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
|
state = next(
|
||||||
|
(sta for sta in state_details if str(sta[STATE_ID]) == str(segm)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if state:
|
||||||
|
row_zero[index + 2] = state["state__name"]
|
||||||
|
|
||||||
|
if segmented == MODULE_ID:
|
||||||
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
|
module = next(
|
||||||
|
(mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if module:
|
||||||
|
row_zero[index + 2] = module["issue_module__module__name"]
|
||||||
|
|
||||||
|
if segmented == CYCLE_ID:
|
||||||
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
|
cycle = next(
|
||||||
|
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if cycle:
|
||||||
|
row_zero[index + 2] = cycle["issue_cycle__cycle__name"]
|
||||||
|
|
||||||
|
return [tuple(row_zero)] + rows
|
||||||
|
|
||||||
|
|
||||||
|
def generate_non_segmented_rows(
|
||||||
|
distribution,
|
||||||
|
x_axis,
|
||||||
|
y_axis,
|
||||||
|
key,
|
||||||
|
assignee_details,
|
||||||
|
label_details,
|
||||||
|
state_details,
|
||||||
|
cycle_details,
|
||||||
|
module_details,
|
||||||
|
):
|
||||||
|
rows = []
|
||||||
|
for item, data in distribution.items():
|
||||||
|
row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")]
|
||||||
|
|
||||||
|
if x_axis == ASSIGNEE_ID:
|
||||||
|
assignee = next(
|
||||||
|
(
|
||||||
|
user
|
||||||
|
for user in assignee_details
|
||||||
|
if str(user[ASSIGNEE_ID]) == str(item)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if assignee:
|
||||||
|
row[
|
||||||
|
0
|
||||||
|
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
|
||||||
|
|
||||||
|
if x_axis == LABEL_ID:
|
||||||
|
label = next(
|
||||||
|
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if label:
|
||||||
|
row[0] = f"{label['labels__name']}"
|
||||||
|
|
||||||
|
if x_axis == STATE_ID:
|
||||||
|
state = next(
|
||||||
|
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if state:
|
||||||
|
row[0] = f"{state['state__name']}"
|
||||||
|
|
||||||
|
if x_axis == CYCLE_ID:
|
||||||
|
cycle = next(
|
||||||
|
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if cycle:
|
||||||
|
row[0] = f"{cycle['issue_cycle__cycle__name']}"
|
||||||
|
|
||||||
|
if x_axis == MODULE_ID:
|
||||||
|
module = next(
|
||||||
|
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if module:
|
||||||
|
row[0] = f"{module['issue_module__module__name']}"
|
||||||
|
|
||||||
|
rows.append(tuple(row))
|
||||||
|
|
||||||
|
row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")]
|
||||||
|
return [tuple(row_zero)] + rows
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def analytic_export_task(email, data, slug):
|
def analytic_export_task(email, data, slug):
|
||||||
@ -43,134 +356,70 @@ def analytic_export_task(email, data, slug):
|
|||||||
segment = data.get("segment", False)
|
segment = data.get("segment", False)
|
||||||
|
|
||||||
distribution = build_graph_plot(
|
distribution = build_graph_plot(
|
||||||
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
||||||
)
|
)
|
||||||
|
|
||||||
key = "count" if y_axis == "issue_count" else "estimate"
|
key = "count" if y_axis == "issue_count" else "estimate"
|
||||||
|
|
||||||
segmented = segment
|
assignee_details = (
|
||||||
|
get_assignee_details(slug, filters)
|
||||||
|
if x_axis == ASSIGNEE_ID or segment == ASSIGNEE_ID
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
assignee_details = {}
|
label_details = (
|
||||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
get_label_details(slug, filters)
|
||||||
assignee_details = (
|
if x_axis == LABEL_ID or segment == LABEL_ID
|
||||||
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
|
else {}
|
||||||
.order_by("assignees__id")
|
)
|
||||||
.distinct("assignees__id")
|
|
||||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
|
state_details = (
|
||||||
)
|
get_state_details(slug, filters)
|
||||||
|
if x_axis == STATE_ID or segment == STATE_ID
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle_details = (
|
||||||
|
get_cycle_details(slug, filters)
|
||||||
|
if x_axis == CYCLE_ID or segment == CYCLE_ID
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
module_details = (
|
||||||
|
get_module_details(slug, filters)
|
||||||
|
if x_axis == MODULE_ID or segment == MODULE_ID
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
if segment:
|
if segment:
|
||||||
segment_zero = []
|
rows = generate_segmented_rows(
|
||||||
for item in distribution:
|
distribution,
|
||||||
current_dict = distribution.get(item)
|
x_axis,
|
||||||
for current in current_dict:
|
y_axis,
|
||||||
segment_zero.append(current.get("segment"))
|
segment,
|
||||||
|
key,
|
||||||
segment_zero = list(set(segment_zero))
|
assignee_details,
|
||||||
row_zero = (
|
label_details,
|
||||||
[
|
state_details,
|
||||||
row_mapping.get(x_axis, "X-Axis"),
|
cycle_details,
|
||||||
]
|
module_details,
|
||||||
+ [
|
|
||||||
row_mapping.get(y_axis, "Y-Axis"),
|
|
||||||
]
|
|
||||||
+ segment_zero
|
|
||||||
)
|
)
|
||||||
rows = []
|
|
||||||
for item in distribution:
|
|
||||||
generated_row = [
|
|
||||||
item,
|
|
||||||
]
|
|
||||||
|
|
||||||
data = distribution.get(item)
|
|
||||||
# Add y axis values
|
|
||||||
generated_row.append(sum(obj.get(key) for obj in data if obj.get(key, None) is not None))
|
|
||||||
|
|
||||||
for segment in segment_zero:
|
|
||||||
value = [x for x in data if x.get("segment") == segment]
|
|
||||||
if len(value):
|
|
||||||
generated_row.append(value[0].get(key))
|
|
||||||
else:
|
|
||||||
generated_row.append("0")
|
|
||||||
# x-axis replacement for names
|
|
||||||
if x_axis in ["assignees__id"]:
|
|
||||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
|
|
||||||
if len(assignee):
|
|
||||||
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
|
||||||
rows.append(tuple(generated_row))
|
|
||||||
|
|
||||||
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
|
|
||||||
if segmented in ["assignees__id"]:
|
|
||||||
for index, segm in enumerate(row_zero[2:]):
|
|
||||||
# find the name of the user
|
|
||||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)]
|
|
||||||
if len(assignee):
|
|
||||||
row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
|
||||||
|
|
||||||
rows = [tuple(row_zero)] + rows
|
|
||||||
csv_buffer = io.StringIO()
|
|
||||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
|
||||||
|
|
||||||
# Write CSV data to the buffer
|
|
||||||
for row in rows:
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
subject = "Your Export is ready"
|
|
||||||
|
|
||||||
html_content = render_to_string("emails/exports/analytics.html", {})
|
|
||||||
|
|
||||||
text_content = strip_tags(html_content)
|
|
||||||
csv_buffer.seek(0)
|
|
||||||
msg = EmailMultiAlternatives(
|
|
||||||
subject, text_content, settings.EMAIL_FROM, [email]
|
|
||||||
)
|
|
||||||
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
|
|
||||||
msg.send(fail_silently=False)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
row_zero = [
|
rows = generate_non_segmented_rows(
|
||||||
row_mapping.get(x_axis, "X-Axis"),
|
distribution,
|
||||||
row_mapping.get(y_axis, "Y-Axis"),
|
x_axis,
|
||||||
]
|
y_axis,
|
||||||
rows = []
|
segment,
|
||||||
for item in distribution:
|
key,
|
||||||
row = [
|
assignee_details,
|
||||||
item,
|
label_details,
|
||||||
distribution.get(item)[0].get("count")
|
state_details,
|
||||||
if y_axis == "issue_count"
|
cycle_details,
|
||||||
else distribution.get(item)[0].get("estimate "),
|
module_details,
|
||||||
]
|
|
||||||
# x-axis replacement to names
|
|
||||||
if x_axis in ["assignees__id"]:
|
|
||||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
|
|
||||||
if len(assignee):
|
|
||||||
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
|
||||||
|
|
||||||
rows.append(tuple(row))
|
|
||||||
rows = [tuple(row_zero)] + rows
|
|
||||||
csv_buffer = io.StringIO()
|
|
||||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
|
||||||
|
|
||||||
# Write CSV data to the buffer
|
|
||||||
for row in rows:
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
subject = "Your Export is ready"
|
|
||||||
|
|
||||||
html_content = render_to_string("emails/exports/analytics.html", {})
|
|
||||||
|
|
||||||
text_content = strip_tags(html_content)
|
|
||||||
|
|
||||||
csv_buffer.seek(0)
|
|
||||||
msg = EmailMultiAlternatives(
|
|
||||||
subject, text_content, settings.EMAIL_FROM, [email]
|
|
||||||
)
|
)
|
||||||
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
|
|
||||||
msg.send(fail_silently=False)
|
|
||||||
|
|
||||||
|
csv_buffer = generate_csv_from_rows(rows)
|
||||||
|
send_export_email(email, slug, csv_buffer)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Print logs if in DEBUG mode
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return
|
|
||||||
|
@ -16,7 +16,7 @@ from plane.db.models import User
|
|||||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
realtivelink = f"/reset-password/?uidb64={uidb64}&token={token}"
|
realtivelink = f"/accounts/reset-password/?uidb64={uidb64}&token={token}"
|
||||||
abs_url = current_site + realtivelink
|
abs_url = current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = settings.EMAIL_FROM
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
@ -33,13 +33,7 @@ from plane.api.serializers import IssueActivitySerializer
|
|||||||
|
|
||||||
# Track Chnages in name
|
# Track Chnages in name
|
||||||
def track_name(
|
def track_name(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("name") != requested_data.get("name"):
|
if current_instance.get("name") != requested_data.get("name"):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -60,13 +54,7 @@ def track_name(
|
|||||||
|
|
||||||
# Track changes in parent issue
|
# Track changes in parent issue
|
||||||
def track_parent(
|
def track_parent(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("parent") != requested_data.get("parent"):
|
if current_instance.get("parent") != requested_data.get("parent"):
|
||||||
if requested_data.get("parent") == None:
|
if requested_data.get("parent") == None:
|
||||||
@ -112,13 +100,7 @@ def track_parent(
|
|||||||
|
|
||||||
# Track changes in priority
|
# Track changes in priority
|
||||||
def track_priority(
|
def track_priority(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("priority") != requested_data.get("priority"):
|
if current_instance.get("priority") != requested_data.get("priority"):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -139,13 +121,7 @@ def track_priority(
|
|||||||
|
|
||||||
# Track chnages in state of the issue
|
# Track chnages in state of the issue
|
||||||
def track_state(
|
def track_state(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("state") != requested_data.get("state"):
|
if current_instance.get("state") != requested_data.get("state"):
|
||||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||||
@ -171,47 +147,43 @@ def track_state(
|
|||||||
|
|
||||||
# Track issue description
|
# Track issue description
|
||||||
def track_description(
|
def track_description(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("description_html") != requested_data.get(
|
if current_instance.get("description_html") != requested_data.get(
|
||||||
"description_html"
|
"description_html"
|
||||||
):
|
):
|
||||||
last_activity = IssueActivity.objects.filter(issue_id=issue_id).order_by("-created_at").first()
|
last_activity = (
|
||||||
if(last_activity is not None and last_activity.field == "description" and actor.id == last_activity.actor_id):
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
last_activity is not None
|
||||||
|
and last_activity.field == "description"
|
||||||
|
and actor.id == last_activity.actor_id
|
||||||
|
):
|
||||||
last_activity.created_at = timezone.now()
|
last_activity.created_at = timezone.now()
|
||||||
last_activity.save(update_fields=["created_at"])
|
last_activity.save(update_fields=["created_at"])
|
||||||
else:
|
else:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
verb="updated",
|
verb="updated",
|
||||||
old_value=current_instance.get("description_html"),
|
old_value=current_instance.get("description_html"),
|
||||||
new_value=requested_data.get("description_html"),
|
new_value=requested_data.get("description_html"),
|
||||||
field="description",
|
field="description",
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||||
epoch=epoch,
|
epoch=epoch,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Track changes in issue target date
|
# Track changes in issue target date
|
||||||
def track_target_date(
|
def track_target_date(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||||
if requested_data.get("target_date") == None:
|
if requested_data.get("target_date") == None:
|
||||||
@ -248,13 +220,7 @@ def track_target_date(
|
|||||||
|
|
||||||
# Track changes in issue start date
|
# Track changes in issue start date
|
||||||
def track_start_date(
|
def track_start_date(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||||
if requested_data.get("start_date") == None:
|
if requested_data.get("start_date") == None:
|
||||||
@ -291,13 +257,7 @@ def track_start_date(
|
|||||||
|
|
||||||
# Track changes in issue labels
|
# Track changes in issue labels
|
||||||
def track_labels(
|
def track_labels(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
# Label Addition
|
# Label Addition
|
||||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||||
@ -346,13 +306,7 @@ def track_labels(
|
|||||||
|
|
||||||
# Track changes in issue assignees
|
# Track changes in issue assignees
|
||||||
def track_assignees(
|
def track_assignees(
|
||||||
requested_data,
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
epoch
|
|
||||||
):
|
):
|
||||||
# Assignee Addition
|
# Assignee Addition
|
||||||
if len(requested_data.get("assignees_list")) > len(
|
if len(requested_data.get("assignees_list")) > len(
|
||||||
@ -404,17 +358,17 @@ def track_assignees(
|
|||||||
def create_issue_activity(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"created the issue",
|
comment=f"created the issue",
|
||||||
verb="created",
|
verb="created",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
epoch=epoch,
|
epoch=epoch,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_estimate_points(
|
def track_estimate_points(
|
||||||
@ -547,7 +501,7 @@ def update_issue_activity(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
epoch
|
epoch,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -868,7 +822,6 @@ def update_link_activity(
|
|||||||
def delete_link_activity(
|
def delete_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
|
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
)
|
)
|
||||||
@ -929,12 +882,19 @@ def delete_attachment_activity(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_reaction_activity(
|
def create_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
if requested_data and requested_data.get("reaction") is not None:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first()
|
issue_reaction = (
|
||||||
|
IssueReaction.objects.filter(
|
||||||
|
reaction=requested_data.get("reaction"), project=project, actor=actor
|
||||||
|
)
|
||||||
|
.values_list("id", flat=True)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
if issue_reaction is not None:
|
if issue_reaction is not None:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -955,7 +915,7 @@ def create_issue_reaction_activity(
|
|||||||
|
|
||||||
|
|
||||||
def delete_issue_reaction_activity(
|
def delete_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -984,9 +944,19 @@ def create_comment_reaction_activity(
|
|||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
if requested_data and requested_data.get("reaction") is not None:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first()
|
comment_reaction_id, comment_id = (
|
||||||
comment = IssueComment.objects.get(pk=comment_id,project=project)
|
CommentReaction.objects.filter(
|
||||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
reaction=requested_data.get("reaction"), project=project, actor=actor
|
||||||
|
)
|
||||||
|
.values_list("id", "comment__id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
comment = IssueComment.objects.get(pk=comment_id, project=project)
|
||||||
|
if (
|
||||||
|
comment is not None
|
||||||
|
and comment_reaction_id is not None
|
||||||
|
and comment_id is not None
|
||||||
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=comment.issue_id,
|
issue_id=comment.issue_id,
|
||||||
@ -1006,13 +976,19 @@ def create_comment_reaction_activity(
|
|||||||
|
|
||||||
|
|
||||||
def delete_comment_reaction_activity(
|
def delete_comment_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
)
|
)
|
||||||
if current_instance and current_instance.get("reaction") is not None:
|
if current_instance and current_instance.get("reaction") is not None:
|
||||||
issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first()
|
issue_id = (
|
||||||
|
IssueComment.objects.filter(
|
||||||
|
pk=current_instance.get("comment_id"), project=project
|
||||||
|
)
|
||||||
|
.values_list("issue_id", flat=True)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
if issue_id is not None:
|
if issue_id is not None:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -1056,7 +1032,7 @@ def create_issue_vote_activity(
|
|||||||
|
|
||||||
|
|
||||||
def delete_issue_vote_activity(
|
def delete_issue_vote_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -1104,7 +1080,7 @@ def create_issue_relation_activity(
|
|||||||
field=relation_type,
|
field=relation_type,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f'added {relation_type} relation',
|
comment=f"added {relation_type} relation",
|
||||||
old_identifier=issue_relation.get("issue"),
|
old_identifier=issue_relation.get("issue"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1134,94 +1110,96 @@ def delete_issue_relation_activity(
|
|||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
)
|
)
|
||||||
if current_instance is not None and requested_data.get("related_list") is None:
|
if current_instance is not None and requested_data.get("related_list") is None:
|
||||||
if current_instance.get("relation_type") == "blocked_by":
|
if current_instance.get("relation_type") == "blocked_by":
|
||||||
relation_type = "blocking"
|
relation_type = "blocking"
|
||||||
else:
|
else:
|
||||||
relation_type = current_instance.get("relation_type")
|
relation_type = current_instance.get("relation_type")
|
||||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=current_instance.get("related_issue"),
|
|
||||||
actor=actor,
|
|
||||||
verb="deleted",
|
|
||||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field=relation_type,
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f'deleted {relation_type} relation',
|
|
||||||
old_identifier=current_instance.get("issue"),
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=current_instance.get("issue"),
|
|
||||||
actor=actor,
|
|
||||||
verb="deleted",
|
|
||||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field=f'{current_instance.get("relation_type")}',
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f'deleted {current_instance.get("relation_type")} relation',
|
|
||||||
old_identifier=current_instance.get("related_issue"),
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_draft_issue_activity(
|
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
|
||||||
):
|
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=current_instance.get("related_issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=relation_type,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"drafted the issue",
|
comment=f"deleted {relation_type} relation",
|
||||||
field="draft",
|
old_identifier=current_instance.get("issue"),
|
||||||
verb="created",
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("issue"),
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=f'{current_instance.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||||
|
old_identifier=current_instance.get("related_issue"),
|
||||||
epoch=epoch,
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"drafted the issue",
|
||||||
|
field="draft",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_draft_issue_activity(
|
def update_draft_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
requested_data.get("is_draft") is not None
|
||||||
|
and requested_data.get("is_draft") == False
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"created the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
|
else:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"created the issue",
|
comment=f"updated the draft issue",
|
||||||
verb="updated",
|
field="draft",
|
||||||
actor=actor,
|
verb="updated",
|
||||||
epoch=epoch,
|
actor=actor,
|
||||||
)
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
else:
|
)
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"updated the draft issue",
|
|
||||||
field="draft",
|
|
||||||
verb="updated",
|
|
||||||
actor=actor,
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def delete_draft_issue_activity(
|
def delete_draft_issue_activity(
|
||||||
@ -1239,6 +1217,7 @@ def delete_draft_issue_activity(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1252,6 +1231,7 @@ def issue_activity(
|
|||||||
subscriber=True,
|
subscriber=True,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
issue_activities = []
|
issue_activities = []
|
||||||
|
|
||||||
actor = User.objects.get(pk=actor_id)
|
actor = User.objects.get(pk=actor_id)
|
||||||
@ -1268,6 +1248,9 @@ def issue_activity(
|
|||||||
"comment_reaction.activity.deleted",
|
"comment_reaction.activity.deleted",
|
||||||
"issue_vote.activity.created",
|
"issue_vote.activity.created",
|
||||||
"issue_vote.activity.deleted",
|
"issue_vote.activity.deleted",
|
||||||
|
"issue_draft.activity.created",
|
||||||
|
"issue_draft.activity.updated",
|
||||||
|
"issue_draft.activity.deleted",
|
||||||
]:
|
]:
|
||||||
issue = Issue.objects.filter(pk=issue_id).first()
|
issue = Issue.objects.filter(pk=issue_id).first()
|
||||||
|
|
||||||
@ -1360,6 +1343,9 @@ def issue_activity(
|
|||||||
"comment_reaction.activity.deleted",
|
"comment_reaction.activity.deleted",
|
||||||
"issue_vote.activity.created",
|
"issue_vote.activity.created",
|
||||||
"issue_vote.activity.deleted",
|
"issue_vote.activity.deleted",
|
||||||
|
"issue_draft.activity.created",
|
||||||
|
"issue_draft.activity.updated",
|
||||||
|
"issue_draft.activity.deleted",
|
||||||
]:
|
]:
|
||||||
# Create Notifications
|
# Create Notifications
|
||||||
bulk_notifications = []
|
bulk_notifications = []
|
||||||
@ -1389,7 +1375,7 @@ def issue_activity(
|
|||||||
):
|
):
|
||||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||||
|
|
||||||
for subscriber in issue_subscribers:
|
for subscriber in list(set(issue_subscribers)):
|
||||||
for issue_activity in issue_activities_created:
|
for issue_activity in issue_activities_created:
|
||||||
bulk_notifications.append(
|
bulk_notifications.append(
|
||||||
Notification(
|
Notification(
|
||||||
|
@ -58,20 +58,23 @@ def archive_old_issues():
|
|||||||
|
|
||||||
# Check if Issues
|
# Check if Issues
|
||||||
if issues:
|
if issues:
|
||||||
|
# Set the archive time to current time
|
||||||
|
archive_at = timezone.now()
|
||||||
|
|
||||||
issues_to_update = []
|
issues_to_update = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.archived_at = timezone.now()
|
issue.archived_at = archive_at
|
||||||
issues_to_update.append(issue)
|
issues_to_update.append(issue)
|
||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
if issues_to_update:
|
||||||
updated_issues = Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
issues_to_update, ["archived_at"], batch_size=100
|
issues_to_update, ["archived_at"], batch_size=100
|
||||||
)
|
)
|
||||||
[
|
[
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||||
actor_id=str(project.created_by_id),
|
actor_id=str(project.created_by_id),
|
||||||
issue_id=issue.id,
|
issue_id=issue.id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -79,7 +82,7 @@ def archive_old_issues():
|
|||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -139,7 +142,7 @@ def close_old_issues():
|
|||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
if issues_to_update:
|
||||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||||
[
|
[
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
@ -151,7 +154,7 @@ def close_old_issues():
|
|||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -33,9 +33,8 @@ def create_issue_relation(apps, schema_editor):
|
|||||||
def update_issue_priority_choice(apps, schema_editor):
|
def update_issue_priority_choice(apps, schema_editor):
|
||||||
IssueModel = apps.get_model("db", "Issue")
|
IssueModel = apps.get_model("db", "Issue")
|
||||||
updated_issues = []
|
updated_issues = []
|
||||||
for obj in IssueModel.objects.all():
|
for obj in IssueModel.objects.filter(priority=None):
|
||||||
if obj.priority is None:
|
obj.priority = "none"
|
||||||
obj.priority = "none"
|
|
||||||
updated_issues.append(obj)
|
updated_issues.append(obj)
|
||||||
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||||
|
|
||||||
|
@ -26,19 +26,19 @@ def workspace_member_props(old_props):
|
|||||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||||
},
|
},
|
||||||
"display_properties": {
|
"display_properties": {
|
||||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||||
"key": old_props.get("properties", {}).get("key", None),
|
"key": old_props.get("properties", {}).get("key", True),
|
||||||
"labels": old_props.get("properties", {}).get("labels", None),
|
"labels": old_props.get("properties", {}).get("labels", True),
|
||||||
"link": old_props.get("properties", {}).get("link", None),
|
"link": old_props.get("properties", {}).get("link", True),
|
||||||
"priority": old_props.get("properties", {}).get("priority", None),
|
"priority": old_props.get("properties", {}).get("priority", True),
|
||||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||||
"state": old_props.get("properties", {}).get("state", None),
|
"state": old_props.get("properties", {}).get("state", True),
|
||||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return new_props
|
return new_props
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def update_issue_activity(apps, schema_editor):
|
|
||||||
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
|
||||||
updated_issue_activity = []
|
|
||||||
for obj in IssueActivityModel.objects.all():
|
|
||||||
if obj.field == "blocks":
|
|
||||||
obj.field = "blocked_by"
|
|
||||||
updated_issue_activity.append(obj)
|
|
||||||
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('db', '0044_auto_20230913_0709'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(update_issue_activity),
|
|
||||||
]
|
|
@ -1,28 +1,47 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-19 14:21
|
# Generated by Django 4.2.5 on 2023-09-29 10:14
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.workspace
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def update_epoch(apps, schema_editor):
|
def update_issue_activity_priority(apps, schema_editor):
|
||||||
IssueActivity = apps.get_model('db', 'IssueActivity')
|
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||||
updated_issue_activity = []
|
updated_issue_activity = []
|
||||||
for obj in IssueActivity.objects.all():
|
for obj in IssueActivity.objects.filter(field="priority"):
|
||||||
obj.epoch = int(obj.created_at.timestamp())
|
# Set the old and new value to none if it is empty for Priority
|
||||||
|
obj.new_value = obj.new_value or "none"
|
||||||
|
obj.old_value = obj.old_value or "none"
|
||||||
updated_issue_activity.append(obj)
|
updated_issue_activity.append(obj)
|
||||||
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
IssueActivity.objects.bulk_update(
|
||||||
|
updated_issue_activity,
|
||||||
|
["new_value", "old_value"],
|
||||||
|
batch_size=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_issue_activity_blocked(apps, schema_editor):
|
||||||
|
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||||
|
updated_issue_activity = []
|
||||||
|
for obj in IssueActivity.objects.filter(field="blocks"):
|
||||||
|
# Set the field to blocked_by
|
||||||
|
obj.field = "blocked_by"
|
||||||
|
updated_issue_activity.append(obj)
|
||||||
|
IssueActivity.objects.bulk_update(
|
||||||
|
updated_issue_activity,
|
||||||
|
["field"],
|
||||||
|
batch_size=1000,
|
||||||
|
)
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('db', '0045_auto_20230915_0655'),
|
('db', '0044_auto_20230913_0709'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='GlobalView',
|
name='GlobalView',
|
||||||
fields=[
|
fields=[
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
@ -33,6 +52,7 @@ class Migration(migrations.Migration):
|
|||||||
('query', models.JSONField(verbose_name='View Query')),
|
('query', models.JSONField(verbose_name='View Query')),
|
||||||
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||||
('query_data', models.JSONField(default=dict)),
|
('query_data', models.JSONField(default=dict)),
|
||||||
|
('sort_order', models.FloatField(default=65535)),
|
||||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||||
@ -44,10 +64,16 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ('-created_at',),
|
'ordering': ('-created_at',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='issue_props',
|
||||||
|
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='issueactivity',
|
model_name='issueactivity',
|
||||||
name='epoch',
|
name='epoch',
|
||||||
field=models.FloatField(null=True),
|
field=models.FloatField(null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(update_epoch),
|
migrations.RunPython(update_issue_activity_priority),
|
||||||
|
migrations.RunPython(update_issue_activity_blocked),
|
||||||
]
|
]
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-21 07:58
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def update_priority_history(apps, schema_editor):
|
|
||||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
|
||||||
updated_issue_activity = []
|
|
||||||
for obj in IssueActivity.objects.all():
|
|
||||||
if obj.field == "priority":
|
|
||||||
obj.new_value = obj.new_value or "none"
|
|
||||||
obj.old_value = obj.old_value or "none"
|
|
||||||
updated_issue_activity.append(obj)
|
|
||||||
IssueActivity.objects.bulk_update(
|
|
||||||
updated_issue_activity, ["new_value", "old_value"], batch_size=100
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("db", "0046_auto_20230919_1421"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(update_priority_history),
|
|
||||||
]
|
|
@ -17,12 +17,23 @@ class GlobalView(BaseModel):
|
|||||||
default=1, choices=((0, "Private"), (1, "Public"))
|
default=1, choices=((0, "Private"), (1, "Public"))
|
||||||
)
|
)
|
||||||
query_data = models.JSONField(default=dict)
|
query_data = models.JSONField(default=dict)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Global View"
|
verbose_name = "Global View"
|
||||||
verbose_name_plural = "Global Views"
|
verbose_name_plural = "Global Views"
|
||||||
db_table = "global_views"
|
db_table = "global_views"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
largest_sort_order = GlobalView.objects.filter(
|
||||||
|
workspace=self.workspace
|
||||||
|
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||||
|
if largest_sort_order is not None:
|
||||||
|
self.sort_order = largest_sort_order + 10000
|
||||||
|
|
||||||
|
super(GlobalView, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the View"""
|
"""Return name of the View"""
|
||||||
|
@ -29,7 +29,7 @@ def get_default_props():
|
|||||||
},
|
},
|
||||||
"display_filters": {
|
"display_filters": {
|
||||||
"group_by": None,
|
"group_by": None,
|
||||||
"order_by": '-created_at',
|
"order_by": "-created_at",
|
||||||
"type": None,
|
"type": None,
|
||||||
"sub_issue": True,
|
"sub_issue": True,
|
||||||
"show_empty_groups": True,
|
"show_empty_groups": True,
|
||||||
@ -54,6 +54,15 @@ def get_default_props():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_issue_props():
|
||||||
|
return {
|
||||||
|
"subscribed": True,
|
||||||
|
"assigned": True,
|
||||||
|
"created": True,
|
||||||
|
"all_issues": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Workspace(BaseModel):
|
class Workspace(BaseModel):
|
||||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||||
@ -89,6 +98,7 @@ class WorkspaceMember(BaseModel):
|
|||||||
company_role = models.TextField(null=True, blank=True)
|
company_role = models.TextField(null=True, blank=True)
|
||||||
view_props = models.JSONField(default=get_default_props)
|
view_props = models.JSONField(default=get_default_props)
|
||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
|
issue_props = models.JSONField(default=get_issue_props)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["workspace", "member"]
|
unique_together = ["workspace", "member"]
|
||||||
|
@ -12,6 +12,10 @@ from .common import * # noqa
|
|||||||
|
|
||||||
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
|
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"*",
|
||||||
|
]
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
|
|
||||||
@ -138,4 +142,6 @@ AWS_PRIVATE_STORAGE_BUCKET_NAME = os.environ.get("AWS_PRIVATE_STORAGE_BUCKET_NAM
|
|||||||
AWS_S3_PRIVATE_FILE_OVERWRITE = False
|
AWS_S3_PRIVATE_FILE_OVERWRITE = False
|
||||||
AWS_PRIVATE_DEFAULT_ACL = "private"
|
AWS_PRIVATE_DEFAULT_ACL = "private"
|
||||||
PRIVATE_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
PRIVATE_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
# End Storage Settings
|
# End Storage Settings
|
||||||
|
# Unsplash Access key
|
||||||
|
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||||
|
@ -7,6 +7,7 @@ import dj_database_url
|
|||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
from sentry_sdk.integrations.redis import RedisIntegration
|
from sentry_sdk.integrations.redis import RedisIntegration
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
@ -135,16 +136,27 @@ CSRF_COOKIE_SECURE = True
|
|||||||
|
|
||||||
REDIS_URL = os.environ.get("REDIS_URL")
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
CACHES = {
|
if DOCKERIZED:
|
||||||
"default": {
|
CACHES = {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"default": {
|
||||||
"LOCATION": REDIS_URL,
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"OPTIONS": {
|
"LOCATION": REDIS_URL,
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
"OPTIONS": {
|
||||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": REDIS_URL,
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
||||||
@ -167,8 +179,12 @@ broker_url = (
|
|||||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
if DOCKERIZED:
|
||||||
CELERY_BROKER_URL = broker_url
|
CELERY_BROKER_URL = REDIS_URL
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URL
|
||||||
|
else:
|
||||||
|
CELERY_BROKER_URL = broker_url
|
||||||
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
|
|
||||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
|
||||||
@ -179,3 +195,7 @@ ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
|||||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||||
SCOUT_NAME = "Plane"
|
SCOUT_NAME = "Plane"
|
||||||
|
|
||||||
|
# Unsplash Access key
|
||||||
|
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||||
|
|
||||||
|
@ -123,3 +123,4 @@ ANALYTICS_BASE_API = False
|
|||||||
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||||
|
|
||||||
|
@ -159,3 +159,7 @@ CELERY_BROKER_URL = broker_url
|
|||||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
|
||||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||||
|
|
||||||
|
|
||||||
|
# Unsplash Access key
|
||||||
|
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||||
|
@ -12,34 +12,47 @@ from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Conc
|
|||||||
from plane.db.models import Issue
|
from plane.db.models import Issue
|
||||||
|
|
||||||
|
|
||||||
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
def annotate_with_monthly_dimension(queryset, field_name):
|
||||||
|
# Get the year and the months
|
||||||
temp_axis = x_axis
|
year = ExtractYear(field_name)
|
||||||
|
month = ExtractMonth(field_name)
|
||||||
|
# Concat the year and month
|
||||||
|
dimension = Concat(year, Value("-"), month, output_field=CharField())
|
||||||
|
# Annotate the dimension
|
||||||
|
return queryset.annotate(dimension=dimension)
|
||||||
|
|
||||||
|
def extract_axis(queryset, x_axis):
|
||||||
|
# Format the dimension when the axis is in date
|
||||||
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
|
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
|
||||||
year = ExtractYear(x_axis)
|
queryset = annotate_with_monthly_dimension(queryset, x_axis)
|
||||||
month = ExtractMonth(x_axis)
|
return queryset, "dimension"
|
||||||
dimension = Concat(year, Value("-"), month, output_field=CharField())
|
|
||||||
queryset = queryset.annotate(dimension=dimension)
|
|
||||||
x_axis = "dimension"
|
|
||||||
else:
|
else:
|
||||||
queryset = queryset.annotate(dimension=F(x_axis))
|
return queryset.annotate(dimension=F(x_axis)), "dimension"
|
||||||
x_axis = "dimension"
|
|
||||||
|
|
||||||
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
|
def sort_data(data, temp_axis):
|
||||||
queryset = queryset.exclude(x_axis__is_null=True)
|
# When the axis is in priority order by
|
||||||
|
if temp_axis == "priority":
|
||||||
|
order = ["low", "medium", "high", "urgent", "none"]
|
||||||
|
return {key: data[key] for key in order if key in data}
|
||||||
|
else:
|
||||||
|
return dict(sorted(data.items(), key=lambda x: (x[0] == "none", x[0])))
|
||||||
|
|
||||||
|
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||||
|
# temp x_axis
|
||||||
|
temp_axis = x_axis
|
||||||
|
# Extract the x_axis and queryset
|
||||||
|
queryset, x_axis = extract_axis(queryset, x_axis)
|
||||||
|
if x_axis == "dimension":
|
||||||
|
queryset = queryset.exclude(dimension__isnull=True)
|
||||||
|
|
||||||
|
#
|
||||||
if segment in ["created_at", "start_date", "target_date", "completed_at"]:
|
if segment in ["created_at", "start_date", "target_date", "completed_at"]:
|
||||||
year = ExtractYear(segment)
|
queryset = annotate_with_monthly_dimension(queryset, segment)
|
||||||
month = ExtractMonth(segment)
|
|
||||||
dimension = Concat(year, Value("-"), month, output_field=CharField())
|
|
||||||
queryset = queryset.annotate(segmented=dimension)
|
|
||||||
segment = "segmented"
|
segment = "segmented"
|
||||||
|
|
||||||
queryset = queryset.values(x_axis)
|
queryset = queryset.values(x_axis)
|
||||||
|
|
||||||
# Group queryset by x_axis field
|
# Issue count
|
||||||
|
|
||||||
if y_axis == "issue_count":
|
if y_axis == "issue_count":
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
is_null=Case(
|
is_null=Case(
|
||||||
@ -49,37 +62,20 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
),
|
),
|
||||||
dimension_ex=Coalesce("dimension", Value("null")),
|
dimension_ex=Coalesce("dimension", Value("null")),
|
||||||
).values("dimension")
|
).values("dimension")
|
||||||
if segment:
|
queryset = queryset.annotate(segment=F(segment)) if segment else queryset
|
||||||
queryset = queryset.annotate(segment=F(segment)).values(
|
queryset = queryset.values("dimension", "segment") if segment else queryset.values("dimension")
|
||||||
"dimension", "segment"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = queryset.values("dimension")
|
|
||||||
|
|
||||||
queryset = queryset.annotate(count=Count("*")).order_by("dimension")
|
queryset = queryset.annotate(count=Count("*")).order_by("dimension")
|
||||||
|
|
||||||
if y_axis == "estimate":
|
# Estimate
|
||||||
|
else:
|
||||||
queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(x_axis)
|
queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(x_axis)
|
||||||
if segment:
|
queryset = queryset.annotate(segment=F(segment)) if segment else queryset
|
||||||
queryset = queryset.annotate(segment=F(segment)).values(
|
queryset = queryset.values("dimension", "segment", "estimate") if segment else queryset.values("dimension", "estimate")
|
||||||
"dimension", "segment", "estimate"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = queryset.values("dimension", "estimate")
|
|
||||||
|
|
||||||
result_values = list(queryset)
|
result_values = list(queryset)
|
||||||
grouped_data = {}
|
grouped_data = {str(key): list(items) for key, items in groupby(result_values, key=lambda x: x[str("dimension")])}
|
||||||
for key, items in groupby(result_values, key=lambda x: x[str("dimension")]):
|
|
||||||
grouped_data[str(key)] = list(items)
|
|
||||||
|
|
||||||
sorted_data = grouped_data
|
|
||||||
if temp_axis == "priority":
|
|
||||||
order = ["low", "medium", "high", "urgent", "none"]
|
|
||||||
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
|
||||||
else:
|
|
||||||
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "none", x[0])))
|
|
||||||
return sorted_data
|
|
||||||
|
|
||||||
|
return sort_data(grouped_data, temp_axis)
|
||||||
|
|
||||||
def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||||
# Total Issues in Cycle or Module
|
# Total Issues in Cycle or Module
|
||||||
|
@ -1,5 +1,62 @@
|
|||||||
from django.utils.timezone import make_aware
|
import re
|
||||||
from django.utils.dateparse import parse_datetime
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# The date from pattern
|
||||||
|
pattern = re.compile(r"\d+_(weeks|months)$")
|
||||||
|
|
||||||
|
|
||||||
|
# Get the 2_weeks, 3_months
|
||||||
|
def string_date_filter(filter, duration, subsequent, term, date_filter, offset):
|
||||||
|
now = timezone.now().date()
|
||||||
|
if term == "months":
|
||||||
|
if subsequent == "after":
|
||||||
|
if offset == "fromnow":
|
||||||
|
filter[f"{date_filter}__gte"] = now + timedelta(days=duration * 30)
|
||||||
|
else:
|
||||||
|
filter[f"{date_filter}__gte"] = now - timedelta(days=duration * 30)
|
||||||
|
else:
|
||||||
|
if offset == "fromnow":
|
||||||
|
filter[f"{date_filter}__lte"] = now + timedelta(days=duration * 30)
|
||||||
|
else:
|
||||||
|
filter[f"{date_filter}__lte"] = now - timedelta(days=duration * 30)
|
||||||
|
if term == "weeks":
|
||||||
|
if subsequent == "after":
|
||||||
|
if offset == "fromnow":
|
||||||
|
filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
|
||||||
|
else:
|
||||||
|
filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
|
||||||
|
else:
|
||||||
|
if offset == "fromnow":
|
||||||
|
filter[f"{date_filter}__lte"] = now + timedelta(days=duration)
|
||||||
|
else:
|
||||||
|
filter[f"{date_filter}__lte"] = now - timedelta(days=duration)
|
||||||
|
|
||||||
|
|
||||||
|
def date_filter(filter, date_term, queries):
|
||||||
|
"""
|
||||||
|
Handle all date filters
|
||||||
|
"""
|
||||||
|
for query in queries:
|
||||||
|
date_query = query.split(";")
|
||||||
|
if len(date_query) >= 2:
|
||||||
|
match = pattern.match(date_query[0])
|
||||||
|
if match:
|
||||||
|
if len(date_query) == 3:
|
||||||
|
digit, term = date_query[0].split("_")
|
||||||
|
string_date_filter(
|
||||||
|
filter=filter,
|
||||||
|
duration=int(digit),
|
||||||
|
subsequent=date_query[1],
|
||||||
|
term=term,
|
||||||
|
date_filter="created_at__date",
|
||||||
|
offset=date_query[2],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if "after" in date_query:
|
||||||
|
filter[f"{date_term}__gte"] = date_query[0]
|
||||||
|
else:
|
||||||
|
filter[f"{date_term}__lte"] = date_query[0]
|
||||||
|
|
||||||
|
|
||||||
def filter_state(params, filter, method):
|
def filter_state(params, filter, method):
|
||||||
@ -97,20 +154,10 @@ def filter_created_at(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
created_ats = params.get("created_at").split(",")
|
created_ats = params.get("created_at").split(",")
|
||||||
if len(created_ats) and "" not in created_ats:
|
if len(created_ats) and "" not in created_ats:
|
||||||
for query in created_ats:
|
date_filter(filter=filter, date_term="created_at__date", queries=created_ats)
|
||||||
created_at_query = query.split(";")
|
|
||||||
if len(created_at_query) == 2 and "after" in created_at_query:
|
|
||||||
filter["created_at__date__gte"] = created_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["created_at__date__lte"] = created_at_query[0]
|
|
||||||
else:
|
else:
|
||||||
if params.get("created_at", None) and len(params.get("created_at")):
|
if params.get("created_at", None) and len(params.get("created_at")):
|
||||||
for query in params.get("created_at"):
|
date_filter(filter=filter, date_term="created_at__date", queries=params.get("created_at", []))
|
||||||
created_at_query = query.split(";")
|
|
||||||
if len(created_at_query) == 2 and "after" in created_at_query:
|
|
||||||
filter["created_at__date__gte"] = created_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["created_at__date__lte"] = created_at_query[0]
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -118,20 +165,10 @@ def filter_updated_at(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
updated_ats = params.get("updated_at").split(",")
|
updated_ats = params.get("updated_at").split(",")
|
||||||
if len(updated_ats) and "" not in updated_ats:
|
if len(updated_ats) and "" not in updated_ats:
|
||||||
for query in updated_ats:
|
date_filter(filter=filter, date_term="created_at__date", queries=updated_ats)
|
||||||
updated_at_query = query.split(";")
|
|
||||||
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
|
||||||
filter["updated_at__date__gte"] = updated_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["updated_at__date__lte"] = updated_at_query[0]
|
|
||||||
else:
|
else:
|
||||||
if params.get("updated_at", None) and len(params.get("updated_at")):
|
if params.get("updated_at", None) and len(params.get("updated_at")):
|
||||||
for query in params.get("updated_at"):
|
date_filter(filter=filter, date_term="created_at__date", queries=params.get("updated_at", []))
|
||||||
updated_at_query = query.split(";")
|
|
||||||
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
|
||||||
filter["updated_at__date__gte"] = updated_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["updated_at__date__lte"] = updated_at_query[0]
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -139,20 +176,10 @@ def filter_start_date(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
start_dates = params.get("start_date").split(",")
|
start_dates = params.get("start_date").split(",")
|
||||||
if len(start_dates) and "" not in start_dates:
|
if len(start_dates) and "" not in start_dates:
|
||||||
for query in start_dates:
|
date_filter(filter=filter, date_term="start_date", queries=start_dates)
|
||||||
start_date_query = query.split(";")
|
|
||||||
if len(start_date_query) == 2 and "after" in start_date_query:
|
|
||||||
filter["start_date__gte"] = start_date_query[0]
|
|
||||||
else:
|
|
||||||
filter["start_date__lte"] = start_date_query[0]
|
|
||||||
else:
|
else:
|
||||||
if params.get("start_date", None) and len(params.get("start_date")):
|
if params.get("start_date", None) and len(params.get("start_date")):
|
||||||
for query in params.get("start_date"):
|
date_filter(filter=filter, date_term="start_date", queries=params.get("start_date", []))
|
||||||
start_date_query = query.split(";")
|
|
||||||
if len(start_date_query) == 2 and "after" in start_date_query:
|
|
||||||
filter["start_date__gte"] = start_date_query[0]
|
|
||||||
else:
|
|
||||||
filter["start_date__lte"] = start_date_query[0]
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -160,21 +187,10 @@ def filter_target_date(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
target_dates = params.get("target_date").split(",")
|
target_dates = params.get("target_date").split(",")
|
||||||
if len(target_dates) and "" not in target_dates:
|
if len(target_dates) and "" not in target_dates:
|
||||||
for query in target_dates:
|
date_filter(filter=filter, date_term="target_date", queries=target_dates)
|
||||||
target_date_query = query.split(";")
|
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
|
||||||
else:
|
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
date_filter(filter=filter, date_term="target_date", queries=params.get("target_date", []))
|
||||||
target_date_query = query.split(";")
|
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
|
||||||
else:
|
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -182,20 +198,10 @@ def filter_completed_at(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
completed_ats = params.get("completed_at").split(",")
|
completed_ats = params.get("completed_at").split(",")
|
||||||
if len(completed_ats) and "" not in completed_ats:
|
if len(completed_ats) and "" not in completed_ats:
|
||||||
for query in completed_ats:
|
date_filter(filter=filter, date_term="completed_at__date", queries=completed_ats)
|
||||||
completed_at_query = query.split(";")
|
|
||||||
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
|
||||||
filter["completed_at__date__gte"] = completed_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["completed_at__lte"] = completed_at_query[0]
|
|
||||||
else:
|
else:
|
||||||
if params.get("completed_at", None) and len(params.get("completed_at")):
|
if params.get("completed_at", None) and len(params.get("completed_at")):
|
||||||
for query in params.get("completed_at"):
|
date_filter(filter=filter, date_term="completed_at__date", queries=params.get("completed_at", []))
|
||||||
completed_at_query = query.split(";")
|
|
||||||
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
|
||||||
filter["completed_at__date__gte"] = completed_at_query[0]
|
|
||||||
else:
|
|
||||||
filter["completed_at__lte"] = completed_at_query[0]
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
python-3.11.5
|
python-3.11.6
|
@ -1,4 +0,0 @@
|
|||||||
# Deploy the Plane image
|
|
||||||
FROM makeplane/plane
|
|
||||||
|
|
||||||
LABEL maintainer="engineering@plane.so"
|
|
168
deploy/selfhost/docker-compose.yml
Normal file
168
deploy/selfhost/docker-compose.yml
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
x-app-env : &app-env
|
||||||
|
environment:
|
||||||
|
- NGINX_PORT=${NGINX_PORT:-84}
|
||||||
|
- DEBUG=${DEBUG:-0}
|
||||||
|
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
|
||||||
|
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||||
|
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
|
||||||
|
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||||
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||||
|
- DOCKERIZED=${DOCKERIZED:-1}
|
||||||
|
#DB SETTINGS
|
||||||
|
- PGHOST=${PGHOST:-plane-db}
|
||||||
|
- PGDATABASE=${PGDATABASE:-plane}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||||
|
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||||
|
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||||
|
# REDIS SETTINGS
|
||||||
|
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||||
|
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||||
|
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||||
|
# EMAIL SETTINGS
|
||||||
|
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||||
|
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||||
|
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||||
|
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||||
|
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"}
|
||||||
|
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||||
|
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||||
|
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||||
|
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||||
|
# OPENAI SETTINGS
|
||||||
|
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
||||||
|
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
||||||
|
# LOGIN/SIGNUP SETTINGS
|
||||||
|
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||||
|
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
||||||
|
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||||
|
# DATA STORE SETTINGS
|
||||||
|
- USE_MINIO=${USE_MINIO:-1}
|
||||||
|
- AWS_REGION=${AWS_REGION:-""}
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
|
||||||
|
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||||
|
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
|
||||||
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
|
||||||
|
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||||
|
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
<<: *app-env
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-frontend:${APP_RELEASE:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
|
deploy:
|
||||||
|
replicas: ${WEB_REPLICAS:-1}
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- worker
|
||||||
|
|
||||||
|
space:
|
||||||
|
<<: *app-env
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-space:${APP_RELEASE:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
|
deploy:
|
||||||
|
replicas: ${SPACE_REPLICAS:-1}
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- worker
|
||||||
|
- web
|
||||||
|
|
||||||
|
api:
|
||||||
|
<<: *app-env
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ./bin/takeoff
|
||||||
|
deploy:
|
||||||
|
replicas: ${API_REPLICAS:-1}
|
||||||
|
depends_on:
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
worker:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: bgworker
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ./bin/worker
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
beat-worker:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: beatworker
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ./bin/beat
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
plane-db:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: plane-db
|
||||||
|
image: postgres:15.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: postgres -c 'max_connections=1000'
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
plane-redis:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: plane-redis
|
||||||
|
image: redis:6.2.7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
|
||||||
|
plane-minio:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: plane-minio
|
||||||
|
image: minio/minio
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /export --console-address ":9090"
|
||||||
|
volumes:
|
||||||
|
- uploads:/export
|
||||||
|
|
||||||
|
createbuckets:
|
||||||
|
<<: *app-env
|
||||||
|
image: minio/mc
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||||
|
depends_on:
|
||||||
|
- plane-minio
|
||||||
|
|
||||||
|
# Comment this if you already have a reverse proxy running
|
||||||
|
proxy:
|
||||||
|
<<: *app-env
|
||||||
|
container_name: proxy
|
||||||
|
platform: linux/amd64
|
||||||
|
image: makeplane/plane-proxy:${APP_RELEASE:-latest}
|
||||||
|
ports:
|
||||||
|
- ${NGINX_PORT}:80
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
redisdata:
|
||||||
|
uploads:
|
111
deploy/selfhost/install.sh
Executable file
111
deploy/selfhost/install.sh
Executable file
@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BRANCH=${BRANCH:-master}
|
||||||
|
SCRIPT_DIR=$PWD
|
||||||
|
PLANE_INSTALL_DIR=$PWD/plane-app
|
||||||
|
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||||
|
|
||||||
|
function install(){
|
||||||
|
echo
|
||||||
|
echo "Installing on $PLANE_INSTALL_DIR"
|
||||||
|
download
|
||||||
|
}
|
||||||
|
function download(){
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
TS=$(date +%s)
|
||||||
|
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
|
||||||
|
then
|
||||||
|
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||||
|
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||||
|
|
||||||
|
if [ -f "$PLANE_INSTALL_DIR/.env" ];
|
||||||
|
then
|
||||||
|
cp $PLANE_INSTALL_DIR/.env $PLANE_INSTALL_DIR/archive/$TS.env
|
||||||
|
else
|
||||||
|
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Latest version is now available for you to use"
|
||||||
|
echo ""
|
||||||
|
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
}
|
||||||
|
function startServices(){
|
||||||
|
cd $PLANE_INSTALL_DIR
|
||||||
|
docker compose up -d
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
}
|
||||||
|
function stopServices(){
|
||||||
|
cd $PLANE_INSTALL_DIR
|
||||||
|
docker compose down
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
}
|
||||||
|
function restartServices(){
|
||||||
|
cd $PLANE_INSTALL_DIR
|
||||||
|
docker compose restart
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
}
|
||||||
|
function upgrade(){
|
||||||
|
echo "***** STOPPING SERVICES ****"
|
||||||
|
stopServices
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "***** DOWNLOADING LATEST VERSION ****"
|
||||||
|
download
|
||||||
|
|
||||||
|
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||||
|
|
||||||
|
}
|
||||||
|
function askForAction(){
|
||||||
|
echo
|
||||||
|
echo "Select a Action you want to perform:"
|
||||||
|
echo " 1) Install"
|
||||||
|
echo " 2) Start"
|
||||||
|
echo " 3) Stop"
|
||||||
|
echo " 4) Restart"
|
||||||
|
echo " 5) Upgrade"
|
||||||
|
echo " 6) Exit"
|
||||||
|
echo
|
||||||
|
read -p "Action [2]: " ACTION
|
||||||
|
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do
|
||||||
|
echo "$ACTION: invalid selection."
|
||||||
|
read -p "Action [2]: " ACTION
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$ACTION" == "1" ]
|
||||||
|
then
|
||||||
|
install
|
||||||
|
askForAction
|
||||||
|
elif [ "$ACTION" == "2" ] || [ "$ACTION" == "" ]
|
||||||
|
then
|
||||||
|
startServices
|
||||||
|
askForAction
|
||||||
|
elif [ "$ACTION" == "3" ]
|
||||||
|
then
|
||||||
|
stopServices
|
||||||
|
askForAction
|
||||||
|
elif [ "$ACTION" == "4" ]
|
||||||
|
then
|
||||||
|
restartServices
|
||||||
|
askForAction
|
||||||
|
elif [ "$ACTION" == "5" ]
|
||||||
|
then
|
||||||
|
upgrade
|
||||||
|
askForAction
|
||||||
|
elif [ "$ACTION" == "6" ]
|
||||||
|
then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "INVALID ACTION SUPPLIED"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
askForAction
|
63
deploy/selfhost/variables.env
Normal file
63
deploy/selfhost/variables.env
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
APP_RELEASE=latest
|
||||||
|
|
||||||
|
WEB_REPLICAS=1
|
||||||
|
SPACE_REPLICAS=1
|
||||||
|
API_REPLICAS=1
|
||||||
|
|
||||||
|
NGINX_PORT=80
|
||||||
|
DEBUG=0
|
||||||
|
DJANGO_SETTINGS_MODULE=plane.settings.selfhosted
|
||||||
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
|
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
||||||
|
SENTRY_DSN=""
|
||||||
|
GITHUB_CLIENT_SECRET=""
|
||||||
|
DOCKERIZED=1
|
||||||
|
|
||||||
|
#DB SETTINGS
|
||||||
|
PGHOST=plane-db
|
||||||
|
PGDATABASE=plane
|
||||||
|
POSTGRES_USER=plane
|
||||||
|
POSTGRES_PASSWORD=plane
|
||||||
|
POSTGRES_DB=plane
|
||||||
|
PGDATA=/var/lib/postgresql/data
|
||||||
|
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# REDIS SETTINGS
|
||||||
|
REDIS_HOST=plane-redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_URL=redis://${REDIS_HOST}:6379/
|
||||||
|
|
||||||
|
# EMAIL SETTINGS
|
||||||
|
EMAIL_HOST=""
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
|
EMAIL_USE_TLS=1
|
||||||
|
EMAIL_USE_SSL=0
|
||||||
|
DEFAULT_EMAIL=captain@plane.so
|
||||||
|
DEFAULT_PASSWORD=password123
|
||||||
|
|
||||||
|
# OPENAI SETTINGS
|
||||||
|
OPENAI_API_BASE=https://api.openai.com/v1
|
||||||
|
OPENAI_API_KEY="sk-"
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo"
|
||||||
|
|
||||||
|
# LOGIN/SIGNUP SETTINGS
|
||||||
|
ENABLE_SIGNUP=1
|
||||||
|
ENABLE_EMAIL_PASSWORD=1
|
||||||
|
ENABLE_MAGIC_LINK_LOGIN=0
|
||||||
|
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
|
||||||
|
|
||||||
|
# DATA STORE SETTINGS
|
||||||
|
USE_MINIO=1
|
||||||
|
AWS_REGION=""
|
||||||
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
|
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||||
|
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
|
||||||
|
AWS_S3_BUCKET_NAME=uploads
|
||||||
|
MINIO_ROOT_USER="access-key"
|
||||||
|
MINIO_ROOT_PASSWORD="secret-key"
|
||||||
|
BUCKET_NAME=uploads
|
||||||
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
@ -1,133 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
container_name: web
|
|
||||||
image: makeplane/plane-frontend:latest
|
|
||||||
restart: always
|
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
|
||||||
env_file:
|
|
||||||
- ./web/.env
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
- worker
|
|
||||||
|
|
||||||
space:
|
|
||||||
container_name: space
|
|
||||||
image: makeplane/plane-space:latest
|
|
||||||
restart: always
|
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
|
||||||
env_file:
|
|
||||||
- ./space/.env
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
- worker
|
|
||||||
- web
|
|
||||||
|
|
||||||
api:
|
|
||||||
container_name: api
|
|
||||||
image: makeplane/plane-backend:latest
|
|
||||||
restart: always
|
|
||||||
command: ./bin/takeoff
|
|
||||||
env_file:
|
|
||||||
- ./apiserver/.env
|
|
||||||
depends_on:
|
|
||||||
- plane-db
|
|
||||||
- plane-redis
|
|
||||||
|
|
||||||
worker:
|
|
||||||
container_name: bgworker
|
|
||||||
image: makeplane/plane-backend:latest
|
|
||||||
restart: always
|
|
||||||
command: ./bin/worker
|
|
||||||
env_file:
|
|
||||||
- ./apiserver/.env
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
- plane-db
|
|
||||||
- plane-redis
|
|
||||||
|
|
||||||
beat-worker:
|
|
||||||
container_name: beatworker
|
|
||||||
image: makeplane/plane-backend:latest
|
|
||||||
restart: always
|
|
||||||
command: ./bin/beat
|
|
||||||
env_file:
|
|
||||||
- ./apiserver/.env
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
- plane-db
|
|
||||||
- plane-redis
|
|
||||||
|
|
||||||
|
|
||||||
plane-db:
|
|
||||||
container_name: plane-db
|
|
||||||
image: postgres:15.2-alpine
|
|
||||||
restart: always
|
|
||||||
command: postgres -c 'max_connections=1000'
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${PGUSER}
|
|
||||||
POSTGRES_DB: ${PGDATABASE}
|
|
||||||
POSTGRES_PASSWORD: ${PGPASSWORD}
|
|
||||||
PGDATA: /var/lib/postgresql/data
|
|
||||||
|
|
||||||
plane-redis:
|
|
||||||
container_name: plane-redis
|
|
||||||
image: redis:6.2.7-alpine
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- redisdata:/data
|
|
||||||
|
|
||||||
plane-minio:
|
|
||||||
container_name: plane-minio
|
|
||||||
image: minio/minio
|
|
||||||
restart: always
|
|
||||||
command: server /export --console-address ":9090"
|
|
||||||
volumes:
|
|
||||||
- uploads:/export
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
|
||||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
|
||||||
|
|
||||||
createbuckets:
|
|
||||||
image: minio/mc
|
|
||||||
entrypoint: >
|
|
||||||
/bin/sh -c "
|
|
||||||
/usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY;
|
|
||||||
/usr/bin/mc mb plane-minio/\$AWS_PUBLIC_STORAGE_BUCKET_NAME;
|
|
||||||
/usr/bin/mc anonymous set download plane-minio/\$AWS_PUBLIC_STORAGE_BUCKET_NAME;
|
|
||||||
/usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY;
|
|
||||||
/usr/bin/mc mb plane-minio/\$AWS_PRIVATE_STORAGE_BUCKET_NAME;
|
|
||||||
/usr/bin/mc anonymous set none plane-minio/\$AWS_PRIVATE_STORAGE_BUCKET_NAME; exit 0;
|
|
||||||
"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- plane-minio
|
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
|
||||||
proxy:
|
|
||||||
container_name: proxy
|
|
||||||
image: makeplane/plane-proxy:latest
|
|
||||||
ports:
|
|
||||||
- ${NGINX_PORT}:80
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
|
||||||
depends_on:
|
|
||||||
- web
|
|
||||||
- api
|
|
||||||
- space
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
redisdata:
|
|
||||||
uploads:
|
|
178
docker-compose-local.yml
Normal file
178
docker-compose-local.yml
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev_env:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redisdata:
|
||||||
|
uploads:
|
||||||
|
pgdata:
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
plane-redis:
|
||||||
|
container_name: plane-redis
|
||||||
|
image: redis:6.2.7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
|
||||||
|
plane-minio:
|
||||||
|
container_name: plane-minio
|
||||||
|
image: minio/minio
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
command: server /export --console-address ":9090"
|
||||||
|
volumes:
|
||||||
|
- uploads:/export
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||||
|
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||||
|
|
||||||
|
createbuckets:
|
||||||
|
image: minio/mc
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- plane-minio
|
||||||
|
|
||||||
|
plane-db:
|
||||||
|
container_name: plane-db
|
||||||
|
image: postgres:15.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
command: postgres -c 'max_connections=1000'
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${PGUSER}
|
||||||
|
POSTGRES_DB: ${PGDATABASE}
|
||||||
|
POSTGRES_PASSWORD: ${PGPASSWORD}
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
|
|
||||||
|
web:
|
||||||
|
container_name: web
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./web/Dockerfile.dev
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: yarn dev --filter=web
|
||||||
|
env_file:
|
||||||
|
- ./web/.env
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- worker
|
||||||
|
|
||||||
|
space:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./space/Dockerfile.dev
|
||||||
|
container_name: space
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: yarn dev --filter=space
|
||||||
|
env_file:
|
||||||
|
- ./space/.env
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- worker
|
||||||
|
- web
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: api
|
||||||
|
build:
|
||||||
|
context: ./apiserver
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- ./apiserver:/code
|
||||||
|
command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||||
|
env_file:
|
||||||
|
- ./apiserver/.env
|
||||||
|
depends_on:
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
worker:
|
||||||
|
container_name: bgworker
|
||||||
|
build:
|
||||||
|
context: ./apiserver
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- ./apiserver:/code
|
||||||
|
command: /bin/sh -c "celery -A plane worker -l info"
|
||||||
|
env_file:
|
||||||
|
- ./apiserver/.env
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
beat-worker:
|
||||||
|
container_name: beatworker
|
||||||
|
build:
|
||||||
|
context: ./apiserver
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
args:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
volumes:
|
||||||
|
- ./apiserver:/code
|
||||||
|
command: /bin/sh -c "celery -A plane beat -l info"
|
||||||
|
env_file:
|
||||||
|
- ./apiserver/.env
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- plane-db
|
||||||
|
- plane-redis
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
container_name: proxy
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dev_env
|
||||||
|
ports:
|
||||||
|
- ${NGINX_PORT}:80
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
- api
|
||||||
|
- space
|
@ -11,6 +11,11 @@ http {
|
|||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Permissions-Policy "interest-cohort=()" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://web:3000/;
|
proxy_pass http://web:3000/;
|
||||||
}
|
}
|
||||||
@ -20,6 +25,7 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /spaces/ {
|
location /spaces/ {
|
||||||
|
rewrite ^/spaces/?$ /spaces/login break;
|
||||||
proxy_pass http://space:3000/spaces/;
|
proxy_pass http://space:3000/spaces/;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,4 +37,4 @@ http {
|
|||||||
proxy_pass http://plane-minio:9000/${PRIVATE_BUCKET_NAME}/;
|
proxy_pass http://plane-minio:9000/${PRIVATE_BUCKET_NAME}/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
package.json
12
package.json
@ -1,11 +1,16 @@
|
|||||||
{
|
{
|
||||||
"repository": "https://github.com/makeplane/plane.git",
|
"repository": "https://github.com/makeplane/plane.git",
|
||||||
|
"version": "0.13.2",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"web",
|
"web",
|
||||||
"space",
|
"space",
|
||||||
"packages/*"
|
"packages/editor/*",
|
||||||
|
"packages/eslint-config-custom",
|
||||||
|
"packages/tailwind-config-custom",
|
||||||
|
"packages/tsconfig",
|
||||||
|
"packages/ui"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
@ -22,7 +27,10 @@
|
|||||||
"prettier": "latest",
|
"prettier": "latest",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"turbo": "latest"
|
"turbo": "^1.10.14"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "18.2.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
}
|
}
|
||||||
|
112
packages/editor/core/Readme.md
Normal file
112
packages/editor/core/Readme.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# @plane/editor-core
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The `@plane/editor-core` package serves as the foundation for our editor system. It provides the base functionality for our other editor packages, but it will not be used directly in any of the projects but only for extending other editors.
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
We provide a wide range of utilities for extending the core itself.
|
||||||
|
|
||||||
|
1. Merging classes and custom styling
|
||||||
|
2. Adding new extensions
|
||||||
|
3. Adding custom props
|
||||||
|
4. Base menu items, and their commands
|
||||||
|
|
||||||
|
This allows for extensive customization and flexibility in the Editors created using our `editor-core` package.
|
||||||
|
|
||||||
|
### Here's a detailed overview of what's exported
|
||||||
|
|
||||||
|
1. useEditor - A hook that you can use to extend the Plane editor.
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features |
|
||||||
|
| `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object |
|
||||||
|
| `uploadFile` | `(file: File) => Promise<string>` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. |
|
||||||
|
| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise<any>` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. |
|
||||||
|
| `value` | `html string` | The initial content of the editor. |
|
||||||
|
| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. |
|
||||||
|
| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. |
|
||||||
|
| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. |
|
||||||
|
| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert in case of content not being "saved". |
|
||||||
|
| `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component |
|
||||||
|
|
||||||
|
2. useReadOnlyEditor - A hook that can be used to extend a Read Only instance of the core editor.
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `value` | `string` | The initial content of the editor. |
|
||||||
|
| `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component |
|
||||||
|
| `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features |
|
||||||
|
| `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object |
|
||||||
|
|
||||||
|
3. Items and Commands - H1, H2, H3, task list, quote, code block, etc's methods.
|
||||||
|
|
||||||
|
4. UI Wrappers
|
||||||
|
|
||||||
|
- `EditorContainer` - Wrap your Editor Container with this to apply base classes and styles.
|
||||||
|
- `EditorContentWrapper` - Use this to get Editor's Content and base menus.
|
||||||
|
|
||||||
|
5. Extending with Custom Styles
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const customEditorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core features
|
||||||
|
|
||||||
|
- **Content Trimming**: The Editor’s content is now automatically trimmed of empty line breaks from the start and end before submitting it to the backend. This ensures cleaner, more consistent data.
|
||||||
|
- **Value Cleaning**: The Editor’s value is cleaned at the editor core level, eliminating the need for additional validation before sending from our app. This results in cleaner code and less potential for errors.
|
||||||
|
- **Turbo Pipeline**: Added a turbo pipeline for both dev and build tasks for projects depending on the editor package.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"web#develop": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"@plane/lite-text-editor#build",
|
||||||
|
"@plane/rich-text-editor#build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"space#develop": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"@plane/lite-text-editor#build",
|
||||||
|
"@plane/rich-text-editor#build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"web#build": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"@plane/lite-text-editor#build",
|
||||||
|
"@plane/rich-text-editor#build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"space#build": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"@plane/lite-text-editor#build",
|
||||||
|
"@plane/rich-text-editor#build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Base extensions included
|
||||||
|
|
||||||
|
- BulletList
|
||||||
|
- OrderedList
|
||||||
|
- Blockquote
|
||||||
|
- Code
|
||||||
|
- Gapcursor
|
||||||
|
- Link
|
||||||
|
- Image
|
||||||
|
- Basic Marks
|
||||||
|
- Underline
|
||||||
|
- TextStyle
|
||||||
|
- Color
|
||||||
|
- TaskList
|
||||||
|
- Markdown
|
||||||
|
- Table
|
77
packages/editor/core/package.json
Normal file
77
packages/editor/core/package.json
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"name": "@plane/editor-core",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Core Editor that powers Plane",
|
||||||
|
"main": "./dist/index.mjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"module": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"next": "12.3.2",
|
||||||
|
"next-themes": "^0.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-moveable" : "^0.54.2",
|
||||||
|
"@blueprintjs/popover2": "^2.0.10",
|
||||||
|
"@tiptap/core": "^2.1.7",
|
||||||
|
"@tiptap/extension-color": "^2.1.11",
|
||||||
|
"@tiptap/extension-image": "^2.1.7",
|
||||||
|
"@tiptap/extension-link": "^2.1.7",
|
||||||
|
"@tiptap/extension-table": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-cell": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-header": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-row": "^2.1.6",
|
||||||
|
"@tiptap/extension-task-item": "^2.1.7",
|
||||||
|
"@tiptap/extension-task-list": "^2.1.7",
|
||||||
|
"@tiptap/extension-text-style": "^2.1.11",
|
||||||
|
"@tiptap/extension-underline": "^2.1.7",
|
||||||
|
"@tiptap/pm": "^2.1.7",
|
||||||
|
"@tiptap/react": "^2.1.7",
|
||||||
|
"@tiptap/starter-kit": "^2.1.10",
|
||||||
|
"@types/react": "^18.2.5",
|
||||||
|
"@types/react-dom": "18.0.11",
|
||||||
|
"@types/node": "18.15.3",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"eslint": "8.36.0",
|
||||||
|
"eslint-config-next": "13.2.4",
|
||||||
|
"eventsource-parser": "^0.1.0",
|
||||||
|
"lucide-react": "^0.244.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
|
"tiptap-markdown": "^0.8.2",
|
||||||
|
"use-debounce": "^9.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"postcss": "^8.4.29",
|
||||||
|
"tailwind-config-custom": "*",
|
||||||
|
"tsconfig": "*",
|
||||||
|
"tsup": "^7.2.0",
|
||||||
|
"typescript": "4.9.5"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"editor",
|
||||||
|
"rich-text",
|
||||||
|
"markdown",
|
||||||
|
"nextjs",
|
||||||
|
"react"
|
||||||
|
]
|
||||||
|
}
|
9
packages/editor/core/postcss.config.js
Normal file
9
packages/editor/core/postcss.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// If you want to use other PostCSS plugins, see the following:
|
||||||
|
// https://tailwindcss.com/docs/using-with-preprocessors
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user