forked from github/plane
Merge branch 'stage-release' of https://github.com/makeplane/plane
This commit is contained in:
commit
dd319deea1
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
.git
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
node_modules
|
||||
npm-debug.log
|
53
.github/workflows/test_runner.yml
vendored
Normal file
53
.github/workflows/test_runner.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Plane Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: github_actions
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
env:
|
||||
REDIS_HOST: localhost
|
||||
REDIS_PORT: 6379
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: psycopg2 prerequisites
|
||||
run: sudo apt-get install libpq-dev
|
||||
- name: Install dependencies
|
||||
working-directory: ./apiserver
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements/test.txt
|
||||
- name: Run Tests
|
||||
working-directory: ./apiserver
|
||||
env:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: coverage run --source='.' manage.py test --settings=plane.settings.test
|
24
.gitignore
vendored
24
.gitignore
vendored
@ -39,4 +39,26 @@ yarn-error.log*
|
||||
.vercel
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
.turbo
|
||||
|
||||
## Django ##
|
||||
venv
|
||||
*.pyc
|
||||
staticfiles
|
||||
mediafiles
|
||||
.env
|
||||
.DS_Store
|
||||
|
||||
node_modules/
|
||||
assets/dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
package-lock.json
|
||||
.vscode
|
||||
|
138
Dockerfile
Normal file
138
Dockerfile
Normal file
@ -0,0 +1,138 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add curl
|
||||
|
||||
COPY ./apps ./apps
|
||||
COPY ./package.json ./package.json
|
||||
COPY ./.eslintrc.json ./.eslintrc.json
|
||||
COPY ./yarn.lock ./yarn.lock
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN turbo prune --scope=app --docker
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
RUN yarn turbo run build --filter=app...
|
||||
|
||||
FROM python:3.8.14-alpine3.16 AS runner
|
||||
|
||||
ENV SECRET_KEY ${SECRET_KEY}
|
||||
ENV DATABASE_URL ${DATABASE_URL}
|
||||
ENV REDIS_URL ${REDIS_URL}
|
||||
ENV EMAIL_HOST ${EMAIL_HOST}
|
||||
ENV EMAIL_HOST_USER ${EMAIL_HOST_USER}
|
||||
ENV EMAIL_HOST_PASSWORD ${EMAIL_HOST_PASSWORD}
|
||||
|
||||
ENV AWS_REGION ${AWS_REGION}
|
||||
ENV AWS_ACCESS_KEY_ID ${AWS_ACCESS_KEY_ID}
|
||||
ENV AWS_SECRET_ACCESS_KEY ${AWS_SECRET_ACCESS_KEY}
|
||||
ENV AWS_S3_BUCKET_NAME ${AWS_S3_BUCKET_NAME}
|
||||
|
||||
|
||||
ENV SENTRY_DSN ${SENTRY_DSN}
|
||||
ENV WEB_URL ${WEB_URL}
|
||||
|
||||
ENV DISABLE_COLLECTSTATIC ${DISABLE_COLLECTSTATIC}
|
||||
|
||||
ENV GITHUB_CLIENT_SECRET ${GITHUB_CLIENT_SECRET}
|
||||
ENV NEXT_PUBLIC_GITHUB_ID ${NEXT_PUBLIC_GITHUB_ID}
|
||||
ENV NEXT_PUBLIC_GOOGLE_CLIENTID ${NEXT_PUBLIC_GOOGLE_CLIENTID}
|
||||
ENV NEXT_PUBLIC_API_BASE_URL ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
# Frontend
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
"libpq~=14" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=18" \
|
||||
"xmlsec~=1.2"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup -S plane && \
|
||||
adduser -S captain -G plane
|
||||
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Backend
|
||||
|
||||
USER root
|
||||
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
|
||||
COPY ./apiserver/requirements.txt ./
|
||||
COPY ./apiserver/requirements ./requirements
|
||||
RUN apk add libffi-dev
|
||||
RUN apk --update --no-cache --virtual .build-deps add \
|
||||
"bash~=5.1" \
|
||||
"g++~=11.2" \
|
||||
"gcc~=11.2" \
|
||||
"cargo~=1.60" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps
|
||||
|
||||
|
||||
RUN chown captain.plane /app
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY ./apiserver/manage.py manage.py
|
||||
COPY ./apiserver/plane plane/
|
||||
COPY ./apiserver/templates templates/
|
||||
|
||||
COPY ./apiserver/gunicorn.config.py ./
|
||||
USER root
|
||||
RUN apk --update --no-cache add "bash~=5.1"
|
||||
COPY ./bin ./bin/
|
||||
USER captain
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk --update add supervisor
|
||||
|
||||
ADD /supervisor /src/supervisor
|
||||
|
||||
CMD ["supervisord","-c","/src/supervisor/service_script.conf"]
|
57
apiserver/Dockerfile.api
Normal file
57
apiserver/Dockerfile.api
Normal file
@ -0,0 +1,57 @@
|
||||
FROM python:3.8.14-alpine3.16 AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
"libpq~=14" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=18" \
|
||||
"xmlsec~=1.2"
|
||||
|
||||
COPY requirements.txt ./
|
||||
COPY requirements ./requirements
|
||||
RUN apk add libffi-dev
|
||||
RUN apk --update --no-cache --virtual .build-deps add \
|
||||
"bash~=5.1" \
|
||||
"g++~=11.2" \
|
||||
"gcc~=11.2" \
|
||||
"cargo~=1.60" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps
|
||||
|
||||
|
||||
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
|
||||
COPY manage.py manage.py
|
||||
COPY plane plane/
|
||||
COPY templates templates/
|
||||
|
||||
COPY gunicorn.config.py ./
|
||||
USER root
|
||||
RUN apk --update --no-cache add "bash~=5.1"
|
||||
COPY ./bin ./bin/
|
||||
USER captain
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/takeoff" ]
|
||||
|
5
apiserver/bin/takeoff
Executable file
5
apiserver/bin/takeoff
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
exec gunicorn plane.wsgi -k gthread --workers 8 --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
6
apiserver/bin/worker
Executable file
6
apiserver/bin/worker
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
python manage.py rqworker
|
6
apiserver/gunicorn.config.py
Normal file
6
apiserver/gunicorn.config.py
Normal file
@ -0,0 +1,6 @@
|
||||
from psycogreen.gevent import patch_psycopg
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
patch_psycopg()
|
||||
worker.log.info("Made Psycopg2 Green")
|
17
apiserver/manage.py
Normal file
17
apiserver/manage.py
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault(
|
||||
'DJANGO_SETTINGS_MODULE',
|
||||
'plane.settings.production')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
0
apiserver/plane/__init__.py
Normal file
0
apiserver/plane/__init__.py
Normal file
0
apiserver/plane/analytics/__init__.py
Normal file
0
apiserver/plane/analytics/__init__.py
Normal file
5
apiserver/plane/analytics/apps.py
Normal file
5
apiserver/plane/analytics/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AnalyticsConfig(AppConfig):
|
||||
name = 'plane.analytics'
|
0
apiserver/plane/api/__init__.py
Normal file
0
apiserver/plane/api/__init__.py
Normal file
5
apiserver/plane/api/apps.py
Normal file
5
apiserver/plane/api/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = "plane.api"
|
2
apiserver/plane/api/permissions/__init__.py
Normal file
2
apiserver/plane/api/permissions/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission
|
||||
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission
|
63
apiserver/plane/api/permissions/project.py
Normal file
63
apiserver/plane/api/permissions/project.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Third Party imports
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
# Module import
|
||||
from plane.db.models import WorkspaceMember, ProjectMember
|
||||
|
||||
|
||||
class ProjectBasePermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
## Only workspace owners or admins can create the projects
|
||||
if request.method == "POST":
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
).exists()
|
||||
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role=20
|
||||
).exists()
|
||||
|
||||
|
||||
class ProjectMemberPermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
## Only workspace owners or admins can create the projects
|
||||
if request.method == "POST":
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
).exists()
|
||||
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
).exists()
|
||||
|
||||
|
||||
class ProjectEntityPermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
## Only workspace owners or admins can create the projects
|
||||
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
).exists()
|
43
apiserver/plane/api/permissions/workspace.py
Normal file
43
apiserver/plane/api/permissions/workspace.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Third Party imports
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import WorkspaceMember, ProjectMember
|
||||
|
||||
|
||||
# TODO: Move the below logic to python match - python v3.10
|
||||
class WorkSpaceBasePermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
# allow anyone to create a workspace
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
if request.method == "POST":
|
||||
return True
|
||||
|
||||
## Safe Methods
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# allow only admins and owners to update the workspace settings
|
||||
if request.method in ["PUT", "PATCH"]:
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role__in=[15, 20]
|
||||
).exists()
|
||||
|
||||
# allow only owner to delete the workspace
|
||||
if request.method == "DELETE":
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role=20
|
||||
).exists()
|
||||
|
||||
|
||||
class WorkSpaceAdminPermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role__in=[15, 20]
|
||||
).exists()
|
42
apiserver/plane/api/serializers/__init__.py
Normal file
42
apiserver/plane/api/serializers/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
from .base import BaseSerializer
|
||||
from .people import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
TokenSerializer,
|
||||
)
|
||||
from .user import UserSerializer, UserLiteSerializer
|
||||
from .workspace import (
|
||||
WorkSpaceSerializer,
|
||||
WorkSpaceMemberSerializer,
|
||||
TeamSerializer,
|
||||
WorkSpaceMemberInviteSerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectIdentifierSerializer,
|
||||
)
|
||||
from .state import StateSerializer
|
||||
from .shortcut import ShortCutSerializer
|
||||
from .view import ViewSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer
|
||||
from .asset import FileAssetSerializer
|
||||
from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
TimeLineIssueSerializer,
|
||||
IssuePropertySerializer,
|
||||
IssueLabelSerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueStateSerializer,
|
||||
)
|
||||
|
||||
from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer
|
14
apiserver/plane/api/serializers/asset.py
Normal file
14
apiserver/plane/api/serializers/asset.py
Normal file
@ -0,0 +1,14 @@
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import FileAsset
|
||||
|
||||
|
||||
class FileAssetSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
5
apiserver/plane/api/serializers/base.py
Normal file
5
apiserver/plane/api/serializers/base.py
Normal file
@ -0,0 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
33
apiserver/plane/api/serializers/cycle.py
Normal file
33
apiserver/plane/api/serializers/cycle.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .issue import IssueStateSerializer
|
||||
from plane.db.models import Cycle, CycleIssue
|
||||
|
||||
|
||||
class CycleSerializer(BaseSerializer):
|
||||
|
||||
owned_by = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"owned_by",
|
||||
]
|
||||
|
||||
|
||||
class CycleIssueSerializer(BaseSerializer):
|
||||
|
||||
issue_details = IssueStateSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = CycleIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"cycle",
|
||||
]
|
364
apiserver/plane/api/serializers/issue.py
Normal file
364
apiserver/plane/api/serializers/issue.py
Normal file
@ -0,0 +1,364 @@
|
||||
# Third Party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .state import StateSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .project import ProjectSerializer
|
||||
from .workspace import WorkSpaceSerializer
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
TimelineIssue,
|
||||
IssueProperty,
|
||||
IssueBlocker,
|
||||
IssueAssignee,
|
||||
IssueLabel,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
Cycle,
|
||||
)
|
||||
|
||||
|
||||
class IssueFlatSerializer(BaseSerializer):
|
||||
## Contain only flat fields
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
]
|
||||
|
||||
|
||||
# Issue Serializer with state details
|
||||
class IssueStateSerializer(BaseSerializer):
|
||||
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
##TODO: Find a better way to write this serializer
|
||||
## Find a better approach to save manytomany?
|
||||
class IssueCreateSerializer(BaseSerializer):
|
||||
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkSpaceSerializer(read_only=True, source="workspace")
|
||||
|
||||
assignees_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
blockers_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
labels_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
|
||||
project = self.context["project"]
|
||||
issue = Issue.objects.create(**validated_data, project=project)
|
||||
|
||||
if blockers is not None:
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=issue,
|
||||
blocked_by=blocker,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=issue.created_by,
|
||||
updated_by=issue.updated_by,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
assignee=user,
|
||||
issue=issue,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=issue.created_by,
|
||||
updated_by=issue.updated_by,
|
||||
)
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
IssueLabel.objects.bulk_create(
|
||||
[
|
||||
IssueLabel(
|
||||
label=label,
|
||||
issue=issue,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=issue.created_by,
|
||||
updated_by=issue.updated_by,
|
||||
)
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
|
||||
if blockers is not None:
|
||||
IssueBlocker.objects.filter(block=instance).delete()
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=instance,
|
||||
blocked_by=blocker,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
updated_by=instance.updated_by,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
assignee=user,
|
||||
issue=instance,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
updated_by=instance.updated_by,
|
||||
)
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
IssueLabel.objects.filter(issue=instance).delete()
|
||||
IssueLabel.objects.bulk_create(
|
||||
[
|
||||
IssueLabel(
|
||||
label=label,
|
||||
issue=instance,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
updated_by=instance.updated_by,
|
||||
)
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueActivitySerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueCommentSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class TimeLineIssueSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = TimelineIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssuePropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"user",
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class LabelSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
# label_details = LabelSerializer(read_only=True, source="label")
|
||||
|
||||
class Meta:
|
||||
model = IssueLabel
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class BlockedIssueSerializer(BaseSerializer):
|
||||
|
||||
blocked_issue_detail = IssueFlatSerializer(source="block", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class BlockerIssueSerializer(BaseSerializer):
|
||||
|
||||
blocker_issue_detail = IssueFlatSerializer(source="blocked_by", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignee")
|
||||
|
||||
class Meta:
|
||||
model = IssueAssignee
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CycleBaseSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssueCycleDetailSerializer(BaseSerializer):
|
||||
|
||||
cycle_detail = CycleBaseSerializer(read_only=True, source="cycle")
|
||||
|
||||
class Meta:
|
||||
model = CycleIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssueSerializer(BaseSerializer):
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
136
apiserver/plane/api/serializers/module.py
Normal file
136
apiserver/plane/api/serializers/module.py
Normal file
@ -0,0 +1,136 @@
|
||||
# Third Party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .project import ProjectSerializer
|
||||
from .issue import IssueFlatSerializer
|
||||
|
||||
from plane.db.models import User, Module, ModuleMember, ModuleIssue
|
||||
|
||||
|
||||
class ModuleWriteSerializer(BaseSerializer):
|
||||
|
||||
members_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
members = validated_data.pop("members_list", None)
|
||||
|
||||
project = self.context["project"]
|
||||
|
||||
module = Module.objects.create(**validated_data, project=project)
|
||||
|
||||
if members is not None:
|
||||
ModuleMember.objects.bulk_create(
|
||||
[
|
||||
ModuleMember(
|
||||
module=module,
|
||||
member=member,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=module.created_by,
|
||||
updated_by=module.updated_by,
|
||||
)
|
||||
for member in members
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return module
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
members = validated_data.pop("members_list", None)
|
||||
|
||||
project = self.context["project"]
|
||||
|
||||
module = Module.objects.create(**validated_data, project=project)
|
||||
|
||||
if members is not None:
|
||||
ModuleMember.objects.bulk_create(
|
||||
[
|
||||
ModuleMember(
|
||||
module=module,
|
||||
member=member,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=module.created_by,
|
||||
updated_by=module.updated_by,
|
||||
)
|
||||
for member in members
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ModuleFlatSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class ModuleIssueSerializer(BaseSerializer):
|
||||
|
||||
module_detail = ModuleFlatSerializer(read_only=True, source="module")
|
||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = ModuleIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"module",
|
||||
]
|
||||
|
||||
|
||||
class ModuleSerializer(BaseSerializer):
|
||||
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||
module_issues = ModuleIssueSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
57
apiserver/plane/api/serializers/people.py
Normal file
57
apiserver/plane/api/serializers/people.py
Normal file
@ -0,0 +1,57 @@
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
Serializer,
|
||||
CharField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
||||
class ChangePasswordSerializer(Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
old_password = CharField(required=True)
|
||||
new_password = CharField(required=True)
|
||||
|
||||
|
||||
class ResetPasswordSerializer(Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
new_password = CharField(required=True)
|
||||
confirm_password = CharField(required=True)
|
||||
|
||||
|
||||
class TokenSerializer(ModelSerializer):
|
||||
|
||||
user = UserSerializer()
|
||||
access_token = SerializerMethodField()
|
||||
refresh_token = SerializerMethodField()
|
||||
|
||||
def get_access_token(self, obj):
|
||||
refresh_token = RefreshToken.for_user(obj.user)
|
||||
return str(refresh_token.access_token)
|
||||
|
||||
def get_refresh_token(self, obj):
|
||||
refresh_token = RefreshToken.for_user(obj.user)
|
||||
return str(refresh_token)
|
||||
|
||||
class Meta:
|
||||
model = Token
|
||||
fields = "__all__"
|
110
apiserver/plane/api/serializers/project.py
Normal file
110
apiserver/plane/api/serializers/project.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.api.serializers.workspace import WorkSpaceSerializer
|
||||
from plane.api.serializers.user import UserLiteSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
)
|
||||
|
||||
|
||||
class ProjectSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
)
|
||||
_ = ProjectIdentifier.objects.create(
|
||||
name=project.identifier,
|
||||
project=project,
|
||||
workspace_id=self.context["workspace_id"],
|
||||
)
|
||||
return project
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
|
||||
# If identifier is not passed update the project and return
|
||||
if identifier == "":
|
||||
project = super().update(instance, validated_data)
|
||||
return project
|
||||
|
||||
# If no Project Identifier is found create it
|
||||
project_identifier = ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=instance.workspace_id
|
||||
).first()
|
||||
|
||||
if project_identifier is None:
|
||||
project = super().update(instance, validated_data)
|
||||
_ = ProjectIdentifier.objects.update(name=identifier, project=project)
|
||||
return project
|
||||
|
||||
# If found check if the project_id to be updated and identifier project id is same
|
||||
if project_identifier.project_id == instance.id:
|
||||
# If same pass update
|
||||
project = super().update(instance, validated_data)
|
||||
return project
|
||||
|
||||
# If not same fail update
|
||||
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
||||
|
||||
|
||||
class ProjectDetailSerializer(BaseSerializer):
|
||||
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
default_assignee = UserLiteSerializer(read_only=True)
|
||||
project_lead = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
project = ProjectSerializer(read_only=True)
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
|
||||
project = ProjectSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMemberInvite
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectIdentifierSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectIdentifier
|
||||
fields = "__all__"
|
14
apiserver/plane/api/serializers/shortcut.py
Normal file
14
apiserver/plane/api/serializers/shortcut.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
|
||||
from plane.db.models import Shortcut
|
||||
|
||||
|
||||
class ShortCutSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Shortcut
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
14
apiserver/plane/api/serializers/state.py
Normal file
14
apiserver/plane/api/serializers/state.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
40
apiserver/plane/api/serializers/user.py
Normal file
40
apiserver/plane/api/serializers/user.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"is_superuser",
|
||||
"is_staff",
|
||||
"last_active",
|
||||
"last_login_time",
|
||||
"last_logout_time",
|
||||
"last_login_ip",
|
||||
"last_logout_ip",
|
||||
"last_login_uagent",
|
||||
"token_updated_at",
|
||||
"is_onboarded",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
||||
class UserLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"avatar",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
]
|
14
apiserver/plane/api/serializers/view.py
Normal file
14
apiserver/plane/api/serializers/view.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
|
||||
from plane.db.models import View
|
||||
|
||||
|
||||
class ViewSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = View
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
100
apiserver/plane/api/serializers/workspace.py
Normal file
100
apiserver/plane/api/serializers/workspace.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
|
||||
from plane.db.models import User, Workspace, WorkspaceMember, Team, TeamMember
|
||||
from plane.db.models import Workspace, WorkspaceMember, Team, WorkspaceMemberInvite
|
||||
|
||||
|
||||
class WorkSpaceSerializer(BaseSerializer):
|
||||
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"slug",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"owner",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"slug": {
|
||||
"required": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
||||
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMemberInvite
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TeamSerializer(BaseSerializer):
|
||||
|
||||
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
|
||||
members = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def create(self, validated_data, **kwargs):
|
||||
if "members" in validated_data:
|
||||
members = validated_data.pop("members")
|
||||
workspace = self.context["workspace"]
|
||||
team = Team.objects.create(**validated_data, workspace=workspace)
|
||||
team_members = [
|
||||
TeamMember(member=member, team=team, workspace=workspace)
|
||||
for member in members
|
||||
]
|
||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||
return team
|
||||
else:
|
||||
team = Team.objects.create(**validated_data)
|
||||
return team
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if "members" in validated_data:
|
||||
members = validated_data.pop("members")
|
||||
TeamMember.objects.filter(team=instance).delete()
|
||||
team_members = [
|
||||
TeamMember(member=member, team=instance, workspace=instance.workspace)
|
||||
for member in members
|
||||
]
|
||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||
return super().update(instance, validated_data)
|
||||
else:
|
||||
return super().update(instance, validated_data)
|
643
apiserver/plane/api/urls.py
Normal file
643
apiserver/plane/api/urls.py
Normal file
@ -0,0 +1,643 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
# Create your urls here.
|
||||
|
||||
from plane.api.views import (
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
PeopleEndpoint,
|
||||
UserEndpoint,
|
||||
VerifyEmailEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
OauthEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
)
|
||||
|
||||
from plane.api.views import (
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
WorkSpaceViewSet,
|
||||
UserWorkSpacesEndpoint,
|
||||
InviteWorkspaceEndpoint,
|
||||
JoinWorkspaceEndpoint,
|
||||
WorkSpaceMemberViewSet,
|
||||
WorkspaceInvitationsViewset,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
ProjectViewSet,
|
||||
InviteProjectEndpoint,
|
||||
ProjectMemberViewSet,
|
||||
ProjectMemberInvitationsViewset,
|
||||
StateViewSet,
|
||||
ShortCutViewSet,
|
||||
ViewViewSet,
|
||||
CycleViewSet,
|
||||
FileAssetEndpoint,
|
||||
IssueViewSet,
|
||||
WorkSpaceIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
TeamMemberViewSet,
|
||||
TimeLineIssueViewSet,
|
||||
CycleIssueViewSet,
|
||||
IssuePropertyViewSet,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectIdentifierEndpoint,
|
||||
LabelViewSet,
|
||||
AddMemberToProjectEndpoint,
|
||||
ProjectJoinEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
BulkAssignIssuesToCycleEndpoint,
|
||||
ProjectUserViewsEndpoint,
|
||||
ModuleViewSet,
|
||||
ModuleIssueViewSet,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
)
|
||||
|
||||
from plane.api.views.project import AddTeamToProjectEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Social Auth
|
||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||
# Auth
|
||||
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"),
|
||||
# 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(
|
||||
"password-reset/<uidb64>/<token>/",
|
||||
ResetPasswordEndpoint.as_view(),
|
||||
name="password-reset",
|
||||
),
|
||||
path(
|
||||
"forgot-password/",
|
||||
ForgotPasswordEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
# List Users
|
||||
path("users/", PeopleEndpoint.as_view()),
|
||||
# User Profile
|
||||
path(
|
||||
"users/me/",
|
||||
UserEndpoint.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/me/change-password/",
|
||||
ChangePasswordEndpoint.as_view(),
|
||||
name="change-password",
|
||||
),
|
||||
path(
|
||||
"users/me/onboard/",
|
||||
UpdateUserOnBoardedEndpoint.as_view(),
|
||||
name="change-password",
|
||||
),
|
||||
# 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="workspace",
|
||||
),
|
||||
# user join workspace
|
||||
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-invitaions",
|
||||
),
|
||||
## Workspaces ##
|
||||
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="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invitations/",
|
||||
WorkspaceInvitationsViewset.as_view({"get": "list"}),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invitations/<uuid:pk>/",
|
||||
WorkspaceInvitationsViewset.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/",
|
||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||
WorkSpaceMemberViewSet.as_view(
|
||||
{
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/teams/",
|
||||
TeamMemberViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/teams/<uuid:pk>/",
|
||||
TeamMemberViewSet.as_view(
|
||||
{
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"users/last-visited-workspace/",
|
||||
UserLastProjectWithWorkspaceEndpoint.as_view(),
|
||||
name="workspace-project-details",
|
||||
),
|
||||
## End Workspaces ##
|
||||
# Projects
|
||||
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="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||
ProjectMemberViewSet.as_view({"get": "list"}),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
|
||||
ProjectMemberViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
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",
|
||||
),
|
||||
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="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
||||
ProjectMemberInvitationsViewset.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
||||
ProjectUserViewsEndpoint.as_view(),
|
||||
name="project-view",
|
||||
),
|
||||
# States
|
||||
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",
|
||||
),
|
||||
# End States ##
|
||||
# Shortcuts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
|
||||
ShortCutViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-shortcut",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/<uuid:pk>/",
|
||||
ShortCutViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-shortcut",
|
||||
),
|
||||
## End Shortcuts
|
||||
# Views
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
|
||||
ViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
|
||||
ViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-view",
|
||||
),
|
||||
## End Views
|
||||
## Cycles
|
||||
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-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-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/bulk-assign-issues/",
|
||||
BulkAssignIssuesToCycleEndpoint.as_view(),
|
||||
name="bulk-assign-cycle-issues",
|
||||
),
|
||||
## End Cycles
|
||||
# Issue
|
||||
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>/issues/",
|
||||
WorkSpaceIssuesEndpoint.as_view(),
|
||||
name="workspace-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-delete-issues/",
|
||||
BulkDeleteIssuesEndpoint.as_view(),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/my-issues/",
|
||||
UserWorkSpaceIssues.as_view(),
|
||||
name="workspace-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
|
||||
## Roadmap
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/roadmaps/",
|
||||
TimeLineIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/roadmaps/<uuid:pk>/",
|
||||
TimeLineIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-roadmap",
|
||||
),
|
||||
## End Roadmap
|
||||
## 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
|
||||
## File Assets
|
||||
path(
|
||||
"file-assets/",
|
||||
FileAssetEndpoint.as_view(),
|
||||
name="File Assets",
|
||||
),
|
||||
## End File Assets
|
||||
## Modules
|
||||
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",
|
||||
),
|
||||
## End Modules
|
||||
# path(
|
||||
# "issues/<int:pk>/all/",
|
||||
# IssueViewSet.as_view({"get": "list_issue_history_comments"}),
|
||||
# name="Issue history and comments",
|
||||
# ),
|
||||
]
|
70
apiserver/plane/api/views/__init__.py
Normal file
70
apiserver/plane/api/views/__init__.py
Normal file
@ -0,0 +1,70 @@
|
||||
from .project import (
|
||||
ProjectViewSet,
|
||||
ProjectMemberViewSet,
|
||||
UserProjectInvitationsViewset,
|
||||
InviteProjectEndpoint,
|
||||
AddTeamToProjectEndpoint,
|
||||
ProjectMemberInvitationsViewset,
|
||||
ProjectMemberInviteDetailViewSet,
|
||||
ProjectIdentifierEndpoint,
|
||||
AddMemberToProjectEndpoint,
|
||||
ProjectJoinEndpoint,
|
||||
ProjectUserViewsEndpoint,
|
||||
)
|
||||
from .people import (
|
||||
PeopleEndpoint,
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
)
|
||||
|
||||
from .oauth import OauthEndpoint
|
||||
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
from .workspace import (
|
||||
WorkSpaceViewSet,
|
||||
UserWorkSpacesEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
InviteWorkspaceEndpoint,
|
||||
JoinWorkspaceEndpoint,
|
||||
WorkSpaceMemberViewSet,
|
||||
TeamMemberViewSet,
|
||||
WorkspaceInvitationsViewset,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .shortcut import ShortCutViewSet
|
||||
from .view import ViewViewSet
|
||||
from .cycle import CycleViewSet, CycleIssueViewSet, BulkAssignIssuesToCycleEndpoint
|
||||
from .asset import FileAssetEndpoint
|
||||
from .issue import (
|
||||
IssueViewSet,
|
||||
WorkSpaceIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
TimeLineIssueViewSet,
|
||||
IssuePropertyViewSet,
|
||||
LabelViewSet,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
VerifyEmailEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
)
|
||||
|
||||
|
||||
from .authentication import (
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
)
|
||||
|
||||
from .module import ModuleViewSet, ModuleIssueViewSet
|
30
apiserver/plane/api/views/asset.py
Normal file
30
apiserver/plane/api/views/asset.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import FileAsset
|
||||
from plane.api.serializers import FileAssetSerializer
|
||||
|
||||
|
||||
class FileAssetEndpoint(BaseAPIView):
|
||||
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
"""
|
||||
A viewset for viewing and editing task instances.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
files = FileAsset.objects.all()
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = FileAssetSerializer(data=request.data)
|
||||
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)
|
159
apiserver/plane/api/views/auth_extended.py
Normal file
159
apiserver/plane/api/views/auth_extended.py
Normal file
@ -0,0 +1,159 @@
|
||||
## Python imports
|
||||
import jwt
|
||||
|
||||
## Django imports
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.utils.encoding import (
|
||||
smart_str,
|
||||
smart_bytes,
|
||||
DjangoUnicodeDecodeError,
|
||||
)
|
||||
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
|
||||
|
||||
## Third Party Imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
## Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.api.serializers.people import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
)
|
||||
from plane.db.models import User
|
||||
from plane.bgtasks.email_verification_task import email_verification
|
||||
from plane.bgtasks.forgot_password_task import forgot_password
|
||||
|
||||
|
||||
class RequestEmailVerificationEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
token = RefreshToken.for_user(request.user).access_token
|
||||
current_site = settings.WEB_URL
|
||||
email_verification.delay(
|
||||
request.user.first_name, request.user.email, token, current_site
|
||||
)
|
||||
return Response(
|
||||
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class VerifyEmailEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
token = request.GET.get("token")
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
|
||||
user = User.objects.get(id=payload["user_id"])
|
||||
|
||||
if not user.is_email_verified:
|
||||
user.is_email_verified = True
|
||||
user.save()
|
||||
return Response(
|
||||
{"email": "Successfully activated"}, status=status.HTTP_200_OK
|
||||
)
|
||||
except jwt.ExpiredSignatureError as indentifier:
|
||||
return Response(
|
||||
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except jwt.exceptions.DecodeError as indentifier:
|
||||
return Response(
|
||||
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class ForgotPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email")
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||
token = PasswordResetTokenGenerator().make_token(user)
|
||||
|
||||
current_site = settings.WEB_URL
|
||||
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"messgae": "Check your email to reset your password"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class ResetPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
try:
|
||||
id = smart_str(urlsafe_base64_decode(uidb64))
|
||||
user = User.objects.get(id=id)
|
||||
if not PasswordResetTokenGenerator().check_token(user, token):
|
||||
return Response(
|
||||
{"error": "token is not valid, please check the new one"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
serializer = ResetPasswordSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.save()
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
}
|
||||
|
||||
return Response(response)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except DjangoUnicodeDecodeError as indentifier:
|
||||
return Response(
|
||||
{"error": "token is not valid, please check the new one"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
|
||||
class ChangePasswordEndpoint(BaseAPIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
if serializer.is_valid():
|
||||
# Check old password
|
||||
if not user.object.check_password(serializer.data.get("old_password")):
|
||||
return Response(
|
||||
{"old_password": ["Wrong password."]},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# set_password also hashes the password that the user will get
|
||||
self.object.set_password(serializer.data.get("new_password"))
|
||||
self.object.save()
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
}
|
||||
|
||||
return Response(response)
|
||||
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
365
apiserver/plane/api/views/authentication.py
Normal file
365
apiserver/plane/api/views/authentication.py
Normal file
@ -0,0 +1,365 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework import status
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from sentry_sdk import capture_exception, capture_message
|
||||
|
||||
# Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.db.models import User
|
||||
from plane.api.serializers import UserSerializer
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return (
|
||||
str(refresh.access_token),
|
||||
str(refresh),
|
||||
)
|
||||
|
||||
|
||||
class SignUpEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
if user is not None:
|
||||
return Response(
|
||||
{"error": "Email ID is already taken"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User.objects.create(email=email)
|
||||
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,
|
||||
}
|
||||
|
||||
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 or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class SignInEndpoint(BaseAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
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,
|
||||
)
|
||||
|
||||
user = User.objects.get(email=email)
|
||||
|
||||
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)
|
||||
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"user": serialized_user,
|
||||
}
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except User.DoesNotExist:
|
||||
return Response(
|
||||
{
|
||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class SignOutEndpoint(BaseAPIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data.get("refresh_token", False)
|
||||
|
||||
if not refresh_token:
|
||||
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(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Please provide a valid email address"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
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(
|
||||
{"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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
user_token = request.data.get("token", "").strip().lower()
|
||||
key = request.data.get("key", False)
|
||||
|
||||
if not key or user_token == "":
|
||||
return Response(
|
||||
{"error": "User token and key are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
ri = redis_instance()
|
||||
|
||||
if ri.exists(key):
|
||||
|
||||
data = json.loads(ri.get(key))
|
||||
|
||||
token = data["token"]
|
||||
email = data["email"]
|
||||
|
||||
if str(token) == str(user_token):
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
else:
|
||||
user = User.objects.create(
|
||||
email=email, username=uuid.uuid4().hex
|
||||
)
|
||||
|
||||
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:
|
||||
return Response(
|
||||
{"error": "Your login code was incorrect. Please try again."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
else:
|
||||
return Response(
|
||||
{"error": "The magic code/link has expired please try again"},
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
142
apiserver/plane/api/views/base.py
Normal file
142
apiserver/plane/api/views/base.py
Normal file
@ -0,0 +1,142 @@
|
||||
# Django imports
|
||||
from django.urls import resolve
|
||||
from django.conf import settings
|
||||
# Third part imports
|
||||
from rest_framework import status
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
|
||||
class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
|
||||
model = None
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = (
|
||||
DjangoFilterBackend,
|
||||
SearchFilter,
|
||||
)
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
search_fields = []
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
return self.model.objects.all()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise APIException(
|
||||
"Please check the view", status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
print(f'# of Queries: {len(connection.queries)}')
|
||||
return response
|
||||
|
||||
@property
|
||||
def workspace_slug(self):
|
||||
return self.kwargs.get("slug", None)
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
if self.workspace_slug:
|
||||
try:
|
||||
return Workspace.objects.get(slug=self.workspace_slug)
|
||||
except Workspace.DoesNotExist:
|
||||
raise NotFound(detail="Workspace does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
project_id = self.kwargs.get("project_id", None)
|
||||
if project_id:
|
||||
return project_id
|
||||
|
||||
if resolve(self.request.path_info).url_name == "project":
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
if self.project_id:
|
||||
try:
|
||||
return Project.objects.get(pk=self.project_id)
|
||||
except Project.DoesNotExist:
|
||||
raise NotFound(detail="Project does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class BaseAPIView(APIView, BasePaginator):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = (
|
||||
DjangoFilterBackend,
|
||||
SearchFilter,
|
||||
)
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
search_fields = []
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
for backend in list(self.filter_backends):
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
print(f'# of Queries: {len(connection.queries)}')
|
||||
return response
|
||||
|
||||
@property
|
||||
def workspace_slug(self):
|
||||
return self.kwargs.get("slug", None)
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
if self.workspace_slug:
|
||||
try:
|
||||
return Workspace.objects.get(slug=self.workspace_slug)
|
||||
except Workspace.DoesNotExist:
|
||||
raise NotFound(detail="Workspace does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.kwargs.get("project_id", None)
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
if self.project_id:
|
||||
try:
|
||||
return Project.objects.get(pk=self.project_id)
|
||||
except Project.DoesNotExist:
|
||||
raise NotFound(detail="Project does not exist")
|
||||
else:
|
||||
return None
|
109
apiserver/plane/api/views/cycle.py
Normal file
109
apiserver/plane/api/views/cycle.py
Normal file
@ -0,0 +1,109 @@
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import CycleSerializer, CycleIssueSerializer
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Cycle, CycleIssue, Issue
|
||||
|
||||
|
||||
class CycleViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = CycleSerializer
|
||||
model = Cycle
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = CycleIssueSerializer
|
||||
model = CycleIssue
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
cycle_id=self.kwargs.get("cycle_id"),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(cycle_id=self.kwargs.get("cycle_id"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("cycle")
|
||||
.select_related("issue")
|
||||
.select_related("issue__state")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class BulkAssignIssuesToCycleEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
try:
|
||||
|
||||
issue_ids = request.data.get("issue_ids")
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
pk__in=issue_ids, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
CycleIssue.objects.bulk_create(
|
||||
[
|
||||
CycleIssue(
|
||||
project_id=project_id,
|
||||
workspace=cycle.workspace,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
cycle=cycle,
|
||||
issue=issue,
|
||||
)
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
except Cycle.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
403
apiserver/plane/api/views/issue.py
Normal file
403
apiserver/plane/api/views/issue.py
Normal file
@ -0,0 +1,403 @@
|
||||
# Python imports
|
||||
from itertools import groupby
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Count, Sum
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
TimeLineIssueSerializer,
|
||||
IssuePropertySerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkSpaceAdminPermission,
|
||||
ProjectMemberPermission,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
TimelineIssue,
|
||||
IssueProperty,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
)
|
||||
|
||||
|
||||
class IssueViewSet(BaseViewSet):
|
||||
def get_serializer_class(self):
|
||||
return (
|
||||
IssueCreateSerializer
|
||||
if self.action in ["create", "update", "partial_update"]
|
||||
else IssueSerializer
|
||||
)
|
||||
|
||||
model = Issue
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"name",
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
"state__name",
|
||||
"assignees__id",
|
||||
"workspace__id",
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocked_issues",
|
||||
queryset=IssueBlocker.objects.select_related("blocked_by", "block"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocker_issues",
|
||||
queryset=IssueBlocker.objects.select_related("block", "blocked_by"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle",
|
||||
queryset=CycleIssue.objects.select_related("cycle", "issue"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def grouper(self, issue, group_by):
|
||||
group_by = issue.get(group_by, "")
|
||||
|
||||
if isinstance(group_by, list):
|
||||
if len(group_by):
|
||||
return group_by[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
else:
|
||||
return group_by
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
try:
|
||||
issue_queryset = self.get_queryset()
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
# TODO: Move this group by from ittertools to ORM for better performance - nk
|
||||
if group_by:
|
||||
issue_dict = dict()
|
||||
|
||||
issues = IssueSerializer(issue_queryset, many=True).data
|
||||
|
||||
for key, value in groupby(
|
||||
issues, lambda issue: self.grouper(issue, group_by)
|
||||
):
|
||||
issue_dict[str(key)] = list(value)
|
||||
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True).data,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = IssueCreateSerializer(
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class UserWorkSpaceIssues(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
issues = Issue.objects.filter(
|
||||
assignees__in=[request.user], workspace__slug=slug
|
||||
)
|
||||
serializer = IssueSerializer(issues, 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
class WorkSpaceIssuesEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
issues = Issue.objects.filter(workspace__slug=slug).filter(
|
||||
project__project_projectmember__member=self.request.user
|
||||
)
|
||||
serializer = IssueSerializer(issues, 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class IssueActivityEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
issue_activities = IssueActivity.objects.filter(issue_id=issue_id).filter(
|
||||
project__project_projectmember__member=self.request.user
|
||||
)
|
||||
serializer = IssueActivitySerializer(issue_activities, 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class IssueCommentViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
|
||||
filterset_fields = [
|
||||
"issue__id",
|
||||
"workspace__id",
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
issue_id=self.kwargs.get("issue_id"),
|
||||
actor=self.request.user if self.request.user is not None else None,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class TimeLineIssueViewSet(BaseViewSet):
|
||||
serializer_class = TimeLineIssueSerializer
|
||||
model = TimelineIssue
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
"issue__id",
|
||||
"workspace__id",
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
issue_id=self.kwargs.get("issue_id"),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class IssuePropertyViewSet(BaseViewSet):
|
||||
serializer_class = IssuePropertySerializer
|
||||
model = IssueProperty
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"), user=self.request.user
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(user=self.request.user)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
serializer = IssuePropertySerializer(queryset, many=True)
|
||||
return Response(
|
||||
serializer.data[0] if len(serializer.data) > 0 else [],
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
issue_property, created = IssueProperty.objects.get_or_create(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
if not created:
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.save()
|
||||
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.save()
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class LabelViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = LabelSerializer
|
||||
model = Label
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("parent")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
return Response(
|
||||
{"error": "Issue IDs are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
)
|
||||
|
||||
total_issues = len(issues)
|
||||
|
||||
issues.delete()
|
||||
|
||||
return Response(
|
||||
{"message": f"{total_issues} issues were deleted"},
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
109
apiserver/plane/api/views/module.py
Normal file
109
apiserver/plane/api/views/module.py
Normal file
@ -0,0 +1,109 @@
|
||||
# Django Imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet
|
||||
from plane.api.serializers import (
|
||||
ModuleWriteSerializer,
|
||||
ModuleSerializer,
|
||||
ModuleIssueSerializer,
|
||||
)
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Module, ModuleIssue, Project
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
|
||||
model = Module
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self):
|
||||
return (
|
||||
ModuleWriteSerializer
|
||||
if self.action in ["create", "update", "partial_update"]
|
||||
else ModuleSerializer
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("lead")
|
||||
.prefetch_related("members")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"module_issues",
|
||||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class ModuleIssueViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ModuleIssueSerializer
|
||||
model = ModuleIssue
|
||||
|
||||
filterset_fields = [
|
||||
"issue__id",
|
||||
"workspace__id",
|
||||
]
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
module_id=self.kwargs.get("module_id"),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(module_id=self.kwargs.get("module_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("module")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
269
apiserver/plane/api/views/oauth.py
Normal file
269
apiserver/plane/api/views/oauth.py
Normal file
@ -0,0 +1,269 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third Party modules
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework import status
|
||||
|
||||
# sso authentication
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as google_auth_request
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import SocialLoginConnection, User
|
||||
from plane.api.serializers import UserSerializer
|
||||
from .base import BaseAPIView
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return (
|
||||
str(refresh.access_token),
|
||||
str(refresh),
|
||||
)
|
||||
|
||||
|
||||
def validate_google_token(token, client_id):
|
||||
try:
|
||||
|
||||
id_info = id_token.verify_oauth2_token(
|
||||
token, google_auth_request.Request(), client_id
|
||||
)
|
||||
email = id_info.get("email")
|
||||
first_name = id_info.get("given_name")
|
||||
last_name = id_info.get("family_name", "")
|
||||
data = {
|
||||
"email": email,
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
}
|
||||
return data
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise exceptions.AuthenticationFailed("Error with Google connection.")
|
||||
|
||||
|
||||
def get_access_token(request_token: str, client_id: str) -> str:
|
||||
"""Obtain the request token from github.
|
||||
Given the client id, client secret and request issued out by GitHub, this method
|
||||
should give back an access token
|
||||
Parameters
|
||||
----------
|
||||
CLIENT_ID: str
|
||||
A string representing the client id issued out by github
|
||||
CLIENT_SECRET: str
|
||||
A string representing the client secret issued out by github
|
||||
request_token: str
|
||||
A string representing the request token issued out by github
|
||||
Throws
|
||||
------
|
||||
ValueError:
|
||||
if CLIENT_ID or CLIENT_SECRET or request_token is empty or not a string
|
||||
Returns
|
||||
-------
|
||||
access_token: str
|
||||
A string representing the access token issued out by github
|
||||
"""
|
||||
|
||||
if not request_token:
|
||||
raise ValueError("The request token has to be supplied!")
|
||||
|
||||
CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
||||
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={CLIENT_SECRET}&code={request_token}"
|
||||
headers = {"accept": "application/json"}
|
||||
|
||||
res = requests.post(url, headers=headers)
|
||||
|
||||
data = res.json()
|
||||
access_token = data["access_token"]
|
||||
|
||||
return access_token
|
||||
|
||||
|
||||
def get_user_data(access_token: str) -> dict:
|
||||
"""
|
||||
Obtain the user data from github.
|
||||
Given the access token, this method should give back the user data
|
||||
"""
|
||||
if not access_token:
|
||||
raise ValueError("The request token has to be supplied!")
|
||||
if not isinstance(access_token, str):
|
||||
raise ValueError("The request token has to be a string!")
|
||||
|
||||
access_token = "token " + access_token
|
||||
url = "https://api.github.com/user"
|
||||
headers = {"Authorization": access_token}
|
||||
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
|
||||
userData = resp.json()
|
||||
|
||||
return userData
|
||||
|
||||
|
||||
class OauthEndpoint(BaseAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
medium = request.data.get("medium", False)
|
||||
id_token = request.data.get("credential", False)
|
||||
client_id = request.data.get("clientId", False)
|
||||
|
||||
if not medium or not id_token:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if medium == "google":
|
||||
data = validate_google_token(id_token, client_id)
|
||||
|
||||
if medium == "github":
|
||||
access_token = get_access_token(id_token, client_id)
|
||||
data = get_user_data(access_token)
|
||||
|
||||
email = data.get("email", None)
|
||||
if email == None:
|
||||
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if "@" in email:
|
||||
user = User.objects.get(email=email)
|
||||
email = data["email"]
|
||||
channel = "email"
|
||||
mobile_number = uuid.uuid4().hex
|
||||
email_verified = True
|
||||
else:
|
||||
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
## Login Case
|
||||
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_medium = f"oauth"
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.is_email_verified = email_verified
|
||||
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,
|
||||
}
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
extra_data={},
|
||||
user=user,
|
||||
defaults={
|
||||
"token_data": {"id_token": id_token},
|
||||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except User.DoesNotExist:
|
||||
## Signup Case
|
||||
|
||||
username = uuid.uuid4().hex
|
||||
|
||||
if "@" in email:
|
||||
email = data["email"]
|
||||
mobile_number = uuid.uuid4().hex
|
||||
channel = "email"
|
||||
email_verified = True
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
mobile_number=mobile_number,
|
||||
first_name=data["first_name"],
|
||||
last_name=data["last_name"],
|
||||
is_email_verified=email_verified,
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_medium = "oauth"
|
||||
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,
|
||||
"permissions": [],
|
||||
}
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
extra_data={},
|
||||
user=user,
|
||||
defaults={
|
||||
"token_data": {"id_token": id_token},
|
||||
"last_login_at": timezone.now(),
|
||||
},
|
||||
)
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
76
apiserver/plane/api/views/people.py
Normal file
76
apiserver/plane/api/views/people.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
UserSerializer,
|
||||
)
|
||||
|
||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
|
||||
class PeopleEndpoint(BaseAPIView):
|
||||
|
||||
filterset_fields = ("date_joined",)
|
||||
|
||||
search_fields = (
|
||||
"^first_name",
|
||||
"^last_name",
|
||||
"^email",
|
||||
"^username",
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
users = User.objects.all().order_by("-date_joined")
|
||||
if (
|
||||
request.GET.get("search", None) is not None
|
||||
and len(request.GET.get("search")) < 3
|
||||
):
|
||||
return Response(
|
||||
{"message": "Search term must be at least 3 characters long"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=self.filter_queryset(users),
|
||||
on_results=lambda data: UserSerializer(data, many=True).data,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"message": "Something went wrong"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
serializer_class = UserSerializer
|
||||
model = User
|
||||
serializers = {}
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
def patch(self, request):
|
||||
try:
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||
user.save()
|
||||
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,
|
||||
)
|
627
apiserver/plane/api/views/project.py
Normal file
627
apiserver/plane/api/views/project.py
Normal file
@ -0,0 +1,627 @@
|
||||
# Python imports
|
||||
import jwt
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectDetailSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
)
|
||||
|
||||
from plane.api.permissions import ProjectBasePermission
|
||||
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
Workspace,
|
||||
ProjectMemberInvite,
|
||||
User,
|
||||
WorkspaceMember,
|
||||
State,
|
||||
TeamMember,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
Workspace,
|
||||
ProjectMemberInvite,
|
||||
User,
|
||||
ProjectIdentifier,
|
||||
)
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
serializer_class = ProjectSerializer
|
||||
model = Project
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action == "update" or self.action == "partial_update":
|
||||
return ProjectSerializer
|
||||
return ProjectDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||
.select_related("workspace", "workspace__owner")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def create(self, request, slug):
|
||||
try:
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
## Add the user as Administrator to the project
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
|
||||
## Default states
|
||||
states = [
|
||||
{
|
||||
"name": "Backlog",
|
||||
"color": "#5e6ad2",
|
||||
"sequence": 15000,
|
||||
"group": "backlog",
|
||||
},
|
||||
{
|
||||
"name": "Todo",
|
||||
"color": "#eb5757",
|
||||
"sequence": 25000,
|
||||
"group": "unstarted",
|
||||
},
|
||||
{
|
||||
"name": "In Progress",
|
||||
"color": "#26b5ce",
|
||||
"sequence": 35000,
|
||||
"group": "started",
|
||||
},
|
||||
{
|
||||
"name": "Done",
|
||||
"color": "#f2c94c",
|
||||
"sequence": 45000,
|
||||
"group": "completed",
|
||||
},
|
||||
{
|
||||
"name": "Cancelled",
|
||||
"color": "#4cb782",
|
||||
"sequence": 55000,
|
||||
"group": "cancelled",
|
||||
},
|
||||
]
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
State(
|
||||
name=state["name"],
|
||||
color=state["color"],
|
||||
project=serializer.instance,
|
||||
sequence=state["sequence"],
|
||||
workspace=serializer.instance.workspace,
|
||||
group=state["group"],
|
||||
)
|
||||
for state in states
|
||||
]
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
[serializer.errors[error][0] for error in serializer.errors],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Workspace.DoesNotExist as e:
|
||||
return Response(
|
||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except serializers.ValidationError as e:
|
||||
return Response(
|
||||
{"identifier": "The project identifier 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
project = Project.objects.get(pk=pk)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
data={**request.data},
|
||||
context={"workspace_id": workspace.id},
|
||||
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 IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except (Project.DoesNotExist or Workspace.DoesNotExist) as e:
|
||||
return Response(
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except serializers.ValidationError as e:
|
||||
return Response(
|
||||
{"identifier": "The project identifier 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class InviteProjectEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
email = request.data.get("email", False)
|
||||
role = request.data.get("role", False)
|
||||
|
||||
# Check if email is provided
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
validate_email(email)
|
||||
# Check if user is already a member of workspace
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id, member__email=email
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "User is already member of workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
if user is None:
|
||||
token = jwt.encode(
|
||||
{"email": email, "timestamp": datetime.now().timestamp()},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
project_invitation_obj = ProjectMemberInvite.objects.create(
|
||||
email=email.strip().lower(),
|
||||
project_id=project_id,
|
||||
token=token,
|
||||
role=role,
|
||||
)
|
||||
domain = settings.WEB_URL
|
||||
project_invitation.delay(email, project_id, token, domain)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": "Email sent successfully",
|
||||
"id": project_invitation_obj.id,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
project_member = ProjectMember.objects.create(
|
||||
member=user, project_id=project_id, role=role
|
||||
)
|
||||
|
||||
return Response(
|
||||
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid email address provided a valid email address is required to send the invite"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
|
||||
return Response(
|
||||
{"error": "Workspace or Project 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UserProjectInvitationsViewset(BaseViewSet):
|
||||
|
||||
serializer_class = ProjectMemberInviteSerializer
|
||||
model = ProjectMemberInvite
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(email=self.request.user.email)
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
try:
|
||||
|
||||
invitations = request.data.get("invitations")
|
||||
project_invitations = ProjectMemberInvite.objects.filter(
|
||||
pk__in=invitations, accepted=True
|
||||
)
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
project=invitation.project,
|
||||
workspace=invitation.project.workspace,
|
||||
member=request.user,
|
||||
role=invitation.role,
|
||||
)
|
||||
for invitation in project_invitations
|
||||
]
|
||||
)
|
||||
|
||||
## Delete joined project invites
|
||||
project_invitations.delete()
|
||||
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ProjectMemberSerializer
|
||||
model = ProjectMember
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.select_related("project")
|
||||
.select_related("member")
|
||||
)
|
||||
|
||||
|
||||
class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
member_id = request.data.get("member_id", False)
|
||||
role = request.data.get("role", False)
|
||||
|
||||
if not member_id or not role:
|
||||
return Response(
|
||||
{"error": "Member ID and role is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the user is a member in the workspace
|
||||
if not WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member_id=member_id
|
||||
).exists():
|
||||
# TODO: Update this error message - nk
|
||||
return Response(
|
||||
{
|
||||
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the user is already member of project
|
||||
if ProjectMember.objects.filter(
|
||||
project=project_id, member_id=member_id
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "User is already a member of the project"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Add the user to project
|
||||
project_member = ProjectMember.objects.create(
|
||||
project_id=project_id, member_id=member_id, role=role
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_member)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id):
|
||||
|
||||
try:
|
||||
team_members = TeamMember.objects.filter(
|
||||
workspace__slug=slug, team__in=request.data.get("teams", [])
|
||||
).values_list("member", flat=True)
|
||||
|
||||
if len(team_members) == 0:
|
||||
return Response(
|
||||
{"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
project_members = []
|
||||
for member in team_members:
|
||||
project_members.append(
|
||||
ProjectMember(
|
||||
project_id=project_id,
|
||||
member_id=member,
|
||||
workspace=workspace,
|
||||
)
|
||||
)
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
project_members, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "The team with the name already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "The requested workspace could not be found"},
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class ProjectMemberInvitationsViewset(BaseViewSet):
|
||||
|
||||
serializer_class = ProjectMemberInviteSerializer
|
||||
model = ProjectMemberInvite
|
||||
|
||||
search_fields = []
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.select_related("project")
|
||||
)
|
||||
|
||||
|
||||
class ProjectMemberInviteDetailViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ProjectMemberInviteSerializer
|
||||
model = ProjectMemberInvite
|
||||
|
||||
search_fields = []
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(super().get_queryset().select_related("project"))
|
||||
|
||||
|
||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
|
||||
name = request.GET.get("name", "").strip().upper()
|
||||
|
||||
if name == "":
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
exists = ProjectIdentifier.objects.filter(
|
||||
name=name, workspace__slug=slug
|
||||
).values("id", "name", "project")
|
||||
|
||||
return Response(
|
||||
{"exists": len(exists), "identifiers": exists},
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def delete(self, request, slug):
|
||||
try:
|
||||
|
||||
name = request.data.get("name", "").strip().upper()
|
||||
|
||||
if name == "":
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
|
||||
return Response(
|
||||
{"error": "Cannot delete an identifier of an existing project"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
|
||||
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class ProjectJoinEndpoint(BaseAPIView):
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
project_ids = request.data.get("project_ids", [])
|
||||
|
||||
# Get the workspace user role
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
member=request.user, workspace__slug=slug
|
||||
)
|
||||
|
||||
workspace_role = workspace_member.role
|
||||
workspace = workspace_member.workspace
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=20
|
||||
if workspace_role >= 15
|
||||
else (15 if workspace_role == 10 else workspace_role),
|
||||
workspace=workspace,
|
||||
)
|
||||
for project_id in project_ids
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Projects joined successfully"},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "User is not a member of workspace"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class ProjectUserViewsEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
member=request.user, project=project
|
||||
).first()
|
||||
|
||||
if project_member is None:
|
||||
return Response(
|
||||
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
project_member.view_props = request.data
|
||||
|
||||
project_member.save()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
except Project.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "The requested resource does not exists"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
29
apiserver/plane/api/views/shortcut.py
Normal file
29
apiserver/plane/api/views/shortcut.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Module imports
|
||||
from . import BaseViewSet
|
||||
from plane.api.serializers import ShortCutSerializer
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Shortcut
|
||||
|
||||
|
||||
class ShortCutViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ShortCutSerializer
|
||||
model = Shortcut
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
29
apiserver/plane/api/views/state.py
Normal file
29
apiserver/plane/api/views/state.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Module imports
|
||||
from . import BaseViewSet
|
||||
from plane.api.serializers import StateSerializer
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = StateSerializer
|
||||
model = State
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
29
apiserver/plane/api/views/view.py
Normal file
29
apiserver/plane/api/views/view.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Module imports
|
||||
from . import BaseViewSet
|
||||
from plane.api.serializers import ViewSerializer
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import View
|
||||
|
||||
|
||||
class ViewViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ViewSerializer
|
||||
model = View
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
510
apiserver/plane/api/views/workspace.py
Normal file
510
apiserver/plane/api/views/workspace.py
Normal file
@ -0,0 +1,510 @@
|
||||
# Python imports
|
||||
import jwt
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.db.models import CharField
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
# Third party modules
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
WorkSpaceSerializer,
|
||||
WorkSpaceMemberSerializer,
|
||||
TeamSerializer,
|
||||
WorkSpaceMemberInviteSerializer,
|
||||
UserLiteSerializer,
|
||||
ProjectMemberSerializer,
|
||||
)
|
||||
from plane.api.views.base import BaseAPIView
|
||||
from . import BaseViewSet
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceMemberInvite,
|
||||
Team,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
model = Workspace
|
||||
serializer_class = WorkSpaceSerializer
|
||||
permission_classes = [
|
||||
WorkSpaceBasePermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"name",
|
||||
]
|
||||
filterset_fields = [
|
||||
"owner",
|
||||
]
|
||||
|
||||
lookup_field = "slug"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(super().get_queryset().select_related("owner"))
|
||||
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
_ = WorkspaceMember.objects.create(
|
||||
workspace_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=20,
|
||||
company_role=request.data.get("company_role", ""),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
[serializer.errors[error][0] for error in serializer.errors],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
## Handling unique integrity error for now
|
||||
## TODO: Extend this to handle other common errors which are not automatically handled by APIException
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The workspace with the name already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong please try again later",
|
||||
"identifier": None,
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
|
||||
search_fields = [
|
||||
"name",
|
||||
]
|
||||
filterset_fields = [
|
||||
"owner",
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
|
||||
)
|
||||
.filter(
|
||||
workspace_member__member=request.user,
|
||||
)
|
||||
.select_related("owner")
|
||||
)
|
||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
name = request.GET.get("name", False)
|
||||
|
||||
if not name:
|
||||
return Response(
|
||||
{"error": "Workspace Name is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.filter(name=name).exists()
|
||||
|
||||
return Response({"status": workspace}, 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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class InviteWorkspaceEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Check if email is provided
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
validate_email(email)
|
||||
# Check if user is already a member of workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace_id=workspace.id, member__email=email
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "User is already member of workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
token = jwt.encode(
|
||||
{"email": email, "timestamp": datetime.now().timestamp()},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
workspace_invitation_obj = WorkspaceMemberInvite.objects.create(
|
||||
email=email.strip().lower(),
|
||||
workspace_id=workspace.id,
|
||||
token=token,
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
|
||||
domain = settings.WEB_URL
|
||||
|
||||
workspace_invitation.delay(
|
||||
email, workspace.id, token, domain, request.user.email
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": "Email sent successfully",
|
||||
"id": workspace_invitation_obj.id,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid email address provided a valid email address is required to send the invite"
|
||||
},
|
||||
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:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class JoinWorkspaceEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def post(self, request, slug, pk):
|
||||
try:
|
||||
|
||||
workspace_invite = WorkspaceMemberInvite.objects.get(
|
||||
pk=pk, workspace__slug=slug
|
||||
)
|
||||
|
||||
email = request.data.get("email", "")
|
||||
|
||||
if email == "" or workspace_invite.email != email:
|
||||
return Response(
|
||||
{"error": "You do not have permission to join the workspace"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if workspace_invite.responded_at is None:
|
||||
workspace_invite.accepted = request.data.get("accepted", False)
|
||||
workspace_invite.responded_at = timezone.now()
|
||||
workspace_invite.save()
|
||||
|
||||
if workspace_invite.accepted:
|
||||
return Response(
|
||||
{"message": "Workspace Invitation Accepted"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Workspace Invitation was not accepted"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"error": "You have already responded to the invitation request"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
except WorkspaceMemberInvite.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "The invitation either got expired or could not be found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
|
||||
serializer_class = WorkSpaceMemberInviteSerializer
|
||||
model = WorkspaceMemberInvite
|
||||
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
|
||||
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||
|
||||
serializer_class = WorkSpaceMemberInviteSerializer
|
||||
model = WorkspaceMemberInvite
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(email=self.request.user.email)
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
try:
|
||||
|
||||
invitations = request.data.get("invitations")
|
||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
||||
pk__in=invitations
|
||||
)
|
||||
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
[
|
||||
WorkspaceMember(
|
||||
workspace=invitation.workspace,
|
||||
member=request.user,
|
||||
role=invitation.role,
|
||||
)
|
||||
for invitation in workspace_invitations
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Delete joined workspace invites
|
||||
workspace_invitations.delete()
|
||||
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = WorkSpaceMemberSerializer
|
||||
model = WorkspaceMember
|
||||
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "workspace__owner")
|
||||
.select_related("member")
|
||||
)
|
||||
|
||||
|
||||
class TeamMemberViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = TeamSerializer
|
||||
model = Team
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "workspace__owner")
|
||||
.prefetch_related("members")
|
||||
)
|
||||
|
||||
def create(self, request, slug):
|
||||
|
||||
try:
|
||||
|
||||
members = list(
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member__id__in=request.data.get("members", [])
|
||||
)
|
||||
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
||||
.distinct()
|
||||
.values_list("member_str_id", flat=True)
|
||||
)
|
||||
|
||||
if len(members) != len(request.data.get("members", [])):
|
||||
|
||||
users = list(set(request.data.get("members", [])).difference(members))
|
||||
users = User.objects.filter(pk__in=users)
|
||||
|
||||
serializer = UserLiteSerializer(users, many=True)
|
||||
return Response(
|
||||
{
|
||||
"error": f"{len(users)} of the member(s) are not a part of the workspace",
|
||||
"members": serializer.data,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = TeamSerializer(
|
||||
data=request.data, context={"workspace": workspace}
|
||||
)
|
||||
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 IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "The team with the name already exists"},
|
||||
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_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UserWorkspaceInvitationEndpoint(BaseViewSet):
|
||||
|
||||
model = WorkspaceMemberInvite
|
||||
serializer_class = WorkSpaceMemberInviteSerializer
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(pk=self.kwargs.get("pk"))
|
||||
.select_related("workspace")
|
||||
)
|
||||
|
||||
|
||||
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
|
||||
last_workspace_id = user.last_workspace_id
|
||||
|
||||
if last_workspace_id is None:
|
||||
return Response(
|
||||
{
|
||||
"project_details": [],
|
||||
"workspace_details": {},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(pk=last_workspace_id)
|
||||
workspace_serializer = WorkSpaceSerializer(workspace)
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace_id=last_workspace_id, member=request.user
|
||||
).select_related("workspace", "project", "member")
|
||||
|
||||
project_member_serializer = ProjectMemberSerializer(
|
||||
project_member, many=True
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"workspace_details": workspace_serializer.data,
|
||||
"project_details": project_member_serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
except User.DoesNotExist:
|
||||
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
0
apiserver/plane/bgtasks/__init__.py
Normal file
0
apiserver/plane/bgtasks/__init__.py
Normal file
5
apiserver/plane/bgtasks/apps.py
Normal file
5
apiserver/plane/bgtasks/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BgtasksConfig(AppConfig):
|
||||
name = 'plane.bgtasks'
|
0
apiserver/plane/bgtasks/celery.py
Normal file
0
apiserver/plane/bgtasks/celery.py
Normal file
40
apiserver/plane/bgtasks/email_verification_task.py
Normal file
40
apiserver/plane/bgtasks/email_verification_task.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
@job("default")
|
||||
def email_verification(first_name, email, token, current_site):
|
||||
|
||||
try:
|
||||
realtivelink = "/request-email-verification/" + "?token=" + str(token)
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Verify your Email!"
|
||||
|
||||
context = {
|
||||
"first_name": first_name,
|
||||
"verification_url": abs_url,
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/auth/email_verification.html", context)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
40
apiserver/plane/bgtasks/forgot_password_task.py
Normal file
40
apiserver/plane/bgtasks/forgot_password_task.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
@job("default")
|
||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
|
||||
try:
|
||||
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Verify your Email!"
|
||||
|
||||
context = {
|
||||
"first_name": first_name,
|
||||
"forgot_password_url": abs_url,
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/auth/forgot_password.html", context)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
35
apiserver/plane/bgtasks/magic_link_code_task.py
Normal file
35
apiserver/plane/bgtasks/magic_link_code_task.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
|
||||
@job("default")
|
||||
def magic_link(email, key, token, current_site):
|
||||
|
||||
try:
|
||||
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Login!"
|
||||
|
||||
context = {"magic_url": abs_url, "code": token}
|
||||
|
||||
html_content = render_to_string("emails/auth/magic_signin.html", context)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
54
apiserver/plane/bgtasks/project_invitation_task.py
Normal file
54
apiserver/plane/bgtasks/project_invitation_task.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, User, ProjectMemberInvite
|
||||
|
||||
|
||||
@job("default")
|
||||
def project_invitation(email, project_id, token, current_site):
|
||||
|
||||
try:
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
project_member_invite = ProjectMemberInvite.objects.get(
|
||||
token=token, email=email
|
||||
)
|
||||
|
||||
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
||||
abs_url = "http://" + current_site + relativelink
|
||||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {email}!"
|
||||
|
||||
context = {
|
||||
"email": email,
|
||||
"first_name": project.created_by.first_name,
|
||||
"project_name": project.name,
|
||||
"invitation_url": abs_url,
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/invitations/project_invitation.html", context)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
project_member_invite.message = text_content
|
||||
project_member_invite.save()
|
||||
|
||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
57
apiserver/plane/bgtasks/workspace_invitation_task.py
Normal file
57
apiserver/plane/bgtasks/workspace_invitation_task.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Workspace, User, WorkspaceMemberInvite
|
||||
|
||||
|
||||
@job("default")
|
||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
|
||||
try:
|
||||
|
||||
workspace = Workspace.objects.get(pk=workspace_id)
|
||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||
token=token, email=email
|
||||
)
|
||||
|
||||
realtivelink = (
|
||||
f"/workspace-member-invitation/{workspace_member_invite.id}?email={email}"
|
||||
)
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {email}!"
|
||||
|
||||
context = {
|
||||
"email": email,
|
||||
"first_name": invitor,
|
||||
"workspace_name": workspace.name,
|
||||
"invitation_url": abs_url,
|
||||
}
|
||||
|
||||
html_content = render_to_string(
|
||||
"emails/invitations/workspace_invitation.html", context
|
||||
)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
workspace_member_invite.message = text_content
|
||||
workspace_member_invite.save()
|
||||
|
||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
0
apiserver/plane/db/__init__.py
Normal file
0
apiserver/plane/db/__init__.py
Normal file
35
apiserver/plane/db/admin.py
Normal file
35
apiserver/plane/db/admin.py
Normal file
@ -0,0 +1,35 @@
|
||||
# from django.contrib import admin
|
||||
# from plane.db.models import User
|
||||
# from plane.db.models.workspace import Workspace, WorkspaceMember, WorkspaceMemberInvite
|
||||
# from plane.db.models.project import Project, ProjectMember, ProjectMemberInvite
|
||||
# from plane.db.models.cycle import Cycle, CycleIssue
|
||||
# from plane.db.models.issue import (
|
||||
# Issue,
|
||||
# IssueActivity,
|
||||
# IssueComment,
|
||||
# IssueProperty,
|
||||
# TimelineIssue,
|
||||
# )
|
||||
# from plane.db.models.shortcut import Shortcut
|
||||
# from plane.db.models.state import State
|
||||
# from plane.db.models.social_connection import SocialLoginConnection
|
||||
# from plane.db.models.view import View
|
||||
|
||||
# admin.site.register(User)
|
||||
# admin.site.register(Workspace)
|
||||
# admin.site.register(WorkspaceMember)
|
||||
# admin.site.register(WorkspaceMemberInvite)
|
||||
# admin.site.register(Project)
|
||||
# admin.site.register(ProjectMember)
|
||||
# admin.site.register(ProjectMemberInvite)
|
||||
# admin.site.register(Cycle)
|
||||
# admin.site.register(CycleIssue)
|
||||
# admin.site.register(Issue)
|
||||
# admin.site.register(IssueActivity)
|
||||
# admin.site.register(IssueComment)
|
||||
# admin.site.register(IssueProperty)
|
||||
# admin.site.register(TimelineIssue)
|
||||
# admin.site.register(Shortcut)
|
||||
# admin.site.register(State)
|
||||
# admin.site.register(SocialLoginConnection)
|
||||
# admin.site.register(View)
|
52
apiserver/plane/db/apps.py
Normal file
52
apiserver/plane/db/apps.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.apps import AppConfig
|
||||
from fieldsignals import post_save_changed
|
||||
|
||||
|
||||
class DbConfig(AppConfig):
|
||||
name = "plane.db"
|
||||
|
||||
def ready(self):
|
||||
|
||||
post_save_changed.connect(
|
||||
self.model_activity,
|
||||
sender=self.get_model("Issue"),
|
||||
)
|
||||
|
||||
def model_activity(self, sender, instance, changed_fields, **kwargs):
|
||||
|
||||
verb = "created" if instance._state.adding else "changed"
|
||||
|
||||
import inspect
|
||||
|
||||
for frame_record in inspect.stack():
|
||||
if frame_record[3] == "get_response":
|
||||
request = frame_record[0].f_locals["request"]
|
||||
REQUEST_METHOD = request.method
|
||||
|
||||
if REQUEST_METHOD == "POST":
|
||||
|
||||
self.get_model("IssueActivity").objects.create(
|
||||
issue=instance, project=instance.project, actor=instance.created_by
|
||||
)
|
||||
|
||||
elif REQUEST_METHOD == "PATCH":
|
||||
|
||||
try:
|
||||
del changed_fields["updated_at"]
|
||||
del changed_fields["updated_by"]
|
||||
except KeyError as e:
|
||||
pass
|
||||
|
||||
for field_name, (old, new) in changed_fields.items():
|
||||
field = field_name
|
||||
old_value = old
|
||||
new_value = new
|
||||
self.get_model("IssueActivity").objects.create(
|
||||
issue=instance,
|
||||
verb=verb,
|
||||
field=field,
|
||||
old_value=old_value,
|
||||
new_value=new_value,
|
||||
project=instance.project,
|
||||
actor=instance.updated_by,
|
||||
)
|
0
apiserver/plane/db/management/__init__.py
Normal file
0
apiserver/plane/db/management/__init__.py
Normal file
0
apiserver/plane/db/management/commands/__init__.py
Normal file
0
apiserver/plane/db/management/commands/__init__.py
Normal file
19
apiserver/plane/db/management/commands/wait_for_db.py
Normal file
19
apiserver/plane/db/management/commands/wait_for_db.py
Normal file
@ -0,0 +1,19 @@
|
||||
import time
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Django command to pause execution until db is available"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Waiting for database...')
|
||||
db_conn = None
|
||||
while not db_conn:
|
||||
try:
|
||||
db_conn = connections['default']
|
||||
except OperationalError:
|
||||
self.stdout.write('Database unavailable, waititng 1 second...')
|
||||
time.sleep(1)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Database available!'))
|
704
apiserver/plane/db/migrations/0001_initial.py
Normal file
704
apiserver/plane/db/migrations/0001_initial.py
Normal file
@ -0,0 +1,704 @@
|
||||
# Generated by Django 3.2.14 on 2022-10-26 19:37
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('username', models.CharField(max_length=128, unique=True)),
|
||||
('mobile_number', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=255)),
|
||||
('last_name', models.CharField(blank=True, max_length=255)),
|
||||
('avatar', models.CharField(blank=True, max_length=255)),
|
||||
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('last_location', models.CharField(blank=True, max_length=255)),
|
||||
('created_location', models.CharField(blank=True, max_length=255)),
|
||||
('is_superuser', models.BooleanField(default=False)),
|
||||
('is_managed', models.BooleanField(default=False)),
|
||||
('is_password_expired', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_staff', models.BooleanField(default=False)),
|
||||
('is_email_verified', models.BooleanField(default=False)),
|
||||
('is_password_autoset', models.BooleanField(default=False)),
|
||||
('is_onboarded', models.BooleanField(default=False)),
|
||||
('token', models.CharField(blank=True, max_length=64)),
|
||||
('billing_address_country', models.CharField(default='INDIA', max_length=255)),
|
||||
('billing_address', models.JSONField(null=True)),
|
||||
('has_billing_address', models.BooleanField(default=False)),
|
||||
('user_timezone', models.CharField(default='Asia/Kolkata', max_length=255)),
|
||||
('last_active', models.DateTimeField(default=django.utils.timezone.now, null=True)),
|
||||
('last_login_time', models.DateTimeField(null=True)),
|
||||
('last_logout_time', models.DateTimeField(null=True)),
|
||||
('last_login_ip', models.CharField(blank=True, max_length=255)),
|
||||
('last_logout_ip', models.CharField(blank=True, max_length=255)),
|
||||
('last_login_medium', models.CharField(default='email', max_length=20)),
|
||||
('last_login_uagent', models.TextField(blank=True)),
|
||||
('token_updated_at', models.DateTimeField(null=True)),
|
||||
('last_workspace_id', models.UUIDField(null=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
'db_table': 'user',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Cycle',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Cycle Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='Cycle Description')),
|
||||
('start_date', models.DateField(verbose_name='Start Date')),
|
||||
('end_date', models.DateField(verbose_name='End Date')),
|
||||
('status', models.CharField(choices=[('started', 'Started'), ('completed', 'Completed')], max_length=255, verbose_name='Cycle Status')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycle_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_by_cycle', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cycle',
|
||||
'verbose_name_plural': 'Cycles',
|
||||
'db_table': 'cycle',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Issue',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Issue Name')),
|
||||
('description', models.JSONField(blank=True, verbose_name='Issue Description')),
|
||||
('priority', models.CharField(blank=True, choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], max_length=30, null=True, verbose_name='Issue Priority')),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('target_date', models.DateField(blank=True, null=True)),
|
||||
('sequence_id', models.IntegerField(default=1, verbose_name='Issue Sequence ID')),
|
||||
('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue',
|
||||
'verbose_name_plural': 'Issues',
|
||||
'db_table': 'issue',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Project Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='Project Description')),
|
||||
('description_rt', models.JSONField(blank=True, null=True, verbose_name='Project Description RT')),
|
||||
('description_html', models.JSONField(blank=True, null=True, verbose_name='Project Description HTML')),
|
||||
('network', models.PositiveSmallIntegerField(choices=[(0, 'Secret'), (2, 'Public')], default=2)),
|
||||
('identifier', models.CharField(blank=True, max_length=5, null=True, verbose_name='Project Identifier')),
|
||||
('slug', models.SlugField(blank=True, max_length=100)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('default_assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_assignee', to=settings.AUTH_USER_MODEL)),
|
||||
('project_lead', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_lead', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project',
|
||||
'verbose_name_plural': 'Projects',
|
||||
'db_table': 'project',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Team Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='Team Description')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team',
|
||||
'verbose_name_plural': 'Teams',
|
||||
'db_table': 'team',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Workspace',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Workspace Name')),
|
||||
('logo', models.URLField(blank=True, null=True, verbose_name='Logo')),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspace_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner_workspace', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspace_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Workspace',
|
||||
'verbose_name_plural': 'Workspaces',
|
||||
'db_table': 'workspace',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('name', 'owner')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkspaceMemberInvite',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('email', models.CharField(max_length=255)),
|
||||
('accepted', models.BooleanField(default=False)),
|
||||
('token', models.CharField(max_length=255)),
|
||||
('message', models.TextField(null=True)),
|
||||
('responded_at', models.DateTimeField(null=True)),
|
||||
('role', models.PositiveSmallIntegerField(choices=[(20, 'Owner'), (15, 'Admin'), (10, 'Member'), (5, 'Guest')], default=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacememberinvite_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='workspacememberinvite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_member_invite', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Workspace Member Invite',
|
||||
'verbose_name_plural': 'Workspace Member Invites',
|
||||
'db_table': 'workspace_member_invite',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='View',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||
('query', models.JSONField(verbose_name='View Query')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='view_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_view', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='view_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_view', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'View',
|
||||
'verbose_name_plural': 'Views',
|
||||
'db_table': 'view',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TimelineIssue',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('sequence_id', models.FloatField(default=1.0)),
|
||||
('links', models.JSONField(blank=True, default=dict)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timelineissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_timeline', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_timelineissue', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timelineissue_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_timelineissue', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Timeline Issue',
|
||||
'verbose_name_plural': 'Timeline Issues',
|
||||
'db_table': 'issue_timeline',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamMember',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teammember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to=settings.AUTH_USER_MODEL)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to='db.team')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teammember_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team Member',
|
||||
'verbose_name_plural': 'Team Members',
|
||||
'db_table': 'team_member',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('team', 'member')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='members',
|
||||
field=models.ManyToManyField(blank=True, related_name='members', through='db.TeamMember', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_team', to='db.workspace'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='State',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='State Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='State Description')),
|
||||
('color', models.CharField(max_length=255, verbose_name='State Color')),
|
||||
('slug', models.SlugField(blank=True, max_length=100)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_state', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_state', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'State',
|
||||
'verbose_name_plural': 'States',
|
||||
'db_table': 'state',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('name', 'project')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SocialLoginConnection',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('medium', models.CharField(choices=[('Google', 'google'), ('Github', 'github')], default=None, max_length=20)),
|
||||
('last_login_at', models.DateTimeField(default=django.utils.timezone.now, null=True)),
|
||||
('last_received_at', models.DateTimeField(default=django.utils.timezone.now, null=True)),
|
||||
('token_data', models.JSONField(null=True)),
|
||||
('extra_data', models.JSONField(null=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socialloginconnection_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='socialloginconnection_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_login_connections', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Social Login Connection',
|
||||
'verbose_name_plural': 'Social Login Connections',
|
||||
'db_table': 'social_login_connection',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Shortcut',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Cycle Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='Cycle Description')),
|
||||
('type', models.CharField(choices=[('repo', 'Repo'), ('direct', 'Direct')], max_length=255, verbose_name='Shortcut Type')),
|
||||
('url', models.URLField(blank=True, null=True, verbose_name='URL')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shortcut_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_shortcut', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shortcut_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_shortcut', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Shortcut',
|
||||
'verbose_name_plural': 'Shortcuts',
|
||||
'db_table': 'shortcut',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectMemberInvite',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('email', models.CharField(max_length=255)),
|
||||
('accepted', models.BooleanField(default=False)),
|
||||
('token', models.CharField(max_length=255)),
|
||||
('message', models.TextField(null=True)),
|
||||
('responded_at', models.DateTimeField(null=True)),
|
||||
('role', models.PositiveSmallIntegerField(choices=[(20, 'Admin'), (15, 'Member'), (10, 'Viewer'), (5, 'Guest')], default=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmemberinvite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectmemberinvite', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmemberinvite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_projectmemberinvite', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Member Invite',
|
||||
'verbose_name_plural': 'Project Member Invites',
|
||||
'db_table': 'project_member_invite',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectIdentifier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('name', models.CharField(max_length=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectidentifier_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='project_identifier', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectidentifier_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Identifier',
|
||||
'verbose_name_plural': 'Project Identifiers',
|
||||
'db_table': 'project_identifier',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_project', to='db.workspace'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Label',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_label', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_label', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Label',
|
||||
'verbose_name_plural': 'Labels',
|
||||
'db_table': 'label',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueSequence',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('sequence', models.PositiveBigIntegerField(default=1)),
|
||||
('deleted', models.BooleanField(default=False)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuesequence_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_sequence', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuesequence', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuesequence_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuesequence', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Sequence',
|
||||
'verbose_name_plural': 'Issue Sequences',
|
||||
'db_table': 'issue_sequence',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueProperty',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('properties', models.JSONField(default=dict)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueproperty_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueproperty', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueproperty_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_property_user', to=settings.AUTH_USER_MODEL)),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueproperty', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Property',
|
||||
'verbose_name_plural': 'Issue Properties',
|
||||
'db_table': 'issue_property',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueLabel',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelabel_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='label_issue', to='db.issue')),
|
||||
('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='label_issue', to='db.label')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuelabel', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelabel_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuelabel', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Label',
|
||||
'verbose_name_plural': 'Issue Labels',
|
||||
'db_table': 'issue_label',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueComment',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuecomment_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuecomment', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuecomment_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuecomment', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Comment',
|
||||
'verbose_name_plural': 'Issue Comments',
|
||||
'db_table': 'issue_comment',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueBlocker',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocker_issues', to='db.issue')),
|
||||
('blocked_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocked_issues', to='db.issue')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueblocker_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueblocker', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueblocker_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueblocker', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Blocker',
|
||||
'verbose_name_plural': 'Issue Blockers',
|
||||
'db_table': 'issue_blocker',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueAssignee',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('assignee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_assignee', to=settings.AUTH_USER_MODEL)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueassignee_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_assignee', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueassignee', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueassignee_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueassignee', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Assignee',
|
||||
'verbose_name_plural': 'Issue Assignees',
|
||||
'db_table': 'issue_assignee',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('issue', 'assignee')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueActivity',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('verb', models.CharField(default='created', max_length=255, verbose_name='Action')),
|
||||
('field', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field Name')),
|
||||
('old_value', models.CharField(blank=True, max_length=255, null=True, verbose_name='Old Value')),
|
||||
('new_value', models.CharField(blank=True, max_length=255, null=True, verbose_name='New Value')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueactivity_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_activity', to='db.issue')),
|
||||
('issue_comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_comment', to='db.issuecomment')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueactivity', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueactivity_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueactivity', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Activity',
|
||||
'verbose_name_plural': 'Issue Activities',
|
||||
'db_table': 'issue_activity',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='assignees',
|
||||
field=models.ManyToManyField(blank=True, related_name='assignee', through='db.IssueAssignee', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='labels',
|
||||
field=models.ManyToManyField(blank=True, related_name='labels', through='db.IssueLabel', to='db.Label'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_issue', to='db.issue'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issue', to='db.project'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='state',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_issue', to='db.state'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issue', to='db.workspace'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FileAsset',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('attributes', models.JSONField(default=dict)),
|
||||
('asset', models.FileField(upload_to='library-assets')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fileasset_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='fileasset_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'File Asset',
|
||||
'verbose_name_plural': 'File Assets',
|
||||
'db_table': 'file_asset',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CycleIssue',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycleissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.cycle')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cycleissue', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycleissue_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_cycleissue', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cycle Issue',
|
||||
'verbose_name_plural': 'Cycle Issues',
|
||||
'db_table': 'cycle_issue',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cycle', to='db.project'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycle_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_cycle', to='db.workspace'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkspaceMember',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('role', models.PositiveSmallIntegerField(choices=[(20, 'Owner'), (15, 'Admin'), (10, 'Member'), (5, 'Guest')], default=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacemember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='member_workspace', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacemember_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_member', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Workspace Member',
|
||||
'verbose_name_plural': 'Workspace Members',
|
||||
'db_table': 'workspace_member',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('workspace', 'member')},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='team',
|
||||
unique_together={('name', 'workspace')},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectMember',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('role', models.PositiveSmallIntegerField(choices=[(20, 'Admin'), (15, 'Member'), (10, 'Viewer'), (5, 'Guest')], default=10)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='member_project', to=settings.AUTH_USER_MODEL)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectmember', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmember_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_projectmember', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Member',
|
||||
'verbose_name_plural': 'Project Members',
|
||||
'db_table': 'project_member',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('project', 'member')},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='project',
|
||||
unique_together={('name', 'workspace')},
|
||||
),
|
||||
]
|
54
apiserver/plane/db/migrations/0002_auto_20221104_2239.py
Normal file
54
apiserver/plane/db/migrations/0002_auto_20221104_2239.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-04 17:09
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='state',
|
||||
options={'ordering': ('sequence',), 'verbose_name': 'State', 'verbose_name_plural': 'States'},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='project',
|
||||
old_name='description_rt',
|
||||
new_name='description_text',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='actor',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_activities', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='actor',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='sequence',
|
||||
field=models.PositiveIntegerField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspace',
|
||||
name='company_size',
|
||||
field=models.PositiveIntegerField(default=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='company_role',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cycleissue',
|
||||
name='issue',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.issue'),
|
||||
),
|
||||
]
|
24
apiserver/plane/db/migrations/0003_auto_20221109_2320.py
Normal file
24
apiserver/plane/db/migrations/0003_auto_20221109_2320.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-09 17:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0002_auto_20221104_2239'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='issueproperty',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_property_user', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='issueproperty',
|
||||
unique_together={('user', 'project')},
|
||||
),
|
||||
]
|
18
apiserver/plane/db/migrations/0004_alter_state_sequence.py
Normal file
18
apiserver/plane/db/migrations/0004_alter_state_sequence.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-10 19:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0003_auto_20221109_2320'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='sequence',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
]
|
23
apiserver/plane/db/migrations/0005_auto_20221114_2127.py
Normal file
23
apiserver/plane/db/migrations/0005_auto_20221114_2127.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-14 15:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0004_alter_state_sequence'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cycle',
|
||||
name='end_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='End Date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cycle',
|
||||
name='start_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Start Date'),
|
||||
),
|
||||
]
|
18
apiserver/plane/db/migrations/0006_alter_cycle_status.py
Normal file
18
apiserver/plane/db/migrations/0006_alter_cycle_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-16 14:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0005_auto_20221114_2127'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cycle',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', 'Draft'), ('started', 'Started'), ('completed', 'Completed')], default='draft', max_length=255, verbose_name='Cycle Status'),
|
||||
),
|
||||
]
|
19
apiserver/plane/db/migrations/0007_label_parent.py
Normal file
19
apiserver/plane/db/migrations/0007_label_parent.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-28 20:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0006_alter_cycle_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_label', to='db.label'),
|
||||
),
|
||||
]
|
18
apiserver/plane/db/migrations/0008_label_colour.py
Normal file
18
apiserver/plane/db/migrations/0008_label_colour.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.14 on 2022-11-29 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0007_label_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='colour',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
23
apiserver/plane/db/migrations/0009_auto_20221213_2328.py
Normal file
23
apiserver/plane/db/migrations/0009_auto_20221213_2328.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.14 on 2022-12-13 17:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0008_label_colour'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='projectmember',
|
||||
name='view_props',
|
||||
field=models.JSONField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='group',
|
||||
field=models.CharField(choices=[('backlog', 'Backlog'), ('unstarted', 'Unstarted'), ('started', 'Started'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='backlog', max_length=20),
|
||||
),
|
||||
]
|
32
apiserver/plane/db/migrations/0010_auto_20221213_2348.py
Normal file
32
apiserver/plane/db/migrations/0010_auto_20221213_2348.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.2.14 on 2022-12-13 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0009_auto_20221213_2328'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='projectidentifier',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_identifiers', to='db.workspace'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='identifier',
|
||||
field=models.CharField(max_length=5, verbose_name='Project Identifier'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='project',
|
||||
unique_together={('name', 'workspace'), ('identifier', 'workspace')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='projectidentifier',
|
||||
unique_together={('name', 'workspace')},
|
||||
),
|
||||
]
|
110
apiserver/plane/db/migrations/0011_auto_20221216_0259.py
Normal file
110
apiserver/plane/db/migrations/0011_auto_20221216_0259.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Generated by Django 3.2.14 on 2022-12-15 21:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0010_auto_20221213_2348'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Module',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Module Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='Module Description')),
|
||||
('description_text', models.JSONField(blank=True, null=True, verbose_name='Module Description RT')),
|
||||
('description_html', models.JSONField(blank=True, null=True, verbose_name='Module Description HTML')),
|
||||
('start_date', models.DateField(null=True)),
|
||||
('target_date', models.DateField(null=True)),
|
||||
('status', models.CharField(choices=[('backlog', 'Backlog'), ('planned', 'Planned'), ('in-progress', 'In Progress'), ('paused', 'Paused'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='planned', max_length=20)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('lead', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_leads', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Module',
|
||||
'verbose_name_plural': 'Modules',
|
||||
'db_table': 'module',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='icon',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModuleMember',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulemember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.module')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulemember', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulemember_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_modulemember', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Module Member',
|
||||
'verbose_name_plural': 'Module Members',
|
||||
'db_table': 'module_member',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('module', 'member')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='members',
|
||||
field=models.ManyToManyField(blank=True, related_name='module_members', through='db.ModuleMember', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_module', to='db.project'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_module', to='db.workspace'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModuleIssue',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moduleissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_issues', to='db.issue')),
|
||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_issues', to='db.module')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_moduleissue', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moduleissue_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_moduleissue', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Module Issue',
|
||||
'verbose_name_plural': 'Module Issues',
|
||||
'db_table': 'module_issues',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('module', 'issue')},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='module',
|
||||
unique_together={('name', 'project')},
|
||||
),
|
||||
]
|
0
apiserver/plane/db/migrations/__init__.py
Normal file
0
apiserver/plane/db/migrations/__init__.py
Normal file
46
apiserver/plane/db/mixins.py
Normal file
46
apiserver/plane/db/mixins.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimeAuditModel(models.Model):
|
||||
|
||||
"""To path when the record was created and last modified"""
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="Created At",
|
||||
)
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UserAuditModel(models.Model):
|
||||
|
||||
"""To path when the record was created and last modified"""
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
"db.User",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
verbose_name="Created By",
|
||||
null=True,
|
||||
)
|
||||
updated_by = models.ForeignKey(
|
||||
"db.User",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
verbose_name="Last Modified By",
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class AuditModel(TimeAuditModel, UserAuditModel):
|
||||
|
||||
"""To path when the record was created and last modified"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
40
apiserver/plane/db/models/__init__.py
Normal file
40
apiserver/plane/db/models/__init__.py
Normal file
@ -0,0 +1,40 @@
|
||||
from .base import BaseModel
|
||||
|
||||
from .user import User
|
||||
|
||||
from .workspace import (
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Team,
|
||||
WorkspaceMemberInvite,
|
||||
TeamMember,
|
||||
)
|
||||
|
||||
from .project import Project, ProjectMember, ProjectBaseModel, ProjectMemberInvite, ProjectIdentifier
|
||||
|
||||
from .issue import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
TimelineIssue,
|
||||
IssueProperty,
|
||||
IssueComment,
|
||||
IssueBlocker,
|
||||
IssueLabel,
|
||||
IssueAssignee,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
)
|
||||
|
||||
from .asset import FileAsset
|
||||
|
||||
from .social_connection import SocialLoginConnection
|
||||
|
||||
from .state import State
|
||||
|
||||
from .cycle import Cycle, CycleIssue
|
||||
|
||||
from .shortcut import Shortcut
|
||||
|
||||
from .view import View
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue
|
24
apiserver/plane/db/models/asset.py
Normal file
24
apiserver/plane/db/models/asset.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Django import
|
||||
from django.db import models
|
||||
|
||||
# Module import
|
||||
from . import BaseModel
|
||||
|
||||
|
||||
class FileAsset(BaseModel):
|
||||
"""
|
||||
A file asset.
|
||||
"""
|
||||
|
||||
attributes = models.JSONField(default=dict)
|
||||
asset = models.FileField(upload_to="library-assets")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "File Asset"
|
||||
verbose_name_plural = "File Assets"
|
||||
db_table = "file_asset"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return self.asset
|
||||
|
39
apiserver/plane/db/models/base.py
Normal file
39
apiserver/plane/db/models/base.py
Normal file
@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Third party imports
|
||||
from crum import get_current_user
|
||||
|
||||
# Module imports
|
||||
from ..mixins import AuditModel
|
||||
|
||||
|
||||
class BaseModel(AuditModel):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
user = get_current_user()
|
||||
|
||||
if user is None or user.is_anonymous:
|
||||
self.created_by = None
|
||||
self.updated_by = None
|
||||
super(BaseModel, self).save(*args, **kwargs)
|
||||
else:
|
||||
# Check if the model is being created or updated
|
||||
if self._state.adding:
|
||||
# If created only set created_by value: set updated_by to None
|
||||
self.created_by = user
|
||||
self.updated_by = None
|
||||
# If updated only set updated_by value don't touch created_by
|
||||
self.updated_by = user
|
||||
super(BaseModel, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
61
apiserver/plane/db/models/cycle.py
Normal file
61
apiserver/plane/db/models/cycle.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class Cycle(ProjectBaseModel):
|
||||
STATUS_CHOICES = (
|
||||
("draft", "Draft"),
|
||||
("started", "Started"),
|
||||
("completed", "Completed"),
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Cycle Name")
|
||||
description = models.TextField(verbose_name="Cycle Description", blank=True)
|
||||
start_date = models.DateField(verbose_name="Start Date", blank=True, null=True)
|
||||
end_date = models.DateField(verbose_name="End Date", blank=True, null=True)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="owned_by_cycle",
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name="Cycle Status",
|
||||
choices=STATUS_CHOICES,
|
||||
default="draft",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
verbose_name_plural = "Cycles"
|
||||
db_table = "cycle"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the cycle"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
|
||||
class CycleIssue(ProjectBaseModel):
|
||||
"""
|
||||
Cycle Issues
|
||||
"""
|
||||
|
||||
issue = models.OneToOneField(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_cycle"
|
||||
)
|
||||
cycle = models.ForeignKey(
|
||||
Cycle, on_delete=models.CASCADE, related_name="issue_cycle"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle Issue"
|
||||
verbose_name_plural = "Cycle Issues"
|
||||
db_table = "cycle_issue"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cycle}"
|
296
apiserver/plane/db/models/issue.py
Normal file
296
apiserver/plane/db/models/issue.py
Normal file
@ -0,0 +1,296 @@
|
||||
# Django imports
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
|
||||
# TODO: Handle identifiers for Bulk Inserts - nk
|
||||
class Issue(ProjectBaseModel):
|
||||
PRIORITY_CHOICES = (
|
||||
("urgent", "Urgent"),
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="parent_issue",
|
||||
)
|
||||
state = models.ForeignKey(
|
||||
"db.State",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="state_issue",
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(verbose_name="Issue Description", blank=True)
|
||||
priority = models.CharField(
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
verbose_name="Issue Priority",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
assignees = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
related_name="assignee",
|
||||
through="IssueAssignee",
|
||||
through_fields=("issue", "assignee"),
|
||||
)
|
||||
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
labels = models.ManyToManyField(
|
||||
"db.Label", blank=True, related_name="labels", through="IssueLabel"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue"
|
||||
verbose_name_plural = "Issues"
|
||||
db_table = "issue"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# This means that the model isn't saved to the database yet
|
||||
if self._state.adding:
|
||||
# Get the maximum display_id value from the database
|
||||
|
||||
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
|
||||
largest=models.Max("sequence")
|
||||
)["largest"]
|
||||
# aggregate can return None! Check it first.
|
||||
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
|
||||
if last_id is not None:
|
||||
self.sequence_id = last_id + 1
|
||||
if self.state is None:
|
||||
try:
|
||||
from plane.db.models import State
|
||||
|
||||
self.state, created = State.objects.get_or_create(
|
||||
project=self.project, name="Backlog"
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the issue"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
|
||||
class IssueBlocker(ProjectBaseModel):
|
||||
block = models.ForeignKey(
|
||||
Issue, related_name="blocker_issues", on_delete=models.CASCADE
|
||||
)
|
||||
blocked_by = models.ForeignKey(
|
||||
Issue, related_name="blocked_issues", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Blocker"
|
||||
verbose_name_plural = "Issue Blockers"
|
||||
db_table = "issue_blocker"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.block.name} {self.blocked_by.name}"
|
||||
|
||||
|
||||
class IssueAssignee(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||
)
|
||||
assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_assignee",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "assignee"]
|
||||
verbose_name = "Issue Assignee"
|
||||
verbose_name_plural = "Issue Assignees"
|
||||
db_table = "issue_assignee"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.assignee.email}"
|
||||
|
||||
|
||||
class IssueActivity(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_activity"
|
||||
)
|
||||
verb = models.CharField(max_length=255, verbose_name="Action", default="created")
|
||||
field = models.CharField(
|
||||
max_length=255, verbose_name="Field Name", blank=True, null=True
|
||||
)
|
||||
old_value = models.CharField(
|
||||
max_length=255, verbose_name="Old Value", blank=True, null=True
|
||||
)
|
||||
new_value = models.CharField(
|
||||
max_length=255, verbose_name="New Value", blank=True, null=True
|
||||
)
|
||||
|
||||
comment = models.TextField(verbose_name="Comment", blank=True)
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
issue_comment = models.ForeignKey(
|
||||
"db.IssueComment",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_comment",
|
||||
null=True,
|
||||
)
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="issue_activities",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Activity"
|
||||
verbose_name_plural = "Issue Activities"
|
||||
db_table = "issue_activity"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return issue of the comment"""
|
||||
return str(self.issue)
|
||||
|
||||
|
||||
class TimelineIssue(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_timeline"
|
||||
)
|
||||
sequence_id = models.FloatField(default=1.0)
|
||||
links = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Timeline Issue"
|
||||
verbose_name_plural = "Timeline Issues"
|
||||
db_table = "issue_timeline"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return project of the project member"""
|
||||
return str(self.issue)
|
||||
|
||||
|
||||
class IssueComment(ProjectBaseModel):
|
||||
comment = models.TextField(verbose_name="Comment", blank=True)
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
|
||||
# System can also create comment
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comments",
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Comment"
|
||||
verbose_name_plural = "Issue Comments"
|
||||
db_table = "issue_comment"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return issue of the comment"""
|
||||
return str(self.issue)
|
||||
|
||||
|
||||
class IssueProperty(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_property_user",
|
||||
)
|
||||
properties = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Property"
|
||||
verbose_name_plural = "Issue Properties"
|
||||
db_table = "issue_property"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project"]
|
||||
|
||||
def __str__(self):
|
||||
"""Return properties status of the issue"""
|
||||
return str(self.user)
|
||||
|
||||
|
||||
class Label(ProjectBaseModel):
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="parent_label",
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
colour = models.CharField(max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Label"
|
||||
verbose_name_plural = "Labels"
|
||||
db_table = "label"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class IssueLabel(ProjectBaseModel):
|
||||
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="label_issue"
|
||||
)
|
||||
label = models.ForeignKey(
|
||||
"db.Label", on_delete=models.CASCADE, related_name="label_issue"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Label"
|
||||
verbose_name_plural = "Issue Labels"
|
||||
db_table = "issue_label"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.label.name}"
|
||||
|
||||
|
||||
class IssueSequence(ProjectBaseModel):
|
||||
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.SET_NULL, related_name="issue_sequence", null=True
|
||||
)
|
||||
sequence = models.PositiveBigIntegerField(default=1)
|
||||
deleted = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Sequence"
|
||||
verbose_name_plural = "Issue Sequences"
|
||||
db_table = "issue_sequence"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
# TODO: Find a better method to save the model
|
||||
@receiver(post_save, sender=Issue)
|
||||
def create_issue_sequence(sender, instance, created, **kwargs):
|
||||
|
||||
if created:
|
||||
IssueSequence.objects.create(
|
||||
issue=instance, sequence=instance.sequence_id, project=instance.project
|
||||
)
|
88
apiserver/plane/db/models/module.py
Normal file
88
apiserver/plane/db/models/module.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class Module(ProjectBaseModel):
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name="Module Name")
|
||||
description = models.TextField(verbose_name="Module Description", blank=True)
|
||||
description_text = models.JSONField(
|
||||
verbose_name="Module Description RT", blank=True, null=True
|
||||
)
|
||||
description_html = models.JSONField(
|
||||
verbose_name="Module Description HTML", blank=True, null=True
|
||||
)
|
||||
start_date = models.DateField(null=True)
|
||||
target_date = models.DateField(null=True)
|
||||
status = models.CharField(
|
||||
choices=(
|
||||
("backlog", "Backlog"),
|
||||
("planned", "Planned"),
|
||||
("in-progress", "In Progress"),
|
||||
("paused", "Paused"),
|
||||
("completed", "Completed"),
|
||||
("cancelled", "Cancelled"),
|
||||
),
|
||||
default="planned",
|
||||
max_length=20,
|
||||
)
|
||||
lead = models.ForeignKey(
|
||||
"db.User", on_delete=models.SET_NULL, related_name="module_leads", null=True
|
||||
)
|
||||
members = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
related_name="module_members",
|
||||
through="ModuleMember",
|
||||
through_fields=("module", "member"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
verbose_name = "Module"
|
||||
verbose_name_plural = "Modules"
|
||||
db_table = "module"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} {self.start_date} {self.target_date}"
|
||||
|
||||
|
||||
class ModuleMember(ProjectBaseModel):
|
||||
|
||||
module = models.ForeignKey("db.Module", on_delete=models.CASCADE)
|
||||
member = models.ForeignKey("db.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "member"]
|
||||
verbose_name = "Module Member"
|
||||
verbose_name_plural = "Module Members"
|
||||
db_table = "module_member"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.module.name} {self.member}"
|
||||
|
||||
|
||||
class ModuleIssue(ProjectBaseModel):
|
||||
|
||||
module = models.ForeignKey(
|
||||
"db.Module", on_delete=models.CASCADE, related_name="module_issues"
|
||||
)
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="module_issues"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "issue"]
|
||||
verbose_name = "Module Issue"
|
||||
verbose_name_plural = "Module Issues"
|
||||
db_table = "module_issues"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.module.name} {self.issue.name}"
|
150
apiserver/plane/db/models/project.py
Normal file
150
apiserver/plane/db/models/project.py
Normal file
@ -0,0 +1,150 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Modeule imports
|
||||
from plane.db.mixins import AuditModel
|
||||
|
||||
# Module imports
|
||||
from . import BaseModel
|
||||
|
||||
ROLE_CHOICES = (
|
||||
(20, "Admin"),
|
||||
(15, "Member"),
|
||||
(10, "Viewer"),
|
||||
(5, "Guest"),
|
||||
)
|
||||
|
||||
|
||||
class Project(BaseModel):
|
||||
|
||||
NETWORK_CHOICES = ((0, "Secret"), (2, "Public"))
|
||||
name = models.CharField(max_length=255, verbose_name="Project Name")
|
||||
description = models.TextField(verbose_name="Project Description", blank=True)
|
||||
description_text = models.JSONField(
|
||||
verbose_name="Project Description RT", blank=True, null=True
|
||||
)
|
||||
description_html = models.JSONField(
|
||||
verbose_name="Project Description HTML", blank=True, null=True
|
||||
)
|
||||
network = models.PositiveSmallIntegerField(default=2, choices=NETWORK_CHOICES)
|
||||
workspace = models.ForeignKey(
|
||||
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
|
||||
)
|
||||
identifier = models.CharField(
|
||||
max_length=5,
|
||||
verbose_name="Project Identifier",
|
||||
)
|
||||
slug = models.SlugField(max_length=100, blank=True)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="default_assignee",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
project_lead = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_lead",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
icon = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the project"""
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = [["identifier", "workspace"], ["name", "workspace"]]
|
||||
verbose_name = "Project"
|
||||
verbose_name_plural = "Projects"
|
||||
db_table = "project"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.name)
|
||||
self.identifier = self.identifier.strip().upper()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ProjectBaseModel(BaseModel):
|
||||
|
||||
project = models.ForeignKey(
|
||||
Project, on_delete=models.CASCADE, related_name="project_%(class)s"
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.workspace = self.project.workspace
|
||||
super(ProjectBaseModel, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class ProjectMemberInvite(ProjectBaseModel):
|
||||
email = models.CharField(max_length=255)
|
||||
accepted = models.BooleanField(default=False)
|
||||
token = models.CharField(max_length=255)
|
||||
message = models.TextField(null=True)
|
||||
responded_at = models.DateTimeField(null=True)
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project Member Invite"
|
||||
verbose_name_plural = "Project Member Invites"
|
||||
db_table = "project_member_invite"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.project.name} {self.email} {self.accepted}"
|
||||
|
||||
|
||||
class ProjectMember(ProjectBaseModel):
|
||||
|
||||
member = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="member_project",
|
||||
)
|
||||
comment = models.TextField(blank=True, null=True)
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
view_props = models.JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "member"]
|
||||
verbose_name = "Project Member"
|
||||
verbose_name_plural = "Project Members"
|
||||
db_table = "project_member"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return members of the project"""
|
||||
return f"{self.member.email} <{self.project.name}>"
|
||||
|
||||
# TODO: Remove workspace relation later
|
||||
class ProjectIdentifier(AuditModel):
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", models.CASCADE, related_name="project_identifiers", null=True
|
||||
)
|
||||
project = models.OneToOneField(
|
||||
Project, on_delete=models.CASCADE, related_name="project_identifier"
|
||||
)
|
||||
name = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "workspace"]
|
||||
verbose_name = "Project Identifier"
|
||||
verbose_name_plural = "Project Identifiers"
|
||||
db_table = "project_identifier"
|
||||
ordering = ("-created_at",)
|
26
apiserver/plane/db/models/shortcut.py
Normal file
26
apiserver/plane/db/models/shortcut.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class Shortcut(ProjectBaseModel):
|
||||
TYPE_CHOICES = (("repo", "Repo"), ("direct", "Direct"))
|
||||
name = models.CharField(max_length=255, verbose_name="Cycle Name")
|
||||
description = models.TextField(verbose_name="Cycle Description", blank=True)
|
||||
type = models.CharField(
|
||||
max_length=255, verbose_name="Shortcut Type", choices=TYPE_CHOICES
|
||||
)
|
||||
url = models.URLField(verbose_name="URL", blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Shortcut"
|
||||
verbose_name_plural = "Shortcuts"
|
||||
db_table = "shortcut"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the shortcut"""
|
||||
return f"{self.name} <{self.project.name}>"
|
34
apiserver/plane/db/models/social_connection.py
Normal file
34
apiserver/plane/db/models/social_connection.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Module import
|
||||
from . import BaseModel
|
||||
|
||||
|
||||
class SocialLoginConnection(BaseModel):
|
||||
medium = models.CharField(
|
||||
max_length=20,
|
||||
choices=(("Google", "google"), ("Github", "github")),
|
||||
default=None,
|
||||
)
|
||||
last_login_at = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_received_at = models.DateTimeField(default=timezone.now, null=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_login_connections",
|
||||
)
|
||||
token_data = models.JSONField(null=True)
|
||||
extra_data = models.JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Social Login Connection"
|
||||
verbose_name_plural = "Social Login Connections"
|
||||
db_table = "social_login_connection"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the user and medium"""
|
||||
return f"{self.medium} <{self.user.email}>"
|
40
apiserver/plane/db/models/state.py
Normal file
40
apiserver/plane/db/models/state.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class State(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="State Name")
|
||||
description = models.TextField(verbose_name="State Description", blank=True)
|
||||
color = models.CharField(max_length=255, verbose_name="State Color")
|
||||
slug = models.SlugField(max_length=100, blank=True)
|
||||
sequence = models.FloatField(default=65535)
|
||||
group = models.CharField(
|
||||
choices=(
|
||||
("backlog", "Backlog"),
|
||||
("unstarted", "Unstarted"),
|
||||
("started", "Started"),
|
||||
("completed", "Completed"),
|
||||
("cancelled", "Cancelled"),
|
||||
),
|
||||
default="backlog",
|
||||
max_length=20,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the state"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
verbose_name = "State"
|
||||
verbose_name_plural = "States"
|
||||
db_table = "state"
|
||||
ordering = ("sequence",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.name)
|
||||
return super().save(*args, **kwargs)
|
126
apiserver/plane/db/models/user.py
Normal file
126
apiserver/plane/db/models/user.py
Normal file
@ -0,0 +1,126 @@
|
||||
# Python imports
|
||||
from enum import unique
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
|
||||
from django.utils import timezone
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
)
|
||||
username = models.CharField(max_length=128, unique=True)
|
||||
|
||||
# user fields
|
||||
mobile_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
email = models.CharField(max_length=255, null=True, blank=True, unique=True)
|
||||
first_name = models.CharField(max_length=255, blank=True)
|
||||
last_name = models.CharField(max_length=255, blank=True)
|
||||
avatar = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# tracking metrics
|
||||
date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At")
|
||||
last_location = models.CharField(max_length=255, blank=True)
|
||||
created_location = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# the is' es
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
is_managed = models.BooleanField(default=False)
|
||||
is_password_expired = models.BooleanField(default=False)
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
is_password_autoset = models.BooleanField(default=False)
|
||||
is_onboarded = models.BooleanField(default=False)
|
||||
|
||||
token = models.CharField(max_length=64, blank=True)
|
||||
|
||||
billing_address_country = models.CharField(max_length=255, default="INDIA")
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
last_logout_time = models.DateTimeField(null=True)
|
||||
last_login_ip = models.CharField(max_length=255, blank=True)
|
||||
last_logout_ip = models.CharField(max_length=255, blank=True)
|
||||
last_login_medium = models.CharField(
|
||||
max_length=20,
|
||||
default="email",
|
||||
)
|
||||
last_login_uagent = models.TextField(blank=True)
|
||||
token_updated_at = models.DateTimeField(null=True)
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
REQUIRED_FIELDS = ["username"]
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User"
|
||||
verbose_name_plural = "Users"
|
||||
db_table = "user"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} <{self.email}>"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.email = self.email.lower().strip()
|
||||
self.mobile_number = self.mobile_number
|
||||
|
||||
if self.token_updated_at is not None:
|
||||
self.token = uuid.uuid4().hex + uuid.uuid4().hex
|
||||
self.token_updated_at = timezone.now()
|
||||
|
||||
if self.is_superuser:
|
||||
self.is_staff = True
|
||||
|
||||
super(User, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def send_welcome_email(sender, instance, created, **kwargs):
|
||||
try:
|
||||
if created:
|
||||
first_name = instance.first_name.capitalize()
|
||||
to_email = instance.email
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {first_name}!"
|
||||
|
||||
context = {"first_name": first_name, "email": instance.email}
|
||||
|
||||
html_content = render_to_string(
|
||||
"emails/auth/user_welcome_email.html", context
|
||||
)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, text_content, from_email_string, [to_email]
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
22
apiserver/plane/db/models/view.py
Normal file
22
apiserver/plane/db/models/view.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
|
||||
# Module import
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class View(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="View Name")
|
||||
description = models.TextField(verbose_name="View Description", blank=True)
|
||||
query = models.JSONField(verbose_name="View Query")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "View"
|
||||
verbose_name_plural = "Views"
|
||||
db_table = "view"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.project.name}>"
|
134
apiserver/plane/db/models/workspace.py
Normal file
134
apiserver/plane/db/models/workspace.py
Normal file
@ -0,0 +1,134 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from . import BaseModel
|
||||
|
||||
|
||||
ROLE_CHOICES = (
|
||||
(20, "Owner"),
|
||||
(15, "Admin"),
|
||||
(10, "Member"),
|
||||
(5, "Guest"),
|
||||
)
|
||||
|
||||
|
||||
class Workspace(BaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="Workspace Name")
|
||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="owner_workspace",
|
||||
)
|
||||
slug = models.SlugField(max_length=100, db_index=True, unique=True)
|
||||
company_size = models.PositiveIntegerField(default=10)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the Workspace"""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "owner"]
|
||||
verbose_name = "Workspace"
|
||||
verbose_name_plural = "Workspaces"
|
||||
db_table = "workspace"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.name)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class WorkspaceMember(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
|
||||
)
|
||||
member = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="member_workspace",
|
||||
)
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
company_role = models.TextField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member"]
|
||||
verbose_name = "Workspace Member"
|
||||
verbose_name_plural = "Workspace Members"
|
||||
db_table = "workspace_member"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return members of the workspace"""
|
||||
return f"{self.member.email} <{self.workspace.name}>"
|
||||
|
||||
|
||||
class WorkspaceMemberInvite(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member_invite"
|
||||
)
|
||||
email = models.CharField(max_length=255)
|
||||
accepted = models.BooleanField(default=False)
|
||||
token = models.CharField(max_length=255)
|
||||
message = models.TextField(null=True)
|
||||
responded_at = models.DateTimeField(null=True)
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Workspace Member Invite"
|
||||
verbose_name_plural = "Workspace Member Invites"
|
||||
db_table = "workspace_member_invite"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.workspace.name} {self.email} {self.accepted}"
|
||||
|
||||
|
||||
class Team(BaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="Team Name")
|
||||
description = models.TextField(verbose_name="Team Description", blank=True)
|
||||
members = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
related_name="members",
|
||||
through="TeamMember",
|
||||
through_fields=("team", "member"),
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
Workspace, on_delete=models.CASCADE, related_name="workspace_team"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the team"""
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "workspace"]
|
||||
verbose_name = "Team"
|
||||
verbose_name_plural = "Teams"
|
||||
db_table = "team"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class TeamMember(BaseModel):
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
Workspace, on_delete=models.CASCADE, related_name="team_member"
|
||||
)
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="team_member")
|
||||
member = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_member"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.team.name
|
||||
|
||||
class Meta:
|
||||
unique_together = ["team", "member"]
|
||||
verbose_name = "Team Member"
|
||||
verbose_name_plural = "Team Members"
|
||||
db_table = "team_member"
|
||||
ordering = ("-created_at",)
|
0
apiserver/plane/middleware/__init__.py
Normal file
0
apiserver/plane/middleware/__init__.py
Normal file
5
apiserver/plane/middleware/apps.py
Normal file
5
apiserver/plane/middleware/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Middleware(AppConfig):
|
||||
name = 'plane.middleware'
|
33
apiserver/plane/middleware/user_middleware.py
Normal file
33
apiserver/plane/middleware/user_middleware.py
Normal file
@ -0,0 +1,33 @@
|
||||
import jwt
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class UserMiddleware(object):
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
try:
|
||||
if request.headers.get("Authorization"):
|
||||
authorization_header = request.headers.get("Authorization")
|
||||
access_token = authorization_header.split(" ")[1]
|
||||
decoded = jwt.decode(
|
||||
access_token, settings.SECRET_KEY, algorithms=["HS256"]
|
||||
)
|
||||
id = decoded['user_id']
|
||||
user = User.objects.get(id=id)
|
||||
user.last_active = timezone.now()
|
||||
user.token_updated_at = None
|
||||
user.save()
|
||||
timezone.activate(pytz.timezone(user.user_timezone))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
return response
|
0
apiserver/plane/settings/__init__.py
Normal file
0
apiserver/plane/settings/__init__.py
Normal file
208
apiserver/plane/settings/common.py
Normal file
208
apiserver/plane/settings/common.py
Normal file
@ -0,0 +1,208 @@
|
||||
import os
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
# Inhouse apps
|
||||
"plane.analytics",
|
||||
"plane.api",
|
||||
"plane.bgtasks",
|
||||
"plane.db",
|
||||
"plane.utils",
|
||||
"plane.web",
|
||||
"plane.middleware",
|
||||
# Third-party things
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"taggit",
|
||||
"fieldsignals",
|
||||
"django_rq",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
# "whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
),
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
|
||||
}
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
"django.contrib.auth.backends.ModelBackend", # default
|
||||
# "guardian.backends.ObjectPermissionBackend",
|
||||
)
|
||||
|
||||
ROOT_URLCONF = "plane.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [
|
||||
"templates",
|
||||
],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
JWT_AUTH = {
|
||||
"JWT_ENCODE_HANDLER": "rest_framework_jwt.utils.jwt_encode_handler",
|
||||
"JWT_DECODE_HANDLER": "rest_framework_jwt.utils.jwt_decode_handler",
|
||||
"JWT_PAYLOAD_HANDLER": "rest_framework_jwt.utils.jwt_payload_handler",
|
||||
"JWT_PAYLOAD_GET_USER_ID_HANDLER": "rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler",
|
||||
"JWT_RESPONSE_PAYLOAD_HANDLER": "rest_framework_jwt.utils.jwt_response_payload_handler",
|
||||
"JWT_SECRET_KEY": SECRET_KEY,
|
||||
"JWT_GET_USER_SECRET_KEY": None,
|
||||
"JWT_PUBLIC_KEY": None,
|
||||
"JWT_PRIVATE_KEY": None,
|
||||
"JWT_ALGORITHM": "HS256",
|
||||
"JWT_VERIFY": True,
|
||||
"JWT_VERIFY_EXPIRATION": True,
|
||||
"JWT_LEEWAY": 0,
|
||||
"JWT_EXPIRATION_DELTA": datetime.timedelta(seconds=604800),
|
||||
"JWT_AUDIENCE": None,
|
||||
"JWT_ISSUER": None,
|
||||
"JWT_ALLOW_REFRESH": False,
|
||||
"JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=7),
|
||||
"JWT_AUTH_HEADER_PREFIX": "JWT",
|
||||
"JWT_AUTH_COOKIE": None,
|
||||
}
|
||||
|
||||
WSGI_APPLICATION = "plane.wsgi.application"
|
||||
|
||||
# Django Sites
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# User Model
|
||||
AUTH_USER_MODEL = "db.User"
|
||||
|
||||
# Database
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static-assets", "collected-static")
|
||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
|
||||
|
||||
# Media Settings
|
||||
MEDIA_ROOT = "mediafiles"
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
# Internationalization
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "Asia/Kolkata"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
# Host for sending e-mail.
|
||||
EMAIL_HOST = os.environ.get("EMAIL_HOST")
|
||||
# Port for sending e-mail.
|
||||
EMAIL_PORT = 587
|
||||
# Optional SMTP authentication information for EMAIL_HOST.
|
||||
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_USE_TLS = True
|
||||
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=43200),
|
||||
"ROTATE_REFRESH_TOKENS": False,
|
||||
"BLACKLIST_AFTER_ROTATION": False,
|
||||
"UPDATE_LAST_LOGIN": False,
|
||||
"ALGORITHM": "HS256",
|
||||
"SIGNING_KEY": SECRET_KEY,
|
||||
"VERIFYING_KEY": None,
|
||||
"AUDIENCE": None,
|
||||
"ISSUER": None,
|
||||
"JWK_URL": None,
|
||||
"LEEWAY": 0,
|
||||
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||
"USER_ID_FIELD": "id",
|
||||
"USER_ID_CLAIM": "user_id",
|
||||
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
|
||||
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
||||
"TOKEN_TYPE_CLAIM": "token_type",
|
||||
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
|
||||
"JTI_CLAIM": "jti",
|
||||
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
|
||||
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
|
||||
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
|
||||
}
|
67
apiserver/plane/settings/local.py
Normal file
67
apiserver/plane/settings/local.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Development settings and globals."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
DEBUG = True
|
||||
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"NAME": "plane",
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
"HOST": "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS += ("debug_toolbar",)
|
||||
|
||||
MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
|
||||
|
||||
DEBUG_TOOLBAR_PATCH_SETTINGS = False
|
||||
|
||||
INTERNAL_IPS = ("127.0.0.1",)
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
integrations=[DjangoIntegration(), RedisIntegration()],
|
||||
# If you wish to associate users to errors (assuming you are using
|
||||
# django.contrib.auth) you may enable sending PII data.
|
||||
send_default_pii=True,
|
||||
environment="local",
|
||||
traces_sample_rate=0.7,
|
||||
)
|
||||
|
||||
REDIS_HOST = "localhost"
|
||||
REDIS_PORT = 6379
|
||||
REDIS_URL = False
|
||||
|
||||
RQ_QUEUES = {
|
||||
"default": {
|
||||
"HOST": "localhost",
|
||||
"PORT": 6379,
|
||||
"DB": 0,
|
||||
"DEFAULT_TIMEOUT": 360,
|
||||
},
|
||||
}
|
||||
|
||||
WEB_URL = "http://localhost:3000"
|
188
apiserver/plane/settings/production.py
Normal file
188
apiserver/plane/settings/production.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Production settings and globals."""
|
||||
from plane.settings.local import WEB_URL
|
||||
from .common import * # noqa
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
# Database
|
||||
DEBUG = True
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"NAME": "plane",
|
||||
"USER": os.environ.get('PGUSER'),
|
||||
"PASSWORD": os.environ.get('PGPASSWORD'),
|
||||
"HOST": os.environ.get('PGHOST'),
|
||||
}
|
||||
}
|
||||
|
||||
# CORS WHITELIST ON PROD
|
||||
CORS_ORIGIN_WHITELIST = [
|
||||
# "https://example.com",
|
||||
# "https://sub.example.com",
|
||||
# "http://localhost:8080",
|
||||
# "http://127.0.0.1:9000"
|
||||
]
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
# DATABASES["default"] = dj_database_url.config()
|
||||
SITE_ID = 1
|
||||
|
||||
# Enable Connection Pooling (if desired)
|
||||
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Allow all host headers
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# Simplified static file serving.
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
integrations=[DjangoIntegration(), RedisIntegration()],
|
||||
# If you wish to associate users to errors (assuming you are using
|
||||
# django.contrib.auth) you may enable sending PII data.
|
||||
traces_sample_rate=1,
|
||||
send_default_pii=True,
|
||||
environment="production",
|
||||
)
|
||||
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION")
|
||||
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
|
||||
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = ""
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
# AWS Settings End
|
||||
|
||||
|
||||
# Enable Connection Pooling (if desired)
|
||||
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Allow all host headers
|
||||
ALLOWED_HOSTS = [
|
||||
"*",
|
||||
]
|
||||
|
||||
|
||||
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
|
||||
# Simplified static file serving.
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
REDIS_TLS_URL = os.environ.get("REDIS_TLS_URL")
|
||||
|
||||
if REDIS_TLS_URL:
|
||||
REDIS_URL = REDIS_TLS_URL
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
# "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
RQ_QUEUES = {
|
||||
"default": {
|
||||
"USE_REDIS_CACHE": "default",
|
||||
}
|
||||
}
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
13
apiserver/plane/settings/redis.py
Normal file
13
apiserver/plane/settings/redis.py
Normal file
@ -0,0 +1,13 @@
|
||||
import redis
|
||||
import os
|
||||
from django.conf import settings
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def redis_instance():
|
||||
if settings.REDIS_URL:
|
||||
ri = redis.from_url(settings.REDIS_URL, db=0)
|
||||
else:
|
||||
ri = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
|
||||
|
||||
return ri
|
188
apiserver/plane/settings/staging.py
Normal file
188
apiserver/plane/settings/staging.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Production settings and globals."""
|
||||
from plane.settings.local import WEB_URL
|
||||
from .common import * # noqa
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
# Database
|
||||
DEBUG = False
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"NAME": "plane",
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
"HOST": "",
|
||||
}
|
||||
}
|
||||
|
||||
# CORS WHITELIST ON PROD
|
||||
CORS_ORIGIN_WHITELIST = [
|
||||
# "https://example.com",
|
||||
# "https://sub.example.com",
|
||||
# "http://localhost:8080",
|
||||
# "http://127.0.0.1:9000"
|
||||
]
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES["default"] = dj_database_url.config()
|
||||
SITE_ID = 1
|
||||
|
||||
# Enable Connection Pooling (if desired)
|
||||
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Allow all host headers
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# Simplified static file serving.
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
integrations=[DjangoIntegration(), RedisIntegration()],
|
||||
# If you wish to associate users to errors (assuming you are using
|
||||
# django.contrib.auth) you may enable sending PII data.
|
||||
traces_sample_rate=1,
|
||||
send_default_pii=True,
|
||||
environment="staging",
|
||||
)
|
||||
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION")
|
||||
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
|
||||
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = ""
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
# AWS Settings End
|
||||
|
||||
|
||||
# Enable Connection Pooling (if desired)
|
||||
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Allow all host headers
|
||||
ALLOWED_HOSTS = [
|
||||
"*",
|
||||
]
|
||||
|
||||
|
||||
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
|
||||
# Simplified static file serving.
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
REDIS_TLS_URL = os.environ.get("REDIS_TLS_URL")
|
||||
|
||||
if REDIS_TLS_URL:
|
||||
REDIS_URL = REDIS_TLS_URL
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
RQ_QUEUES = {
|
||||
"default": {
|
||||
"USE_REDIS_CACHE": "default",
|
||||
}
|
||||
}
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
45
apiserver/plane/settings/test.py
Normal file
45
apiserver/plane/settings/test.py
Normal file
@ -0,0 +1,45 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
DEBUG = True
|
||||
|
||||
INSTALLED_APPS.append("plane.tests")
|
||||
|
||||
if os.environ.get('GITHUB_WORKFLOW'):
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'github_actions',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': 'postgres',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'plane_test',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': 'password123',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
|
||||
REDIS_HOST = "localhost"
|
||||
REDIS_PORT = 6379
|
||||
REDIS_URL = False
|
||||
|
||||
RQ_QUEUES = {
|
||||
"default": {
|
||||
"HOST": "localhost",
|
||||
"PORT": 6379,
|
||||
"DB": 0,
|
||||
"DEFAULT_TIMEOUT": 360,
|
||||
},
|
||||
}
|
||||
|
||||
WEB_URL = "http://localhost:3000"
|
0
apiserver/plane/static/css/style.css
Normal file
0
apiserver/plane/static/css/style.css
Normal file
0
apiserver/plane/static/humans.txt
Normal file
0
apiserver/plane/static/humans.txt
Normal file
0
apiserver/plane/static/js/script.js
Normal file
0
apiserver/plane/static/js/script.js
Normal file
1
apiserver/plane/tests/__init__.py
Normal file
1
apiserver/plane/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .api import *
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user