diff --git a/alter-scripts/alter-1.59.py b/alter-scripts/alter-1.59.py new file mode 100644 index 00000000..d7f84111 --- /dev/null +++ b/alter-scripts/alter-1.59.py @@ -0,0 +1,96 @@ +import os +import sys +import uuid +from decouple import config +import django + +from connection import execute + +os.chdir("..") +sys.path.append(os.getcwd()) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mulearnbackend.settings") +django.setup() + + +def get_recurring_lc_ids(): + query = "SELECT id, day from learning_circle where day is not null" + return execute(query) + + +def migrate_to_new_lc(): + lcs = get_recurring_lc_ids() + execute("DROP TABLE IF EXISTS circle_meet_attendees;") + execute("DROP TABLE IF EXISTS circle_meet_tasks;") + execute("DROP TABLE IF EXISTS circle_meet_attendee_report;") + execute("DROP TABLE IF EXISTS circle_meeting_log;") + execute( + """ +ALTER TABLE learning_circle + MODIFY COLUMN name VARCHAR(255), + MODIFY COLUMN circle_code VARCHAR(15), + DROP COLUMN meet_time, + DROP COLUMN day, + DROP COLUMN meet_place, + DROP FOREIGN KEY fk_learning_circle_ref_updated_by, + DROP COLUMN updated_by, + ADD COLUMN is_recurring BOOLEAN DEFAULT FALSE NOT NULL, + ADD COLUMN recurrence_type VARCHAR(10), + ADD COLUMN recurrence INT; +""" + ) + execute( + """ +CREATE TABLE circle_meeting_log +( + id VARCHAR(36) PRIMARY KEY NOT NULL, + circle_id VARCHAR(36) NOT NULL, + meet_code VARCHAR(6) NOT NULL, + title VARCHAR(100) NOT NULL, + is_report_needed BOOLEAN DEFAULT TRUE NOT NULL, + report_description VARCHAR(1000), + coord_x FLOAT NOT NULL NOT NULL, + coord_y FLOAT NOT NULL NOT NULL, + meet_place VARCHAR(255) NOT NULL, + meet_time DATETIME NOT NULL, + duration INT NOT NULL, + is_report_submitted BOOLEAN DEFAULT FALSE NOT NULL, + is_approved BOOLEAN DEFAULT FALSE NOT NULL, + report_text VARCHAR(1000), + created_by VARCHAR(36) NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + CONSTRAINT fk_circle_meeting_log_ref_circle_id FOREIGN KEY (circle_id) REFERENCES learning_circle (id) ON DELETE CASCADE, + CONSTRAINT fk_circle_meeting_log_ref_created_by FOREIGN KEY (created_by) REFERENCES user (id) ON DELETE CASCADE +); +""" + ) + execute( + """ +CREATE TABLE circle_meet_attendees ( + id VARCHAR(36) PRIMARY KEY NOT NULL, + user_id VARCHAR(36) NOT NULL, + meet_id VARCHAR(36) NOT NULL, + is_joined BOOLEAN DEFAULT FALSE NOT NULL, + joined_at DATETIME, + is_report_submitted BOOLEAN DEFAULT FALSE NOT NULL, + is_lc_approved BOOLEAN DEFAULT FALSE NOT NULL, + report_text VARCHAR(1000), + report_link VARCHAR(200), + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + CONSTRAINT fk_circle_meet_attendees_ref_meet_id FOREIGN KEY (meet_id) REFERENCES circle_meeting_log (id) ON DELETE CASCADE, + CONSTRAINT fk_circle_meet_attendees_ref_user_id FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE +); +""" + ) + for id, day in lcs: + day = str(day).split(",")[0] + query = f"""UPDATE learning_circle SET is_recurring = TRUE, recurrence_type = 'weekly', recurrence = {day} WHERE id = '{id}'""" + execute(query) + + +if __name__ == "__main__": + migrate_to_new_lc() + execute( + "UPDATE system_setting SET value = '1.59', updated_at = now() WHERE `key` = 'db.version';" + ) diff --git a/api/dashboard/learningcircle/learningcircle_serializer.py b/api/dashboard/learningcircle/learningcircle_serializer.py new file mode 100644 index 00000000..39f48576 --- /dev/null +++ b/api/dashboard/learningcircle/learningcircle_serializer.py @@ -0,0 +1,301 @@ +from datetime import datetime, timedelta, timezone + +import pytz +from db.learning_circle import LearningCircle, CircleMeetingLog, CircleMeetingAttendees +from rest_framework import serializers + +from db.organization import Organization +from db.task import InterestGroup +from utils.types import LearningCircleRecurrenceType +from utils.utils import DateTimeUtils + + +class LearningCircleCreateEditSerialzier(serializers.ModelSerializer): + ig = serializers.PrimaryKeyRelatedField( + queryset=InterestGroup.objects.all(), required=True + ) + org = serializers.PrimaryKeyRelatedField( + queryset=Organization.objects.all(), required=False, allow_null=True + ) + + def update(self, instance, validated_data): + instance.ig_id = validated_data.get("ig_id", instance.ig_id) + instance.org_id = validated_data.get("org_id", instance.org_id) + instance.is_recurring = validated_data.get( + "is_recurring", instance.is_recurring + ) + instance.recurrence_type = validated_data.get( + "recurrence_type", instance.recurrence_type + ) + instance.recurrence = validated_data.get("recurrence", instance.recurrence) + instance.updated_at = DateTimeUtils.get_current_utc_time() + instance.save() + return instance + + def create(self, validated_data): + user_id = self.context.get("user_id") + validated_data["created_by_id"] = user_id + return LearningCircle.objects.create(**validated_data) + + def validate(self, attrs): + is_recurring = attrs.get("is_recurring") + recurrence_type = attrs.get("recurrence_type") + recurrence = attrs.get("recurrence") + if not is_recurring: + attrs["recurrence_type"] = None + attrs["recurrence"] = None + else: + if not recurrence_type or not recurrence: + raise serializers.ValidationError( + "Recurrence type and recurrence are required for recurring learning circles" + ) + if recurrence_type not in LearningCircleRecurrenceType.get_all_values(): + raise serializers.ValidationError("Invalid recurrence type.") + if recurrence_type == LearningCircleRecurrenceType.WEEKLY.value: + if recurrence < 1 or recurrence > 7: + raise serializers.ValidationError( + "Recurrence should be between 1 and 7 for weekly learning circles" + ) + elif recurrence_type == LearningCircleRecurrenceType.MONTHLY.value: + if recurrence < 1 or recurrence > 28: + raise serializers.ValidationError( + "Recurrence should be between 1 and 28 for monthly learning circles" + ) + return super().validate(attrs) + + class Meta: + model = LearningCircle + fields = ["ig", "org", "is_recurring", "recurrence_type", "recurrence"] + + +class LearningCircleListSerializer(serializers.ModelSerializer): + ig = serializers.CharField(source="ig.name", read_only=True) + org = serializers.CharField(source="org.name", read_only=True, allow_null=True) + created_by = serializers.CharField(source="created_by_id.full_name", read_only=True) + next_meetup = serializers.SerializerMethodField() + + def _get_next_weekday(self, target_day: int): + today = datetime.now() + current_day = today.isoweekday() + 2 + current_day = current_day if current_day <= 7 else 1 + days_until_next = ((target_day - current_day + 7) % 7) + 1 + days_until_next = days_until_next or 7 + next_day_date = today + timedelta(days=days_until_next) + return next_day_date.date() + + def _get_month_day(self, target_day: int): + today = datetime.now() + current_day = today.day + current_month = today.month + current_year = today.year + if current_day >= target_day: + current_month += 1 + if current_month > 12: + current_month = 1 + current_year += 1 + return datetime(current_year, current_month, target_day).date() + + def get_next_meetup(self, obj): + next_meetup = ( + CircleMeetingLog.objects.filter(circle_id=obj.id) + .filter( + # meet_time__gte=DateTimeUtils.get_current_utc_time(), + is_report_submitted=False, + ) + .order_by("-meet_time") + .first() + ) + if next_meetup: + return { + **CircleMeetingLogListSerializer(next_meetup).data, + "is_scheduled": True, + } + if not obj.is_recurring: + return None + if obj.recurrence_type == LearningCircleRecurrenceType.WEEKLY.value: + return { + "is_scheduled": False, + "meet_time": self._get_next_weekday(obj.recurrence), + } + if obj.recurrence_type == LearningCircleRecurrenceType.MONTHLY.value: + return { + "is_scheduled": False, + "meet_time": self._get_month_day(obj.recurrence), + } + return {"is_scheduled": False, "meet_time": None} + + class Meta: + model = LearningCircle + fields = [ + "id", + "ig", + "org", + "is_recurring", + "recurrence_type", + "recurrence", + "created_by", + "next_meetup", + ] + + +class CircleMeetingLogCreateEditSerializer(serializers.ModelSerializer): + circle_id = serializers.PrimaryKeyRelatedField( + queryset=LearningCircle.objects.all(), required=True + ) + + def update(self, instance, validated_data): + instance.title = validated_data.get("title", instance.title) + instance.is_report_needed = validated_data.get( + "is_report_needed", instance.is_report_needed + ) + instance.report_description = validated_data.get( + "report_description", instance.report_description + ) + instance.coord_x = validated_data.get("coord_x", instance.coord_x) + instance.coord_y = validated_data.get("coord_y", instance.coord_y) + instance.meet_place = validated_data.get("meet_place", instance.meet_place) + instance.meet_time = validated_data.get("meet_time", instance.meet_time) + instance.duration = validated_data.get("duration", instance.duration) + instance.updated_at = DateTimeUtils.get_current_utc_time() + instance.save() + return instance + + def create(self, validated_data): + user_id = self.context.get("user_id") + meet_code = self.context.get("meet_code") + validated_data["created_by_id"] = user_id + validated_data["meet_code"] = meet_code + meet = CircleMeetingLog.objects.create(**validated_data) + CircleMeetingAttendees.objects.create( + meet_id=meet, user_id_id=user_id, is_joined=True, joined_at=datetime.now() + ) + return meet + + def validate(self, attrs): + is_report_needed = attrs.get("is_report_needed") + report_description = attrs.get("report_description") + if not is_report_needed: + attrs["report_description"] = None + else: + if not report_description: + raise serializers.ValidationError("Report description is required") + return super().validate(attrs) + + def validate_circle_id(self, value): + if CircleMeetingLog.objects.filter( + circle_id=value, is_report_submitted=False + ).exists(): + raise serializers.ValidationError( + "There is already an ongoing meeting for this learning circle" + ) + return value + + class Meta: + model = CircleMeetingLog + fields = [ + "circle_id", + "title", + "is_report_needed", + "report_description", + "coord_x", + "coord_y", + "meet_place", + "meet_time", + "duration", + ] + + +class CircleMeetingLogListSerializer(serializers.ModelSerializer): + circle = serializers.CharField(source="circle_id.id", read_only=True) + created_by = serializers.CharField(source="created_by_id.full_name", read_only=True) + is_started = serializers.SerializerMethodField() + is_ended = serializers.SerializerMethodField() + + def get_is_started(self, obj): + return obj.meet_time <= DateTimeUtils.get_current_utc_time() + + def get_is_ended(self, obj): + return (obj.meet_time + timedelta(hours=obj.duration + 1)) <= datetime.now( + timezone.utc + ) + + class Meta: + model = CircleMeetingLog + fields = [ + "id", + "circle", + "meet_code", + "title", + "is_report_needed", + "report_description", + "coord_x", + "coord_y", + "meet_place", + "meet_time", + "duration", + "created_by", + "is_started", + "is_ended", + "is_report_submitted", + ] + + +class CircleMeetingAttendeeSerializer(serializers.ModelSerializer): + + class Meta: + model = CircleMeetingAttendees + fields = ["is_joined", "is_report_submitted", "is_lc_approved"] + + +class CircleMeetupInfoSerializer(serializers.ModelSerializer): + title = serializers.CharField(read_only=True) + is_report_needed = serializers.BooleanField(read_only=True) + report_description = serializers.CharField(read_only=True) + coord_x = serializers.FloatField(read_only=True) + coord_y = serializers.FloatField(read_only=True) + meet_place = serializers.CharField(read_only=True) + meet_time = serializers.DateTimeField(read_only=True) + duration = serializers.IntegerField(read_only=True) + is_approved = serializers.BooleanField(read_only=True) + is_started = serializers.SerializerMethodField() + is_ended = serializers.SerializerMethodField() + attendee = serializers.SerializerMethodField() + + def get_is_started(self, obj): + return obj.meet_time <= DateTimeUtils.get_current_utc_time() + + def get_is_ended(self, obj): + return (obj.meet_time + timedelta(hours=obj.duration + 1)) <= datetime.now( + timezone.utc + ) + + def get_attendee(self, obj): + if user_id := self.context.get("user_id"): + user_obj = obj.circle_meeting_attendance_meet_id.filter( + user_id=user_id + ).first() + if not user_obj: + return None + return CircleMeetingAttendeeSerializer( + user_obj, + many=False, + ).data + return None + + class Meta: + model = CircleMeetingLog + fields = [ + "id", + "title", + "is_report_needed", + "report_description", + "coord_x", + "coord_y", + "meet_place", + "meet_time", + "duration", + "is_approved", + "is_started", + "is_ended", + "attendee", + ] diff --git a/api/dashboard/learningcircle/learningcircle_views.py b/api/dashboard/learningcircle/learningcircle_views.py new file mode 100644 index 00000000..a7c6938a --- /dev/null +++ b/api/dashboard/learningcircle/learningcircle_views.py @@ -0,0 +1,534 @@ +from datetime import datetime, timedelta, timezone +import requests +from rest_framework.views import APIView +from db.learning_circle import LearningCircle, CircleMeetingLog, CircleMeetingAttendees +from db.user import UserInterests +from utils.karma import add_karma +from utils.permission import CustomizePermission, JWTUtils +from utils.response import CustomResponse +from utils.types import Lc +from utils.utils import DateTimeUtils, generate_code +from .learningcircle_serializer import ( + CircleMeetingLogCreateEditSerializer, + CircleMeetingLogListSerializer, + CircleMeetupInfoSerializer, + LearningCircleCreateEditSerialzier, + LearningCircleListSerializer, +) +from django.db.models import Q + + +class LearningCircleView(APIView): + permission_classes = [CustomizePermission] + + def get(self, request, circle_id: str = None): + if circle_id: + learning_circle = LearningCircle.objects.get(id=circle_id) + circle_meetings = CircleMeetingLog.objects.filter( + circle_id=learning_circle, is_report_submitted=True + ) + serializer = LearningCircleListSerializer(learning_circle) + meetings_serializer = CircleMeetingLogListSerializer( + circle_meetings, many=True + ) + return CustomResponse( + general_message="Learning Circle fetched successfully", + response={**serializer.data, "past_meetups": meetings_serializer.data}, + ).get_success_response() + learning_circles = ( + LearningCircle.objects.filter(created_by_id=JWTUtils.fetch_user_id(request)) + .order_by("-created_at", "-updated_at") + .select_related("ig", "org", "created_by") + ) + serializer = LearningCircleListSerializer(learning_circles, many=True) + return CustomResponse( + general_message="Learning Circles fetched successfully", + response=serializer.data, + ).get_success_response() + + def post(self, request): + user_id = JWTUtils.fetch_user_id(request) + serializer = LearningCircleCreateEditSerialzier( + data=request.data, context={"user_id": user_id} + ) + if not serializer.is_valid(): + return CustomResponse( + general_message="Learning Circle creation failed", + response=serializer.errors, + ).get_failure_response() + result = serializer.save() + add_karma( + user_id, Lc.MEET_CREATE_HASHTAG.value, user_id, Lc.MEET_CREATE_KARMA.value + ) + return CustomResponse( + general_message="Learning Circle created successfully", + response={"circle_id": result.id}, + ).get_success_response() + + def put(self, request, circle_id: str): + user_id = JWTUtils.fetch_user_id(request) + learning_circle = LearningCircle.objects.get(id=circle_id) + if learning_circle.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to edit this Learning Circle" + ).get_failure_response() + serializer = LearningCircleCreateEditSerialzier( + learning_circle, + data=request.data, + context={"user_id": user_id}, + partial=True, + ) + if not serializer.is_valid(): + return CustomResponse( + general_message="Learning Circle update failed", + response=serializer.errors, + ).get_failure_response() + serializer.update(learning_circle, serializer.validated_data) + return CustomResponse( + general_message="Learning Circle updated successfully" + ).get_success_response() + + def delete(self, request, circle_id: str): + user_id = JWTUtils.fetch_user_id(request) + learning_circle = LearningCircle.objects.get(id=circle_id) + if learning_circle.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to delete this Learning Circle" + ).get_failure_response() + learning_circle.delete() + return CustomResponse( + general_message="Learning Circle deleted successfully" + ).get_success_response() + + +class LearningCircleMeetingInfoAPI(APIView): + def get(self, request, meet_id: str): + meet = CircleMeetingLog.objects.get(id=meet_id) + serializer = CircleMeetingLogListSerializer(meet) + return CustomResponse( + general_message="Meeting fetched successfully", + response=serializer.data, + ).get_success_response() + + +class LearningCircleMeetingView(APIView): + permission_classes = [CustomizePermission] + + def get(self, request, circle_id: str): + learning_circle = LearningCircle.objects.get(id=circle_id) + circle_meetings = CircleMeetingLog.objects.filter(circle_id=learning_circle) + serializer = CircleMeetingLogListSerializer(circle_meetings, many=True) + return CustomResponse( + general_message="Circle Meetings fetched successfully", + response=serializer.data, + ).get_success_response() + + def post(self, request): + user_id = JWTUtils.fetch_user_id(request) + meet_code = generate_code() + serializer = CircleMeetingLogCreateEditSerializer( + data=request.data, context={"user_id": user_id, "meet_code": meet_code} + ) + if not serializer.is_valid(): + return CustomResponse( + general_message="Circle Meeting creation failed", + response=serializer.errors, + ).get_failure_response() + serializer.save() + return CustomResponse( + general_message="Circle Meeting created successfully" + ).get_success_response() + + def put(self, request, meet_id: str): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + if circle_meeting.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to edit this Circle Meeting" + ).get_failure_response() + serializer = CircleMeetingLogCreateEditSerializer( + circle_meeting, + data=request.data, + context={"user_id": user_id}, + partial=True, + ) + if not serializer.is_valid(): + return CustomResponse( + general_message="Circle Meeting update failed", + response=serializer.errors, + ).get_failure_response() + serializer.update(circle_meeting, serializer.validated_data) + return CustomResponse( + general_message="Circle Meeting updated successfully" + ).get_success_response() + + def delete(self, request, meet_id: str): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.select_related( + "created_by", "circle_id" + ).get(id=meet_id) + if circle_meeting.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to delete this Circle Meeting" + ).get_failure_response() + circle_meeting.delete() + return CustomResponse( + general_message="Circle Meeting deleted successfully" + ).get_success_response() + + +class LearningCircleJoinAPI(APIView): + permission_classes = [CustomizePermission] + + def post(self, request, meet_id: str): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + is_meet_started = ( + circle_meeting.meet_time <= DateTimeUtils.get_current_utc_time() + ) + is_meet_ended = ( + circle_meeting.meet_time + timedelta(hours=circle_meeting.duration + 2) + ) <= DateTimeUtils.get_current_utc_time() + if is_meet_ended: + return CustomResponse( + general_message="The Circle Meeting has already ended" + ).get_failure_response() + is_joined = False + joined_at = None + if is_meet_started: + meet_code = request.data.get("meet_code") + if not meet_code or meet_code != circle_meeting.meet_code: + return CustomResponse( + general_message="Invalid Circle Meeting code" + ).get_failure_response() + is_joined = True + joined_at = DateTimeUtils.get_current_utc_time() + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=user_id + ).first() + if attendee: + if attendee.is_joined: + return CustomResponse( + general_message="You have already joined the Circle Meeting" + ).get_failure_response() + if not is_meet_started: + return CustomResponse( + general_message="You can only join the Circle Meeting after it has started" + ).get_failure_response() + attendee.is_joined = is_joined + attendee.joined_at = joined_at + attendee.save() + add_karma( + user_id, Lc.MEET_JOIN_HASHTAG.value, user_id, Lc.MEET_JOIN_KARMA.value + ) + return CustomResponse( + general_message="You have successfully joined the Circle Meeting" + ).get_success_response() + CircleMeetingAttendees.objects.create( + meet_id=circle_meeting, + user_id_id=user_id, + is_joined=is_joined, + joined_at=joined_at, + ) + add_karma( + user_id, Lc.MEET_JOIN_HASHTAG.value, user_id, Lc.MEET_JOIN_KARMA.value + ) + return CustomResponse( + general_message=( + "You have successfully joined the Circle Meeting" + if is_joined + else "Saved Learning Circle" + ) + ).get_success_response() + + def delete(self, request, meet_id: str): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=user_id + ).first() + if not attendee: + return CustomResponse( + general_message="You have not joined the Circle Meeting" + ).get_failure_response() + if attendee.is_report_submitted: + return CustomResponse( + general_message="You have already submitted the report" + ).get_failure_response() + attendee.delete() + return CustomResponse( + general_message=( + "You have successfully left the Circle Meeting" + if attendee.is_joined + else "Removed from saved list." + ) + ).get_success_response() + + +class LearningCircleAttendeeReportAPI(APIView): + def get(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=user_id + ).first() + if not attendee or not attendee.is_joined: + return CustomResponse( + general_message="You have not joined the Circle Meeting" + ).get_failure_response() + if not attendee.is_report_submitted: + return CustomResponse( + general_message="You have not submitted the report" + ).get_failure_response() + return CustomResponse( + general_message="Report fetched successfully", + response={ + "report": attendee.report_text, + "report_link": attendee.report_link, + }, + ).get_success_response() + + def post(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=user_id + ).first() + if not attendee or not attendee.is_joined: + return CustomResponse( + general_message="You have not joined the Circle Meeting" + ).get_failure_response() + if attendee.is_report_submitted: + return CustomResponse( + general_message="You have already submitted the report" + ).get_failure_response() + report = request.data.get("report") + report_link = request.data.get("report_link") + if not report and not report_link: + return CustomResponse( + general_message="Please provide the report or report link" + ).get_failure_response() + attendee.is_report_submitted = True + attendee.report_text = report + attendee.report_link = report_link + attendee.save() + add_karma( + user_id, + Lc.ATTENDEE_REPORT_SUBMIT_HASHTAG.value, + user_id, + Lc.ATTENDEE_REPORT_SUBMIT_KARMA.value, + ) + return CustomResponse( + general_message="You have successfully submitted the report" + ).get_success_response() + + def delete(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=user_id + ).first() + if not attendee or not attendee.is_joined: + return CustomResponse( + general_message="You have not joined the Circle Meeting" + ).get_failure_response() + if not attendee.is_report_submitted: + return CustomResponse( + general_message="You have not submitted the report" + ).get_failure_response() + if circle_meeting.is_report_submitted: + return CustomResponse( + general_message="The report has already been submitted by the Circle Meeting organizer" + ).get_failure_response() + attendee.is_report_submitted = False + attendee.report_text = None + attendee.report_link = None + attendee.save() + return CustomResponse( + general_message="You have successfully deleted the report" + ).get_success_response() + + +class LearningCircleReportAPI(APIView): + permission_classes = [CustomizePermission] + + def get(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + if circle_meeting.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to view the report" + ).get_failure_response() + attendees = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, is_joined=True + ) + return CustomResponse( + general_message="Report fetched successfully", + response={ + "is_report_submitted": circle_meeting.is_report_submitted, + "report": circle_meeting.report_text, + "attendees": { + attendee.user_id_id: { + "is_lc_approved": attendee.is_lc_approved, + "report": attendee.report_text, + "report_link": attendee.report_link, + } + for attendee in attendees + }, + }, + ).get_success_response() + + def post(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + if circle_meeting.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to submit the report" + ).get_failure_response() + if circle_meeting.is_report_submitted: + return CustomResponse( + general_message="The report has already been submitted" + ).get_failure_response() + attendees = request.data.get("attendees") + if not attendees or len(attendees) < 2: + return CustomResponse( + general_message="Need minimum of 2 attendees." + ).get_failure_response() + report = request.data.get("report") + if not report: + return CustomResponse( + general_message="Please provide the report" + ).get_failure_response() + karma_user_ids = [] + for attendee_id, approved in attendees.items(): + attendee = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, user_id_id=attendee_id + ).first() + if not attendee or not attendee.is_joined: + return CustomResponse( + general_message="Attendee has not joined the Circle Meeting" + ).get_failure_response() + if not attendee.is_report_submitted: + return CustomResponse( + general_message="Attendee has not submitted the report" + ).get_failure_response() + attendee.is_lc_approved = approved + attendee.save() + if attendee.is_lc_approved: + karma_user_ids.append(attendee_id) + circle_meeting.is_report_submitted = True + circle_meeting.report_text = report + circle_meeting.save() + add_karma( + karma_user_ids, + Lc.LC_REPORT_HASHTAG.value, + user_id, + Lc.LC_REPORT_KARMA.value, + ) + return CustomResponse( + general_message="The report has been submitted successfully" + ).get_success_response() + + def delete(self, request, meet_id): + user_id = JWTUtils.fetch_user_id(request) + circle_meeting = CircleMeetingLog.objects.get(id=meet_id) + if circle_meeting.created_by_id != user_id: + return CustomResponse( + general_message="You do not have permission to delete the report" + ).get_failure_response() + if not circle_meeting.is_report_submitted: + return CustomResponse( + general_message="The report has not been submitted" + ).get_failure_response() + if circle_meeting.is_approved: + return CustomResponse( + general_message="The report has been approved by the Learning Circle organizer" + ).get_failure_response() + attendees = CircleMeetingAttendees.objects.filter( + meet_id=circle_meeting, is_joined=True + ) + for attendee in attendees: + attendee.is_lc_approved = False + attendee.save() + circle_meeting.is_report_submitted = False + circle_meeting.report_text = None + circle_meeting.save() + return CustomResponse( + general_message="The report has been deleted successfully" + ).get_success_response() + + +class LearningCircleMeetingListAPI(APIView): + + def get(self, request): + request_data = request.query_params + category = request_data.get("category", None) + saved = request_data.get("saved", "0") + participated = request_data.get("participated", "0") + saved = str(saved).lower() in ("true", "1") + participated = str(participated).lower() in ("true", "1") + # no_location = request_data.get("no_location") + lat = request_data.get("lat") + lon = request_data.get("lon") + user_id = None + if JWTUtils.is_jwt_authenticated(request): + user_id = JWTUtils.fetch_user_id(request) + if saved or participated: + if not user_id: + return CustomResponse( + general_message="User not authenticated" + ).get_failure_response() + category = "all" + if saved and participated: + return CustomResponse( + general_message="Please provide either saved or participated" + ).get_failure_response() + if user_id and not category and category != "all": + user_id = JWTUtils.fetch_user_id(request) + interests = UserInterests.objects.filter(user_id=user_id).first() + if interests: + category = interests.choosen_interests + if category != "all" and type(category) == str: + category = [category] + # if not no_location and not lat and not lon: + # user_ip = request.META.get("REMOTE_ADDR") + # ipinfo_api_url = f"http://ip-api.com/json/{user_ip}?fields=status,lat,lon" + # response = requests.get(ipinfo_api_url) + # location_data = response.json() + # if location_data.get("status") == "success": + # lat = location_data.get("lat") + # lon = location_data.get("lon") + if saved: + filter = Q(user_id=user_id, is_joined=False) + elif participated: + filter = Q(user_id=user_id, is_joined=True) + else: + filter = Q(user_id=user_id, is_report_submitted=False) + user_meetups = ( + [] + if not user_id + else CircleMeetingAttendees.objects.filter(filter).values_list( + "meet_id_id", flat=True + ) + ) + if saved or participated: + filter = Q(id__in=user_meetups) + else: + filter = Q( + meet_time__gte=DateTimeUtils.get_current_utc_time() - timedelta(hours=2) + ) | Q(id__in=user_meetups) + meetings = ( + CircleMeetingLog.objects.filter(filter).order_by("meet_time") + # .prefetch_related("circle_meeting_attendance_meet_id") + ) + if category and category != "all" and type(category) == list: + meetings = meetings.select_related("circle_id__ig").filter( + circle_id__ig__category__in=category + ) + serializer = CircleMeetupInfoSerializer( + meetings, many=True, context={"user_id": user_id} + ) + return CustomResponse( + general_message="Meetings fetched successfully", + response=serializer.data, + ).get_success_response() diff --git a/api/dashboard/learningcircle/urls.py b/api/dashboard/learningcircle/urls.py new file mode 100644 index 00000000..c5f758eb --- /dev/null +++ b/api/dashboard/learningcircle/urls.py @@ -0,0 +1,45 @@ +from django.urls import path + +from . import learningcircle_views + +urlpatterns = [ + path("create/", learningcircle_views.LearningCircleView.as_view()), + path("list/", learningcircle_views.LearningCircleView.as_view()), + path("info//", learningcircle_views.LearningCircleView.as_view()), + path("edit//", learningcircle_views.LearningCircleView.as_view()), + path("delete//", learningcircle_views.LearningCircleView.as_view()), + path("meeting/create/", learningcircle_views.LearningCircleMeetingView.as_view()), + path("meeting/list/", learningcircle_views.LearningCircleMeetingListAPI.as_view()), + path( + "meeting/list//", + learningcircle_views.LearningCircleMeetingView.as_view(), + ), + path( + "meeting/edit//", + learningcircle_views.LearningCircleMeetingView.as_view(), + ), + path( + "meeting/info//", + learningcircle_views.LearningCircleMeetingInfoAPI.as_view(), + ), + path( + "meeting/delete//", + learningcircle_views.LearningCircleMeetingView.as_view(), + ), + path( + "meeting/join//", + learningcircle_views.LearningCircleJoinAPI.as_view(), + ), + path( + "meeting/leave//", + learningcircle_views.LearningCircleJoinAPI.as_view(), + ), + path( + "meeting/attendee-report//", + learningcircle_views.LearningCircleAttendeeReportAPI.as_view(), + ), + path( + "meeting/report//", + learningcircle_views.LearningCircleReportAPI.as_view(), + ), +] diff --git a/api/dashboard/organisation/serializers.py b/api/dashboard/organisation/serializers.py index 067ee33b..46bcb875 100644 --- a/api/dashboard/organisation/serializers.py +++ b/api/dashboard/organisation/serializers.py @@ -21,6 +21,7 @@ ) from utils.permission import JWTUtils from utils.types import OrganizationType +from utils.utils import DateTimeUtils class InstitutionSerializer(serializers.ModelSerializer): @@ -451,7 +452,7 @@ def update(self, instance, validated_data): instance.verified = validated_data.get("verified") instance.org = validated_data.get("org_id") instance.verified_by_id = self.context.get("user_id") - instance.verified_at = datetime.now(timezone.utc) + instance.verified_at = DateTimeUtils.get_current_utc_time() instance.save() if instance.verified: if UserOrganizationLink.objects.filter( diff --git a/api/dashboard/urls.py b/api/dashboard/urls.py index 03b361f7..02721ca8 100644 --- a/api/dashboard/urls.py +++ b/api/dashboard/urls.py @@ -2,29 +2,27 @@ # app_name will help us do a reverse look-up latter. urlpatterns = [ - path('user/', include('api.dashboard.user.urls')), - path('zonal/', include('api.dashboard.zonal.urls')), - path('district/', include('api.dashboard.district.urls')), - path('campus/', include('api.dashboard.campus.urls')), - path('roles/', include('api.dashboard.roles.urls')), - path('ig/', include('api.dashboard.ig.urls')), - path('task/', include('api.dashboard.task.urls')), - path('profile/', include('api.dashboard.profile.urls')), - path('lc/', include('api.dashboard.lc.urls')), - path('referral/', include('api.dashboard.referral.urls')), - path('college/', include('api.dashboard.college.urls')), - path('karma-voucher/', include('api.dashboard.karma_voucher.urls')), - path('location/', include('api.dashboard.location.urls')), - path('organisation/', include('api.dashboard.organisation.urls')), - path('dynamic-management/', include('api.dashboard.dynamic_management.urls')), - path('error-log/', include('api.dashboard.error_log.urls')), - - path('affiliation/', include('api.dashboard.affiliation.urls')), - path('channels/', include('api.dashboard.channels.urls')), - path('discord-moderator/', include('api.dashboard.discord_moderator.urls')), - path('events/', include('api.dashboard.events.urls')), - - path('coupon/', include('api.dashboard.coupon.urls')), - - path('projects/', include('api.dashboard.projects.urls')), + path("user/", include("api.dashboard.user.urls")), + path("zonal/", include("api.dashboard.zonal.urls")), + path("district/", include("api.dashboard.district.urls")), + path("campus/", include("api.dashboard.campus.urls")), + path("roles/", include("api.dashboard.roles.urls")), + path("ig/", include("api.dashboard.ig.urls")), + path("task/", include("api.dashboard.task.urls")), + path("profile/", include("api.dashboard.profile.urls")), + # path("lc/", include("api.dashboard.lc.urls")), + path("learningcircle/", include("api.dashboard.learningcircle.urls")), + path("referral/", include("api.dashboard.referral.urls")), + path("college/", include("api.dashboard.college.urls")), + path("karma-voucher/", include("api.dashboard.karma_voucher.urls")), + path("location/", include("api.dashboard.location.urls")), + path("organisation/", include("api.dashboard.organisation.urls")), + path("dynamic-management/", include("api.dashboard.dynamic_management.urls")), + path("error-log/", include("api.dashboard.error_log.urls")), + path("affiliation/", include("api.dashboard.affiliation.urls")), + path("channels/", include("api.dashboard.channels.urls")), + path("discord-moderator/", include("api.dashboard.discord_moderator.urls")), + path("events/", include("api.dashboard.events.urls")), + path("coupon/", include("api.dashboard.coupon.urls")), + path("projects/", include("api.dashboard.projects.urls")), ] diff --git a/db/learning_circle.py b/db/learning_circle.py index 23a9c5e8..3ffd7737 100644 --- a/db/learning_circle.py +++ b/db/learning_circle.py @@ -11,29 +11,22 @@ # noinspection PyPep8 class LearningCircle(models.Model): - id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4())) - name = models.CharField(max_length=255, unique=True) - circle_code = models.CharField(unique=True, max_length=36) - ig = models.ForeignKey(InterestGroup, on_delete=models.CASCADE, blank=True, - related_name="learning_circle_ig") - org = models.ForeignKey(Organization, on_delete=models.CASCADE, blank=True, null=True, - related_name="learning_circle_org") - meet_place = models.CharField(max_length=255, blank=True, null=True) - meet_time = models.CharField(max_length=10, blank=True, null=True) - day = models.CharField(max_length=20, blank=True, null=True) + id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4())) + ig = models.ForeignKey(InterestGroup, on_delete=models.CASCADE, related_name="learning_circle_ig_id") + org = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="learning_circle_org_id", null=True, blank=True) + is_recurring = models.BooleanField(default=True, null=False) + recurrence_type = models.CharField(max_length=10, blank=True, null=True) + recurrence = models.IntegerField(blank=True, null=True) note = models.CharField(max_length=500, blank=True, null=True) - updated_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column="updated_by", - related_name="learning_circle_updated_by") - updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column="created_by", related_name="learning_circle_created_by") created_at = models.DateTimeField(auto_now=True) + updated_at = models.DateTimeField(auto_now=True) class Meta: managed = False db_table = "learning_circle" - class UserCircleLink(models.Model): id = models.CharField(primary_key=True, max_length=36) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_circle_link_user') @@ -48,79 +41,43 @@ class Meta: managed = False db_table = "user_circle_link" - class CircleMeetingLog(models.Model): - id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4()), unique=True) - meet_code = models.CharField(max_length=6, default=generate_code,null=False,blank=False) - circle = models.ForeignKey(LearningCircle, on_delete=models.CASCADE, - related_name='circle_meeting_log_learning_circle') - title = models.CharField(max_length=100, null=False, blank=False) - meet_time = models.DateTimeField(null=True, blank=False) - meet_place = models.CharField(max_length=255, blank=True, null=True) - location = models.CharField(max_length=200, blank=False, null=False) - day = models.CharField(max_length=20, null=True, blank=False) - agenda = models.CharField(max_length=2000) - pre_requirements = models.CharField(max_length=1000,null=True,blank=True) - is_public = models.BooleanField(default=True, null=False) - max_attendees = models.IntegerField(default=-1, null=False, blank=False) - is_online = models.BooleanField(default=False, null=False) - report_text = models.CharField(max_length=1000, null=True, blank=True) - is_verified = models.BooleanField(default=False, null=False) - is_started = models.BooleanField(default=False, null=False) + id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4())) + circle_id = models.ForeignKey(LearningCircle, on_delete=models.CASCADE, db_column="circle_id", related_name="circle_meeting_log_circle_id") + meet_code = models.CharField(max_length=10, blank=True, null=True) + title = models.CharField(max_length=100, blank=False, null=False) + is_report_needed = models.BooleanField(default=True, null=False) + report_description = models.CharField(max_length=1000, blank=True, null=True) + coord_x = models.FloatField(blank=False, null=False) + coord_y = models.FloatField(blank=False, null=False) + meet_place = models.CharField(max_length=100, blank=False, null=False) + meet_time = models.DateTimeField(blank=False, null=False) + duration = models.IntegerField(blank=False, null=False) is_report_submitted = models.BooleanField(default=False, null=False) - images = models.ImageField(max_length=200, upload_to='lc/meet-report') - created_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column='created_by', - related_name='circle_meeting_log_created_by') + report_text = models.CharField(max_length=1000, blank=True, null=True) + is_approved = models.BooleanField(default=False, null=False) + created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column="created_by", related_name="circle_meeting_log_created_by") created_at = models.DateTimeField(auto_now=True) - updated_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column='updated_by', - related_name='circle_meeting_log_updated_by') updated_at = models.DateTimeField(auto_now=True) class Meta: managed = False - db_table = 'circle_meeting_log' + db_table = "circle_meeting_log" -class CircleMeetAttendees(models.Model): - id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4()), unique=True) - meet = models.ForeignKey(CircleMeetingLog, on_delete=models.CASCADE, related_name='circle_meet_attendees_meet') - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='circle_meet_attendees_user') - note = models.CharField(max_length=1000, blank=True, null=True) + +class CircleMeetingAttendees(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4())) + meet_id = models.ForeignKey(CircleMeetingLog, on_delete=models.CASCADE, db_column="meet_id", related_name="circle_meeting_attendance_meet_id") + user_id = models.ForeignKey(User, on_delete=models.CASCADE, db_column="user_id", related_name="circle_meeting_attendance_user_id") + is_joined = models.BooleanField(default=False, null=False) + joined_at = models.DateTimeField(blank=True, null=True) is_report_submitted = models.BooleanField(default=False, null=False) - report = models.CharField(max_length=500, null=True, blank=True) - lc_member_rating = models.IntegerField(null=True) - kal = models.ForeignKey(KarmaActivityLog,on_delete=models.SET_NULL, related_name="circle_meet_attendees_kal", null=True) - karma_given = models.IntegerField(null=True) - joined_at = models.DateTimeField(null=True, blank=False) - approved_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column='approved_by', - related_name='circle_meet_attendees_approved_by') + report_text = models.CharField(max_length=1000, blank=True, null=True) + report_link = models.CharField(max_length=100, blank=True, null=True) + is_lc_approved = models.BooleanField(default=False, null=False) created_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True) class Meta: managed = False - db_table = 'circle_meet_attendees' - -class CircleMeetTasks(models.Model): - id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4()), unique=True) - meet = models.ForeignKey(CircleMeetingLog, null=True, on_delete=models.CASCADE, related_name="circle_meet_tasks_meet") - title = models.CharField(max_length=100, null=False,blank=False) - description = models.CharField(max_length=500, null=True) - task = models.ForeignKey(TaskList, on_delete=models.CASCADE, related_name="circle_meet_tasks_task") - created_at = models.DateTimeField(auto_now=True) - - class Meta: - managed = False - db_table = 'circle_meet_tasks' - -class CircleMeetAttendeeReport(models.Model): - id = models.CharField(primary_key=True, max_length=36, default=lambda: str(uuid.uuid4()), unique=True) - meet_task = models.ForeignKey(CircleMeetTasks, on_delete=models.CASCADE, related_name="circle_meet_attendee_report_meet_task") - attendee = models.ForeignKey(CircleMeetAttendees, on_delete=models.CASCADE, related_name="circle_meet_attendee_report_attendee") - is_image = models.BooleanField(default=False,null=False,blank=False) - image_url = models.ImageField(max_length=300,upload_to="lc/meet-report/attendee/") - proof_url = models.URLField(max_length=300, null=True, blank=True) - created_at = models.DateTimeField(auto_now=True) - - class Meta: - managed = False - db_table = 'circle_meet_attendee_report' + db_table = "circle_meet_attendees" diff --git a/utils/karma.py b/utils/karma.py new file mode 100644 index 00000000..d066f705 --- /dev/null +++ b/utils/karma.py @@ -0,0 +1,72 @@ +import uuid +from db.task import KarmaActivityLog, TaskList, Wallet +from db.user import User +from utils.utils import DateTimeUtils +from django.db.models import F + + +def add_karma( + user_id: str | list[str], hashtag: str, approved_by: str, karma: int | None = None +): + task = TaskList.objects.filter(hashtag=hashtag).first() + if not task: + return False + if not karma: + karma = task.karma + if not User.objects.filter(id=approved_by).exists(): + return False + if isinstance(user_id, list): + count = User.objects.filter(id__in=user_id).count() + if count != len(user_id): + return False + user_ids = user_id + KarmaActivityLog.objects.bulk_create( + [ + KarmaActivityLog( + id=str(uuid.uuid4()), + user_id=user_id, + karma=karma, + task=task, + updated_by_id=user_id, + created_by_id=user_id, + appraiser_approved=True, + peer_approved=True, + appraiser_approved_by_id=approved_by, + peer_approved_by_id=approved_by, + task_message_id="none", + lobby_message_id="none", + dm_message_id="none", + ) + for user_id in user_ids + ] + ) + Wallet.objects.filter(user_id__in=user_ids).update( + karma=F("karma") + karma, + karma_last_updated_at=DateTimeUtils.get_current_utc_time(), + updated_at=DateTimeUtils.get_current_utc_time(), + ) + else: + if not User.objects.filter(id=user_id).exists(): + return False + KarmaActivityLog.objects.create( + id=str(uuid.uuid4()), + user_id=user_id, + karma=karma, + task=task, + updated_by_id=user_id, + created_by_id=user_id, + appraiser_approved=True, + peer_approved=True, + appraiser_approved_by_id=approved_by, + peer_approved_by_id=approved_by, + task_message_id="none", + lobby_message_id="none", + dm_message_id="none", + ) + + wallet = Wallet.objects.filter(user_id=user_id).first() + wallet.karma += karma + wallet.karma_last_updated_at = DateTimeUtils.get_current_utc_time() + wallet.updated_at = DateTimeUtils.get_current_utc_time() + wallet.save() + return True diff --git a/utils/types.py b/utils/types.py index 126ca516..a589a953 100644 --- a/utils/types.py +++ b/utils/types.py @@ -125,12 +125,21 @@ def get_all_values(cls): class Lc(Enum): RECORD_SUBMIT_KARMA = 20 RECORD_SUBMIT_HASHTAG = "#lcmeetreport" - - MEET_JOIN_KARMA = 10 - MEET_JOIN_HASHTAG = "#lcmeetjoin" - + # Karma for creating a learning circle + MEET_CREATE_KARMA = 1 + MEET_CREATE_HASHTAG = "#lc-meet-create" + # Karma for joining a learning circle + MEET_JOIN_KARMA = 1 + MEET_JOIN_HASHTAG = "#lc-meet-join" + # Karma for submitting an attendee report + ATTENDEE_REPORT_SUBMIT_KARMA = 2 + ATTENDEE_REPORT_SUBMIT_HASHTAG = "#lc-attendee-report" + # Karma for submitting a learning circle report + LC_REPORT_KARMA = 5 + LC_REPORT_HASHTAG = "#lc-meet-report" + # karma when appraiser approved VERIFY_MAX_KARMA = 200 - VERIFY_HASHTAG = "#lcmeetverify" + VERIFY_HASHTAG = "#lc-meet-verify" class CouponResponseKey(Enum): @@ -174,6 +183,15 @@ def get_all_values(cls): return [member.value for member in cls] +class LearningCircleRecurrenceType(Enum): + WEEKLY = "weekly" + MONTHLY = "monthly" + + @classmethod + def get_all_values(cls): + return [member.value for member in cls] + + DEFAULT_HACKATHON_FORM_FIELDS = { "name": "system", "gender": "system",