Skip to content

Commit

Permalink
add google meet provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jefer94 committed Jun 17, 2024
1 parent 2952706 commit c25f9ec
Show file tree
Hide file tree
Showing 15 changed files with 687 additions and 230 deletions.
17 changes: 16 additions & 1 deletion breathecode/authenticate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/<str:token>', ConfirmEmailView.as_view(), name='confirmation_token'),
Expand Down Expand Up @@ -142,7 +157,7 @@

# authorize
path('authorize/<str:app_slug>',
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
Expand Down
10 changes: 5 additions & 5 deletions breathecode/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,15 +123,15 @@ 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:
available[i] = True
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
Expand All @@ -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

Expand All @@ -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
Expand Down
60 changes: 53 additions & 7 deletions breathecode/mentorship/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
162 changes: 110 additions & 52 deletions breathecode/mentorship/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit c25f9ec

Please sign in to comment.