forked from github/plane
refactor: cycle views endpoint (#1128)
This commit is contained in:
parent
a16514ed11
commit
e608b58e70
@ -96,12 +96,8 @@ from plane.api.views import (
|
|||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CurrentUpcomingCyclesEndpoint,
|
|
||||||
CompletedCyclesEndpoint,
|
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
DraftCyclesEndpoint,
|
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
InCompleteCyclesEndpoint,
|
|
||||||
## End Cycles
|
## End Cycles
|
||||||
# Modules
|
# Modules
|
||||||
ModuleViewSet,
|
ModuleViewSet,
|
||||||
@ -664,21 +660,6 @@ urlpatterns = [
|
|||||||
CycleDateCheckEndpoint.as_view(),
|
CycleDateCheckEndpoint.as_view(),
|
||||||
name="project-cycle",
|
name="project-cycle",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/current-upcoming-cycles/",
|
|
||||||
CurrentUpcomingCyclesEndpoint.as_view(),
|
|
||||||
name="project-cycle-upcoming",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/completed-cycles/",
|
|
||||||
CompletedCyclesEndpoint.as_view(),
|
|
||||||
name="project-cycle-completed",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/draft-cycles/",
|
|
||||||
DraftCyclesEndpoint.as_view(),
|
|
||||||
name="project-cycle-draft",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
|
||||||
CycleFavoriteViewSet.as_view(
|
CycleFavoriteViewSet.as_view(
|
||||||
@ -703,11 +684,6 @@ urlpatterns = [
|
|||||||
TransferCycleIssueEndpoint.as_view(),
|
TransferCycleIssueEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/incomplete-cycles/",
|
|
||||||
InCompleteCyclesEndpoint.as_view(),
|
|
||||||
name="transfer-issues",
|
|
||||||
),
|
|
||||||
## End Cycles
|
## End Cycles
|
||||||
# Issue
|
# Issue
|
||||||
path(
|
path(
|
||||||
|
@ -49,12 +49,8 @@ from .cycle import (
|
|||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CurrentUpcomingCyclesEndpoint,
|
|
||||||
CompletedCyclesEndpoint,
|
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
DraftCyclesEndpoint,
|
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
InCompleteCyclesEndpoint,
|
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint
|
from .asset import FileAssetEndpoint, UserAssetsEndpoint
|
||||||
from .issue import (
|
from .issue import (
|
||||||
|
@ -152,6 +152,75 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
cycle_view = request.GET.get("cycle_view", False)
|
||||||
|
if not cycle_view:
|
||||||
|
return Response(
|
||||||
|
{"error": "Cycle View parameter is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# All Cycles
|
||||||
|
if cycle_view == "all":
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current Cycle
|
||||||
|
if cycle_view == "current":
|
||||||
|
queryset = queryset.filter(
|
||||||
|
start_date__lte=timezone.now(),
|
||||||
|
end_date__gte=timezone.now(),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upcoming Cycles
|
||||||
|
if cycle_view == "upcoming":
|
||||||
|
queryset = queryset.filter(start_date__gt=timezone.now())
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Completed Cycles
|
||||||
|
if cycle_view == "completed":
|
||||||
|
queryset = queryset.filter(end_date__lt=timezone.now())
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draft Cycles
|
||||||
|
if cycle_view == "draft":
|
||||||
|
queryset = queryset.filter(
|
||||||
|
end_date=None,
|
||||||
|
start_date=None,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Incomplete Cycles
|
||||||
|
if cycle_view == "incomplete":
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
if (
|
if (
|
||||||
@ -478,352 +547,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CurrentUpcomingCyclesEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
subquery = CycleFavorite.objects.filter(
|
|
||||||
user=self.request.user,
|
|
||||||
cycle_id=OuterRef("pk"),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
)
|
|
||||||
current_cycle = (
|
|
||||||
Cycle.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
start_date__lte=timezone.now(),
|
|
||||||
end_date__gte=timezone.now(),
|
|
||||||
)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("owned_by")
|
|
||||||
.annotate(is_favorite=Exists(subquery))
|
|
||||||
.annotate(total_issues=Count("issue_cycle"))
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
cancelled_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
unstarted_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
backlog_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_cycle__issue__assignees",
|
|
||||||
queryset=User.objects.only(
|
|
||||||
"avatar", "first_name", "id"
|
|
||||||
).distinct(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("name", "-is_favorite")
|
|
||||||
)
|
|
||||||
|
|
||||||
upcoming_cycle = (
|
|
||||||
Cycle.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
start_date__gt=timezone.now(),
|
|
||||||
)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("owned_by")
|
|
||||||
.annotate(is_favorite=Exists(subquery))
|
|
||||||
.annotate(total_issues=Count("issue_cycle"))
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
cancelled_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
unstarted_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
backlog_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_cycle__issue__assignees",
|
|
||||||
queryset=User.objects.only(
|
|
||||||
"avatar", "first_name", "id"
|
|
||||||
).distinct(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("name", "-is_favorite")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"current_cycle": CycleSerializer(current_cycle, many=True).data,
|
|
||||||
"upcoming_cycle": CycleSerializer(upcoming_cycle, many=True).data,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CompletedCyclesEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
subquery = CycleFavorite.objects.filter(
|
|
||||||
user=self.request.user,
|
|
||||||
cycle_id=OuterRef("pk"),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
)
|
|
||||||
completed_cycles = (
|
|
||||||
Cycle.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
end_date__lt=timezone.now(),
|
|
||||||
)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("owned_by")
|
|
||||||
.annotate(is_favorite=Exists(subquery))
|
|
||||||
.annotate(total_issues=Count("issue_cycle"))
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
cancelled_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
unstarted_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
backlog_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_cycle__issue__assignees",
|
|
||||||
queryset=User.objects.only(
|
|
||||||
"avatar", "first_name", "id"
|
|
||||||
).distinct(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("name", "-is_favorite")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"completed_cycles": CycleSerializer(
|
|
||||||
completed_cycles, many=True
|
|
||||||
).data,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DraftCyclesEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
subquery = CycleFavorite.objects.filter(
|
|
||||||
user=self.request.user,
|
|
||||||
cycle_id=OuterRef("pk"),
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
)
|
|
||||||
draft_cycles = (
|
|
||||||
Cycle.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
end_date=None,
|
|
||||||
start_date=None,
|
|
||||||
)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("owned_by")
|
|
||||||
.annotate(is_favorite=Exists(subquery))
|
|
||||||
.annotate(total_issues=Count("issue_cycle"))
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
cancelled_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
unstarted_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
backlog_issues=Count(
|
|
||||||
"issue_cycle__issue__state__group",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_cycle__issue__assignees",
|
|
||||||
queryset=User.objects.only(
|
|
||||||
"avatar", "first_name", "id"
|
|
||||||
).distinct(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("name", "-is_favorite")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{"draft_cycles": CycleSerializer(draft_cycles, many=True).data},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CycleFavoriteViewSet(BaseViewSet):
|
class CycleFavoriteViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -948,22 +671,3 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InCompleteCyclesEndpoint(BaseAPIView):
|
|
||||||
def get(self, request, slug, project_id):
|
|
||||||
try:
|
|
||||||
cycles = Cycle.objects.filter(
|
|
||||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
).select_related("owned_by")
|
|
||||||
|
|
||||||
serializer = CycleSerializer(cycles, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
capture_exception(e)
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong please try again later"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user