Skip to content

Commit

Permalink
Merge branch 'development' of https://github.com/breatheco-de/apiv2 i…
Browse files Browse the repository at this point in the history
…nto feat/provisioning-multiplier
  • Loading branch information
jefer94 committed May 28, 2024
2 parents 6c5432d + 1804393 commit d4f04ad
Show file tree
Hide file tree
Showing 21 changed files with 566 additions and 350 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,5 @@ dump.rdb
*:Zone.Identifier

node_modules/
.venv*
.env*
2 changes: 1 addition & 1 deletion .gitpod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RUN sudo apt-get update \
# RUN curl https://pyenv.run | bash


# RUN pyenv update && pyenv install 3.12.3 && pyenv global 3.12.3
# RUN pyenv update && pyenv install 3.12.2 && pyenv global 3.12.2
RUN pyenv install 3.12.2 && pyenv global 3.12.2
RUN pip install pipenv yapf

Expand Down
617 changes: 318 additions & 299 deletions Pipfile.lock

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions breathecode/assignments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
logger = logging.getLogger(__name__)


class ProfileSmallSerializer(serpy.Serializer):
avatar_url = serpy.Field()


class UserMediumSerializer(serpy.Serializer):
id = serpy.Field()
first_name = serpy.Field()
last_name = serpy.Field()
profile = ProfileSmallSerializer(required=False)


class UserSmallSerializer(serpy.Serializer):
id = serpy.Field()
first_name = serpy.Field()
Expand Down Expand Up @@ -227,8 +238,8 @@ def validate(self, data):
return data

def update(self, instance, validated_data):
if 'opened_at' in validated_data and validated_data['opened_at'] is not None and (instance.opened_at is None
or validated_data['opened_at'] > instance.opened_at):
if 'opened_at' in validated_data and validated_data['opened_at'] is not None and (
instance.opened_at is None or validated_data['opened_at'] > instance.opened_at):
tasks_activity.add_activity.delay(self.context['request'].user.id,
'read_assignment',
related_type='assignments.Task',
Expand Down Expand Up @@ -277,7 +288,7 @@ class FinalProjectGETSerializer(serpy.Serializer):
members = serpy.MethodField()

def get_members(self, obj):
return [UserSmallSerializer(m).data for m in obj.members.all()]
return [UserMediumSerializer(m).data for m in obj.members.all()]


class PostFinalProjectSerializer(serializers.ModelSerializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def members_serializer(member, data={}):
'id': member.id,
'first_name': member.first_name,
'last_name': member.last_name,
'profile': {
'avatar_url': member.profile.avatar_url
} if member.profile is not None else None
}


Expand Down
3 changes: 2 additions & 1 deletion breathecode/marketing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from breathecode.services.activecampaign import ActiveCampaign
from breathecode.utils import GenerateLookupsMixin, HeaderLimitOffsetPagination, capable_of, localize_query
from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions
from breathecode.utils.decorators import validate_captcha
from breathecode.utils.decorators import validate_captcha, validate_captcha_challenge
from breathecode.utils.find_by_full_name import query_like_by_full_name
from breathecode.utils.i18n import translation
from capyc.rest_framework.exceptions import ValidationException
Expand Down Expand Up @@ -128,6 +128,7 @@ def get_alias(request):

@api_view(['POST'])
@permission_classes([AllowAny])
@validate_captcha_challenge
def create_lead(request):
data = request.data.copy()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,17 @@ def setup():
def db(data={}):
return {
'delta': timedelta(seconds=3600),
'id': 0,
'ran_at': None,
'task_module': '',
'task_name': '',
**data,
}


def remove_ids(dbs):
return [x for x in dbs if x.pop('id')]


class TestIssue:

@pytest.mark.parametrize('with_supervisor, with_issues', [
Expand Down Expand Up @@ -131,10 +134,9 @@ def tests_older_issues_are_removed(self, database: dfx.Database, supervisor: Sup
} in supervisor.list()
assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == []
assert db({
'id': 1,
'task_module': 'breathecode.payments.supervisors',
'task_name': 'supervise_all_consumption_sessions',
}) in database.list_of('monitoring.Supervisor')
}) in remove_ids(database.list_of('monitoring.Supervisor'))
assert database.list_of('monitoring.SupervisorIssue') == []
assert call(supervisor.id('breathecode.payments.supervisors',
'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list
Expand Down Expand Up @@ -166,10 +168,9 @@ def tests_recent_issues_keeps__available_attempts(self, bc: Breathecode, databas
assert supervisor.log('breathecode.payments.supervisors',
'supervise_all_consumption_sessions') == [x.error for x in model.supervisor_issue]
assert db({
'id': 1,
'task_module': 'breathecode.payments.supervisors',
'task_name': 'supervise_all_consumption_sessions',
}) in database.list_of('monitoring.Supervisor')
}) in remove_ids(database.list_of('monitoring.Supervisor'))
assert database.list_of('monitoring.SupervisorIssue') == bc.format.to_dict(model.supervisor_issue)
assert call(supervisor.id('breathecode.payments.supervisors',
'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list
Expand Down Expand Up @@ -201,10 +202,9 @@ def tests_recent_issues_keeps__no_available_attempts(self, bc: Breathecode, data
assert supervisor.log('breathecode.payments.supervisors',
'supervise_all_consumption_sessions') == [x.error for x in model.supervisor_issue]
assert db({
'id': 1,
'task_module': 'breathecode.payments.supervisors',
'task_name': 'supervise_all_consumption_sessions',
}) in database.list_of('monitoring.Supervisor')
}) in remove_ids(database.list_of('monitoring.Supervisor'))
assert database.list_of('monitoring.SupervisorIssue') == bc.format.to_dict(model.supervisor_issue)
assert call(supervisor.id('breathecode.payments.supervisors',
'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list
Expand Down Expand Up @@ -245,12 +245,11 @@ def tests_pending_to_be_scheduled(self, database: dfx.Database, supervisor: Supe
} in supervisor.list()
assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == []
assert db({
'id': 1,
'delta': delta,
'ran_at': ran_at,
'task_module': 'breathecode.payments.supervisors',
'task_name': 'supervise_all_consumption_sessions',
}) in database.list_of('monitoring.Supervisor')
}) in remove_ids(database.list_of('monitoring.Supervisor'))
assert database.list_of('monitoring.SupervisorIssue') == []
assert call(supervisor.id('breathecode.payments.supervisors',
'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list
Expand Down Expand Up @@ -283,12 +282,11 @@ def tests_in_cooldown(self, database: dfx.Database, supervisor: Supervisor, patc
} in supervisor.list()
assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == []
assert db({
'id': 1,
'delta': delta,
'ran_at': ran_at,
'task_module': 'breathecode.payments.supervisors',
'task_name': 'supervise_all_consumption_sessions',
}) in database.list_of('monitoring.Supervisor')
}) in remove_ids(database.list_of('monitoring.Supervisor'))
assert database.list_of('monitoring.SupervisorIssue') == []
assert call(supervisor.id('breathecode.payments.supervisors',
'supervise_all_consumption_sessions')) not in run_supervisor_mock.call_args_list
Expand Down
27 changes: 27 additions & 0 deletions breathecode/notify/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from breathecode.mentorship.models import MentorshipSession
from breathecode.mentorship.serializers import SessionHookSerializer
from breathecode.mentorship.signals import mentorship_session_status
from breathecode.payments.models import PlanFinancing, Subscription
from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer
from breathecode.payments.signals import planfinancing_created, subscription_created
from breathecode.registry.models import Asset
from breathecode.registry.serializers import AssetHookSerializer
from breathecode.registry.signals import asset_status_updated
Expand Down Expand Up @@ -159,3 +162,27 @@ def edu_status_updated(sender, instance, **kwargs):
'edu_status_updated',
payload_override=serializer.data,
academy_override=academy)


@receiver(planfinancing_created, sender=PlanFinancing)
def new_planfinancing_created(sender, instance, **kwargs):
logger.debug('Sending new PlanFinancing to hook')
model_label = get_model_label(instance)
serializer = GetPlanFinancingSerializer(instance)
HookManager.process_model_event(instance,
model_label,
'planfinancing_created',
payload_override=serializer.data,
academy_override=instance.academy)


@receiver(subscription_created, sender=Subscription)
def new_subscription_created(sender, instance, **kwargs):
logger.debug('Sending new Subscription to hook')
model_label = get_model_label(instance)
serializer = GetSubscriptionSerializer(instance)
HookManager.process_model_event(instance,
model_label,
'subscription_created',
payload_override=serializer.data,
academy_override=instance.academy)
14 changes: 12 additions & 2 deletions breathecode/payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,12 @@ def clean(self) -> None:

def save(self, *args, **kwargs) -> None:
self.full_clean()
return super().save(*args, **kwargs)
on_create = self.pk is None

super().save(*args, **kwargs)

if on_create:
signals.planfinancing_created.send(instance=self, sender=self.__class__)


class Subscription(AbstractIOweYou):
Expand Down Expand Up @@ -1141,7 +1146,12 @@ def clean(self) -> None:

def save(self, *args, **kwargs) -> None:
self.full_clean()
return super().save(*args, **kwargs)
on_create = self.pk is None

super().save(*args, **kwargs)

if on_create:
signals.subscription_created.send(instance=self, sender=self.__class__)


class SubscriptionServiceItem(models.Model):
Expand Down
4 changes: 4 additions & 0 deletions breathecode/payments/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@

# proxy to m2m_changed in Event.service_items
update_plan_m2m_service_items = Signal()

# Plan aquired
planfinancing_created = Signal()
subscription_created = Signal()
45 changes: 45 additions & 0 deletions breathecode/services/google_cloud/recaptcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,48 @@ def create_assessment(self, project_id: str, recaptcha_site_key: str, token: str
assessment_name = client.parse_assessment_path(response.name).get('assessment')
logger.info(f'Assessment name: {assessment_name}')
return response

def create_assessment_v2(self, project_id: str, recaptcha_site_key: str, token: str) -> Assessment:
"""Create an assessment to analyze the risk of a UI action.
Args:
project_id: GCloud Project ID
recaptcha_site_key: Site key obtained by registering a domain/app to use recaptcha services.
token: The token obtained from the client on passing the recaptchaSiteKey.
"""
client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient()

# Set the properties of the event to be tracked.
event = recaptchaenterprise_v1.Event()
event.site_key = recaptcha_site_key
event.token = token

assessment = recaptchaenterprise_v1.Assessment()
assessment.event = event

project_name = f'projects/{project_id}'

# Build the assessment request.
request = recaptchaenterprise_v1.CreateAssessmentRequest()
request.assessment = assessment
request.parent = project_name

response = client.create_assessment(request)

# Check if the token is valid.
if not response.token_properties.valid:
from breathecode.utils.validation_exception import ValidationException
logger.error('The CreateAssessment call failed because the token was ' +
'invalid for for the following reasons: ' + str(response.token_properties.invalid_reason))
raise ValidationException(
f'Invalid token for the following reasons: {str(response.token_properties.invalid_reason)}', code=400)

# Get the risk score and the reason(s)
# For more information on interpreting the assessment,
# see: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment
for reason in response.risk_analysis.reasons:
logger.info(reason)
logger.info('The reCAPTCHA score for this token is: ' + str(response.risk_analysis.score))
# Get the assessment name (id). Use this to annotate the assessment.
assessment_name = client.parse_assessment_path(response.name).get('assessment')
logger.info(f'Assessment name: {assessment_name}')
return response
2 changes: 2 additions & 0 deletions breathecode/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ def get(self, key, *args, **kwargs):
'form_entry.won_or_lost': 'marketing.FormEntry.won_or_lost',
'form_entry.new_deal': 'marketing.FormEntry.new_deal',
'session.mentorship_session_status': 'mentorship.MentorshipSession.mentorship_session_status',
'planfinancing.planfinancing_created': 'payments.PlanFinancing.planfinancing_created',
'subscription.subscription_created': 'payments.Subscription.subscription_created',
}

# Websocket
Expand Down
1 change: 1 addition & 0 deletions breathecode/utils/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .supervisor import * # noqa: F401
from .task import * # noqa: F401
from .validate_captcha import * # noqa: F401
from .validate_captcha_challenge import * # noqa: F401
64 changes: 64 additions & 0 deletions breathecode/utils/decorators/validate_captcha_challenge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging
import os

from rest_framework.views import APIView

from breathecode.services.google_cloud import Recaptcha
from breathecode.utils.exceptions import ProgrammingError

from capyc.rest_framework.exceptions import ValidationException

logger = logging.getLogger(__name__)
__all__ = ['validate_captcha_challenge']


def validate_captcha_challenge(function):

def wrapper(*args, **kwargs):
try:
if hasattr(args[0], '__class__') and isinstance(args[0], APIView):
data = args[1].data.copy()

elif hasattr(args[0], 'user') and hasattr(args[0].user, 'has_perm'):
data = args[0].data.copy()

# websocket support
elif hasattr(args[0], 'ws_request'):
data = args[0].data.copy()

else:
raise IndexError()

apply_captcha = os.getenv('APPLY_CAPTCHA', False)

if not apply_captcha:
return function(*args, **kwargs)

logger.info('VERIFYING THE CAPTCHA')
print('VERIFYING THE CAPTCHA')

project_id = os.getenv('GOOGLE_PROJECT_ID', '')

site_key = os.getenv('GOOGLE_CAPTCHA_KEY', '')

token = data['token'] if 'token' in data else None
if token is None:
raise ValidationException('Missing ReCaptcha Token', code=400)

recaptcha = Recaptcha()
response = recaptcha.create_assessment_v2(project_id=project_id, recaptcha_site_key=site_key, token=token)

# TEMPORALILY DISABLING SCORE ANALYSIS
# Google Recaptcha needs to work some time to learn about the site's traffic
# It may be enabled in the future, though it is not recommended to just block the traffic based on punctuation
# read more: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment-website?authuser=1&hl=es&_gl=1*1yex6v*_ga*MzE4Mjc4NTMzLjE3MDAxNzgzMDU.*_ga_WH2QY8WWF5*MTcxNTk2NTkzOS41NC4xLjE3MTU5NjYyNDMuMC4wLjA.&_ga=2.84385883.-318278533.1700178305#interpret_scores

# if (response.risk_analysis.score < 0.6):
# raise ValidationException('The action was denied because it was considered suspicious', code=429)

except IndexError:
raise ProgrammingError('Missing request information, use this decorator with DRF View')

return function(*args, **kwargs)

return wrapper
1 change: 1 addition & 0 deletions capyc/core/pytest/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .image import * # noqa: F401
from .no_http_requests import * # noqa: F401
from .random import * # noqa: F401
from .seed import * # noqa: F401
11 changes: 6 additions & 5 deletions capyc/core/pytest/fixtures/fake.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import Generator
from typing import Generator, Optional

import pytest
from faker import Faker as Fake

__all__ = ['fake', 'Fake']

FAKE = Fake()

@pytest.fixture(autouse=True)
def fake(seed: Optional[int]) -> Generator[Fake, None, None]:
f = Fake()
f.seed_instance(seed)

@pytest.fixture(scope='module')
def fake() -> Generator[Fake, None, None]:
return FAKE
yield f
Loading

0 comments on commit d4f04ad

Please sign in to comment.