Skip to content

Commit

Permalink
Merge pull request #1470 from jefer94/feat/bypass-consumption
Browse files Browse the repository at this point in the history
bypass consumption
  • Loading branch information
tommygonzaleza authored Oct 29, 2024
2 parents bef6dd5 + ffc4a58 commit 82fb142
Show file tree
Hide file tree
Showing 22 changed files with 2,896 additions and 314 deletions.
1 change: 1 addition & 0 deletions .flags
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BYPASS_CONSUMPTION=0
559 changes: 286 additions & 273 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion breathecode/admissions/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from collections import OrderedDict

from capyc.rest_framework.exceptions import ValidationException
from django.contrib.auth.models import User
from django.db.models import Q

Expand All @@ -9,7 +10,6 @@
from breathecode.assignments.serializers import TaskGETSmallSerializer
from breathecode.authenticate.models import CredentialsGithub, ProfileAcademy
from breathecode.utils import localize_query, serializers, serpy
from capyc.rest_framework.exceptions import ValidationException

from .actions import haversine, test_syllabus
from .models import (
Expand Down
25 changes: 14 additions & 11 deletions breathecode/mentorship/permissions/consumers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import logging

from capyc.core.managers import feature
from capyc.rest_framework.exceptions import PaymentException, ValidationException

from breathecode.admissions.actions import is_no_saas_student_up_to_date_in_any_cohort
from breathecode.authenticate.actions import get_user_language
from breathecode.authenticate.models import User
from breathecode.mentorship.models import MentorProfile, MentorshipService
from breathecode.payments.models import Consumable, ConsumptionSession
from breathecode.utils.decorators import ServiceContext
from breathecode.utils.i18n import translation
from capyc.rest_framework.exceptions import PaymentException, ValidationException

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -111,16 +113,17 @@ def mentorship_service_by_url_param(context: ServiceContext, args: tuple, kwargs
)
)
):

raise ValidationException(
translation(
lang,
en=f'Mentee do not have enough credits to access this service: {context["service"]}',
es="El mentee no tiene suficientes créditos para acceder a este servicio: " f'{context["service"]}',
),
slug="mentee-not-enough-consumables",
code=402,
)
c = feature.context(context=context, kwargs=kwargs, user=mentee)
if feature.is_enabled("payments.bypass_consumption", c, False) is False:
raise ValidationException(
translation(
lang,
en=f'Mentee do not have enough credits to access this service: {context["service"]}',
es="El mentee no tiene suficientes créditos para acceder a este servicio: " f'{context["service"]}',
),
slug="mentee-not-enough-consumables",
code=402,
)

if consumable:
session = ConsumptionSession.build_session(request, consumable, mentorship_service.max_duration, mentee)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import capyc.pytest as capy
import pytest
import timeago
from capyc.core.managers import feature
from django.core.handlers.wsgi import WSGIRequest
from django.template import loader
from django.test.client import FakePayload
Expand Down Expand Up @@ -3241,7 +3242,8 @@ def test_with_mentor_profile__redirect_to_session__no_saas(self):
),
)
@patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW))
def test_with_mentor_profile__redirect_to_session__saas(self):
@patch("capyc.core.managers.feature.is_enabled", MagicMock(return_value=False))
def test_with_mentor_profile__redirect_to_session__saas__bypass_consumption_false(self):
mentor_profile_cases = [
{
"status": x,
Expand Down Expand Up @@ -3322,12 +3324,231 @@ def test_with_mentor_profile__redirect_to_session__saas(self):
)
self.assertEqual(self.bc.database.list_of("payments.Consumable"), [])
self.assertEqual(self.bc.database.list_of("payments.ConsumptionSession"), [])
calls = [
call(
args[0],
{
**args[1],
"context": {
**args[1]["context"],
"request": type(args[1]["context"]["request"]),
"consumer": callable(args[1]["context"]["consumer"]),
"consumables": [x for x in args[1]["context"]["consumables"]],
},
},
*args[2:],
**kwargs,
)
for args, kwargs in feature.is_enabled.call_args_list
]
context1 = feature.context(
context={
"utc_now": UTC_NOW,
"consumer": True,
"service": "join_mentorship",
"request": WSGIRequest,
"consumables": [],
"lifetime": None,
"price": 0,
"is_consumption_session": False,
"flags": {"bypass_consumption": False},
},
)
context2 = feature.context(
context={
"utc_now": UTC_NOW,
"consumer": True,
"service": "join_mentorship",
"request": WSGIRequest,
"consumables": [],
"lifetime": None,
"price": 0,
"is_consumption_session": False,
"flags": {"bypass_consumption": False},
},
user=base.user,
)
assert calls == [
call("payments.bypass_consumption", context1, False),
call("payments.bypass_consumption", context2, False),
]

# teardown
self.bc.database.delete("mentorship.MentorProfile")

self.bc.database.delete("auth.Permission")
self.bc.database.delete("payments.Service")
feature.is_enabled.call_args_list = []

@patch("breathecode.mentorship.actions.mentor_is_ready", MagicMock())
@patch(
"os.getenv",
MagicMock(
side_effect=apply_get_env(
{
"DAILY_API_URL": URL,
"DAILY_API_KEY": API_KEY,
}
)
),
)
@patch(
"requests.request",
apply_requests_request_mock(
[
(
201,
f"{URL}/v1/rooms",
{
"name": ROOM_NAME,
"url": ROOM_URL,
},
)
]
),
)
@patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW))
@patch("capyc.core.managers.feature.is_enabled", MagicMock(side_effect=[False, True, False, True]))
def test_with_mentor_profile__redirect_to_session__saas__bypass_consumption_true(self):
mentor_profile_cases = [
{
"status": x,
"online_meeting_url": self.bc.fake.url(),
"booking_url": self.bc.fake.url(),
}
for x in ["ACTIVE", "UNLISTED"]
]

id = 0
for mentor_profile in mentor_profile_cases:
id += 1

user = {"first_name": "", "last_name": ""}
service = {"consumer": "JOIN_MENTORSHIP"}
base = self.bc.database.create(user=user, token=1, service=service)

ends_at = UTC_NOW - timedelta(seconds=3600 / 2 + 1)

academy = {"available_as_saas": True}
mentorship_session = {
"mentee_id": base.user.id,
"ends_at": ends_at,
"allow_mentee_to_extend": True,
}
token = 1

model = self.bc.database.create(
mentor_profile=mentor_profile,
mentorship_session=mentorship_session,
user=user,
token=token,
mentorship_service={"language": "en", "video_provider": "DAILY"},
service=base.service,
academy=academy,
)

model.mentorship_session.mentee = None
model.mentorship_session.save()

token = model.token if "token" in model else base.token

querystring = self.bc.format.to_querystring(
{
"token": token.key,
"extend": "true",
"mentee": base.user.id,
"session": model.mentorship_session.id,
}
)
url = (
reverse_lazy(
"mentorship_shortner:meet_slug_service_slug",
kwargs={"mentor_slug": model.mentor_profile.slug, "service_slug": model.mentorship_service.slug},
)
+ f"?{querystring}"
)
response = self.client.get(url)

content = self.bc.format.from_bytes(response.content)
expected = ""

# dump error in external files
if content != expected:
with open("content.html", "w") as f:
f.write(content)

with open("expected.html", "w") as f:
f.write(expected)

self.assertEqual(content, expected)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
assert (
response.url
== f"/mentor/session/{model.mentorship_session.id}?token={token.key}&message=You%20have%20a%20session%20that%20expired%2030%20minutes%20ago.%20Only%20sessions%20with%20less%20than%2030min%20from%20expiration%20can%20be%20extended%20(if%20allowed%20by%20the%20academy)"
)
self.assertEqual(
self.bc.database.list_of("mentorship.MentorProfile"),
[
self.bc.format.to_dict(model.mentor_profile),
],
)
self.assertEqual(self.bc.database.list_of("payments.Consumable"), [])
self.assertEqual(self.bc.database.list_of("payments.ConsumptionSession"), [])
calls = [
call(
args[0],
{
**args[1],
"context": {
**args[1]["context"],
"request": type(args[1]["context"]["request"]),
"consumer": callable(args[1]["context"]["consumer"]),
"consumables": [x for x in args[1]["context"]["consumables"]],
},
},
*args[2:],
**kwargs,
)
for args, kwargs in feature.is_enabled.call_args_list
]
context1 = feature.context(
context={
"utc_now": UTC_NOW,
"consumer": True,
"service": "join_mentorship",
"request": WSGIRequest,
"consumables": [],
"lifetime": None,
"price": 0,
"is_consumption_session": False,
"flags": {"bypass_consumption": False},
},
)
context2 = feature.context(
context={
"utc_now": UTC_NOW,
"consumer": True,
"service": "join_mentorship",
"request": WSGIRequest,
"consumables": [],
"lifetime": None,
"price": 0,
"is_consumption_session": False,
"flags": {"bypass_consumption": False},
},
user=base.user,
)
assert calls == [
call("payments.bypass_consumption", context1, False),
call("payments.bypass_consumption", context2, False),
]

# teardown
self.bc.database.delete("mentorship.MentorProfile")

self.bc.database.delete("auth.Permission")
self.bc.database.delete("payments.Service")
feature.is_enabled.call_args_list = []

@patch("breathecode.mentorship.actions.mentor_is_ready", MagicMock())
@patch(
Expand Down
50 changes: 48 additions & 2 deletions breathecode/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import brotli
import zstandard
from asgiref.sync import iscoroutinefunction
from django.http import HttpResponseRedirect
from asgiref.sync import iscoroutinefunction, sync_to_async
from django.http import HttpRequest, HttpResponseRedirect
from django.utils.decorators import sync_and_async_middleware
from django.utils.deprecation import MiddlewareMixin

Expand Down Expand Up @@ -148,3 +148,49 @@ def middleware(request):
return response

return middleware


NO_PAGINATED = set()


@sync_and_async_middleware
def detect_pagination_issues_middleware(get_response):
from breathecode.monitoring.models import NoPagination

def instrument_no_pagination(request: HttpRequest) -> None:
if request.method not in ["GET"]:
return

path = request.path
method = request.method

if (path, method) in NO_PAGINATED:
return

is_paginated = request.GET.get("limit") and request.GET.get("offset")

if is_paginated is False and NoPagination.objects.filter(path=path, method=method).exists() is False:
NO_PAGINATED.add((path, method))
NoPagination.objects.create(path=path, method=method)

@sync_to_async
def ainstrument_no_pagination(request: HttpRequest) -> None:
instrument_no_pagination(request)

if iscoroutinefunction(get_response):

async def middleware(request: HttpRequest):
await ainstrument_no_pagination(request)

response = await get_response(request)
return response

else:

def middleware(request: HttpRequest):
instrument_no_pagination(request)

response = get_response(request)
return response

return middleware
13 changes: 13 additions & 0 deletions breathecode/monitoring/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
CSVUpload,
Endpoint,
MonitorScript,
NoPagination,
RepositorySubscription,
RepositoryWebhook,
Supervisor,
Expand Down Expand Up @@ -315,3 +316,15 @@ class SupervisorIssueAdmin(admin.ModelAdmin):
list_filter = ["supervisor"]
search_fields = ["supervisor__task_module", "supervisor__task_name"]
actions = []


def delete_all(modeladmin, request, queryset):
NoPagination.objects.all().delete()


@admin.register(NoPagination)
class NoPaginationAdmin(admin.ModelAdmin):
list_display = ("path", "method")
list_filter = ["method"]
search_fields = ["path", "method"]
actions = [delete_all]
Loading

0 comments on commit 82fb142

Please sign in to comment.