Skip to content
Merged

Dev #141

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/space/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64

from common.apps.organization_user.models import OrganizationUser
from common.apps.space.models import Space
from common.apps.space_role.models import SpaceRoleUser
from django.db.models import (
Case,
Expand Down Expand Up @@ -66,3 +67,27 @@ def get_spaces_queryset_for_user(queryset, user_id):
default_display=Exists(default_membership),
)
)


def get_users_default_spaces_payload(user_ids):
user_ids = list(user_ids)
default_spaces = {
str(item["created_by"]): item["slug_name"]
for item in Space.objects.filter(
created_by__in=user_ids,
is_default=True,
is_active=True,
).values("created_by", "slug_name")
}

users = [
{
"id": str(user_id),
"slug_name": default_spaces.get(str(user_id)),
}
for user_id in user_ids
]
return {
"total_users": len(user_ids),
"users": users,
}
76 changes: 62 additions & 14 deletions apps/space/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,40 @@
from common.apps.billing.constants import FeatureCode
from common.apps.space.models import Space
from common.celery import constants
from common.celery.tasks import task
from common.celery.tasks import PermanentTaskError, task
from django.db import transaction
from django.db.models import BooleanField, Case, F, IntegerField, Value, When
from django.db.models import BooleanField, Case, Count, F, IntegerField, Value, When
from django.db.models.functions import Greatest
from django.db.utils import ProgrammingError
from django_tenants.utils import schema_context

logger = logging.getLogger(__name__)


def _is_unlimited(kwargs, feature_code):
return feature_code in set(kwargs.get("unlimited_features") or [])


@task(
name="spacedf.tasks.space_downgrade",
name="spacedf.tasks.auth_downgrade",
autoretry_for=(Exception,),
retry_backoff=2,
max_retries=3,
)
def space_downgrade_task(**kwargs):
def auth_downgrade_task(**kwargs):
org_slug = kwargs["org_slug"]
limits = kwargs.get("limits") or {}
max_spaces = limits.get(FeatureCode.SPACE_MAX_COUNT)
if max_spaces is None:
logger.warning(
"Skipping space deactivation for %s: %s not in event",
org_slug,
FeatureCode.SPACE_MAX_COUNT,
raise PermanentTaskError(
"auth downgrade requires limit %s for org %s"
% (FeatureCode.SPACE_MAX_COUNT, org_slug)
)
if max_spaces < 0:
raise PermanentTaskError(
"auth downgrade limit %s must be >= 0 for org %s"
% (FeatureCode.SPACE_MAX_COUNT, org_slug)
)
return 0

downgraded_at = kwargs.get("downgraded_at")

Expand Down Expand Up @@ -72,17 +79,58 @@ def space_downgrade_task(**kwargs):


@task(
name="spacedf.tasks.space_upgrade",
name="spacedf.tasks.auth_upgrade",
autoretry_for=(Exception,),
retry_backoff=2,
max_retries=3,
)
def space_upgrade_task(**kwargs):
def auth_upgrade_task(**kwargs):
org_slug = kwargs["org_slug"]
with schema_context(org_slug):
count = Space.objects.filter(is_deactivated=True).update(
is_deactivated=False, deactivated_at=None
limits = kwargs.get("limits") or {}
max_spaces = limits.get(FeatureCode.SPACE_MAX_COUNT)
unlimited_spaces = _is_unlimited(kwargs, FeatureCode.SPACE_MAX_COUNT)
if max_spaces is None and not unlimited_spaces:
raise PermanentTaskError(
"auth upgrade requires limit or explicit unlimited feature %s for org %s"
% (FeatureCode.SPACE_MAX_COUNT, org_slug)
)
if max_spaces is not None and max_spaces < 0:
raise PermanentTaskError(
"auth upgrade limit %s must be >= 0 for org %s"
% (FeatureCode.SPACE_MAX_COUNT, org_slug)
)

with schema_context(org_slug):
if max_spaces is None:
count = Space.objects.filter(is_deactivated=True).update(
is_deactivated=False, deactivated_at=None
)
else:
active_counts = {
row["created_by"]: row["count"]
for row in Space.objects.filter(is_deactivated=False)
.values("created_by")
.annotate(count=Count("id"))
}
reactivated_ids = []
for space_id, owner_id in (
Space.objects.filter(is_deactivated=True)
.values_list("id", "created_by")
.order_by("created_by", "created_at")
):
owner_active_count = active_counts.get(owner_id, 0)
if owner_active_count >= max_spaces:
continue
reactivated_ids.append(space_id)
active_counts[owner_id] = owner_active_count + 1

count = (
Space.objects.filter(id__in=reactivated_ids).update(
is_deactivated=False, deactivated_at=None
)
if reactivated_ids
else 0
)
if count:
logger.info(
"Renewal: reactivated %s spaces for org %s.",
Expand Down
2 changes: 1 addition & 1 deletion apps/space/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
name="space_check",
),
path(
"spaces/<str:space_slug>/users",
"users",
GetSpaceUsersAPIView.as_view(),
name="get_user_ids",
),
Expand Down
16 changes: 11 additions & 5 deletions apps/space/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@

from apps.space.quotas import SpaceQuota
from apps.space.serializers import InviteUserSerial, SpaceSerializer
from apps.space.services import get_spaces_queryset_for_user
from apps.space.services import (
get_spaces_queryset_for_user,
get_users_default_spaces_payload,
)
from apps.space_role.services import clear_user_permission_cache

console_client = ConsoleServiceClient()
Expand Down Expand Up @@ -271,11 +274,13 @@ class GetSpaceUsersAPIView(UseTenantFromRequestMixin, APIView):
swagger_schema = None

def get(self, request, *args, **kwargs):
space_slug = kwargs.get("space_slug")
space_slug = kwargs.get("space_slug") or request.headers.get("X-Space")
if not space_slug:
user_ids = OrganizationUser.objects.values_list("id", flat=True).distinct()

return Response(
{"error": "Space slug is required"},
status=status.HTTP_400_BAD_REQUEST,
get_users_default_spaces_payload(user_ids),
status=status.HTTP_200_OK,
)

space = get_object_or_404(Space, slug_name=space_slug)
Expand All @@ -287,9 +292,10 @@ def get(self, request, *args, **kwargs):
.distinct()
)

user_ids = list(user_ids)
return Response(
{
"user_ids": list(user_ids),
"user_ids": [str(user_id) for user_id in user_ids],
"total_users": len(user_ids),
},
status=status.HTTP_200_OK,
Expand Down
26 changes: 21 additions & 5 deletions auth_service/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from celery import Celery
from common.celery import constants # noqa
from common.celery.routing import (
append_unique_task_queues,
setup_organization_task_routing,
setup_subscription_task_routing,
setup_synchronous_model_task_routing,
Expand All @@ -27,7 +28,20 @@

setup_organization_task_routing()
setup_synchronous_model_task_routing()
setup_subscription_task_routing(["space_downgrade", "space_upgrade"])
setup_subscription_task_routing(
[
{
"task_name": "auth_downgrade",
"service": "auth",
"lifecycle": "downgrade",
},
{
"task_name": "auth_upgrade",
"service": "auth",
"lifecycle": "upgrade",
},
]
)

app.autodiscover_tasks(settings.CELERY_TASKS)

Expand All @@ -37,20 +51,22 @@
constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
]

existing = {queue.name: queue for queue in (app.conf.task_queues or ())}

routes = dict(app.conf.task_routes or {})
queues = []

for name in TASKS_AUTH:
if name not in existing:
existing[name] = Queue(
queues.append(
Queue(
name,
exchange=Exchange(name, type="direct"),
routing_key=f"spacedf.tasks.{name}",
)
)
routes[f"spacedf.tasks.{name}"] = {
"queue": name,
"routing_key": f"spacedf.tasks.{name}",
}

app.conf.task_queues = tuple(existing.values())
append_unique_task_queues(app, queues)
app.conf.task_routes = routes
Loading