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/reverse-service-sets
  • Loading branch information
jefer94 committed Jun 25, 2024
2 parents 1c11dd7 + fd31ddd commit 2d6a389
Show file tree
Hide file tree
Showing 55 changed files with 1,702 additions and 747 deletions.
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@ linked-services = {extras = ["django", "aiohttp", "requests"], version = "*"}
celery-task-manager = {extras = ["django"], version = "*"}
django-sql-explorer = {extras = ["xls"], version = "*"}
contextlib2 = "*"
google-apps-meet = "*"
google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
595 changes: 338 additions & 257 deletions Pipfile.lock

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions breathecode/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""

# keeps this above
import newrelic.agent

newrelic.agent.initialize()

# the rest of your ASGI file contents go here
import os

Expand All @@ -20,5 +15,3 @@
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'breathecode.settings')

application = get_asgi_application()
if os.getenv('NOWRAP_APP') != '1':
application = newrelic.agent.ASGIApplicationWrapper(application)
4 changes: 2 additions & 2 deletions breathecode/assessment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def from_status(s):

@admin.register(AssessmentThreshold)
class UserAssessmentThresholdAdmin(admin.ModelAdmin):
search_fields = ['assessment__slug', 'assessment__title']
list_display = ['id', 'score_threshold', 'assessment']
search_fields = ['assessment__slug', 'assessment__title', 'tags']
list_display = ['id', 'title', 'score_threshold', 'assessment', 'tags']
list_filter = ['assessment__slug']
actions = []

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.0.3 on 2024-06-17 20:58

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('assessment', '0008_alter_answer_option_alter_userassessment_status'),
]

operations = [
migrations.AddField(
model_name='assessmentthreshold',
name='title',
field=models.CharField(blank=True,
default=None,
help_text='Title is good for internal use',
max_length=255,
null=True),
),
]
24 changes: 24 additions & 0 deletions breathecode/assessment/migrations/0010_assessmentthreshold_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.0.3 on 2024-06-20 16:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('assessment', '0009_assessmentthreshold_title'),
]

operations = [
migrations.AddField(
model_name='assessmentthreshold',
name='tags',
field=models.CharField(
blank=True,
default=None,
help_text=
'Ideal to group thresholds under a taxonomy, that way you can have several groups of thresholds for the same quiz',
max_length=255,
null=True),
),
]
15 changes: 15 additions & 0 deletions breathecode/assessment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ class AssessmentThreshold(models.Model):
blank=True,
null=True)

title = models.CharField(max_length=255,
default=None,
blank=True,
null=True,
help_text='Title is good for internal use')

tags = models.CharField(
max_length=255,
default=None,
blank=True,
null=True,
help_text=
'Ideal to group thresholds under a taxonomy, that way you can have several groups of thresholds for the same quiz'
)

academy = models.ForeignKey(
Academy,
on_delete=models.CASCADE,
Expand Down
1 change: 1 addition & 0 deletions breathecode/assessment/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
path('academy/layout', AcademyAssessmentLayoutView.as_view()),
path('academy/layout/<str:layout_slug>', AcademyAssessmentLayoutView.as_view()),
path('<str:assessment_slug>/threshold', GetThresholdView.as_view()),
path('<str:assessment_slug>/threshold/<int:threshold_id>', GetThresholdView.as_view()),
path('<str:assessment_slug>/question/<int:question_id>', AssessmentQuestionView.as_view()),
path('<str:assessment_slug>/option/<int:option_id>', AssessmentOptionView.as_view()),
path('<str:assessment_slug>', GetAssessmentView.as_view()),
Expand Down
39 changes: 25 additions & 14 deletions breathecode/assessment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,28 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from breathecode.utils import (
APIViewExtensions,
GenerateLookupsMixin,
)

from breathecode.authenticate.actions import get_user_language
from breathecode.utils import capable_of
from breathecode.utils import APIViewExtensions, GenerateLookupsMixin, capable_of
from breathecode.utils.i18n import translation
from capyc.rest_framework.exceptions import ValidationException

from .models import Assessment, AssessmentThreshold, Option, Question, UserAssessment, Answer, AssessmentLayout
from .models import Answer, Assessment, AssessmentLayout, AssessmentThreshold, Option, Question, UserAssessment
from .serializers import (
AnswerSerializer,
AnswerSmallSerializer,
AssessmentPUTSerializer,
GetAssessmentBigSerializer,
GetAssessmentLayoutSerializer,
GetAssessmentSerializer,
GetAssessmentThresholdSerializer,
OptionSerializer,
QuestionSerializer,
GetAssessmentLayoutSerializer,
SmallUserAssessmentSerializer,
GetUserAssessmentSerializer,
OptionSerializer,
PostUserAssessmentSerializer,
PUTUserAssessmentSerializer,
AnswerSerializer,
AnswerSmallSerializer,
PublicUserAssessmentSerializer,
PUTUserAssessmentSerializer,
QuestionSerializer,
SmallUserAssessmentSerializer,
)


Expand Down Expand Up @@ -381,12 +377,20 @@ class GetThresholdView(APIView):
"""
permission_classes = [AllowAny]

def get(self, request, assessment_slug):
def get(self, request, assessment_slug, threshold_id=None):

item = Assessment.objects.filter(slug=assessment_slug).first()
if item is None:
raise ValidationException('Assessment not found', 404)

if threshold_id is not None:
single = AssessmentThreshold.objects.filter(id=threshold_id, assessment__slug=assessment_slug).first()
if single is None:
raise ValidationException(f'Threshold {threshold_id} not found', 404, slug='threshold-not-found')

serializer = GetAssessmentThresholdSerializer(single, many=False)
return Response(serializer.data, status=status.HTTP_200_OK)

# get original all assessments (assessments that have no parent)
items = AssessmentThreshold.objects.filter(assessment__slug=assessment_slug)
lookup = {}
Expand All @@ -401,6 +405,13 @@ def get(self, request, assessment_slug):
else:
lookup['academy__isnull'] = True

if 'tag' in self.request.GET:
param = self.request.GET.get('tags')
if param != 'all':
lookup['tags__icontains'] = param
else:
lookup['tags__in'] = ['', None]

items = items.filter(**lookup).order_by('-created_at')

serializer = GetAssessmentThresholdSerializer(items, many=True)
Expand Down
2 changes: 2 additions & 0 deletions breathecode/assignments/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class UserAttachmentAdmin(admin.ModelAdmin):
class FinalProjectAdmin(admin.ModelAdmin):
list_display = ['name', 'cohort', 'project_status', 'revision_status', 'visibility_status']
search_fields = ('name', 'cohort__slug', 'repo_url', 'members__email')
filter_horizontal = ['members']
raw_id_fields = ['cohort']
list_filter = ['project_status', 'revision_status', 'visibility_status', 'cohort__academy__slug']
# actions = [mark_as_delivered, mark_as_approved, mark_as_rejected, mark_as_ignored]

Expand Down
17 changes: 12 additions & 5 deletions breathecode/authenticate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,18 @@
sync_gitpod_users_view,
)

IS_TEST_EMV = os.getenv('EMV', 'test') == 'test'

# 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 app_url is None and IS_TEST_EMV:
app_url = 'http://localhost:3000'

if TEST_ENV and not app_url:
import faker

fake = faker.Faker()

app_url = fake.url().replace('http://', 'https://')

app_name = 'authenticate'
urlpatterns = [
Expand Down Expand Up @@ -150,7 +157,7 @@

# authorize
path('authorize/<str:app_slug>',
authorize_view(login_url='/v1/auth/view/login', app_url=app_url, 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
12 changes: 6 additions & 6 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, timedelta
from typing import TypedDict

from celery import Celery
Expand Down Expand Up @@ -121,17 +121,17 @@ def __exit__(self, *args, **kwargs):
data[i] = []

if len(data[i]) >= 2:
data[i].sort(key=lambda x: x['created_at'])
data[i].sort(key=lambda x: x['created_at'].replace(tzinfo=UTC))

if datetime.utcnow() - data[i][-1]['created_at'] < delta:
if datetime.now(UTC) - data[i][-1]['created_at'].replace(tzinfo=UTC) < 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'].replace(tzinfo=UTC) < 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
1 change: 1 addition & 0 deletions breathecode/marketing/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ def formentry(self, obj):
@admin.register(Downloadable)
class DownloadableAdmin(admin.ModelAdmin):
list_display = ('slug', 'name', 'academy', 'status', 'open_link')
raw_id_fields = ['author']

def open_link(self, obj):
return format_html(f"<a href='{obj.destination_url}' target='parent'>open link</a>")
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)
Loading

0 comments on commit 2d6a389

Please sign in to comment.