diff --git a/breathecode/authenticate/urls.py b/breathecode/authenticate/urls.py index 77d21d94d..267f79fb6 100644 --- a/breathecode/authenticate/urls.py +++ b/breathecode/authenticate/urls.py @@ -13,6 +13,8 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import os + from django.urls import path from linked_services.rest_framework.views import app_webhook, authorize_view @@ -67,6 +69,19 @@ sync_gitpod_users_view, ) +# avoiding issues on test environment due that the fixture are loaded after this app +ENV = os.getenv('ENV') +TEST_ENV = (ENV == 'test' or ENV not in ['development', 'staging', 'production']) +LOGIN_URL = '/v1/auth/view/login' +app_url = os.getenv('APP_URL') + +if TEST_ENV and not app_url: + import faker + + fake = faker.Faker() + + app_url = fake.url().replace('http://', 'https://') + app_name = 'authenticate' urlpatterns = [ path('confirmation/', ConfirmEmailView.as_view(), name='confirmation_token'), @@ -142,7 +157,7 @@ # authorize path('authorize/', - authorize_view(login_url='/v1/auth/view/login', get_language=get_user_language), + authorize_view(login_url=LOGIN_URL, app_url=app_url, get_language=get_user_language), name='authorize_slug'), # apps diff --git a/breathecode/celery.py b/breathecode/celery.py index 09302387a..6fc8a9910 100644 --- a/breathecode/celery.py +++ b/breathecode/celery.py @@ -7,7 +7,7 @@ # the rest of your Celery file contents go here import os -from datetime import datetime, timedelta +from datetime import UTC, datetime from typing import TypedDict from celery import Celery @@ -123,7 +123,7 @@ def __exit__(self, *args, **kwargs): if len(data[i]) >= 2: data[i].sort(key=lambda x: x['created_at']) - if datetime.utcnow() - data[i][-1]['created_at'] < delta: + if datetime.now(UTC) - data[i][-1]['created_at'] < delta: available[i] = False data[i] = data[i][-2:] else: @@ -131,7 +131,7 @@ def __exit__(self, *args, **kwargs): data[i] = data[i][-1:] elif len(data[i]) == 1: - if datetime.utcnow() - data[i][0]['created_at'] < delta: + if datetime.now(UTC) - data[i][0]['created_at'] < delta: available[i] = False else: available[i] = True @@ -142,7 +142,7 @@ def __exit__(self, *args, **kwargs): found = False for i in range(workers): if available[i]: - data[i].append({'pid': worker_id, 'created_at': datetime.utcnow()}) + data[i].append({'pid': worker_id, 'created_at': datetime.now(UTC)}) found = True break @@ -153,7 +153,7 @@ def __exit__(self, *args, **kwargs): if len(data[i]) < len(pointer): pointer = data[i] - pointer.append({'pid': worker_id, 'created_at': datetime.utcnow()}) + pointer.append({'pid': worker_id, 'created_at': datetime.now(UTC)}) cache.set('workers', data, timeout=None) break diff --git a/breathecode/mentorship/actions.py b/breathecode/mentorship/actions.py index aaaa87b65..204bcd54b 100644 --- a/breathecode/mentorship/actions.py +++ b/breathecode/mentorship/actions.py @@ -3,18 +3,21 @@ from datetime import timedelta import pytz +from asgiref.sync import sync_to_async from dateutil.relativedelta import relativedelta from django.db.models import Q, QuerySet from django.shortcuts import render from django.utils import timezone +from google.apps.meet_v2.types import Space, SpaceConfig import breathecode.activity.tasks as tasks_activity from breathecode.mentorship.exceptions import ExtendSessionException from breathecode.services.daily.client import DailyClient +from breathecode.services.google_meet.google_meet import GoogleMeet from breathecode.utils.datetime_integer import duration_to_str from capyc.rest_framework.exceptions import ValidationException -from .models import MentorProfile, MentorshipBill, MentorshipSession +from .models import MentorProfile, MentorshipBill, MentorshipService, MentorshipSession logger = logging.getLogger(__name__) @@ -91,12 +94,20 @@ def get_pending_sessions_or_create(token, mentor, service, mentee=None): is_online=True, service=service, ends_at=timezone.now() + duration) - daily = DailyClient() - room = daily.create_room(exp_in_seconds=service.duration.seconds) - session.online_meeting_url = room['url'] - session.name = room['name'] - session.mentee = mentee - session.save() + + if session.service.video_provider == MentorshipService.VideoProvider.GOOGLE_MEET: + create_room_on_google_meet(session) + + elif session.service.video_provider == MentorshipService.VideoProvider.DAILY: + daily = DailyClient() + room = daily.create_room(exp_in_seconds=service.duration.seconds) + session.online_meeting_url = room['url'] + session.name = room['name'] + session.mentee = mentee + session.save() + + else: + raise Exception('Invalid video provider') if mentee: tasks_activity.add_activity.delay(mentee.id, @@ -392,3 +403,38 @@ def mentor_is_ready(mentor: MentorProfile): raise Exception(f'Mentor {mentor.name} online meeting URL is failing.') return True + + +def create_room_on_google_meet(session: MentorshipSession) -> None: + """Create a room on google meet for a mentorship session.""" + + if isinstance(session, MentorshipSession) is False: + raise Exception('session argument must be a MentorshipSession') + + if session.service.video_provider != session.service.VideoProvider.GOOGLE_MEET: + raise Exception('Video provider must be Google Meet') + + if not session.service: + raise Exception('Mentorship session doesn\'t have a service associated with it') + + mentor = session.mentor + + meet = GoogleMeet() + if session.id is None: + session.save() + + title = (f'{session.service.name} {session.id} | ' + f'{mentor.user.first_name} {mentor.user.last_name}') + s = Space( + name=title, + config=SpaceConfig(access_type=SpaceConfig.AccessType.OPEN), + ) + space = meet.create_space(space=s) + session.online_meeting_url = space.meeting_uri + session.name = title + session.save() + + +@sync_to_async +def acreate_room_on_google_meet(session: MentorshipSession) -> None: + return create_room_on_google_meet(session) diff --git a/breathecode/mentorship/models.py b/breathecode/mentorship/models.py index 41c532011..385639717 100644 --- a/breathecode/mentorship/models.py +++ b/breathecode/mentorship/models.py @@ -12,73 +12,133 @@ from breathecode.notify.models import SlackChannel from breathecode.utils.validators.language import validate_language_code -# settings customizable for each academy -# class AcademySettings(models.Model): -# is_video_streaming_active = models.BooleanField(default=False) -# academy = models.OneToOneField(Academy, on_delete=models.CASCADE) -# @staticmethod -# def get(pk): -# settings = AcademySettings.objects.filter(academy__id=pk).first() -# # lets create the settings if they dont exist for this academy -# if settings is None: -# settings = AcademySettings.objects.create(academy=pk) -# return settings -# def warnings(self): -# # return a dictionary with a list of the fields and warning messages related to them -# # for example: { "is_video_streaming_active": "Please settup a video streaming" } -# return {} -# def errors(self): -# # return a dictionary with a list of the fields and errors messages related to them -# return {} - -DRAFT = 'DRAFT' -ACTIVE = 'ACTIVE' -UNLISTED = 'UNLISTED' -INNACTIVE = 'INNACTIVE' -MENTORSHIP_STATUS = ( - (DRAFT, 'Draft'), - (ACTIVE, 'Active'), - (UNLISTED, 'Unlisted'), - (INNACTIVE, 'Innactive'), -) + +class VideoProvider(models.TextChoices): + DAILY = ('DAILY', 'Daily') + GOOGLE_MEET = ('GOOGLE_MEET', 'Google Meet') + + +MENTORSHIP_SETTINGS = { + 'duration': timedelta(hours=1), + 'max_duration': timedelta(hours=2), + 'missed_meeting_duration': timedelta(minutes=10), + 'language': 'en', + 'allow_mentee_to_extend': True, + 'allow_mentors_to_extend': True, + 'video_provider': VideoProvider.GOOGLE_MEET, +} + + +class AcademyMentorshipSettings(models.Model): + VideoProvider = VideoProvider + + academy = models.OneToOneField(Academy, on_delete=models.CASCADE) + duration = models.DurationField(default=MENTORSHIP_SETTINGS['duration'], + help_text='Default duration for mentorship sessions of this service') + + max_duration = models.DurationField( + default=MENTORSHIP_SETTINGS['max_duration'], + help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') + + missed_meeting_duration = models.DurationField( + default=MENTORSHIP_SETTINGS['missed_meeting_duration'], + help_text='Duration that will be paid when the mentee doesn\'t come to the session') + + language = models.CharField(max_length=5, + default=MENTORSHIP_SETTINGS['language'], + validators=[validate_language_code], + help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US') + + allow_mentee_to_extend = models.BooleanField(default=MENTORSHIP_SETTINGS['allow_mentee_to_extend'], + help_text='If true, mentees will be able to extend mentorship session') + allow_mentors_to_extend = models.BooleanField( + default=MENTORSHIP_SETTINGS['allow_mentors_to_extend'], + help_text='If true, mentors will be able to extend mentorship session') + + video_provider = models.CharField(max_length=15, choices=VideoProvider, default=VideoProvider.GOOGLE_MEET) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + + def __str__(self): + return self.academy.name + + def clean(self) -> None: + return super().clean() + + def save(self, **kwargs) -> None: + return super().save(**kwargs) class MentorshipService(models.Model): + VideoProvider = VideoProvider + + class Status(models.TextChoices): + DRAFT = ('DRAFT', 'Draft') + ACTIVE = ('ACTIVE', 'Active') + UNLISTED = ('UNLISTED', 'Unlisted') + INNACTIVE = ('INNACTIVE', 'Innactive') + slug = models.SlugField(max_length=150, unique=True) name = models.CharField(max_length=150) logo_url = models.CharField(max_length=150, default=None, blank=True, null=True) description = models.TextField(max_length=500, default=None, blank=True, null=True) - duration = models.DurationField(default=timedelta(hours=1), + duration = models.DurationField(default=None, + blank=True, help_text='Default duration for mentorship sessions of this service') max_duration = models.DurationField( - default=timedelta(hours=2), - help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') + default=None, blank=True, help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') missed_meeting_duration = models.DurationField( - default=timedelta(minutes=10), - help_text='Duration that will be paid when the mentee doesn\'t come to the session') + default=None, blank=True, help_text='Duration that will be paid when the mentee doesn\'t come to the session') - status = models.CharField(max_length=15, choices=MENTORSHIP_STATUS, default=DRAFT) + status = models.CharField(max_length=15, choices=Status, default=Status.DRAFT) language = models.CharField(max_length=5, - default='en', + default=None, + blank=True, validators=[validate_language_code], help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US') - allow_mentee_to_extend = models.BooleanField(default=True, + allow_mentee_to_extend = models.BooleanField(blank=True, + default=None, help_text='If true, mentees will be able to extend mentorship session') allow_mentors_to_extend = models.BooleanField( - default=True, help_text='If true, mentors will be able to extend mentorship session') + default=None, blank=True, help_text='If true, mentors will be able to extend mentorship session') academy = models.ForeignKey(Academy, on_delete=models.CASCADE) + video_provider = models.CharField(max_length=15, default=None, choices=VideoProvider, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) def __str__(self): - return f'{self.name} ({self.id})' + return f'{self.name} ({self.slug})' + + def clean(self) -> None: + fetched = False + academy_settings = None + for field, value in MENTORSHIP_SETTINGS.items(): + current = getattr(self, field) + if current is None: + if fetched is False: + fetched = True + academy_settings = AcademyMentorshipSettings.objects.filter(academy=self.academy).first() + + if academy_settings: + academy_value = getattr(academy_settings, field) + setattr(self, field, academy_value) + + else: + setattr(self, field, value) + + return super().clean() + + def save(self, **kwargs) -> None: + self.full_clean() + return super().save(**kwargs) class SupportChannel(models.Model): @@ -91,13 +151,11 @@ class SupportChannel(models.Model): updated_at = models.DateTimeField(auto_now=True, editable=False) -INVITED = 'INVITED' -MENTOR_STATUS = ( - (INVITED, 'Invited'), - (ACTIVE, 'Active'), - (UNLISTED, 'Unlisted'), - (INNACTIVE, 'Innactive'), -) +class MentorStatus(models.TextChoices): + INVITED = ('INVITED', 'Invited') + ACTIVE = ('ACTIVE', 'Active') + UNLISTED = ('UNLISTED', 'Unlisted') + INNACTIVE = ('INNACTIVE', 'Innactive') class SupportAgent(models.Model): @@ -109,9 +167,9 @@ class SupportAgent(models.Model): unique=True, help_text='Used for inviting the user to become a support agent') status = models.CharField(max_length=15, - choices=MENTOR_STATUS, - default=INVITED, - help_text=f'Options are: {", ".join([key for key,label in MENTOR_STATUS])}') + choices=MentorStatus, + default=MentorStatus.INVITED, + help_text=f'Options are: {", ".join([key for key,label in MentorStatus.choices])}') email = models.CharField(blank=True, max_length=150, @@ -176,9 +234,9 @@ class MentorProfile(models.Model): help_text='What syllabis is this mentor going to be menting to?') status = models.CharField(max_length=15, - choices=MENTOR_STATUS, - default=INVITED, - help_text=f'Options are: {", ".join([key for key,label in MENTOR_STATUS])}') + choices=MentorStatus, + default=MentorStatus.INVITED, + help_text=f'Options are: {", ".join([key for key,label in MentorStatus.choices])}') email = models.CharField(blank=True, max_length=150, diff --git a/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py b/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py index 256070783..7dfb3b69e 100644 --- a/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py +++ b/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py @@ -53,19 +53,29 @@ def format_mentorship_session_attrs(attrs={}): } +class GoogleMeetMock: + + def __init__(self, meeting_uri='https://meet.google.com/fake'): + self.meeting_uri = meeting_uri + + +def get_title(pk, service, mentor) -> str: + return (f'{service.name} {pk} | {mentor.user.first_name} {mentor.user.last_name}') + + class GetOrCreateSessionTestSuite(MentorshipTestCase): @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentor_first_no_previous_nothing(self): + def test_create_session_mentor_first_no_previous_nothing__daily(self): """ When the mentor gets into the room before the mentee if should create a room with status 'pending' """ - models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -91,6 +101,44 @@ def test_create_session_mentor_first_no_previous_nothing(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentor_first_no_previous_nothing__google_meet(self): + """ + When the mentor gets into the room before the mentee + if should create a room with status 'pending' + """ + + models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service={'video_provider': 'GOOGLE_MEET'}) + + mentor = models.mentor_profile + mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') + + pending_sessions = get_pending_sessions_or_create(mentor_token, mentor, models.mentorship_service, mentee=None) + + self.bc.check.queryset_of(pending_sessions, MentorshipSession) + self.bc.check.queryset_with_pks(pending_sessions, [1]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'status': 'PENDING', + 'mentor_id': 1, + 'mentee_id': None, + 'service_id': 1, + 'is_online': True, + 'name': get_title(1, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + 'ends_at': ENDS_AT + timedelta(seconds=3600), + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -102,7 +150,9 @@ def test_create_session_mentor_first_previous_pending_without_mentee(self): """ mentorship_session = {'mentee_id': None} - models = self.bc.database.create(mentor_profile=1, mentorship_session=mentorship_session, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -141,7 +191,7 @@ def test_create_session_mentor_first_previous_pending_with_mentee(self): models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile session = models.mentorship_session @@ -181,7 +231,7 @@ def test_create_session_mentor_first_started_without_mentee(self): models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -209,13 +259,13 @@ def test_create_session_mentor_first_started_without_mentee(self): @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentee_first_no_previous_nothing(self): + def test_create_session_mentee_first_no_previous_nothing__daily(self): """ Mentee comes first, there is nothing previously created it should return a brand new sessions with started at already started """ - models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentee = models.user[1] @@ -241,6 +291,44 @@ def test_create_session_mentee_first_no_previous_nothing(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentee_first_no_previous_nothing__google_meet(self): + """ + Mentee comes first, there is nothing previously created + it should return a brand new sessions with started at already started + """ + + models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service={'video_provider': 'GOOGLE_MEET'}) + mentor = models.mentor_profile + mentee = models.user[1] + + mentee_token, created = Token.get_or_create(mentee, token_type='permanent') + sessions = get_pending_sessions_or_create(mentee_token, mentor, models.mentorship_service, mentee) + + self.bc.check.queryset_of(sessions, MentorshipSession) + self.bc.check.queryset_with_pks(sessions, [1]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'status': 'PENDING', + 'mentor_id': 1, + 'mentee_id': 2, + 'service_id': 1, + 'is_online': True, + 'ends_at': ENDS_AT + timedelta(seconds=3600), + 'name': get_title(1, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -255,7 +343,7 @@ def test_create_session_mentee_first_with_wihout_mentee(self): models = self.bc.database.create(mentor_profile=1, user=2, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) new_mentee = models.user[1] mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') @@ -284,7 +372,7 @@ def test_create_session_mentee_first_with_wihout_mentee(self): @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentee_first_with_another_mentee(self): + def test_create_session_mentee_first_with_another_mentee__daily(self): """ Mentee comes first, there is a previous pending meeting with another mentee it should keep and ignore old one (untouched) and create and return new one for this mentee @@ -293,12 +381,15 @@ def test_create_session_mentee_first_with_another_mentee(self): # other random mentoring session precreated just for better testing mentorship_session = {'status': 'PENDING'} - self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, mentorship_service=1) + self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) new_mentee = self.bc.database.create(user=1).user mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') @@ -340,6 +431,71 @@ def test_create_session_mentee_first_with_another_mentee(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentee_first_with_another_mentee__google_meet(self): + """ + Mentee comes first, there is a previous pending meeting with another mentee + it should keep and ignore old one (untouched) and create and return new one for this mentee + """ + + # other random mentoring session precreated just for better testing + + mentorship_session = {'status': 'PENDING'} + self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) + + models = self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'GOOGLE_MEET'}) + new_mentee = self.bc.database.create(user=1).user + + mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') + sessions_to_render = get_pending_sessions_or_create(mentee_token, + models.mentor_profile, + models.mentorship_service, + mentee=new_mentee) + + self.bc.check.queryset_of(sessions_to_render, MentorshipSession) + self.bc.check.queryset_with_pks(sessions_to_render, [3]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'ends_at': None, + 'mentee_id': 1, + 'mentor_id': 1, + 'service_id': 1, + }), + format_mentorship_session_attrs({ + 'id': 2, + 'ends_at': None, + 'mentee_id': 2, + 'mentor_id': 2, + 'service_id': 2, + }), + format_mentorship_session_attrs({ + 'id': 3, + 'status': 'PENDING', + 'mentor_id': 2, + 'mentee_id': 3, + 'is_online': True, + 'ends_at': ENDS_AT + timedelta(seconds=3600), + 'name': get_title(3, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + 'service_id': 2, + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -354,13 +510,13 @@ def test_create_session_mentee_first_with_another_same_mentee(self): self.bc.database.create(mentor_profile=1, user=1, mentorship_session={'status': 'PENDING'}, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) # old meeting with SAME mentee, should be re-used models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session={'status': 'PENDING'}, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) same_mentee = models.user mentee_token, created = Token.get_or_create(same_mentee, token_type='permanent') diff --git a/breathecode/mentorship/tests/urls/tests_academy_service.py b/breathecode/mentorship/tests/urls/tests_academy_service.py index c95b0181c..e5502a12d 100644 --- a/breathecode/mentorship/tests/urls/tests_academy_service.py +++ b/breathecode/mentorship/tests/urls/tests_academy_service.py @@ -52,6 +52,7 @@ def post_serializer(data={}): 'name': '', 'slug': '', 'status': 'DRAFT', + 'video_provider': 'GOOGLE_MEET', **data, } @@ -71,6 +72,7 @@ def mentorship_service_columns(data={}): 'name': '', 'slug': '', 'status': 'DRAFT', + 'video_provider': 'GOOGLE_MEET', **data, } diff --git a/breathecode/mentorship/tests/urls/tests_academy_service_id.py b/breathecode/mentorship/tests/urls/tests_academy_service_id.py index 59939d353..ce081fc58 100644 --- a/breathecode/mentorship/tests/urls/tests_academy_service_id.py +++ b/breathecode/mentorship/tests/urls/tests_academy_service_id.py @@ -85,6 +85,7 @@ def mentorship_service_columns(mentorship_service, data={}): 'name': mentorship_service.name, 'slug': mentorship_service.slug, 'status': mentorship_service.status, + 'video_provider': mentorship_service.video_provider, **data, } diff --git a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py index c3584a97d..cbd36d274 100644 --- a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py +++ b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py @@ -92,10 +92,19 @@ def get_env(key, value=None): return get_env -def render_message(message, data={}): +def render_message(message, data={}, academy=None): request = None context = {'MESSAGE': message, 'BUTTON': None, 'BUTTON_TARGET': '_blank', 'LINK': None, **data} + if academy: + context['COMPANY_INFO_EMAIL'] = academy.feedback_email + context['COMPANY_LEGAL_NAME'] = academy.legal_name or academy.name + context['COMPANY_LOGO'] = academy.logo_url + context['COMPANY_NAME'] = academy.name + + if 'heading' not in data: + context['heading'] = academy.name + return loader.render_to_string('message.html', context, request) @@ -540,7 +549,10 @@ def test_with_mentor_profile(self): model = self.bc.database.create(user=1, token=1, mentor_profile=1, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -587,7 +599,10 @@ def test_with_mentor_profile__bad_statuses(self): model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -639,7 +654,10 @@ def test_with_mentor_profile__good_statuses__without_mentor_urls(self): model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -706,7 +724,10 @@ def test_with_mentor_profile__good_statuses__with_mentor_urls__with_mentee(self) model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -778,7 +799,10 @@ def test_with_mentor_profile__good_statuses__with_mentor_urls__with_mentee__not_ model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -1177,7 +1201,10 @@ def test_with_mentor_profile__without_user_name(self): model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy) model.mentorship_session.mentee = None @@ -1222,6 +1249,86 @@ def test_with_mentor_profile__without_user_name(self): self.bc.database.delete('auth.Permission') self.bc.database.delete('auth.User') + """ + 🔽🔽🔽 GET without MentorProfile, good statuses with mentor urls, MentorshipSession without mentee + passing session and mentee but mentee does not exist, user without name + """ + + @patch('breathecode.mentorship.actions.get_pending_sessions_or_create', + MagicMock(side_effect=Exception('Error inside get_pending_sessions_or_create'))) + @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, + })])) + def test_error_inside_get_pending_sessions_or_create(self): + cases = [{ + 'status': x, + 'online_meeting_url': self.bc.fake.url(), + 'booking_url': self.bc.fake.url(), + } for x in ['ACTIVE', 'UNLISTED']] + permission = {'codename': 'join_mentorship'} + + id = 0 + for mentor_profile in cases: + id += 1 + + user = {'first_name': '', 'last_name': ''} + base = self.bc.database.create(user=user, token=1, group=1, permission=permission) + + mentorship_session = {'mentee_id': None} + academy = {'available_as_saas': False} + model = self.bc.database.create(mentor_profile=mentor_profile, + mentorship_session=mentorship_session, + user=user, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, + academy=academy) + + model.mentorship_session.mentee = None + model.mentorship_session.save() + + querystring = self.bc.format.to_querystring({ + 'token': base.token.key, + }) + 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 = render_message('Error inside get_pending_sessions_or_create', academy=model.academy) + + # 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_400_BAD_REQUEST) + 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'), []) + + # teardown + self.bc.database.delete('mentorship.MentorProfile') + self.bc.database.delete('auth.Permission') + self.bc.database.delete('auth.User') + # TODO: disabled until have a new feature flags manager # """ # 🔽🔽🔽 GET without MentorProfile, good statuses with mentor urls, MentorshipSession without mentee @@ -1261,7 +1368,7 @@ def test_with_mentor_profile__without_user_name(self): # model = self.bc.database.create(mentor_profile=mentor_profile, # mentorship_session=mentorship_session, # user=user, - # mentorship_service=1, + # mentorship_service={'language': 'en', 'video_provider': 'DAILY'}, # academy=academy) # model.mentorship_session.mentee = None @@ -1337,7 +1444,10 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy) model.mentorship_session.mentee = None @@ -1415,7 +1525,10 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy, plan={ 'is_renewable': False, @@ -1505,7 +1618,7 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi how_many = random.randint(1, 100) consumable = {'how_many': how_many} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, @@ -1618,7 +1731,7 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__bypass_me mentorship_session = {'mentee_id': None} academy = {'available_as_saas': True} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, @@ -1791,20 +1904,28 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true(self): mentorship_session_base = {'mentee_id': base.user.id, 'ends_at': ends_at} # session, token - cases = [({ - **mentorship_session_base, - 'allow_mentors_to_extend': True, - }, None), ({ - **mentorship_session_base, - 'allow_mentee_to_extend': True, - }, 1)] + cases = [ + ({ + **mentorship_session_base, + 'allow_mentee_to_extend': True, + 'allow_mentors_to_extend': False, + }, None), + ({ + **mentorship_session_base, + 'allow_mentee_to_extend': False, + 'allow_mentors_to_extend': True, + }, 1), + ] for mentorship_session, token in cases: model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -1898,9 +2019,13 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__extend_session # session, token cases = [({ 'allow_mentors_to_extend': True, - }, None), ({ + 'allow_mentee_to_extend': False, + 'language': 'en', + }, 1), ({ + 'allow_mentee_to_extend': False, 'allow_mentee_to_extend': True, - }, 1)] + 'language': 'en', + }, None)] for mentorship_service, token in cases: model = self.bc.database.create(mentor_profile=mentor_profile, @@ -2000,10 +2125,12 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__session_can_no ({ 'allow_mentors_to_extend': False, 'allow_mentee_to_extend': False, + 'language': 'en', }, None), ({ 'allow_mentors_to_extend': False, 'allow_mentee_to_extend': False, + 'language': 'en', }, 1), ] @@ -2114,7 +2241,10 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__redirect_to_se mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2214,7 +2344,10 @@ def test_with_mentor_profile__redirect_to_session__no_saas(self): mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2313,7 +2446,10 @@ def test_with_mentor_profile__redirect_to_session__saas(self): mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission, academy=academy) @@ -2390,7 +2526,7 @@ def test_with_mentor_profile__redirect_to_session__saas__(self): consumable = {'how_many': how_many} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} base = self.bc.database.create(user=user, token=1, group=1, @@ -2666,6 +2802,7 @@ def test__post__auth__no_saas__finantial_status_no_late(bc: Breathecode, client: mentorship_session = { **mentorship_session_base, 'allow_mentee_to_extend': True, + 'name': 'Session 1', } token = 1 @@ -2673,7 +2810,10 @@ def test__post__auth__no_saas__finantial_status_no_late(bc: Breathecode, client: mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2797,7 +2937,10 @@ def test__post__auth__no_saas__finantial_status_late(bc: Breathecode, client: fx mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) diff --git a/breathecode/mentorship/views.py b/breathecode/mentorship/views.py index d6a295d92..c51ca8f84 100644 --- a/breathecode/mentorship/views.py +++ b/breathecode/mentorship/views.py @@ -488,7 +488,10 @@ def __call__(self): # if the mentor is not the user, then we assume is the mentee mentee = self.token.user if is_token_of_mentee else None - sessions = self.get_pending_sessions_or_create(mentor, service, mentee) + try: + sessions = self.get_pending_sessions_or_create(mentor, service, mentee) + except Exception as e: + return render_message(self.request, str(e), status=400, academy=mentor.academy) if not is_token_of_mentee and sessions.count() > 0 and str(sessions.first().id) != self.query_params['session']: return self.render_pick_session(mentor, sessions) diff --git a/breathecode/services/__init__.py b/breathecode/services/__init__.py index 9dddcef2b..052345c3f 100644 --- a/breathecode/services/__init__.py +++ b/breathecode/services/__init__.py @@ -1,4 +1,5 @@ from .datetime_to_iso_format import datetime_to_iso_format # noqa: F401 from .eventbrite import CAMPAIGN, SOURCE, Eventbrite # noqa: F401 from .google_cloud import Datastore, Function, Storage # noqa: F401 +from .google_meet import * # noqa: F401 from .launch_darkly import * # noqa: F401 diff --git a/breathecode/services/google_meet/__init__.py b/breathecode/services/google_meet/__init__.py new file mode 100644 index 000000000..1919cdab5 --- /dev/null +++ b/breathecode/services/google_meet/__init__.py @@ -0,0 +1 @@ +from .google_meet import * # noqa: F401 diff --git a/breathecode/services/google_meet/google_meet.py b/breathecode/services/google_meet/google_meet.py index 2b0d1909f..9fe838196 100644 --- a/breathecode/services/google_meet/google_meet.py +++ b/breathecode/services/google_meet/google_meet.py @@ -1,7 +1,82 @@ -from typing import Optional +from typing import Optional, TypedDict, Unpack +import google.apps.meet_v2.services.conference_records_service.pagers as pagers from asgiref.sync import async_to_sync from google.apps import meet_v2 +from google.apps.meet_v2.types import Space +from google.protobuf.field_mask_pb2 import FieldMask +from google.protobuf.timestamp_pb2 import Timestamp + +__all__ = ['GoogleMeet'] + + +class CreateSpaceRequest(TypedDict): + space: Space + + +class EndActiveConferenceRequest(TypedDict): + name: str + + +class GetConferenceRecordRequest(TypedDict): + name: str + + +class GetParticipantRequest(TypedDict): + name: str + + +class GetParticipantSessionRequest(TypedDict): + name: str + + +class GetRecordingRequest(TypedDict): + name: str + + +class GetSpaceRequest(TypedDict): + name: str + + +class UpdateSpaceRequest(TypedDict): + space: Space + update_mask: FieldMask + + +class GetTranscriptRequest(TypedDict): + name: str + + +class ListConferenceRecordsRequest(TypedDict): + page_size: int + page_token: str + filter: str # in EBNF format, space.meeting_code, space.name, start_time and end_time + + +class ListRecordingsRequest(TypedDict): + parent: str + page_size: int + page_token: str + + +class ListParticipantSessionsRequest(TypedDict): + parent: str + page_size: int + page_token: str + filter: str # in EBNF format, start_time and end_time + + +class ListTranscriptsRequest(TypedDict): + parent: str + page_size: int + page_token: str + + +class ListParticipantsRequest(TypedDict): + parent: str + page_size: int + page_token: str + filter: str # in EBNF format, start_time and end_time class GoogleMeet: @@ -9,9 +84,12 @@ class GoogleMeet: _conference_records_service_client: Optional[meet_v2.ConferenceRecordsServiceAsyncClient] def __init__(self): + from breathecode.setup import resolve_gcloud_credentials + + resolve_gcloud_credentials() + self._spaces_service_client = None self._conference_records_service_client = None - pass async def spaces_service_client(self): if self._spaces_service_client is None: @@ -25,31 +103,28 @@ async def conference_records_service_client(self): return self._conference_records_service_client - async def acreate_meeting(self, **kwargs): + async def acreate_space(self, **kwargs: Unpack[CreateSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.CreateSpaceRequest() + request = meet_v2.CreateSpaceRequest(**kwargs) # Make the request - response = await client.create_space(request=request) - - # Handle the response - print(response) + return await client.create_space(request=request) @async_to_sync - async def create_meeting(self): - return await self.acreate_meeting() + async def create_space(self, **kwargs: Unpack[CreateSpaceRequest]) -> meet_v2.Space: + return await self.acreate_space(**kwargs) - async def aget_meeting(self): + async def aget_space(self, **kwargs: Unpack[GetSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.GetSpaceRequest() + request = meet_v2.GetSpaceRequest(**kwargs) # Make the request response = await client.get_space(request=request) @@ -58,15 +133,15 @@ async def aget_meeting(self): print(response) @async_to_sync - async def get_meeting(self): - return await self.aget_meeting() + async def get_space(self, **kwargs: Unpack[GetSpaceRequest]) -> meet_v2.Space: + return await self.aget_space(**kwargs) - async def aupdate_space(self): + async def aupdate_space(self, **kwargs: Unpack[UpdateSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.UpdateSpaceRequest() + request = meet_v2.UpdateSpaceRequest(**kwargs) # Make the request response = await client.update_space(request=request) @@ -75,117 +150,90 @@ async def aupdate_space(self): print(response) @async_to_sync - async def update_space(self): - return await self.aupdate_space() + async def update_space(self, **kwargs: Unpack[UpdateSpaceRequest]) -> meet_v2.Space: + return await self.aupdate_space(**kwargs) - async def aend_meeting(self, name: str): + async def aend_active_conference(self, **kwargs: Unpack[EndActiveConferenceRequest]) -> None: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.EndActiveConferenceRequest(name=name) + request = meet_v2.EndActiveConferenceRequest(**kwargs) # Make the request - await client.end_active_conference(request=request) + return await client.end_active_conference(request=request) @async_to_sync - async def end_meeting(self, name: str): - return await self.aend_meeting(name) + async def end_active_conference(self, **kwargs: Unpack[EndActiveConferenceRequest]) -> None: + return await self.aend_active_conference(**kwargs) - async def alist_participants(self, parent: str): + async def alist_participants(self, **kwargs: Unpack[ListParticipantsRequest]) -> pagers.ListParticipantsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListParticipantsRequest(parent=parent) + request = meet_v2.ListParticipantsRequest(**kwargs) # Make the request - page_result = client.list_participants(request=request) - - # Handle the response - async for response in page_result: - print(response) + return await client.list_participants(request=request) - @async_to_sync - async def list_participants(self, parent: str): - return await self.alist_participants(parent) - - async def aget_participant(self, name: str): + async def aget_participant(self, **kwargs: Unpack[GetParticipantRequest]) -> meet_v2.Participant: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetParticipantRequest(name=name) + request = meet_v2.GetParticipantRequest(**kwargs) # Make the request - response = await client.get_participant(request=request) - - # Handle the response - print(response) + return await client.get_participant(request=request) @async_to_sync - async def get_participant(self, name: str): - return await self.aget_participant(name) + async def get_participant(self, **kwargs: Unpack[GetParticipantRequest]) -> meet_v2.Participant: + return await self.aget_participant(**kwargs) - async def alist_participant_sessions(self, parent: str): + async def alist_participant_sessions( + self, **kwargs: Unpack[ListParticipantSessionsRequest]) -> pagers.ListParticipantSessionsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListParticipantSessionsRequest(parent=parent) + request = meet_v2.ListParticipantSessionsRequest(**kwargs) # Make the request - page_result = client.list_participant_sessions(request=request) - - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_participant_sessions(self, parent: str): - return await self.alist_participant_sessions(parent) + return await client.list_participant_sessions(request=request) - async def aget_participant_session(self, name: str): + async def aget_participant_session(self, + **kwargs: Unpack[GetParticipantSessionRequest]) -> meet_v2.ParticipantSession: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetParticipantSessionRequest(name=name) + request = meet_v2.GetParticipantSessionRequest(**kwargs) # Make the request - response = await client.get_participant_session(request=request) - - # Handle the response - print(response) + return await client.get_participant_session(request=request) @async_to_sync - async def get_participant_session(self, name: str): - return await self.aget_participant_session(name) + async def get_participant_session(self, + **kwargs: Unpack[GetParticipantSessionRequest]) -> meet_v2.ParticipantSession: + return await self.aget_participant_session(**kwargs) - async def alist_recordings(self, parent: str): + async def alist_recordings(self, **kwargs: Unpack[ListRecordingsRequest]) -> pagers.ListRecordingsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListRecordingsRequest(parent=parent) + request = meet_v2.ListRecordingsRequest(**kwargs) # Make the request - page_result = client.list_recordings(request=request) + return await client.list_recordings(request=request) - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_recordings(self, parent: str): - return await self.alist_recordings(parent) - - async def aget_recording(self, name: str): + async def aget_recording(self, **kwargs: Unpack[GetRecordingRequest]) -> meet_v2.Recording: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetRecordingRequest(name=name) + request = meet_v2.GetRecordingRequest(**kwargs) # Make the request response = await client.get_recording(request=request) @@ -194,33 +242,25 @@ async def aget_recording(self, name: str): print(response) @async_to_sync - async def get_recording(self, name: str): - return await self.aget_recording(name) + async def get_recording(self, **kwargs: Unpack[GetRecordingRequest]) -> meet_v2.Recording: + return await self.aget_recording(**kwargs) - async def alist_transcripts(self, parent: str): + async def alist_transcripts(self, **kwargs: Unpack[ListTranscriptsRequest]) -> pagers.ListTranscriptsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListTranscriptsRequest(parent=parent) + request = meet_v2.ListTranscriptsRequest(**kwargs) # Make the request - page_result = client.list_transcripts(request=request) - - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_transcripts(self, parent: str): - return await self.alist_transcripts(parent) + return await client.list_transcripts(request=request) - async def aget_transcript(self, name: str): + async def aget_transcript(self, **kwargs: Unpack[GetTranscriptRequest]) -> meet_v2.Transcript: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetTranscriptRequest(name=name) + request = meet_v2.GetTranscriptRequest(**kwargs) # Make the request response = await client.get_transcript(request=request) @@ -229,40 +269,30 @@ async def aget_transcript(self, name: str): print(response) @async_to_sync - async def get_transcript(self, name: str): - return await self.aget_transcript(name) + async def get_transcript(self, **kwargs: Unpack[GetTranscriptRequest]) -> meet_v2.Transcript: + return await self.aget_transcript(**kwargs) - async def alist_conference_records(self): + async def alist_conference_records( + self, **kwargs: Unpack[ListConferenceRecordsRequest]) -> pagers.ListConferenceRecordsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListConferenceRecordsRequest() + request = meet_v2.ListConferenceRecordsRequest(**kwargs) # Make the request - page_result = client.list_conference_records(request=request) + return await client.list_conference_records(request=request) - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_conference_records(self): - return await self.alist_conference_records() - - async def aget_conference_record(self, name: str): + async def aget_conference_record(self, **kwargs: Unpack[GetConferenceRecordRequest]) -> meet_v2.ConferenceRecord: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetConferenceRecordRequest(name=name) + request = meet_v2.GetConferenceRecordRequest(**kwargs) # Make the request - response = await client.get_conference_record(request=request) - - # Handle the response - print(response) + return await client.get_conference_record(request=request) @async_to_sync - async def get_conference_record(self, name: str): - return await self.aget_conference_record(name) + async def get_conference_record(self, **kwargs: Unpack[GetConferenceRecordRequest]) -> meet_v2.ConferenceRecord: + return await self.aget_conference_record(**kwargs) diff --git a/breathecode/tests/mixins/datetime_mixin.py b/breathecode/tests/mixins/datetime_mixin.py index 0791259e6..9abc55adb 100644 --- a/breathecode/tests/mixins/datetime_mixin.py +++ b/breathecode/tests/mixins/datetime_mixin.py @@ -3,17 +3,11 @@ """ import re -from datetime import datetime, timedelta -from django.utils import timezone -from breathecode.utils.datetime_integer import DatetimeInteger - - -def get_utc(): - date = timezone.now() - return date.tzinfo +from datetime import UTC, datetime +from django.utils import timezone -UTC = get_utc() +from breathecode.utils.datetime_integer import DatetimeInteger __all__ = ['DatetimeMixin'] @@ -36,7 +30,7 @@ def datetime_now(self) -> datetime: """ return timezone.now() - def datetime_to_iso(self, date=datetime.utcnow()) -> str: + def datetime_to_iso(self, date=datetime.now(UTC)) -> str: """ Transform a datetime to ISO 8601 format. @@ -72,7 +66,7 @@ def iso_to_datetime(self, iso: str) -> datetime: date = datetime.fromisoformat(string) return timezone.make_aware(date) - def datetime_to_ical(self, date=datetime.utcnow(), utc=True) -> str: + def datetime_to_ical(self, date=datetime.now(UTC), utc=True) -> str: s = f'{date.year:04}{date.month:02}{date.day:02}T{date.hour:02}{date.minute:02}{date.second:02}' if utc: s += 'Z' diff --git a/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py b/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py index 2b286e4a0..e5b2c5d2a 100644 --- a/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py +++ b/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py @@ -1,9 +1,10 @@ import logging from typing import Any -from breathecode.tests.mixins.generate_models_mixin.exceptions import BadArgument from mixer.backend.django import mixer +from breathecode.tests.mixins.generate_models_mixin.exceptions import BadArgument + from .argument_parser import argument_parser __all__ = ['create_models'] @@ -44,7 +45,7 @@ def debug_mixer(attr, path, **kwargs): def create_models(attr, path, **kwargs): - # does not remove this line are use very often + # does not remove this line is used very often # debug_mixer(attr, path, **kwargs) result = [ diff --git a/capyc/django/pytest/fixtures/database.py b/capyc/django/pytest/fixtures/database.py index 7766346b0..24163884a 100644 --- a/capyc/django/pytest/fixtures/database.py +++ b/capyc/django/pytest/fixtures/database.py @@ -352,6 +352,12 @@ def create(cls, **models): pending = {} + keys = [*models.keys()] + + for key in keys: + if models[key] is None or models[key] == 0: + del models[key] + # get descriptors for model_alias, _value in models.items(): try: