mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: workspace export endpoint
This commit is contained in:
parent
82ba9833f2
commit
60232130f4
@ -1,33 +1,32 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
UserWorkspaceInvitationsViewSet,
|
ExportWorkspaceEndpoint,
|
||||||
WorkSpaceViewSet,
|
ExportWorkspaceUserActivityEndpoint,
|
||||||
WorkspaceJoinEndpoint,
|
|
||||||
WorkSpaceMemberViewSet,
|
|
||||||
WorkspaceInvitationsViewset,
|
|
||||||
WorkspaceMemberUserEndpoint,
|
|
||||||
WorkspaceMemberUserViewsEndpoint,
|
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
|
||||||
TeamMemberViewSet,
|
TeamMemberViewSet,
|
||||||
UserLastProjectWithWorkspaceEndpoint,
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
|
UserWorkspaceInvitationsViewSet,
|
||||||
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
|
WorkspaceCyclesEndpoint,
|
||||||
|
WorkspaceEstimatesEndpoint,
|
||||||
|
WorkspaceInvitationsViewset,
|
||||||
|
WorkspaceJoinEndpoint,
|
||||||
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceMemberUserEndpoint,
|
||||||
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
|
WorkSpaceMemberViewSet,
|
||||||
|
WorkspaceModulesEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceStatesEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
WorkspaceUserProfileStatsEndpoint,
|
|
||||||
WorkspaceUserActivityEndpoint,
|
WorkspaceUserActivityEndpoint,
|
||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceUserProfileStatsEndpoint,
|
||||||
WorkspaceProjectMemberEndpoint,
|
|
||||||
WorkspaceUserPropertiesEndpoint,
|
WorkspaceUserPropertiesEndpoint,
|
||||||
WorkspaceStatesEndpoint,
|
WorkSpaceViewSet,
|
||||||
WorkspaceEstimatesEndpoint,
|
|
||||||
ExportWorkspaceUserActivityEndpoint,
|
|
||||||
WorkspaceModulesEndpoint,
|
|
||||||
WorkspaceCyclesEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspace-slug-check/",
|
"workspace-slug-check/",
|
||||||
@ -237,4 +236,9 @@ urlpatterns = [
|
|||||||
WorkspaceCyclesEndpoint.as_view(),
|
WorkspaceCyclesEndpoint.as_view(),
|
||||||
name="workspace-cycles",
|
name="workspace-cycles",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/export/",
|
||||||
|
ExportWorkspaceEndpoint.as_view(),
|
||||||
|
name="workspace-exports",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -37,7 +37,8 @@ from .workspace.base import (
|
|||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
ExportWorkspaceUserActivityEndpoint
|
ExportWorkspaceUserActivityEndpoint,
|
||||||
|
ExportWorkspaceEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .workspace.member import (
|
from .workspace.member import (
|
||||||
|
@ -1,49 +1,52 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from datetime import date
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import (
|
||||||
|
Count,
|
||||||
|
F,
|
||||||
|
Func,
|
||||||
|
OuterRef,
|
||||||
|
Prefetch,
|
||||||
|
Q,
|
||||||
|
)
|
||||||
|
from django.db.models.fields import DateField
|
||||||
|
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import (
|
|
||||||
Prefetch,
|
|
||||||
OuterRef,
|
|
||||||
Func,
|
|
||||||
F,
|
|
||||||
Q,
|
|
||||||
Count,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
|
||||||
from django.db.models.fields import DateField
|
|
||||||
|
|
||||||
# Third party modules
|
# Third party modules
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from plane.app.permissions import (
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
WorkSpaceBasePermission,
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
)
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
WorkSpaceSerializer,
|
WorkSpaceSerializer,
|
||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||||
|
from plane.bgtasks.workspace_export_task import workspace_export
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Workspace,
|
|
||||||
IssueActivity,
|
|
||||||
Issue,
|
Issue,
|
||||||
WorkspaceTheme,
|
IssueActivity,
|
||||||
|
Workspace,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
)
|
WorkspaceTheme,
|
||||||
from plane.app.permissions import (
|
|
||||||
WorkSpaceBasePermission,
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
)
|
)
|
||||||
from plane.utils.cache import cache_response, invalidate_cache
|
from plane.utils.cache import cache_response, invalidate_cache
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceViewSet(BaseViewSet):
|
class WorkSpaceViewSet(BaseViewSet):
|
||||||
model = Workspace
|
model = Workspace
|
||||||
serializer_class = WorkSpaceSerializer
|
serializer_class = WorkSpaceSerializer
|
||||||
@ -138,6 +141,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
{"slug": "The workspace with the slug already exists"},
|
{"slug": "The workspace with the slug already exists"},
|
||||||
status=status.HTTP_410_GONE,
|
status=status.HTTP_410_GONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@cache_response(60 * 60 * 2)
|
@cache_response(60 * 60 * 2)
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
@ -412,3 +416,28 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
|||||||
'attachment; filename="workspace-user-activity.csv"'
|
'attachment; filename="workspace-user-activity.csv"'
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportWorkspaceEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug):
|
||||||
|
current_origin = (
|
||||||
|
request.META.get("HTTP_ORIGIN")
|
||||||
|
or f"{request.scheme}://{request.get_host()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_export.delay(
|
||||||
|
slug=slug,
|
||||||
|
origin=current_origin,
|
||||||
|
email=request.user.email,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "An email will be sent to download the exports when they are ready"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
311
apiserver/plane/bgtasks/workspace_export_task.py
Normal file
311
apiserver/plane/bgtasks/workspace_export_task.py
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
# Python imports
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import (
|
||||||
|
APIToken,
|
||||||
|
CommentReaction,
|
||||||
|
Cycle,
|
||||||
|
CycleFavorite,
|
||||||
|
CycleIssue,
|
||||||
|
CycleUserProperties,
|
||||||
|
Estimate,
|
||||||
|
EstimatePoint,
|
||||||
|
FileAsset,
|
||||||
|
Inbox,
|
||||||
|
InboxIssue,
|
||||||
|
Issue,
|
||||||
|
IssueActivity,
|
||||||
|
IssueAssignee,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueComment,
|
||||||
|
IssueLabel,
|
||||||
|
IssueLink,
|
||||||
|
IssueMention,
|
||||||
|
IssueProperty,
|
||||||
|
IssueReaction,
|
||||||
|
IssueRelation,
|
||||||
|
IssueSequence,
|
||||||
|
IssueSubscriber,
|
||||||
|
IssueView,
|
||||||
|
IssueViewFavorite,
|
||||||
|
IssueVote,
|
||||||
|
Label,
|
||||||
|
Module,
|
||||||
|
ModuleFavorite,
|
||||||
|
ModuleIssue,
|
||||||
|
ModuleLink,
|
||||||
|
ModuleMember,
|
||||||
|
ModuleUserProperties,
|
||||||
|
Notification,
|
||||||
|
Page,
|
||||||
|
PageFavorite,
|
||||||
|
PageLabel,
|
||||||
|
PageLog,
|
||||||
|
Project,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
ProjectFavorite,
|
||||||
|
ProjectIdentifier,
|
||||||
|
ProjectMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectPublicMember,
|
||||||
|
State,
|
||||||
|
User,
|
||||||
|
UserNotificationPreference,
|
||||||
|
Webhook,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceMember,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceTheme,
|
||||||
|
WorkspaceUserProperties,
|
||||||
|
)
|
||||||
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
|
from plane.utils.exception_logger import log_exception
|
||||||
|
|
||||||
|
|
||||||
|
def create_zip_file(files):
|
||||||
|
# Create zip
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for file in files:
|
||||||
|
filename = file.get("filename")
|
||||||
|
file_content = file.get("data")
|
||||||
|
zipf.writestr(filename, file_content)
|
||||||
|
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
return zip_buffer
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_s3(zip_file, workspace_id, slug):
|
||||||
|
# Upload the zip to s3
|
||||||
|
file_name = f"{workspace_id}/export-{slug}-{timezone.now()}.zip"
|
||||||
|
expires_in = 7 * 24 * 60 * 60
|
||||||
|
|
||||||
|
if settings.USE_MINIO:
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
s3.upload_fileobj(
|
||||||
|
zip_file,
|
||||||
|
settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
file_name,
|
||||||
|
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||||
|
)
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={
|
||||||
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
"Key": file_name,
|
||||||
|
},
|
||||||
|
ExpiresIn=expires_in,
|
||||||
|
)
|
||||||
|
# Create the new url with updated domain and protocol
|
||||||
|
presigned_url = presigned_url.replace(
|
||||||
|
f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/",
|
||||||
|
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=settings.AWS_REGION,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
s3.upload_fileobj(
|
||||||
|
zip_file,
|
||||||
|
settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
file_name,
|
||||||
|
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||||
|
)
|
||||||
|
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={
|
||||||
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
"Key": file_name,
|
||||||
|
},
|
||||||
|
ExpiresIn=expires_in,
|
||||||
|
)
|
||||||
|
|
||||||
|
return presigned_url
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def workspace_export(slug, origin, email):
|
||||||
|
# Get the workspace
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
workspace_id = workspace.id
|
||||||
|
# Store all files
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# Users that need to be exported
|
||||||
|
emails = WorkspaceMember.objects.filter(workspace__slug=slug).values_list(
|
||||||
|
"member__email", flat=True
|
||||||
|
)
|
||||||
|
users = User.objects.filter(email__in=emails).values()
|
||||||
|
|
||||||
|
users_json = json.dumps(list(users), cls=DjangoJSONEncoder)
|
||||||
|
files.append({"filename": "users.json", "data": users_json})
|
||||||
|
|
||||||
|
workspace = list(Workspace.objects.filter(slug=slug).values())
|
||||||
|
workspace_json = json.dumps(workspace, cls=DjangoJSONEncoder)
|
||||||
|
files.append({"filename": "workspaces.json", "data": workspace_json})
|
||||||
|
|
||||||
|
models = {
|
||||||
|
# Workspace
|
||||||
|
WorkspaceMemberInvite: "workspace_member_invites.json",
|
||||||
|
WorkspaceMember: "workspace_members.json",
|
||||||
|
WorkspaceTheme: "workspace_themes.json",
|
||||||
|
WorkspaceUserProperties: "workspace_user_properties.json",
|
||||||
|
# Projects
|
||||||
|
Project: "projects.json",
|
||||||
|
ProjectDeployBoard: "project_deploy_boards.json",
|
||||||
|
ProjectFavorite: "project_favorites.json",
|
||||||
|
ProjectIdentifier: "project_identifier.json",
|
||||||
|
ProjectMember: "project_members.json",
|
||||||
|
ProjectMemberInvite: "project_member_invites.json",
|
||||||
|
ProjectPublicMember: "project_public_members.json",
|
||||||
|
# APIToken
|
||||||
|
APIToken: "api_tokens.json",
|
||||||
|
# Assets
|
||||||
|
FileAsset: "file_assets.json",
|
||||||
|
# States
|
||||||
|
State: "states.json",
|
||||||
|
# Issues
|
||||||
|
Issue: "issues.json",
|
||||||
|
IssueAssignee: "issue_assignees.json",
|
||||||
|
Label: "labels.json",
|
||||||
|
IssueLabel: "issue_labels.json",
|
||||||
|
IssueLink: "issue_links.json",
|
||||||
|
IssueMention: "issue_mention.json",
|
||||||
|
IssueVote: "issue_votes.json",
|
||||||
|
IssueSubscriber: "issue_subscribers.json",
|
||||||
|
IssueProperty: "issue_properties.json",
|
||||||
|
IssueSequence: "issue_sequences.json",
|
||||||
|
IssueReaction: "issue_reactions.json",
|
||||||
|
IssueRelation: "issue_relations.json",
|
||||||
|
IssueAttachment: "issue_attachments.json",
|
||||||
|
IssueActivity: "issue_activities.json",
|
||||||
|
CommentReaction: "comment_reactions.json",
|
||||||
|
IssueComment: "issue_comments.json",
|
||||||
|
# Cycles
|
||||||
|
Cycle: "cycles.json",
|
||||||
|
CycleIssue: "cycle_issues.json",
|
||||||
|
CycleFavorite: "cycle_favorites.json",
|
||||||
|
CycleUserProperties: "cycle_user_properties.json",
|
||||||
|
# Modules
|
||||||
|
Module: "modules.json",
|
||||||
|
ModuleIssue: "module_issues.json",
|
||||||
|
ModuleFavorite: "module_favorites.json",
|
||||||
|
ModuleLink: "module_links.json",
|
||||||
|
ModuleMember: "module_members.json",
|
||||||
|
ModuleUserProperties: "module_user_properties",
|
||||||
|
# Page
|
||||||
|
Page: "pages.json",
|
||||||
|
PageLog: "page_logs.json",
|
||||||
|
PageLabel: "page_labels.json",
|
||||||
|
PageFavorite: "page_favorites.json",
|
||||||
|
# Estimate
|
||||||
|
Estimate: "estimates.json",
|
||||||
|
EstimatePoint: "estimate_points.json",
|
||||||
|
# Webhook
|
||||||
|
Webhook: "webhooks.json",
|
||||||
|
# Views
|
||||||
|
IssueView: "views.json",
|
||||||
|
IssueViewFavorite: "view_favorites.json",
|
||||||
|
# Notification
|
||||||
|
Notification: "notifications.json",
|
||||||
|
UserNotificationPreference: "user_notification_preferences.json",
|
||||||
|
# Inbox
|
||||||
|
Inbox: "inboxes.json",
|
||||||
|
InboxIssue: "inbox_issues.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Loop through the models
|
||||||
|
for model in models:
|
||||||
|
file_name = models[model]
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"filename": file_name,
|
||||||
|
"data": json.dumps(
|
||||||
|
list(
|
||||||
|
model.objects.filter(
|
||||||
|
workspace_id=workspace_id
|
||||||
|
).values()
|
||||||
|
),
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create zip
|
||||||
|
zip_buffer = create_zip_file(files)
|
||||||
|
|
||||||
|
# Get the presigned url
|
||||||
|
url = upload_to_s3(
|
||||||
|
workspace_id=workspace_id, slug=slug, zip_file=zip_buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send mail
|
||||||
|
try:
|
||||||
|
(
|
||||||
|
EMAIL_HOST,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
EMAIL_PORT,
|
||||||
|
EMAIL_USE_TLS,
|
||||||
|
EMAIL_FROM,
|
||||||
|
) = get_email_configuration()
|
||||||
|
|
||||||
|
# Send the mail
|
||||||
|
subject = "Your Plane Export Link"
|
||||||
|
context = {"url": url, "email": email}
|
||||||
|
|
||||||
|
html_content = render_to_string(
|
||||||
|
"emails/exports/workspace_exports.html", context
|
||||||
|
)
|
||||||
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
|
connection = get_connection(
|
||||||
|
host=EMAIL_HOST,
|
||||||
|
port=int(EMAIL_PORT),
|
||||||
|
username=EMAIL_HOST_USER,
|
||||||
|
password=EMAIL_HOST_PASSWORD,
|
||||||
|
use_tls=EMAIL_USE_TLS == "1",
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject=subject,
|
||||||
|
body=text_content,
|
||||||
|
from_email=EMAIL_FROM,
|
||||||
|
to=[email],
|
||||||
|
connection=connection,
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
logging.getLogger("plane").info("Email sent successfully.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
log_exception(e)
|
||||||
|
return
|
@ -1,6 +1,6 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import ProjectBaseModel
|
from . import ProjectBaseModel
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseModel
|
from . import BaseModel
|
||||||
|
|
||||||
|
|
||||||
ROLE_CHOICES = (
|
ROLE_CHOICES = (
|
||||||
(20, "Owner"),
|
(20, "Owner"),
|
||||||
(15, "Admin"),
|
(15, "Admin"),
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html>
|
||||||
|
Hey there,<br />
|
||||||
|
Your requested url {{ url }}
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user