dev: promote stage release to production (#155)
* refractor: removed modules from user.context * refractor: removed cycles from user context * refractor: removed state from user context * feat: implement channel protocol for tracking issue-activites * refactor: remove blocking code and add todo * refactor: refactor the consumer with function modules * feat: add columns for identifiers for easier redirection * style: minor padding, coloring and consistency changes * feat: track blocker issues * feat: track issue after creation * feat: add runworker in procfile * refractor: moved all context provider to _app for more clarity * dev: added our icons * refractor: removed issues from user context * refactor: rename db names to plural and remove admin register file * refactor: integrate permission layer in endpoints * feat: create product email html templates * refractor: changed to getServerSide from getInitialProps, removed unused component imports and minor refractoring * feat: remirror added * feat: workspace member user details endpoint * fix: resolved build issue * refactor: remove www * feat: workspace details on user endpoint * feat: added authorization in project settings refractor: improved code readability * fix: removed hard-coded workspace slug value, and added workspace in user interface * refactor: invitation workflow for already existing users * feat: modified remirror, fix: issue details sidebar * fix: merge conflicts * fix: merge conflicts * fix: added missing dependencies * refactor: remove user dependency from invitations * refactor: issue description context is updated with manager * dev: redis instance rewrite for ssl settings and remove REDIS_TLS env variable * chore: upgrade python package requirements * dev: added new migrations for changes * dev: ssl config for django channels redis connection * chore: upgrade channels requirements * refactor: better function for connecting with redis ssl django channels * chore: cleanup on manifest file * revert: user endpoint changes * build: setup asgi * refactor: update invitation endpoint to do bulk operations * style: cycles page, custom listbox, issue details page * refractor: removed folder that were moved to workspaceSlug * dev: uvicorn in requirements * Update index.tsx * refactor: get workspace slug on user endpoint * fix: workspace slug redirections and slug value in user context * fix: user context bugs, drag and drop in cycles and modules * fix: merge conflicts * fix: user context and create issue modal * refactor: add extra columns for json and html description and script for back migrating old issues * refactor: move all 500 errors to 400 * refractor: removed active project, active workspace, projects, and workspaces from user context * refractor: change from /home to /, added home page redirection logic added explict GET method on fetch request, and fixed invitation page not fetching all invitations * fix: passing project id in command palette * style: home page, feat: image in remirror * fix: bugs * chore: remove test_runner workflow from github actions * dev: update Procfile worker count and python runtime upgrade * refactor: update response from 404 to 403 * feat: filtering using both name and issue identifier in command palette showing my issues instead of project issue in command palette, hiding again according to route in command palette * fix: mutation on different CRUD operations * fix: redirection in my issues pages * feat: added authorization in workspace settings, moved command palette to app-layout * feat: endpoint and column to store my issue props * style: authorization new design, fix: made whole button on authorization page clickable, lib/auth on unsuccessful api call redirecting to error page * feat: return project details on modules and cycles * fix: create cycle and state coming below issue modal, showing loader for rich text editor refractor: changed from sprint to cycle in issue type * fix: issue delete mustation and some code refractor * fix: mutation bugs, remirror bugs, style: consistent droopdowns and buttons * feat: user role in model * dev: added new migrations * fix: add url for workspace availability check * feat: onboarding screens * fix: update url for workspace name check and add authentication layer and fix invitation endpoint * refactor: bulk invitations message * refactor: response on workspace invitarions * refactor: update identifier endpoint * refactor: invitations endpoint * feat: onboarding logic and validations * fix: email striep * dev: added workspace space member unique_together * chore: back populate neccesary data for description field * feat: emoji-picker gets close on select, public will be default option in create project * fix: update error in project creation * fix: mutation error on issue count in kanban view some minor code refractoring * fix: module bugs * fix: issue activities and issue comments mutation handled at issue detail * fix: error message for creating updates without permissions * fix: showing no user left to invite in project invite fix: - mutation in project settings control, style: - showing loader in project settings controller, - showing request pending for user that hasn't accepted invitation * refactor: file asset upload directory * fix: update last workspace id on user invitation accept * style: onboarding screens * style: cycles, issue activity * feat: add json and html column in issue comments * fix: submitting create issue modal on enter click, project not getting deselected * feat: file size validator * fix: emoji picker not closing on all emoji select * feat: added validation in identifier such that it only accept uppercase text * dev: commenting is now richer * fix: shortcuts not getting opened in settings layouts * style: showing sidebar on unauthorized pages * fix: error code on exception * fix: add issue button is working on my issues pages * feat: new way of assets * fix: updated activity content for description field * fix: mutation on project settings control style: blocker and blocked changed to outline button * fix: description activity logging * refactor: check for workspace slug on workspace creation * fix: typo on workspace url check * fix: workspace name uniqueness * fix: remove workspace from read only field * fix: file upload endpoint, workspace slug check * chore: drop unique_together constraint for name and workspace * chore: settings files cleanup and use PubSub backend on django channels * chore: change in channels backend * refactor: issue activity api to combine comments * fix: instance created at key * fix: result list * style: create project, cycle modal, view dropdown * feat: merged issue activities and issue comments into a single section * fix: remirror dynamic update of issue description * fix: removed commented code * fix: issue acitivties mutation * fix: empty comments cant be submitted * fix: workspace avatar has been updated while loading * refactor: update docker-compose to run redis and database in heroku and docker environment * refactor: removesingle docker file configuration * refactor: update take off script to run in asgi * docs: added workspace, quickstart documentation * fix: reading editor values on focus out * refactor: cleanup environment variables and create .env.example * refactor: add extra variables in example env * fix: warning and erros on console lazy loading images with low priority, added validation on onboarding for user to either join or create workspace, on onboarding user can't click button while form is getting submitted, profile page going into loading state when updated, refractor: made some state local, removed unnecessary console logs and comments, changed some variable and function name to make more sence * feat: env examples * fix: workspace member does not exist * fi: remove pagination from issue list api * refactor: remove env example from root * feat: documentation for projects on plane * feat: create code of conduct and contributing guidelines * fix: update docker setup to check handle redis * revert: bring back pagination to avoid breaking * feat: made image uploader modal, used it in profile page and workspace page, delete project from project settings page, join project modal in project list page * feat: create workspace page, style: made ui consistent * style: updated onboarding and create workspace page design * style: responsive sidebar * fix: updated ui imports
This commit is contained in:
parent
a960ddedf7
commit
bef166a65f
395 changed files with 20119 additions and 18322 deletions
1
apiserver/plane/api/consumers/__init__.py
Normal file
1
apiserver/plane/api/consumers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .issue_consumer import IssueConsumer
|
||||
547
apiserver/plane/api/consumers/issue_consumer.py
Normal file
547
apiserver/plane/api/consumers/issue_consumer.py
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
from channels.generic.websocket import SyncConsumer
|
||||
import json
|
||||
from plane.db.models import IssueActivity, Project, User, Issue, State, Label
|
||||
|
||||
|
||||
class IssueConsumer(SyncConsumer):
|
||||
|
||||
# Track Chnages in name
|
||||
def track_name(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("name") != requested_data.get("name"):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("name"),
|
||||
new_value=requested_data.get("name"),
|
||||
field="name",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('name')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in parent issue
|
||||
def track_parent(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
|
||||
if requested_data.get("parent") == None:
|
||||
old_parent = Issue.objects.get(pk=current_instance.get("parent"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}",
|
||||
new_value=None,
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_parent = Issue.objects.get(pk=requested_data.get("parent"))
|
||||
old_parent = Issue.objects.filter(
|
||||
pk=current_instance.get("parent")
|
||||
).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}"
|
||||
if old_parent is not None
|
||||
else None,
|
||||
new_value=f"{project.identifier}-{new_parent.sequence_id}",
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id
|
||||
if old_parent is not None
|
||||
else None,
|
||||
new_identifier=new_parent.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in priority
|
||||
def track_priority(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("priority") != requested_data.get("priority"):
|
||||
if requested_data.get("priority") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("parent"),
|
||||
new_value=requested_data.get("parent"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=requested_data.get("priority"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track chnages in state of the issue
|
||||
def track_state(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
old_state = State.objects.get(pk=current_instance.get("state", None))
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_state.name,
|
||||
new_value=new_state.name,
|
||||
field="state",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track issue description
|
||||
def track_description(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get("description_html"):
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("description_html"),
|
||||
new_value=requested_data.get("description_html"),
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue target date
|
||||
def track_target_date(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||
if requested_data.get("target_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue start date
|
||||
def track_start_date(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
if requested_data.get("start_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue labels
|
||||
def track_labels(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
# Label Addition
|
||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||
|
||||
for label in requested_data.get("labels_list"):
|
||||
if label not in current_instance.get("labels"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=label.name,
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Label Removal
|
||||
if len(requested_data.get("labels_list")) < len(current_instance.get("labels")):
|
||||
|
||||
for label in current_instance.get("labels"):
|
||||
if label not in requested_data.get("labels_list"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=label.name,
|
||||
new_value="",
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue assignees
|
||||
def track_assignees(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
|
||||
# Assignee Addition
|
||||
if len(requested_data.get("assignees_list")) > len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in requested_data.get("assignees_list"):
|
||||
if assignee not in current_instance.get("assignees"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=assignee.email,
|
||||
field="assignees",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added assignee {assignee.email}",
|
||||
new_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Assignee Removal
|
||||
if len(requested_data.get("assignees_list")) < len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in current_instance.get("assignees"):
|
||||
if assignee not in requested_data.get("assignees_list"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=assignee.email,
|
||||
new_value="",
|
||||
field="assignee",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed assignee {assignee.email}",
|
||||
old_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in blocking issues
|
||||
def track_blocks(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blocks_list")) > len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blocks_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocked_issues")
|
||||
if blocked.get("block") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blocks_list")) < len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocked_issues"):
|
||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in blocked_by issues
|
||||
def track_blockings(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blockers_list")) > len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blockers_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocker_issues")
|
||||
if blocked.get("blocked_by") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blockers_list")) < len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocker_issues"):
|
||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
def issue_activity(self, event):
|
||||
|
||||
issue_activities = []
|
||||
# Remove event type:
|
||||
event.pop("type")
|
||||
|
||||
requested_data = json.loads(event.get("requested_data"))
|
||||
current_instance = json.loads(event.get("current_instance"))
|
||||
issue_id = event.get("issue_id")
|
||||
actor_id = event.get("actor_id")
|
||||
project_id = event.get("project_id")
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": self.track_name,
|
||||
"parent": self.track_parent,
|
||||
"priority": self.track_priority,
|
||||
"state": self.track_state,
|
||||
"description": self.track_description,
|
||||
"target_date": self.track_target_date,
|
||||
"start_date": self.track_start_date,
|
||||
"labels_list": self.track_labels,
|
||||
"assignees_list": self.track_assignees,
|
||||
"blocks_list": self.track_blocks,
|
||||
"blockers_list": self.track_blockings,
|
||||
}
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||
if func is not None:
|
||||
func(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
IssueActivity.objects.bulk_create(issue_activities)
|
||||
|
|
@ -47,6 +47,7 @@ class IssueFlatSerializer(BaseSerializer):
|
|||
class IssueStateSerializer(BaseSerializer):
|
||||
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
|
|
@ -67,6 +68,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocking this issue
|
||||
blockers_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
|
|
@ -77,6 +80,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocked by this issue
|
||||
blocks_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
|
|
@ -421,10 +426,12 @@ class IssueSerializer(BaseSerializer):
|
|||
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)
|
||||
# List of issues blocked by this issue
|
||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
||||
# List of issues that block this issue
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True, many=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ from .user import UserLiteSerializer
|
|||
from .project import ProjectSerializer
|
||||
from .issue import IssueStateSerializer
|
||||
|
||||
from plane.db.models import User, Module, ModuleMember, ModuleIssue
|
||||
from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink
|
||||
|
||||
|
||||
class LinkCreateSerializer(serializers.Serializer):
|
||||
|
||||
url = serializers.CharField(required=True)
|
||||
title = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class ModuleWriteSerializer(BaseSerializer):
|
||||
|
|
@ -17,6 +23,11 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
links_list = serializers.ListField(
|
||||
child=LinkCreateSerializer(),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
|
|
@ -33,6 +44,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||
def create(self, validated_data):
|
||||
|
||||
members = validated_data.pop("members_list", None)
|
||||
links = validated_data.pop("links_list", None)
|
||||
|
||||
project = self.context["project"]
|
||||
|
||||
|
|
@ -55,11 +67,31 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if links is not None:
|
||||
ModuleLink.objects.bulk_create(
|
||||
[
|
||||
ModuleLink(
|
||||
module=module,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=module.created_by,
|
||||
updated_by=module.updated_by,
|
||||
title=link.get("title", None),
|
||||
url=link.get("url", None),
|
||||
)
|
||||
for link in links
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
return module
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
members = validated_data.pop("members_list", None)
|
||||
links = validated_data.pop("links_list", None)
|
||||
|
||||
if members is not None:
|
||||
ModuleIssue.objects.filter(module=instance).delete()
|
||||
ModuleMember.objects.bulk_create(
|
||||
|
|
@ -75,7 +107,26 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||
for member in members
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if links is not None:
|
||||
ModuleLink.objects.filter(module=instance).delete()
|
||||
ModuleLink.objects.bulk_create(
|
||||
[
|
||||
ModuleLink(
|
||||
module=instance,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
updated_by=instance.updated_by,
|
||||
title=link.get("title", None),
|
||||
url=link.get("url", None),
|
||||
)
|
||||
for link in links
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
|
@ -114,12 +165,30 @@ class ModuleIssueSerializer(BaseSerializer):
|
|||
]
|
||||
|
||||
|
||||
class ModuleLinkSerializer(BaseSerializer):
|
||||
|
||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
||||
|
||||
class Meta:
|
||||
model = ModuleLink
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
issue_module = ModuleIssueSerializer(read_only=True, many=True)
|
||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
|
|
@ -131,4 +200,4 @@ class ModuleSerializer(BaseSerializer):
|
|||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
]
|
||||
|
|
@ -18,18 +18,12 @@ class WorkSpaceSerializer(BaseSerializer):
|
|||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"slug",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"owner",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"slug": {
|
||||
"required": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ from plane.api.views import (
|
|||
UserLastProjectWithWorkspaceEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
ProjectMemberUserEndpoint,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
)
|
||||
|
||||
from plane.api.views.project import AddTeamToProjectEndpoint
|
||||
|
|
@ -147,6 +150,11 @@ urlpatterns = [
|
|||
name="user-project-invitaions",
|
||||
),
|
||||
## Workspaces ##
|
||||
path(
|
||||
"workspace-slug-check/",
|
||||
WorkSpaceAvailabilityCheckEndpoint.as_view(),
|
||||
name="workspace-availability",
|
||||
),
|
||||
path(
|
||||
"workspaces/",
|
||||
WorkSpaceViewSet.as_view(
|
||||
|
|
@ -234,6 +242,16 @@ urlpatterns = [
|
|||
UserLastProjectWithWorkspaceEndpoint.as_view(),
|
||||
name="workspace-project-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-members/me/",
|
||||
WorkspaceMemberUserEndpoint.as_view(),
|
||||
name="workspace-member-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-views/",
|
||||
WorkspaceMemberUserViewsEndpoint.as_view(),
|
||||
name="workspace-member-details",
|
||||
),
|
||||
## End Workspaces ##
|
||||
# Projects
|
||||
path(
|
||||
|
|
@ -585,7 +603,7 @@ urlpatterns = [
|
|||
## IssueProperty Ebd
|
||||
## File Assets
|
||||
path(
|
||||
"file-assets/",
|
||||
"workspaces/<str:slug>/file-assets/",
|
||||
FileAssetEndpoint.as_view(),
|
||||
name="File Assets",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ from .workspace import (
|
|||
UserWorkspaceInvitationsEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .shortcut import ShortCutViewSet
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import FileAsset
|
||||
from plane.db.models import FileAsset, Workspace
|
||||
from plane.api.serializers import FileAssetSerializer
|
||||
|
||||
|
||||
|
|
@ -22,9 +23,23 @@ class FileAssetEndpoint(BaseAPIView):
|
|||
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)
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
serializer = FileAssetSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
|
||||
if request.user.last_workspace_id is None:
|
||||
return Response(
|
||||
{"error": "Workspace id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer.save(workspace_id=request.user.last_workspace_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -155,5 +155,5 @@ class ChangePasswordEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -361,5 +361,5 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
|
|||
except Exception as e:
|
||||
print(e)
|
||||
raise APIException(
|
||||
"Please check the view", status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
"Please check the view", status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -113,5 +113,5 @@ class CycleIssueViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
# Python imports
|
||||
from itertools import groupby
|
||||
import json
|
||||
from itertools import groupby, chain
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Count, Sum
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
|
|
@ -37,7 +41,7 @@ from plane.db.models import (
|
|||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
ModuleIssue
|
||||
ModuleIssue,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -67,6 +71,28 @@ class IssueViewSet(BaseViewSet):
|
|||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def perform_update(self, serializer):
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = Issue.objects.filter(pk=self.kwargs.get("pk", None)).first()
|
||||
if current_instance is not None:
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.send)(
|
||||
"issue-activites",
|
||||
{
|
||||
"type": "issue.activity",
|
||||
"requested_data": requested_data,
|
||||
"actor_id": str(self.request.user.id),
|
||||
"issue_id": str(self.kwargs.get("pk", None)),
|
||||
"project_id": str(self.kwargs.get("project_id", None)),
|
||||
"current_instance": json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
|
|
@ -146,7 +172,7 @@ class IssueViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
|
|
@ -158,6 +184,17 @@ class IssueViewSet(BaseViewSet):
|
|||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Track the issue
|
||||
IssueActivity.objects.create(
|
||||
issue_id=serializer.data["id"],
|
||||
project_id=project_id,
|
||||
workspace_id=serializer["workspace"],
|
||||
comment=f"{request.user.email} created the issue",
|
||||
verb="created",
|
||||
actor=request.user,
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
@ -179,7 +216,7 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -200,23 +237,42 @@ class WorkSpaceIssuesEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class IssueActivityEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
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
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("actor")
|
||||
).order_by("created_by")
|
||||
issue_comments = (
|
||||
IssueComment.objects.filter(issue_id=issue_id)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.order_by("created_at")
|
||||
)
|
||||
serializer = IssueActivitySerializer(issue_activities, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
|
||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||
|
||||
result_list = sorted(
|
||||
chain(issue_activities, issue_comments),
|
||||
key=lambda instance: instance["created_at"],
|
||||
)
|
||||
|
||||
return Response(result_list, 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,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -224,6 +280,9 @@ class IssueCommentViewSet(BaseViewSet):
|
|||
|
||||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
"issue__id",
|
||||
|
|
@ -343,7 +402,7 @@ class IssuePropertyViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -407,5 +466,5 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from plane.api.serializers import (
|
|||
ModuleIssueSerializer,
|
||||
)
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Module, ModuleIssue, Project, Issue
|
||||
from plane.db.models import Module, ModuleIssue, Project, Issue, ModuleLink
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
|
|
@ -48,6 +48,12 @@ class ModuleViewSet(BaseViewSet):
|
|||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"link_module",
|
||||
queryset=ModuleLink.objects.select_related("module"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
|
|
@ -76,7 +82,7 @@ class ModuleViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -130,6 +136,9 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
pk__in=issues, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
# Delete old records in order to maintain the database integrity
|
||||
ModuleIssue.objects.filter(issue_id__in=issues).delete()
|
||||
|
||||
ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
ModuleIssue(
|
||||
|
|
@ -154,5 +163,5 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
@ -7,11 +7,11 @@ from sentry_sdk import capture_exception
|
|||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
UserSerializer,
|
||||
WorkSpaceSerializer,
|
||||
)
|
||||
|
||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User
|
||||
|
||||
from plane.db.models import User, Workspace
|
||||
|
||||
|
||||
class PeopleEndpoint(BaseAPIView):
|
||||
|
|
@ -44,8 +44,8 @@ class PeopleEndpoint(BaseAPIView):
|
|||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"message": "Something went wrong"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
{"message": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -56,6 +56,19 @@ class UserEndpoint(BaseViewSet):
|
|||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def retrieve(self, request):
|
||||
try:
|
||||
workspace = Workspace.objects.get(pk=request.user.last_workspace_id)
|
||||
return Response(
|
||||
{"user": UserSerializer(request.user).data, "slug": workspace.slug}
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response({"user": UserSerializer(request.user).data, "slug": None})
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Workspace.DoesNotExist as e:
|
||||
|
|
@ -159,7 +159,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
|
|
@ -199,7 +199,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -280,7 +280,7 @@ class InviteProjectEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -353,6 +353,11 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
|
|
@ -399,11 +404,16 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
|
||||
try:
|
||||
|
|
@ -449,7 +459,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -517,7 +527,7 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def delete(self, request, slug):
|
||||
|
|
@ -545,7 +555,7 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -590,7 +600,7 @@ class ProjectJoinEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -629,7 +639,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||
except Exception as e:
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -647,11 +657,11 @@ class ProjectMemberUserEndpoint(BaseAPIView):
|
|||
except ProjectMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "User not a member of the project"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
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,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The workspace with the name already exists"},
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -96,7 +96,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||
"error": "Something went wrong please try again later",
|
||||
"identifier": None,
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -128,34 +128,28 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
name = request.GET.get("name", False)
|
||||
slug = request.GET.get("slug", False)
|
||||
|
||||
if not name:
|
||||
if not slug or slug == "":
|
||||
return Response(
|
||||
{"error": "Workspace Name is required"},
|
||||
{"error": "Workspace Slug is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.filter(name=name).exists()
|
||||
|
||||
return Response({"status": workspace}, status=status.HTTP_200_OK)
|
||||
workspace = Workspace.objects.filter(slug=slug).exists()
|
||||
return Response({"status": not 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,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -168,70 +162,92 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||
def post(self, request, slug):
|
||||
try:
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
emails = request.data.get("emails", False)
|
||||
# Check if email is provided
|
||||
if not email:
|
||||
if not emails or not len(emails):
|
||||
return Response(
|
||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Emails are 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():
|
||||
# Check if user is already a member of workspace
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace_id=workspace.id,
|
||||
member__email__in=[email.get("email") for email in emails],
|
||||
)
|
||||
|
||||
if len(workspace_members):
|
||||
return Response(
|
||||
{"error": "User is already member of workspace"},
|
||||
{
|
||||
"error": "Some users are already member of workspace",
|
||||
"workspace_users": WorkSpaceMemberSerializer(
|
||||
workspace_members, many=True
|
||||
).data,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
token = jwt.encode(
|
||||
{"email": email, "timestamp": datetime.now().timestamp()},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
workspace_invitations = []
|
||||
for email in emails:
|
||||
try:
|
||||
validate_email(email.get("email"))
|
||||
workspace_invitations.append(
|
||||
WorkspaceMemberInvite(
|
||||
email=email.get("email").strip().lower(),
|
||||
workspace_id=workspace.id,
|
||||
token=jwt.encode(
|
||||
{
|
||||
"email": email,
|
||||
"timestamp": datetime.now().timestamp(),
|
||||
},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
),
|
||||
role=email.get("role", 10),
|
||||
)
|
||||
)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
WorkspaceMemberInvite.objects.bulk_create(
|
||||
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
workspace_invitation_obj = WorkspaceMemberInvite.objects.create(
|
||||
email=email.strip().lower(),
|
||||
workspace_id=workspace.id,
|
||||
token=token,
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
||||
email__in=[email.get("email") for email in emails]
|
||||
).select_related("workspace")
|
||||
|
||||
domain = settings.WEB_URL
|
||||
|
||||
workspace_invitation.delay(
|
||||
email, workspace.id, token, domain, request.user.email
|
||||
)
|
||||
for invitation in workspace_invitations:
|
||||
workspace_invitation.delay(
|
||||
invitation.email,
|
||||
workspace.id,
|
||||
invitation.token,
|
||||
settings.WEB_URL,
|
||||
request.user.email,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": "Email sent successfully",
|
||||
"id": workspace_invitation_obj.id,
|
||||
"message": "Emails sent successfully",
|
||||
},
|
||||
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,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -261,6 +277,24 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||
workspace_invite.save()
|
||||
|
||||
if workspace_invite.accepted:
|
||||
|
||||
# Check if the user created account after invitation
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# If the user is present then create the workspace member
|
||||
if user is not None:
|
||||
WorkspaceMember.objects.create(
|
||||
workspace=workspace_invite.workspace,
|
||||
member=user,
|
||||
role=workspace_invite.role,
|
||||
)
|
||||
|
||||
user.last_workspace_id = workspace_invite.workspace.id
|
||||
user.save()
|
||||
|
||||
# Delete the invitation
|
||||
workspace_invite.delete()
|
||||
|
||||
return Response(
|
||||
{"message": "Workspace Invitation Accepted"},
|
||||
status=status.HTTP_200_OK,
|
||||
|
|
@ -286,7 +320,7 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -349,7 +383,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -381,6 +415,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||
|
||||
serializer_class = TeamSerializer
|
||||
model = Team
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
|
|
@ -442,7 +479,7 @@ class TeamMemberViewSet(BaseViewSet):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -506,5 +543,47 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
|||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
member=request.user, workspace__slug=slug
|
||||
)
|
||||
serializer = WorkSpaceMemberSerializer(workspace_member)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except (Workspace.DoesNotExist, WorkspaceMember.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_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user
|
||||
)
|
||||
workspace_member.view_props = request.data.get("view_props", {})
|
||||
workspace_member.save()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "User 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_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
24
apiserver/plane/asgi.py
Normal file
24
apiserver/plane/asgi.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import os
|
||||
|
||||
from channels.routing import ProtocolTypeRouter, ChannelNameRouter
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
|
||||
from plane.api.consumers import IssueConsumer
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"channel": ChannelNameRouter(
|
||||
{
|
||||
"issue-activites": IssueConsumer.as_asgi(),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
@ -17,7 +17,7 @@ def magic_link(email, key, token, current_site):
|
|||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Login!"
|
||||
subject = f"Login for Plane"
|
||||
|
||||
context = {"magic_url": abs_url, "code": token}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ def project_invitation(email, project_id, token, current_site):
|
|||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {email}!"
|
||||
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
||||
|
||||
context = {
|
||||
"email": email,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {email}!"
|
||||
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
||||
|
||||
context = {
|
||||
"email": email,
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -5,48 +5,48 @@ from fieldsignals import post_save_changed
|
|||
class DbConfig(AppConfig):
|
||||
name = "plane.db"
|
||||
|
||||
def ready(self):
|
||||
# def ready(self):
|
||||
|
||||
post_save_changed.connect(
|
||||
self.model_activity,
|
||||
sender=self.get_model("Issue"),
|
||||
)
|
||||
# post_save_changed.connect(
|
||||
# self.model_activity,
|
||||
# sender=self.get_model("Issue"),
|
||||
# )
|
||||
|
||||
def model_activity(self, sender, instance, changed_fields, **kwargs):
|
||||
# def model_activity(self, sender, instance, changed_fields, **kwargs):
|
||||
|
||||
verb = "created" if instance._state.adding else "changed"
|
||||
# verb = "created" if instance._state.adding else "changed"
|
||||
|
||||
import inspect
|
||||
# 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
|
||||
# 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":
|
||||
# if REQUEST_METHOD == "POST":
|
||||
|
||||
self.get_model("IssueActivity").objects.create(
|
||||
issue=instance, project=instance.project, actor=instance.created_by
|
||||
)
|
||||
# self.get_model("IssueActivity").objects.create(
|
||||
# issue=instance, project=instance.project, actor=instance.created_by
|
||||
# )
|
||||
|
||||
elif REQUEST_METHOD == "PATCH":
|
||||
# elif REQUEST_METHOD == "PATCH":
|
||||
|
||||
try:
|
||||
del changed_fields["updated_at"]
|
||||
del changed_fields["updated_by"]
|
||||
except KeyError as e:
|
||||
pass
|
||||
# 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,
|
||||
)
|
||||
# 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,
|
||||
# )
|
||||
|
|
|
|||
172
apiserver/plane/db/migrations/0012_auto_20230104_0117.py
Normal file
172
apiserver/plane/db/migrations/0012_auto_20230104_0117.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-03 19:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0011_auto_20221222_2357'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='new_identifier',
|
||||
field=models.UUIDField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='old_identifier',
|
||||
field=models.UUIDField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='moduleissue',
|
||||
name='issue',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='moduleissue',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='cycle',
|
||||
table='cycles',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='cycleissue',
|
||||
table='cycle_issues',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='fileasset',
|
||||
table='file_assets',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issue',
|
||||
table='issues',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issueactivity',
|
||||
table='issue_activities',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issueassignee',
|
||||
table='issue_assignees',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issueblocker',
|
||||
table='issue_blockers',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issuecomment',
|
||||
table='issue_comments',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issuelabel',
|
||||
table='issue_labels',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issueproperty',
|
||||
table='issue_properties',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='issuesequence',
|
||||
table='issue_sequences',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='label',
|
||||
table='labels',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='module',
|
||||
table='modules',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='modulemember',
|
||||
table='module_members',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='project',
|
||||
table='projects',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='projectidentifier',
|
||||
table='project_identifiers',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='projectmember',
|
||||
table='project_members',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='projectmemberinvite',
|
||||
table='project_member_invites',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='shortcut',
|
||||
table='shortcuts',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='socialloginconnection',
|
||||
table='social_login_connections',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='state',
|
||||
table='states',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='team',
|
||||
table='teams',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='teammember',
|
||||
table='team_members',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='timelineissue',
|
||||
table='issue_timelines',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='user',
|
||||
table='users',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='view',
|
||||
table='views',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='workspace',
|
||||
table='workspaces',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='workspacemember',
|
||||
table='workspace_members',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='workspacememberinvite',
|
||||
table='workspace_member_invites',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModuleLink',
|
||||
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)),
|
||||
('title', models.CharField(max_length=255, null=True)),
|
||||
('url', models.URLField()),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulelink_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_module', to='db.module')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulelink', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulelink_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_modulelink', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Module Link',
|
||||
'verbose_name_plural': 'Module Links',
|
||||
'db_table': 'module_links',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
]
|
||||
38
apiserver/plane/db/migrations/0013_auto_20230107_0041.py
Normal file
38
apiserver/plane/db/migrations/0013_auto_20230107_0041.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-06 19:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0012_auto_20230104_0117'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='description_html',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='description_stripped',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.CharField(blank=True, max_length=300, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='view_props',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='description',
|
||||
field=models.JSONField(blank=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-07 05:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0013_auto_20230107_0041'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='workspacememberinvite',
|
||||
unique_together={('email', 'workspace')},
|
||||
),
|
||||
]
|
||||
28
apiserver/plane/db/migrations/0015_auto_20230107_1636.py
Normal file
28
apiserver/plane/db/migrations/0015_auto_20230107_1636.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-07 11:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0014_alter_workspacememberinvite_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='issuecomment',
|
||||
old_name='comment',
|
||||
new_name='comment_stripped',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='comment_html',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='comment_json',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
25
apiserver/plane/db/migrations/0016_auto_20230107_1735.py
Normal file
25
apiserver/plane/db/migrations/0016_auto_20230107_1735.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-07 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.asset
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0015_auto_20230107_1636'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='fileasset',
|
||||
name='workspace',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='db.workspace'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fileasset',
|
||||
name='asset',
|
||||
field=models.FileField(upload_to=plane.db.models.asset.get_upload_path, validators=[plane.db.models.asset.file_size]),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-07 17:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0016_auto_20230107_1735'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='workspace',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
|
|
@ -37,4 +37,4 @@ from .shortcut import Shortcut
|
|||
|
||||
from .view import View
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink
|
||||
|
|
|
|||
|
|
@ -1,24 +1,41 @@
|
|||
# Django import
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Module import
|
||||
from . import BaseModel
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
return f"{instance.workspace.id}/{filename}"
|
||||
|
||||
|
||||
def file_size(value):
|
||||
limit = 5 * 1024 * 1024
|
||||
if value.size > limit:
|
||||
raise ValidationError("File too large. Size should not exceed 5 MB.")
|
||||
|
||||
class FileAsset(BaseModel):
|
||||
"""
|
||||
A file asset.
|
||||
"""
|
||||
|
||||
attributes = models.JSONField(default=dict)
|
||||
asset = models.FileField(upload_to="library-assets")
|
||||
asset = models.FileField(
|
||||
upload_to=get_upload_path,
|
||||
validators=[
|
||||
file_size,
|
||||
],
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, null=True, related_name="assets"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "File Asset"
|
||||
verbose_name_plural = "File Assets"
|
||||
db_table = "file_asset"
|
||||
db_table = "file_assets"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return self.asset
|
||||
|
||||
return str(self.asset)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Cycle(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
verbose_name_plural = "Cycles"
|
||||
db_table = "cycle"
|
||||
db_table = "cycles"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -54,7 +54,7 @@ class CycleIssue(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Cycle Issue"
|
||||
verbose_name_plural = "Cycle Issues"
|
||||
db_table = "cycle_issue"
|
||||
db_table = "cycle_issues"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from django.dispatch import receiver
|
|||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
from plane.utils.html_processor import strip_tags
|
||||
|
||||
# TODO: Handle identifiers for Bulk Inserts - nk
|
||||
class Issue(ProjectBaseModel):
|
||||
|
|
@ -31,7 +32,9 @@ class Issue(ProjectBaseModel):
|
|||
related_name="state_issue",
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(verbose_name="Issue Description", blank=True)
|
||||
description = models.JSONField(blank=True)
|
||||
description_html = models.TextField(blank=True)
|
||||
description_stripped = models.TextField(blank=True)
|
||||
priority = models.CharField(
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
|
|
@ -57,7 +60,7 @@ class Issue(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Issue"
|
||||
verbose_name_plural = "Issues"
|
||||
db_table = "issue"
|
||||
db_table = "issues"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
@ -81,6 +84,11 @@ class Issue(ProjectBaseModel):
|
|||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
strip_tags(self.description_html) if self.description_html != "" else ""
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -99,7 +107,7 @@ class IssueBlocker(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Issue Blocker"
|
||||
verbose_name_plural = "Issue Blockers"
|
||||
db_table = "issue_blocker"
|
||||
db_table = "issue_blockers"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -120,7 +128,7 @@ class IssueAssignee(ProjectBaseModel):
|
|||
unique_together = ["issue", "assignee"]
|
||||
verbose_name = "Issue Assignee"
|
||||
verbose_name_plural = "Issue Assignees"
|
||||
db_table = "issue_assignee"
|
||||
db_table = "issue_assignees"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -156,11 +164,13 @@ class IssueActivity(ProjectBaseModel):
|
|||
null=True,
|
||||
related_name="issue_activities",
|
||||
)
|
||||
old_identifier = models.UUIDField(null=True)
|
||||
new_identifier = models.UUIDField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Activity"
|
||||
verbose_name_plural = "Issue Activities"
|
||||
db_table = "issue_activity"
|
||||
db_table = "issue_activities"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -178,7 +188,7 @@ class TimelineIssue(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Timeline Issue"
|
||||
verbose_name_plural = "Timeline Issues"
|
||||
db_table = "issue_timeline"
|
||||
db_table = "issue_timelines"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -187,7 +197,9 @@ class TimelineIssue(ProjectBaseModel):
|
|||
|
||||
|
||||
class IssueComment(ProjectBaseModel):
|
||||
comment = models.TextField(verbose_name="Comment", blank=True)
|
||||
comment_stripped = models.TextField(verbose_name="Comment", blank=True)
|
||||
comment_json = models.JSONField(blank=True, null=True)
|
||||
comment_html = models.TextField(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
|
||||
|
|
@ -198,10 +210,15 @@ class IssueComment(ProjectBaseModel):
|
|||
null=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.comment_stripped = strip_tags(self.comment_html) if self.comment_html != "" else ""
|
||||
return super(IssueComment, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Comment"
|
||||
verbose_name_plural = "Issue Comments"
|
||||
db_table = "issue_comment"
|
||||
db_table = "issue_comments"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -220,7 +237,7 @@ class IssueProperty(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Issue Property"
|
||||
verbose_name_plural = "Issue Properties"
|
||||
db_table = "issue_property"
|
||||
db_table = "issue_properties"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project"]
|
||||
|
||||
|
|
@ -245,7 +262,7 @@ class Label(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Label"
|
||||
verbose_name_plural = "Labels"
|
||||
db_table = "label"
|
||||
db_table = "labels"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -264,7 +281,7 @@ class IssueLabel(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Issue Label"
|
||||
verbose_name_plural = "Issue Labels"
|
||||
db_table = "issue_label"
|
||||
db_table = "issue_labels"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -282,7 +299,7 @@ class IssueSequence(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Issue Sequence"
|
||||
verbose_name_plural = "Issue Sequences"
|
||||
db_table = "issue_sequence"
|
||||
db_table = "issue_sequences"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,12 @@ class Module(ProjectBaseModel):
|
|||
through_fields=("module", "member"),
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
verbose_name = "Module"
|
||||
verbose_name_plural = "Modules"
|
||||
db_table = "module"
|
||||
db_table = "modules"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -61,7 +62,7 @@ class ModuleMember(ProjectBaseModel):
|
|||
unique_together = ["module", "member"]
|
||||
verbose_name = "Module Member"
|
||||
verbose_name_plural = "Module Members"
|
||||
db_table = "module_member"
|
||||
db_table = "module_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -73,12 +74,11 @@ class ModuleIssue(ProjectBaseModel):
|
|||
module = models.ForeignKey(
|
||||
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
|
||||
)
|
||||
issue = models.ForeignKey(
|
||||
issue = models.OneToOneField(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "issue"]
|
||||
verbose_name = "Module Issue"
|
||||
verbose_name_plural = "Module Issues"
|
||||
db_table = "module_issues"
|
||||
|
|
@ -86,3 +86,19 @@ class ModuleIssue(ProjectBaseModel):
|
|||
|
||||
def __str__(self):
|
||||
return f"{self.module.name} {self.issue.name}"
|
||||
|
||||
|
||||
class ModuleLink(ProjectBaseModel):
|
||||
|
||||
title = models.CharField(max_length=255, null=True)
|
||||
url = models.URLField()
|
||||
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name="link_module")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Module Link"
|
||||
verbose_name_plural = "Module Links"
|
||||
db_table = "module_links"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.module.name} {self.url}"
|
||||
|
|
@ -72,7 +72,7 @@ class Project(BaseModel):
|
|||
unique_together = [["identifier", "workspace"], ["name", "workspace"]]
|
||||
verbose_name = "Project"
|
||||
verbose_name_plural = "Projects"
|
||||
db_table = "project"
|
||||
db_table = "projects"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
@ -109,7 +109,7 @@ class ProjectMemberInvite(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Project Member Invite"
|
||||
verbose_name_plural = "Project Member Invites"
|
||||
db_table = "project_member_invite"
|
||||
db_table = "project_member_invites"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -134,7 +134,7 @@ class ProjectMember(ProjectBaseModel):
|
|||
unique_together = ["project", "member"]
|
||||
verbose_name = "Project Member"
|
||||
verbose_name_plural = "Project Members"
|
||||
db_table = "project_member"
|
||||
db_table = "project_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -156,5 +156,5 @@ class ProjectIdentifier(AuditModel):
|
|||
unique_together = ["name", "workspace"]
|
||||
verbose_name = "Project Identifier"
|
||||
verbose_name_plural = "Project Identifiers"
|
||||
db_table = "project_identifier"
|
||||
db_table = "project_identifiers"
|
||||
ordering = ("-created_at",)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Shortcut(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Shortcut"
|
||||
verbose_name_plural = "Shortcuts"
|
||||
db_table = "shortcut"
|
||||
db_table = "shortcuts"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SocialLoginConnection(BaseModel):
|
|||
class Meta:
|
||||
verbose_name = "Social Login Connection"
|
||||
verbose_name_plural = "Social Login Connections"
|
||||
db_table = "social_login_connection"
|
||||
db_table = "social_login_connections"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class State(ProjectBaseModel):
|
|||
unique_together = ["name", "project"]
|
||||
verbose_name = "State"
|
||||
verbose_name_plural = "States"
|
||||
db_table = "state"
|
||||
db_table = "states"
|
||||
ordering = ("sequence",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||
token_updated_at = models.DateTimeField(null=True)
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
my_issues_prop = models.JSONField(null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||
class Meta:
|
||||
verbose_name = "User"
|
||||
verbose_name_plural = "Users"
|
||||
db_table = "user"
|
||||
db_table = "users"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -105,7 +106,7 @@ def send_welcome_email(sender, instance, created, **kwargs):
|
|||
to_email = instance.email
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome {first_name}!"
|
||||
subject = f"Welcome to Plane ✈️!"
|
||||
|
||||
context = {"first_name": first_name, "email": instance.email}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class View(ProjectBaseModel):
|
|||
class Meta:
|
||||
verbose_name = "View"
|
||||
verbose_name_plural = "Views"
|
||||
db_table = "view"
|
||||
db_table = "views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Django imports
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
|
|
@ -31,15 +30,11 @@ class Workspace(BaseModel):
|
|||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "owner"]
|
||||
verbose_name = "Workspace"
|
||||
verbose_name_plural = "Workspaces"
|
||||
db_table = "workspace"
|
||||
db_table = "workspaces"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.name)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class WorkspaceMember(BaseModel):
|
||||
|
|
@ -53,12 +48,13 @@ class WorkspaceMember(BaseModel):
|
|||
)
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
company_role = models.TextField(null=True, blank=True)
|
||||
view_props = models.JSONField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member"]
|
||||
verbose_name = "Workspace Member"
|
||||
verbose_name_plural = "Workspace Members"
|
||||
db_table = "workspace_member"
|
||||
db_table = "workspace_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -78,9 +74,10 @@ class WorkspaceMemberInvite(BaseModel):
|
|||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["email", "workspace"]
|
||||
verbose_name = "Workspace Member Invite"
|
||||
verbose_name_plural = "Workspace Member Invites"
|
||||
db_table = "workspace_member_invite"
|
||||
db_table = "workspace_member_invites"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -109,7 +106,7 @@ class Team(BaseModel):
|
|||
unique_together = ["name", "workspace"]
|
||||
verbose_name = "Team"
|
||||
verbose_name_plural = "Teams"
|
||||
db_table = "team"
|
||||
db_table = "teams"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
|
@ -130,5 +127,5 @@ class TeamMember(BaseModel):
|
|||
unique_together = ["team", "member"]
|
||||
verbose_name = "Team Member"
|
||||
verbose_name_plural = "Team Members"
|
||||
db_table = "team_member"
|
||||
db_table = "team_members"
|
||||
ordering = ("-created_at",)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from datetime import timedelta
|
|||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
|
@ -36,6 +36,7 @@ INSTALLED_APPS = [
|
|||
"taggit",
|
||||
"fieldsignals",
|
||||
"django_rq",
|
||||
"channels",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -109,6 +110,7 @@ JWT_AUTH = {
|
|||
}
|
||||
|
||||
WSGI_APPLICATION = "plane.wsgi.application"
|
||||
ASGI_APPLICATION = "plane.asgi.application"
|
||||
|
||||
# Django Sites
|
||||
|
||||
|
|
|
|||
|
|
@ -64,4 +64,13 @@ RQ_QUEUES = {
|
|||
},
|
||||
}
|
||||
|
||||
WEB_URL = "http://localhost:3000"
|
||||
WEB_URL = "http://localhost:3000"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(REDIS_HOST, REDIS_PORT)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
"""Production settings and globals."""
|
||||
from plane.settings.local import WEB_URL
|
||||
from .common import * # noqa
|
||||
import ssl
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
from redis.asyncio.connection import Connection, RedisSSLContext
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
# Database
|
||||
DEBUG = True
|
||||
DEBUG = False
|
||||
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'),
|
||||
"USER": os.environ.get("PGUSER"),
|
||||
"PASSWORD": os.environ.get("PGPASSWORD"),
|
||||
"HOST": os.environ.get("PGHOST"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,4 +185,65 @@ RQ_QUEUES = {
|
|||
}
|
||||
}
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
||||
class CustomSSLConnection(Connection):
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.ssl_context = RedisSSLContext(ssl_context)
|
||||
|
||||
|
||||
class RedisSSLContext:
|
||||
__slots__ = ("context",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context,
|
||||
):
|
||||
self.context = ssl_context
|
||||
|
||||
def get(self):
|
||||
return self.context
|
||||
|
||||
|
||||
url = urlparse(os.environ.get("REDIS_URL"))
|
||||
|
||||
DOCKERIZED = os.environ.get("DOCKERIZED", False) # Set the variable true if running in docker-compose environment
|
||||
|
||||
if not DOCKERIZED:
|
||||
|
||||
ssl_context = ssl.SSLContext()
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [
|
||||
{
|
||||
"host": url.hostname,
|
||||
"port": url.port,
|
||||
"username": url.username,
|
||||
"password": url.password,
|
||||
"connection_class": CustomSSLConnection,
|
||||
"ssl_context": ssl_context,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(os.environ.get("REDIS_URL"))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
|
|
|||
|
|
@ -3,18 +3,21 @@ from django.conf import settings
|
|||
from urllib.parse import urlparse
|
||||
|
||||
def redis_instance():
|
||||
if settings.REDIS_URL:
|
||||
url = urlparse(settings.REDIS_URL)
|
||||
ri = redis.Redis(
|
||||
host=url.hostname,
|
||||
port=url.port,
|
||||
password=url.password,
|
||||
ssl=True,
|
||||
ssl_cert_reqs=None,
|
||||
)
|
||||
# Run in local redis url is false
|
||||
if not settings.REDIS_URL:
|
||||
ri = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
|
||||
else:
|
||||
ri = redis.StrictRedis(
|
||||
host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0
|
||||
)
|
||||
|
||||
# Run in prod redis url is true check with dockerized value
|
||||
if settings.DOCKERIZED:
|
||||
ri = redis.from_url(settings.REDIS_URL, db=0)
|
||||
else:
|
||||
url = urlparse(settings.REDIS_URL)
|
||||
ri = redis.Redis(
|
||||
host=url.hostname,
|
||||
port=url.port,
|
||||
password=url.password,
|
||||
ssl=True,
|
||||
ssl_cert_reqs=None,
|
||||
)
|
||||
|
||||
return ri
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
"""Production settings and globals."""
|
||||
from plane.settings.local import WEB_URL
|
||||
from .common import * # noqa
|
||||
import ssl
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
from redis.asyncio.connection import Connection, RedisSSLContext
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
# Database
|
||||
DEBUG = False
|
||||
DATABASES = {
|
||||
|
|
@ -180,4 +185,52 @@ RQ_QUEUES = {
|
|||
}
|
||||
}
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
class CustomSSLConnection(Connection):
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.ssl_context = RedisSSLContext(ssl_context)
|
||||
|
||||
class RedisSSLContext:
|
||||
__slots__ = (
|
||||
"context",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context,
|
||||
):
|
||||
self.context = ssl_context
|
||||
|
||||
def get(self):
|
||||
return self.context
|
||||
|
||||
|
||||
url = urlparse(os.environ.get("REDIS_URL"))
|
||||
|
||||
ssl_context = ssl.SSLContext()
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [
|
||||
{
|
||||
'host': url.hostname,
|
||||
'port': url.port,
|
||||
'username': url.username,
|
||||
'password': url.password,
|
||||
'connection_class': CustomSSLConnection,
|
||||
'ssl_context': ssl_context,
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
|
|
|||
24
apiserver/plane/utils/html_processor.py
Normal file
24
apiserver/plane/utils/html_processor.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from io import StringIO
|
||||
from html.parser import HTMLParser
|
||||
|
||||
class MLStripper(HTMLParser):
|
||||
"""
|
||||
Markup Language Stripper
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.reset()
|
||||
self.strict = False
|
||||
self.convert_charrefs= True
|
||||
self.text = StringIO()
|
||||
|
||||
def handle_data(self, d):
|
||||
self.text.write(d)
|
||||
|
||||
def get_data(self):
|
||||
return self.text.getvalue()
|
||||
|
||||
def strip_tags(html):
|
||||
s = MLStripper()
|
||||
s.feed(html)
|
||||
return s.get_data()
|
||||
Loading…
Add table
Add a link
Reference in a new issue