Skip to content
This repository was archived by the owner on Mar 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Makefile
venv/
.env
**/__pycache__/
# Dockerfile
docker-compose.yml
.Dockerignore
.env.docker
.env.docker
8 changes: 0 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
# Use the official Python image from the Docker Hub
FROM python:3.12-alpine

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set the working directory
WORKDIR /app

# Install system dependencies
RUN apk add --no-cache gcc musl-dev mariadb-connector-c-dev mariadb-dev pkgconfig

# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --upgrade 'sentry-sdk[django]'

# Copy the project files
COPY . /app/

# Expose the port your app runs on
EXPOSE 8000

# Run the application
CMD ["daphne", "FD.asgi:application", "--bind", "0.0.0.0", "--port", "8000"]
3 changes: 3 additions & 0 deletions FD/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .celery import app as celery_app

__all__ = ('celery_app',)
23 changes: 23 additions & 0 deletions FD/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
from celery import Celery
from celery.schedules import crontab

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FD.settings')

app = Celery('FD')

app.config_from_object('django.conf:settings', namespace='CELERY')
app.conf.broker_connection_retry_on_startup = True

app.autodiscover_tasks()

app.conf.beat_schedule = {
'cache-matching-groups': {
'task': 'home.tasks.cache_matching_groups',
'schedule': crontab(hour=2),
},
'test-celery': {
'task': 'home.tasks.test_celery',
'schedule': crontab(minute=1),
},
}
25 changes: 23 additions & 2 deletions FD/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from datetime import timedelta
from pathlib import Path
from dotenv import load_dotenv
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

import os

load_dotenv()
Expand Down Expand Up @@ -81,7 +84,6 @@
},
]

# WSGI_APPLICATION = 'FD.wsgi.application'
ASGI_APPLICATION = "FD.asgi.application"


Expand Down Expand Up @@ -186,14 +188,33 @@
},
}

import sentry_sdk

sentry_sdk.init(
dsn="https://ccb764743ed51a9f962c8417f8dfe02a@o4508450288631808.ingest.de.sentry.io/4508450290663504",
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={
"continuous_profiling_auto_start": True,
},
)

SILKY_PYTHON_PROFILER = True

REDIS_URL = os.getenv("REDIS_URL")

CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}

CELERY_BROKER_URL = os.getenv("REDIS_URL")
CELERY_RESULT_BACKEND = os.getenv("REDIS_URL")
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Asia/Tehran'
1 change: 0 additions & 1 deletion chat/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def get_user_from_token(self, token):
return None

def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name, self.channel_name
)
Expand Down
21 changes: 21 additions & 0 deletions home/migrations/0002_usersuggestions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2024-12-19 21:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('home', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='UserSuggestions',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_id', models.IntegerField()),
('group_ids', models.TextField()),
],
),
]
6 changes: 5 additions & 1 deletion home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ class GroupTimeSlot(models.Model):
group = models.ForeignKey('groups.Group', models.CASCADE, related_name='time_slots')

def __str__(self):
return f"{self.day_of_week} {self.start_time} - {self.end_time}"
return f"{self.day_of_week} {self.start_time} - {self.end_time}"

class UserSuggestions(models.Model):
user_id = models.IntegerField()
group_ids = models.TextField()
108 changes: 77 additions & 31 deletions home/services.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import redis
from dotenv import load_dotenv
import os
from groups.models import Group

from django.db.models import Case, When, Value, FloatField, F, ExpressionWrapper, Sum
from django.db.models.functions import Coalesce
import random

load_dotenv()

redis_url = os.getenv('REDIS_URL')
redis_client = redis.StrictRedis.from_url(redis_url)

from django.db.models import QuerySet, Q

from rest_framework.exceptions import ValidationError
from rest_framework import status


from .models import *
from groups.models import *

from .filters import GroupFilter


class UserTimeSlotService:
@staticmethod
def validate_time_range(start_time: float, end_time: float) -> None:
Expand Down Expand Up @@ -107,23 +121,18 @@ def create_group_time_slot(cls, group, day_of_week: str, start_time: float, end_
def get_group_time_slots(title) -> QuerySet[GroupTimeSlot]:
if not Group.objects.filter(title=title).exists():
raise ValidationError({'detail': 'Group not found', 'status': status.HTTP_404_NOT_FOUND})

group = Group.objects.get(title=title)
return GroupTimeSlot.objects.filter(group=group)
return group.time_slots.all()

@staticmethod
def delete_time_slot(title, time_slot_id: int) -> None:
if not Group.objects.filter(title=title).exists():
raise ValidationError({'detail': 'Group not found', 'status': status.HTTP_404_NOT_FOUND})
group = Group.objects.get(title=title)
time_slot = GroupTimeSlot.objects.filter(
id=time_slot_id,
group=group
).first()

if not time_slot:
if not group.time_slots.filter(id=time_slot_id).exists():
raise ValidationError({'detail': 'Time slot not found', 'status': status.HTTP_404_NOT_FOUND})


time_slot = group.time_slots.filter(id=time_slot_id).first()
time_slot.delete()


Expand All @@ -142,28 +151,65 @@ def get_joined_groups(user, request) -> list[Group]:
class SuggestionService:

def get_suggestions(user) -> list[Group]:

if UserSuggestions.objects.filter(user_id=user.id).exists():
group_ids = UserSuggestions.objects.get(user_id=user.id).group_ids
group_ids = group_ids.split(',')
group_ids = [int(group_id) for group_id in group_ids]
groups = Group.objects.filter(id__in=group_ids)
return groups
VALID_LEVELS = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
user_level = user.level.upper()
level_index = VALID_LEVELS.index(user_level)
levels_to_consider = [user_level]
if level_index > 0:
levels_to_consider.append(VALID_LEVELS[level_index - 1])
if level_index < len(VALID_LEVELS) - 1:
levels_to_consider.append(VALID_LEVELS[level_index + 1])

groups = Group.objects.filter(level__in=levels_to_consider).order_by('?')[:100]

groups = list(groups)

user_time_slots = UserTimeSlotService.get_user_time_slots(user)
groups = Group.objects.exclude(members=user)
group_matches = []

for group in groups:
group_time_slots = GroupTimeSlotService.get_group_time_slots(group)
total_overlap = 0

for user_slot in user_time_slots:
for group_slot in group_time_slots:
if user_slot.day_of_week == group_slot.day_of_week:
overlap = min(user_slot.end_time, group_slot.end_time) - max(user_slot.start_time, group_slot.start_time)
if overlap > 0:
total_overlap += overlap

# if total_overlap > 0:
group_matches.append((group, total_overlap))

group_matches.sort(key=lambda x: x[1], reverse=True)
group_matches = [group[0] for group in group_matches]
return group_matches


groups = Group.objects.filter(id__in=[group.id for group in groups]).annotate(
total_overlap=Coalesce(
Sum(
ExpressionWrapper(
Case(
*[
When(
time_slots__day_of_week=user_slot.day_of_week,
then=F('time_slots__end_time') - F('time_slots__start_time')
)
for user_slot in user_time_slots
],
output_field=FloatField()
),
output_field=FloatField()
)
),
Value(0.0),
output_field=FloatField()
)
)

if user.neighborhood:
groups = groups.annotate(
total_overlap=ExpressionWrapper(
F('total_overlap') * Case(
When(neighborhood=user.neighborhood, then=1.3),
default=1,
output_field=FloatField()
),
output_field=FloatField()
)
)

best_fit_groups = groups.order_by('-total_overlap')[:25]
UserSuggestions.objects.create(user_id=user.id, group_ids=','.join([str(group.id) for group in best_fit_groups]))
return best_fit_groups

class AllGroupsService:
@staticmethod
Expand Down
55 changes: 55 additions & 0 deletions home/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.db.models import F, ExpressionWrapper, FloatField, Case, When
from celery import shared_task
from django.contrib.auth import get_user_model
import redis
from dotenv import load_dotenv
import os

from groups.models import Group
from home.services import UserTimeSlotService, GroupTimeSlotService
from home.models import GroupTimeSlot

load_dotenv()

redis_url = os.getenv('REDIS_URL')
redis_client = redis.StrictRedis.from_url(redis_url)

@shared_task
def find_matching_groups(user):
loc = None
if user.neighborhood:
loc = user.neighborhood
user_time_slots = UserTimeSlotService.get_user_time_slots(user)
groups = Group.objects.all()
group_matches = []
for group in groups:
group_time_slots = GroupTimeSlotService.get_group_time_slots(group.title)
total_overlap = 0

for user_slot in user_time_slots:
for group_slot in group_time_slots:
if user_slot.day_of_week == group_slot.day_of_week:
overlap = min(user_slot.end_time, group_slot.end_time) - max(user_slot.start_time, group_slot.start_time)
if overlap > 0:
total_overlap += overlap
if group.neighborhood == loc:
total_overlap *= 1.3

if user not in group.members.all():
group_matches.append((group, total_overlap))

if len(group_matches) == 25:
break

group_matches.sort(key=lambda x: x[1], reverse=True)
group_ids = [group[0].id for group in group_matches]
return group_ids

@shared_task
def cache_matching_groups():
User = get_user_model()
users = User.objects.all()

for user in users:
matching_groups = find_matching_groups(user)
redis_client.set(f'user:{user.id}:matching_groups', matching_groups)
Loading