diff --git a/alter-scripts/alter-1.46.py b/alter-scripts/alter-1.46.py new file mode 100644 index 00000000..402a2496 --- /dev/null +++ b/alter-scripts/alter-1.46.py @@ -0,0 +1,69 @@ +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() + +from utils.utils import DiscordWebhooks +from utils.types import WebHookActions, WebHookCategory + +def is_role_exist(title): + query = f"SELECT id FROM role WHERE title = '{title}'" + return True if execute(query) else False + +def get_intrest_groups(): + query = f"SELECT name,code, created_by FROM interest_group" + return execute(query) + +def create_ig_lead_roles(): + for name,code,created_by in get_intrest_groups(): + role_name = f"{code} CampusLead" + if not is_role_exist(role_name): + query = f"""INSERT INTO role (id, title, description ,created_by, updated_by,updated_at,created_at) + VALUES ( + '{uuid.uuid4()}', + '{role_name}', + '{f'Campus Lead of {name} Interest Group'}', + '{created_by}', + '{created_by}', + UTC_TIMESTAMP, + UTC_TIMESTAMP + ) + """ + execute(query) + DiscordWebhooks.general_updates( + WebHookCategory.ROLE.value, + WebHookActions.CREATE.value, + role_name + ) + role_name = f'{code} IGLead' + if not is_role_exist(role_name): + query = f"""INSERT INTO role (id, title, description ,created_by, updated_by,updated_at,created_at) + VALUES ( + '{uuid.uuid4()}', + '{role_name}', + '{f'Interest Group Lead of {name} Interest Group'}', + '{created_by}', + '{created_by}', + UTC_TIMESTAMP, + UTC_TIMESTAMP + ) + """ + execute(query) + DiscordWebhooks.general_updates( + WebHookCategory.ROLE.value, + WebHookActions.CREATE.value, + role_name + ) + +if __name__ == '__main__': + create_ig_lead_roles() + execute("UPDATE system_setting SET value = '1.46', updated_at = now() WHERE `key` = 'db.version';") + diff --git a/api/dashboard/error_log/error_helper.py b/api/dashboard/error_log/error_helper.py new file mode 100644 index 00000000..e69de29b diff --git a/api/dashboard/error_log/error_view.py b/api/dashboard/error_log/error_view.py index 5b150562..2809dee2 100644 --- a/api/dashboard/error_log/error_view.py +++ b/api/dashboard/error_log/error_view.py @@ -1,16 +1,15 @@ import logging import os -from decouple import config +from django.conf import settings from django.http import FileResponse from rest_framework.views import APIView -from api.dashboard.error_log.log_helper import logHandler from utils.permission import CustomizePermission, role_required from utils.response import CustomResponse from utils.types import RoleType -LOG_PATH = config("LOGGER_DIR_PATH") +from .log_helper import ManageURLPatterns, logHandler class DownloadErrorLogAPI(APIView): @@ -20,7 +19,7 @@ class DownloadErrorLogAPI(APIView): [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] ) def get(self, request, log_name): - error_log = f"{LOG_PATH}/{log_name}.log" + error_log = f"{settings.LOG_PATH}/{log_name}.log" if os.path.exists(error_log): response = FileResponse( open(error_log, "rb"), content_type="application/octet-stream" @@ -39,7 +38,7 @@ class ViewErrorLogAPI(APIView): [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] ) def get(self, request, log_name): - error_log = f"{LOG_PATH}/{log_name}.log" + error_log = f"{settings.LOG_PATH}/{log_name}.log" if os.path.exists(error_log): try: with open(error_log, "r") as log_file: @@ -62,7 +61,7 @@ class ClearErrorLogAPI(APIView): [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] ) def post(self, request, log_name): - error_log = f"{LOG_PATH}/{log_name}.log" + error_log = f"{settings.LOG_PATH}/{log_name}.log" if os.path.exists(error_log): try: with open(error_log, "w") as log_file: @@ -82,27 +81,166 @@ def post(self, request, log_name): class LoggerAPI(APIView): + """ + API view for logging errors. + + Args: + request: The HTTP request object. + + Returns: + CustomResponse: The response object containing formatted error logs. + + Raises: + IOError: If there is an error reading the error log file. + + Examples: + >>> logger_api = LoggerAPI() + >>> response = logger_api.get(request) + """ + authentication_classes = [CustomizePermission] - + @role_required( [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] ) def get(self, request): - error_log = f"{LOG_PATH}/error.log" + """ + Get the error logs. + + Args: + request: The HTTP request object. + + Returns: + CustomResponse: The response object containing formatted error logs. + + Raises: + IOError: If there is an error reading the error log file. + + Examples: + >>> logger_api = LoggerAPI() + >>> response = logger_api.get(request) + """ + error_log = f"{settings.LOG_PATH}/error.log" try: with open(error_log, "r") as file: log_data = file.read() except IOError as e: return CustomResponse(response=str(e)).get_failure_response() - - log_handler = logHandler() - formatted_errors = log_handler.parse_logs(log_data) + + log_handler = logHandler(log_data) + formatted_errors = log_handler.parse_logs() return CustomResponse(response=formatted_errors).get_success_response() - + @role_required( [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] ) def patch(self, request, error_id): + """ + Patch the error log. + + Args: + request: The HTTP request object. + error_id: The ID of the error to be marked as patched. + + Returns: + CustomResponse: The response object indicating the success of the patch. + + Examples: + >>> logger_api = LoggerAPI() + >>> response = logger_api.patch(request, error_id) + """ logger = logging.getLogger("django") logger.error(f"PATCHED : {error_id}") return CustomResponse(response="Updated patch list").get_success_response() + + +class ErrorGraphAPI(APIView): + """ + A class representing the ErrorGraphAPI view. + + This view handles the GET request to retrieve formatted error data including a heatmap of URL hits, + incident information, and affected users. It requires authentication and specific roles to access. + + Args: + self: The instance of the class itself. + """ + + authentication_classes = [CustomizePermission] + + @role_required( + [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] + ) + def get(self, request): + """ + Handle the GET request to retrieve formatted error data. + + Returns: + CustomResponse: The success response containing the formatted error data. + + Raises: + IOError: If an error occurs while reading the error log file. + + """ + try: + error_log = f"{settings.LOG_PATH}/error.log" + + with open(error_log, "r") as file: + log_data = file.read() + + log_handler = logHandler(log_data) + + formatted_errors = { + "heatmap": log_handler.get_urls_heatmap(), + "incident_info": log_handler.get_incident_info(), + "affected_users": log_handler.get_affected_users(), + } + + return CustomResponse(response=formatted_errors).get_success_response() + + except IOError as e: + return CustomResponse(response=str(e)).get_failure_response() + + +class ErrorTabAPI(APIView): + """ + A class representing the ErrorTabAPI view. + + This view handles the GET request to retrieve grouped URL patterns based on user roles. + It requires authentication and specific roles to access. + + Args: + self: The instance of the class itself. + """ + + authentication_classes = [CustomizePermission] + + @role_required( + [RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value] + ) + def get(self, request): + """ + Handle the GET request to retrieve grouped URL patterns. + + Returns: + CustomResponse: The success response containing the grouped URL patterns. + + Raises: + IOError: If an error occurs while retrieving the URL patterns. + + """ + try: + error_log = f"{settings.LOG_PATH}/error.log" + + with open(error_log, "r") as file: + log_data = file.read() + + log_handler = logHandler(log_data) + parsed_errors = log_handler.parse_logs() + + urlpatterns = ManageURLPatterns().urlpatterns + grouped_patterns = ManageURLPatterns.group_patterns(urlpatterns) + + return CustomResponse(response=parsed_errors).get_success_response() + + except IOError as e: + return CustomResponse(response=str(e)).get_failure_response() diff --git a/api/dashboard/error_log/log_helper.py b/api/dashboard/error_log/log_helper.py index afea889d..93a18a16 100644 --- a/api/dashboard/error_log/log_helper.py +++ b/api/dashboard/error_log/log_helper.py @@ -1,10 +1,111 @@ import json import re -from datetime import datetime +from collections import defaultdict +from datetime import datetime, timezone + +from django.urls import Resolver404, URLPattern, URLResolver, get_resolver, resolve + +from db.user import User +from utils.utils import DateTimeUtils + + +def check_url_match(url_to_check: str, pattern_to_match: str) -> bool: + """ + Check if the given URL matches the specified pattern. + + Args: + url_to_check (str): The URL to be checked. + pattern_to_match (str): The pattern to match against. + + Returns: + bool: True if the URL matches the pattern, False otherwise. + """ + try: + match = resolve(url_to_check) + return match.url_name == pattern_to_match + except Resolver404: + return False + + +class ManageURLPatterns: + def __init__(self): + """ + Initialize a new instance of the FetchURLPatterns class. + + Args: + self: The instance of the class itself. + """ + self._url_patterns_cache = None + self.urlpatterns = self._get_url_patterns() + + def _get_url_patterns(self): + """ + Get the URL patterns by extracting them from the resolver. + + Returns: + list: The list of extracted URL patterns. + """ + if self._url_patterns_cache is not None: + return self._url_patterns_cache + self._url_patterns_cache = self._extract_url_patterns( + get_resolver().url_patterns + ) + return self._url_patterns_cache + + def _extract_url_patterns(self, url_patterns, prefix=""): + """ + Recursively extract URL patterns including those nested within 'include()'. + + Args: + url_patterns (list): The list of URL patterns to extract from. + prefix (str): The prefix to prepend to the extracted patterns. + + Returns: + list: The list of extracted URL patterns. + """ + all_patterns = [] + for pattern in url_patterns: + if isinstance(pattern, URLPattern): + # This is a final URL pattern + all_patterns.append(prefix + str(pattern.pattern)) + elif isinstance(pattern, URLResolver): + # This is an 'include()' pattern, recurse into it + nested_patterns = self._extract_url_patterns( + pattern.url_patterns, prefix + str(pattern.pattern) + ) + all_patterns.extend(nested_patterns) + return all_patterns + + @classmethod + def group_patterns(cls, urlpatterns): + """ + Group the URL patterns by their respective apps. + + Returns: + dict: The dictionary of grouped URL patterns. + """ + grouped_apis = defaultdict(lambda: defaultdict(list)) + for api in urlpatterns: + # Split the API path and extract the primary and secondary category + parts = api.split("/") + if len(parts) > 3: + primary_category = parts[2] # e.g., 'register', 'dashboard' + api_dictionary = {"url": api, "error": []} + if primary_category in ["dashboard", "integrations"]: + # Subgroup for dashboard and integrations + secondary_category = parts[3] # e.g., 'user', 'zonal' + grouped_apis[primary_category][secondary_category].append( + api_dictionary + ) + else: + # Single group for other categories + grouped_apis[primary_category]["_general"].append(api_dictionary) + return grouped_apis class logHandler: - def __init__(self) -> None: + def __init__(self, log_data) -> None: + self.log_data = log_data self.log_pattern = ( r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} ERROR EXCEPTION INFO:" r".*?(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} ERROR EXCEPTION INFO:|\Z)" @@ -12,7 +113,7 @@ def __init__(self) -> None: # Log entries their types and how to find them self.log_entries = { "id": {"regex": r"ID: (.+?)\n(?=TYPE:)", "type": str}, - "timestamp": {"regex": r"^(.+?) ERROR.*", "type": datetime}, + "timestamp": {"regex": r"\n(.+?) ERROR.*", "type": datetime}, "type": {"regex": r"TYPE: (.+?)\n(?=MESSAGE:)", "type": str}, "message": {"regex": r"MESSAGE: (.+?)\n(?=METHOD:)", "type": str}, "method": {"regex": r"METHOD: (.+?)\n(?=PATH:)", "type": str}, @@ -22,23 +123,20 @@ def __init__(self) -> None: "traceback": {"regex": r"TRACEBACK: (.+)$", "type": str}, } - def parse_logs(self, log_data: str) -> list[dict]: + def parse_logs(self) -> list[dict]: """parse a log value as str and convert it into appropriate types - Args: - log_data (str): the single log data - Returns: list[dict]: formatted errors """ self.patch_pattern = ( r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) ERROR PATCHED : (\w+)" ) - self.patched_errors = self.extract_patches(log_data) + self.patched_errors = self.extract_patches(self.log_data) # Extract all logs in string format - matches = reversed(re.findall(self.log_pattern, log_data, re.DOTALL)) + matches = reversed(re.findall(self.log_pattern, self.log_data, re.DOTALL)) formatted_errors = {} for error in matches: # Separate each item in log @@ -129,8 +227,8 @@ def get_patterns(self) -> list: return [value["regex"] for value in values] def already_patched(self, log_entry: dict) -> bool: - """checks if log entry id is in patched - errors and its timestamp is before the error was patched + """checks if log entry id is in patched + errors and its timestamp is before the error was patched """ return ( log_entry["id"] in self.patched_errors @@ -159,4 +257,61 @@ def aggregate_log_entry( and log_entry[key] and log_entry[key] not in formatted_errors[log_id][key] ): - formatted_errors[log_id][key].append(log_entry[key]) \ No newline at end of file + formatted_errors[log_id][key].append(log_entry[key]) + + def get_urls_heatmap(self): + """get the number of times each url is hit + + Args: + urlpatterns (list): the list of url patterns + + Returns: + dict: the number of times each url is hit + """ + url_hits = {} + + for url_hit in re.finditer(self.log_entries["path"]["regex"], self.log_data): + hit = url_hit.group(1) + resolved = resolve(hit) + matched_pattern = resolved.route + + if matched_pattern in url_hits: + url_hits[matched_pattern] += 1 + else: + url_hits[matched_pattern] = 1 + + return url_hits + + def get_incident_info(self): + """Get the time since the last incident in UTC. + + Returns: + str: The time since the last incident in UTC. + """ + last_incident = re.findall( + self.log_entries["timestamp"]["regex"], self.log_data + )[-1] + + last_incident_datetime = self.get_formatted_time(last_incident).replace( + tzinfo=timezone.utc + ) + current_datetime = DateTimeUtils.get_current_utc_time() + + time_since_then = current_datetime - last_incident_datetime + + return { + "last_incident": last_incident_datetime, + "time_since_then": time_since_then.total_seconds(), + } + + def get_affected_users(self): + """Get the number of affected users. + + Returns: + int: The number of affected users. + """ + affected_users = set( + re.findall(r"\n *\"muid\" *: * \"(.+?@mulearn)\",", self.log_data) + ) + + return (len(affected_users) / User.objects.count()) * 100 diff --git a/api/dashboard/error_log/urls.py b/api/dashboard/error_log/urls.py index f9e37ccf..979419e0 100644 --- a/api/dashboard/error_log/urls.py +++ b/api/dashboard/error_log/urls.py @@ -4,6 +4,8 @@ urlpatterns = [ path('', error_view.LoggerAPI.as_view()), + path('graph/', error_view.ErrorGraphAPI.as_view()), + path('tab/', error_view.ErrorTabAPI.as_view()), path('patch//', error_view.LoggerAPI.as_view()), path('/', error_view.DownloadErrorLogAPI.as_view()), path('view//', error_view.ViewErrorLogAPI.as_view()), diff --git a/api/dashboard/ig/dash_ig_view.py b/api/dashboard/ig/dash_ig_view.py index b21d6f60..f1c53d8a 100644 --- a/api/dashboard/ig/dash_ig_view.py +++ b/api/dashboard/ig/dash_ig_view.py @@ -11,7 +11,8 @@ InterestGroupSerializer, InterestGroupCreateUpdateSerializer, ) - +from api.dashboard.roles.dash_roles_serializer import RoleDashboardSerializer +from db.user import Role class InterestGroupAPI(APIView): authentication_classes = [CustomizePermission] @@ -64,10 +65,47 @@ def post(self, request): if serializer.is_valid(): serializer.save() + role_serializer = RoleDashboardSerializer(data={ + 'title': request_data.get("name"), + 'description': request_data.get("name") + " Interest Group Member", + 'created_by': request_data.get("created_by"), + 'updated_by': request_data.get("updated_by"), + },context={'request': request}) + + if role_serializer.is_valid(): + role_serializer.save() + else: + return CustomResponse(general_message=role_serializer.errors).get_failure_response() + + campus_role_serializer = RoleDashboardSerializer(data={ + 'title': RoleType.ig_campus_lead_role(request_data.get("code")), + 'description': request_data.get("name") + " Intrest Group Campus Lead", + 'created_by': request_data.get("created_by"), + 'updated_by': request_data.get("updated_by"), + },context={'request': request}) + + if campus_role_serializer.is_valid(): + campus_role_serializer.save() + else: + return CustomResponse(general_message=campus_role_serializer.errors).get_failure_response() + + ig_lead_role_serializer = RoleDashboardSerializer(data={ + 'title': RoleType.get_ig_lead_role(request_data.get("code")), + 'description': request_data.get("name") + " Interest Group Lead", + 'created_by': request_data.get("created_by"), + 'updated_by': request_data.get("updated_by"), + },context={'request': request}) + + if ig_lead_role_serializer.is_valid(): + ig_lead_role_serializer.save() + else: + return CustomResponse(general_message=ig_lead_role_serializer.errors).get_failure_response() + DiscordWebhooks.general_updates( WebHookCategory.INTEREST_GROUP.value, WebHookActions.CREATE.value, request_data.get("name"), + request_data.get("code") ) return CustomResponse( @@ -82,6 +120,7 @@ def put(self, request, pk): ig = InterestGroup.objects.get(id=pk) ig_old_name = ig.name + ig_old_code = ig.code request_data = request.data request_data["updated_by"] = user_id @@ -93,12 +132,36 @@ def put(self, request, pk): if serializer.is_valid(): serializer.save() ig_new_name = ig.name - + ig_new_code = ig.code + + ig_role = Role.objects.filter(title=ig_old_name).first() + + if ig_role: + ig_role.title = ig_new_name + ig_role.description = ig_new_name + " Interest Group Member" + ig_role.save() + + ig_campus_lead_role = Role.objects.filter(title=RoleType.ig_campus_lead_role(ig_old_code)).first() + + if ig_campus_lead_role: + ig_campus_lead_role.title = ig_new_code+' CampusLead' + ig_campus_lead_role.description = ig_new_name + " Interest Group Campus Lead" + ig_campus_lead_role.save() + + ig_lead_role = Role.objects.filter(title=RoleType.get_ig_lead_role(ig_old_code)).first() + + if ig_lead_role: + ig_lead_role.title = RoleType.get_ig_lead_role(ig_new_code) + ig_lead_role.description = ig_new_name + " Interest Group Lead" + ig_lead_role.save() + DiscordWebhooks.general_updates( WebHookCategory.INTEREST_GROUP.value, WebHookActions.EDIT.value, ig_new_name, + ig_new_code, ig_old_name, + ig_old_code ) return CustomResponse( response={"interestGroup": serializer.data} @@ -112,13 +175,19 @@ def delete(self, request, pk): if ig is None: return CustomResponse(general_message="invalid ig").get_success_response() - + ig_role = Role.objects.filter(title=ig.name).first() + if ig_role:ig_role.delete() + ig_campus_role = Role.objects.filter(title=RoleType.ig_campus_lead_role(ig.code)).first() + if ig_campus_role:ig_campus_role.delete() + ig_lead_role = Role.objects.filter(title=RoleType.get_ig_lead_role(ig.code)).first() + if ig_lead_role:ig_lead_role.delete() ig.delete() DiscordWebhooks.general_updates( WebHookCategory.INTEREST_GROUP.value, WebHookActions.DELETE.value, ig.name, + ig.code ) return CustomResponse( general_message="ig deleted successfully" diff --git a/api/dashboard/lc/dash_lc_view.py b/api/dashboard/lc/dash_lc_view.py index 09e00004..49ccb2c6 100644 --- a/api/dashboard/lc/dash_lc_view.py +++ b/api/dashboard/lc/dash_lc_view.py @@ -1,31 +1,41 @@ import uuid from collections import defaultdict -from decouple import config +from django.conf import settings from django.core.mail import send_mail -from django.db.models import Q, F, Value, CharField -from django.db.models.functions import Concat +from django.db.models import Q from django.shortcuts import redirect from rest_framework.views import APIView -from rest_framework import authentication from api.notification.notifications_utils import NotificationUtils -from db.learning_circle import LearningCircle, UserCircleLink, CircleMeetingLog +from db.learning_circle import CircleMeetingLog, LearningCircle, UserCircleLink +from db.task import TaskList from db.user import User -from db.task import TaskList, KarmaActivityLog from utils.permission import JWTUtils from utils.response import CustomResponse -from utils.utils import send_template_mail, DateTimeUtils,DiscordWebhooks -from .dash_lc_serializer import LearningCircleSerializer, LearningCircleCreateSerializer, LearningCircleDetailsSerializer, \ - LearningCircleUpdateSerializer, LearningCircleJoinSerializer, \ - LearningCircleMainSerializer, LearningCircleNoteSerializer, LearningCircleStatsSerializer, \ - LearningCircleMemberListSerializer, MeetRecordsCreateEditDeleteSerializer, IgTaskDetailsSerializer, \ - ScheduleMeetingSerializer, ListAllMeetRecordsSerializer, AddMemberSerializer - -from .dash_ig_helper import is_learning_circle_member, is_valid_learning_circle, get_today_start_end, get_week_start_end - -domain = config("FR_DOMAIN_NAME") -from_mail = config("FROM_MAIL") +from utils.utils import DateTimeUtils, send_template_mail + +from .dash_ig_helper import ( + get_today_start_end, + get_week_start_end, + is_learning_circle_member, + is_valid_learning_circle, +) +from .dash_lc_serializer import ( + AddMemberSerializer, + IgTaskDetailsSerializer, + LearningCircleCreateSerializer, + LearningCircleDetailsSerializer, + LearningCircleJoinSerializer, + LearningCircleMainSerializer, + LearningCircleMemberListSerializer, + LearningCircleNoteSerializer, + LearningCircleSerializer, + LearningCircleStatsSerializer, + LearningCircleUpdateSerializer, + MeetRecordsCreateEditDeleteSerializer, + ScheduleMeetingSerializer, +) class UserLearningCircleListApi(APIView): @@ -44,26 +54,21 @@ def get(self, request): # Lists user's learning circle learning_queryset = LearningCircle.objects.filter( user_circle_link_circle__user_id=user_id, - user_circle_link_circle__accepted=1 + user_circle_link_circle__accepted=1, ) - learning_serializer = LearningCircleSerializer( - learning_queryset, - many=True - ) + learning_serializer = LearningCircleSerializer(learning_queryset, many=True) - return CustomResponse( - response=learning_serializer.data - ).get_success_response() + return CustomResponse(response=learning_serializer.data).get_success_response() class LearningCircleMainApi(APIView): def post(self, request): all_circles = LearningCircle.objects.all() if JWTUtils.is_logged_in(request): - ig_id = request.data.get('ig_id') - org_id = request.data.get('org_id') - district_id = request.data.get('district_id') + ig_id = request.data.get("ig_id") + org_id = request.data.get("org_id") + district_id = request.data.get("district_id") if district_id: all_circles = all_circles.filter(org__district_id=district_id) @@ -75,72 +80,52 @@ def post(self, request): all_circles = all_circles.filter(ig_id=ig_id) if ig_id or org_id or district_id: - serializer = LearningCircleMainSerializer( - all_circles, - many=True - ) + serializer = LearningCircleMainSerializer(all_circles, many=True) else: - - random_circles = all_circles.exclude( - Q(meet_time__isnull=True) | Q(meet_time='') and - Q(meet_place__isnull=True) | Q(meet_place='') - ).order_by('?')[:9] + Q(meet_time__isnull=True) | Q(meet_time="") + and Q(meet_place__isnull=True) | Q(meet_place="") + ).order_by("?")[:9] # random_circles = all_circles.order_by('?')[:9] - serializer = LearningCircleMainSerializer( - random_circles, - many=True - ) - sorted_data = sorted(serializer.data, key=lambda x: x.get('karma', 0), reverse=True) - return CustomResponse( - response=sorted_data - ).get_success_response() + serializer = LearningCircleMainSerializer(random_circles, many=True) + sorted_data = sorted( + serializer.data, key=lambda x: x.get("karma", 0), reverse=True + ) + return CustomResponse(response=sorted_data).get_success_response() else: - - random_circles = all_circles.exclude( - Q(meet_time__isnull=True) | Q(meet_time='') & - Q(meet_place__isnull=True) | Q(meet_place='') - ).order_by('?')[:9] + Q(meet_time__isnull=True) + | Q(meet_time="") & Q(meet_place__isnull=True) + | Q(meet_place="") + ).order_by("?")[:9] - serializer = LearningCircleMainSerializer( - random_circles, - many=True - ) + serializer = LearningCircleMainSerializer(random_circles, many=True) # for ordered_dict in serializer.data: # ordered_dict.pop('ismember', None) - sorted_data = sorted(serializer.data, key=lambda x: x.get('karma', 0), reverse=True) - return CustomResponse( - response=sorted_data - ).get_success_response() - - - + sorted_data = sorted( + serializer.data, key=lambda x: x.get("karma", 0), reverse=True + ) + return CustomResponse(response=sorted_data).get_success_response() class LearningCircleStatsAPI(APIView): """ - API endpoint for retrieving basic data about all learning circles. + API endpoint for retrieving basic data about all learning circles. - Endpoint: /api/v1/dashboard/lc/data/ (GET) + Endpoint: /api/v1/dashboard/lc/data/ (GET) - Returns: - CustomResponse: A custom response containing data about all learning circles. - """ + Returns: + CustomResponse: A custom response containing data about all learning circles. + """ def get(self, request): learning_circle = LearningCircle.objects.all() - serializer = LearningCircleStatsSerializer( - learning_circle, - many=False - ) + serializer = LearningCircleStatsSerializer(learning_circle, many=False) - return CustomResponse( - response=serializer.data - ).get_success_response() + return CustomResponse(response=serializer.data).get_success_response() class LearningCircleCreateApi(APIView): @@ -148,24 +133,17 @@ def post(self, request): user_id = JWTUtils.fetch_user_id(request) serializer = LearningCircleCreateSerializer( - data=request.data, - context={ - 'user_id': user_id - } + data=request.data, context={"user_id": user_id} ) if serializer.is_valid(): circle = serializer.save() return CustomResponse( - general_message='LearningCircle created successfully', - response={ - 'circle_id': circle.id - } + general_message="LearningCircle created successfully", + response={"circle_id": circle.id}, ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() class LearningCircleListMembersApi(APIView): @@ -173,26 +151,18 @@ def get(self, request, circle_id): # learning_circle = LearningCircle.objects.filter( # id=circle_id # ) - user_learning_circle = UserCircleLink.objects.filter( - circle_id=circle_id - ) + user_learning_circle = UserCircleLink.objects.filter(circle_id=circle_id) if user_learning_circle is None: return CustomResponse( - general_message='Learning Circle Not Exists' + general_message="Learning Circle Not Exists" ).get_failure_response() serializer = LearningCircleMemberListSerializer( - user_learning_circle, - many=True, - context={ - 'circle_id': circle_id - } + user_learning_circle, many=True, context={"circle_id": circle_id} ) - return CustomResponse( - response=serializer.data - ).get_success_response() + return CustomResponse(response=serializer.data).get_success_response() class TotalLearningCircleListApi(APIView): @@ -202,45 +172,31 @@ def post(self, request, circle_code=None): filters &= ~Q( user_circle_link_circle__accepted=1, - user_circle_link_circle__user_id=user_id + user_circle_link_circle__user_id=user_id, ) - if district_id := request.data.get('district_id'): - filters &= Q( - org__district_id=district_id - ) - if org_id := request.data.get('org_id'): + if district_id := request.data.get("district_id"): + filters &= Q(org__district_id=district_id) + if org_id := request.data.get("org_id"): filters &= Q(org_id=org_id) - if interest_group_id := request.data.get('ig_id'): + if interest_group_id := request.data.get("ig_id"): filters &= Q(ig_id=interest_group_id) if circle_code: if not LearningCircle.objects.filter( - Q(circle_code=circle_code) | - Q(name__icontains=circle_code) + Q(circle_code=circle_code) | Q(name__icontains=circle_code) ).exists(): - return CustomResponse( - general_message='invalid circle code or Circle Name' + general_message="invalid circle code or Circle Name" ).get_failure_response() - filters &= ( - Q(circle_code=circle_code) | - Q(name__icontains=circle_code) - ) + filters &= Q(circle_code=circle_code) | Q(name__icontains=circle_code) - learning_queryset = LearningCircle.objects.filter( - filters - ) + learning_queryset = LearningCircle.objects.filter(filters) - learning_serializer = LearningCircleSerializer( - learning_queryset, - many=True - ) + learning_serializer = LearningCircleSerializer(learning_queryset, many=True) - return CustomResponse( - response=learning_serializer.data - ).get_success_response() + return CustomResponse(response=learning_serializer.data).get_success_response() class LearningCircleJoinApi(APIView): @@ -249,34 +205,25 @@ def post(self, request, circle_id): user = User.objects.filter(id=user_id).first() - full_name = f'{user.full_name}' + full_name = f"{user.full_name}" serializer = LearningCircleJoinSerializer( - data=request.data, - context={ - 'user_id': user_id, - 'circle_id': circle_id - } + data=request.data, context={"user_id": user_id, "circle_id": circle_id} ) if serializer.is_valid(): serializer.save() - lead = UserCircleLink.objects.filter( - circle_id=circle_id, lead=True).first() + lead = UserCircleLink.objects.filter(circle_id=circle_id, lead=True).first() NotificationUtils.insert_notification( user=lead.user, title="Member Request", description=f"{full_name} has requested to join your learning circle", button="LC", - url=f'{domain}/api/v1/dashboard/lc/{circle_id}/{user_id}/', - created_by=user + url=f"{settings.FR_DOMAIN_NAME}/api/v1/dashboard/lc/{circle_id}/{user_id}/", + created_by=user, ) - return CustomResponse( - general_message='Request sent' - ).get_success_response() + return CustomResponse(general_message="Request sent").get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() class LearningCircleDetailsApi(APIView): @@ -285,196 +232,173 @@ def get(self, request, circle_id, member_id=None): if not is_valid_learning_circle(circle_id): return CustomResponse( - general_message='invalid learning circle' + general_message="invalid learning circle" ).get_failure_response() if not is_learning_circle_member(user_id, circle_id): return CustomResponse( - general_message='unauthorized access' + general_message="unauthorized access" ).get_failure_response() - learning_circle = LearningCircle.objects.filter( - id=circle_id - ).first() + learning_circle = LearningCircle.objects.filter(id=circle_id).first() serializer = LearningCircleDetailsSerializer( learning_circle, many=False, - context={ - "user_id": user_id, - "circle_id": circle_id - } + context={"user_id": user_id, "circle_id": circle_id}, ) - return CustomResponse( - response=serializer.data - ).get_success_response() + return CustomResponse(response=serializer.data).get_success_response() def post(self, request, member_id, circle_id): - learning_circle_link = UserCircleLink.objects.filter( - user_id=member_id, - circle_id=circle_id + user_id=member_id, circle_id=circle_id ).first() if learning_circle_link is None: return CustomResponse( - general_message='User not part of circle' + general_message="User not part of circle" ).get_failure_response() serializer = LearningCircleUpdateSerializer() serializer.destroy(learning_circle_link) return CustomResponse( - general_message='Removed successfully' + general_message="Removed successfully" ).get_success_response() def patch(self, request, member_id, circle_id): user_id = JWTUtils.fetch_user_id(request) - if not UserCircleLink.objects.filter( - user_id=member_id, - circle_id=circle_id - ).exists() or not LearningCircle.objects.filter( - id=circle_id - ).exists(): - + if ( + not UserCircleLink.objects.filter( + user_id=member_id, circle_id=circle_id + ).exists() + or not LearningCircle.objects.filter(id=circle_id).exists() + ): return CustomResponse( - general_message='Learning Circle Not Available' + general_message="Learning Circle Not Available" ).get_failure_response() learning_circle_link = UserCircleLink.objects.filter( - user_id=member_id, - circle_id=circle_id + user_id=member_id, circle_id=circle_id ).first() if learning_circle_link.accepted is not None: return CustomResponse( - general_message='Already evaluated' + general_message="Already evaluated" ).get_failure_response() serializer = LearningCircleUpdateSerializer( - learning_circle_link, - data=request.data, - context={ - 'user_id': user_id - } + learning_circle_link, data=request.data, context={"user_id": user_id} ) if serializer.is_valid(): serializer.save() - is_accepted = request.data.get('is_accepted') + is_accepted = request.data.get("is_accepted") user = User.objects.filter(id=member_id).first() - if is_accepted == '1': + if is_accepted == "1": NotificationUtils.insert_notification( - user, title="Request approved", + user, + title="Request approved", description="You request to join the learning circle has been approved", button="LC", - url=f'{domain}/api/v1/dashboard/lc/{circle_id}/', - created_by=User.objects.filter(id=user_id).first()) + url=f"{settings.FR_DOMAIN_NAME}/api/v1/dashboard/lc/{circle_id}/", + created_by=User.objects.filter(id=user_id).first(), + ) else: NotificationUtils.insert_notification( - user, title="Request rejected", + user, + title="Request rejected", description="You request to join the learning circle has been rejected", button="LC", - url=f'{domain}/api/v1/dashboard/lc/join', - created_by=User.objects.filter(id=user_id).first()) + url=f"{settings.FR_DOMAIN_NAME}/api/v1/dashboard/lc/join", + created_by=User.objects.filter(id=user_id).first(), + ) return CustomResponse( - general_message='Approved successfully' + general_message="Approved successfully" ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() def put(self, request, circle_id): learning_circle = LearningCircle.objects.filter(id=circle_id).first() - serializer = LearningCircleNoteSerializer( - learning_circle, data=request.data) + serializer = LearningCircleNoteSerializer(learning_circle, data=request.data) if serializer.is_valid(): serializer.save() return CustomResponse( - general_message='Note updated successfully').get_success_response() + general_message="Note updated successfully" + ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() def delete(self, request, circle_id): user_id = JWTUtils.fetch_user_id(request) usr_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, - user__id=user_id + circle__id=circle_id, user__id=user_id ).first() if not usr_circle_link: - return CustomResponse(general_message='User not part of circle').get_failure_response() + return CustomResponse( + general_message="User not part of circle" + ).get_failure_response() if usr_circle_link.lead: if ( - next_lead := UserCircleLink.objects.filter( - circle__id=circle_id, accepted=1 - ) + next_lead := UserCircleLink.objects.filter( + circle__id=circle_id, accepted=1 + ) .exclude(user__id=user_id) - .order_by('accepted_at') + .order_by("accepted_at") .first() ): next_lead.lead = True next_lead.save() usr_circle_link.delete() - return CustomResponse(general_message='Leadership transferred').get_success_response() + return CustomResponse( + general_message="Leadership transferred" + ).get_success_response() usr_circle_link.delete() if not UserCircleLink.objects.filter(circle__id=circle_id).exists(): - if learning_circle := LearningCircle.objects.filter( - id=circle_id - ).first(): + if learning_circle := LearningCircle.objects.filter(id=circle_id).first(): learning_circle.delete() - return CustomResponse(general_message='Learning Circle Deleted').get_success_response() + return CustomResponse( + general_message="Learning Circle Deleted" + ).get_success_response() - return CustomResponse(general_message='Left').get_success_response() + return CustomResponse(general_message="Left").get_success_response() class SingleReportDetailAPI(APIView): - def get(self, request, circle_id, report_id=None): circle_meeting_log = CircleMeetingLog.objects.get(id=report_id) serializer = MeetRecordsCreateEditDeleteSerializer( - circle_meeting_log, - many=False + circle_meeting_log, many=False ) - return CustomResponse( - response=serializer.data - ).get_success_response() + return CustomResponse(response=serializer.data).get_success_response() def post(self, request, circle_id): user_id = JWTUtils.fetch_user_id(request) - time = request.data.get('time') - - + time = request.data.get("time") + serializer = MeetRecordsCreateEditDeleteSerializer( data=request.data, - context={ - 'user_id': user_id, - 'circle_id': circle_id, - 'time': time - } + context={"user_id": user_id, "circle_id": circle_id, "time": time}, ) if serializer.is_valid(): circle_meet_log = serializer.save() return CustomResponse( - general_message=f'Meet scheduled at {circle_meet_log.meet_time}' + general_message=f"Meet scheduled at {circle_meet_log.meet_time}" ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() # def patch(self, request, circle_id): # user_id = JWTUtils.fetch_user_id(request) @@ -507,30 +431,26 @@ def patch(self, request, circle_id, new_lead_id): user_id = JWTUtils.fetch_user_id(request) user_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, - user__id=user_id + circle__id=circle_id, user__id=user_id ).first() new_lead_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, - user__id=new_lead_id + circle__id=circle_id, user__id=new_lead_id ).first() - if not LearningCircle.objects.filter( - id=circle_id - ).exists(): + if not LearningCircle.objects.filter(id=circle_id).exists(): return CustomResponse( - general_message='Learning Circle not found' + general_message="Learning Circle not found" ).get_failure_response() if user_circle_link is None or user_circle_link.lead != 1: return CustomResponse( - general_message='User is not lead' + general_message="User is not lead" ).get_failure_response() if new_lead_circle_link is None: return CustomResponse( - general_message='New lead not found in the circle' + general_message="New lead not found in the circle" ).get_failure_response() user_circle_link.lead = None @@ -539,24 +459,28 @@ def patch(self, request, circle_id, new_lead_id): new_lead_circle_link.save() return CustomResponse( - general_message='Lead transferred successfully' + general_message="Lead transferred successfully" ).get_success_response() class LearningCircleInviteLeadAPI(APIView): - def post(self, request): - circle_id = request.POST.get('lc') - muid = request.POST.get('muid') + circle_id = request.POST.get("lc") + muid = request.POST.get("muid") user_id = JWTUtils.fetch_user_id(request) usr_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, user__id=user_id).first() + circle__id=circle_id, user__id=user_id + ).first() if not usr_circle_link: - return CustomResponse(general_message='User not part of circle').get_failure_response() + return CustomResponse( + general_message="User not part of circle" + ).get_failure_response() if usr_circle_link.lead: user = User.objects.filter(muid=muid).first() if not user: - return CustomResponse(general_message='Muid is Invalid').get_failure_response() + return CustomResponse( + general_message="Muid is Invalid" + ).get_failure_response() # send_template_mail( # context=user, # subject="LC µFAM IS HERE!", @@ -565,11 +489,11 @@ def post(self, request): send_mail( "LC Invite", "Join our lc", - from_mail, + settings.FROM_MAIL, [user.email], fail_silently=False, ) - return CustomResponse(general_message='User Invited').get_success_response() + return CustomResponse(general_message="User Invited").get_success_response() class LearningCircleInviteMemberAPI(APIView): @@ -587,35 +511,30 @@ def post(self, request, circle_id, muid): user = User.objects.filter(muid=muid).first() if not user: return CustomResponse( - general_message='Muid is Invalid' + general_message="Muid is Invalid" ).get_failure_response() user_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, - user__id=user.id + circle__id=circle_id, user__id=user.id ).first() if user_circle_link: if user_circle_link.accepted: - return CustomResponse( - general_message='User already part of circle' + general_message="User already part of circle" ).get_failure_response() elif user_circle_link.is_invited: return CustomResponse( - general_message='User already invited' + general_message="User already invited" ).get_failure_response() receiver_email = user.email html_address = ["lc_invitation.html"] - inviter = User.objects.filter( - id=JWTUtils.fetch_user_id(request)).first() + inviter = User.objects.filter(id=JWTUtils.fetch_user_id(request)).first() inviter_name = inviter.full_name context = { - "circle_name": LearningCircle.objects.filter( - id=circle_id - ).first().name, + "circle_name": LearningCircle.objects.filter(id=circle_id).first().name, "inviter_name": inviter_name, "circle_id": circle_id, "muid": muid, @@ -637,13 +556,9 @@ def post(self, request, circle_id, muid): created_at=DateTimeUtils.get_current_utc_time(), ) - return CustomResponse( - general_message='User Invited' - ).get_success_response() + return CustomResponse(general_message="User Invited").get_success_response() - return CustomResponse( - general_message='Mail not sent' - ).get_failure_response() + return CustomResponse(general_message="Mail not sent").get_failure_response() class LearningCircleInvitationStatus(APIView): @@ -662,17 +577,16 @@ def post(self, request, circle_id, muid, status): user = User.objects.filter(muid=muid).first() if not user: return CustomResponse( - general_message='Muid is Invalid' + general_message="Muid is Invalid" ).get_failure_response() user_circle_link = UserCircleLink.objects.filter( - circle__id=circle_id, - user__id=user.id + circle__id=circle_id, user__id=user.id ).first() if not user_circle_link: return CustomResponse( - general_message='User not invited' + general_message="User not invited" ).get_failure_response() if status == "accepted": @@ -680,26 +594,21 @@ def post(self, request, circle_id, muid, status): user_circle_link.accepted_at = DateTimeUtils.get_current_utc_time() user_circle_link.save() # return CustomResponse(general_message='User added to circle').get_success_response() - return redirect(f'{domain}/dashboard/learning-circle/') + return redirect(f"{settings.FR_DOMAIN_NAME}/dashboard/learning-circle/") elif status == "rejected": user_circle_link.delete() return CustomResponse( - general_message='User rejected invitation' + general_message="User rejected invitation" ).get_failure_response() class ScheduleMeetAPI(APIView): def put(self, request, circle_id): - learning_circle = LearningCircle.objects.filter( - id=circle_id - ).first() + learning_circle = LearningCircle.objects.filter(id=circle_id).first() - serializer = ScheduleMeetingSerializer( - learning_circle, - data=request.data - ) + serializer = ScheduleMeetingSerializer(learning_circle, data=request.data) if serializer.is_valid(): data = serializer.save() @@ -707,15 +616,14 @@ def put(self, request, circle_id): general_message=f"meet scheduled on {data.meet_time}" ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() class IgTaskDetailsAPI(APIView): def get(self, request, circle_id): task_list = TaskList.objects.filter( - ig__learning_circle_ig__id=circle_id).order_by('level__level_order') + ig__learning_circle_ig__id=circle_id + ).order_by("level__level_order") serializer = IgTaskDetailsSerializer( task_list, many=True, @@ -723,43 +631,37 @@ def get(self, request, circle_id): serialized_data = serializer.data grouped_tasks = defaultdict(list) for task in serialized_data: - task_level = task['task_level'] - task.pop('task_level') - grouped_tasks[f'Level {task_level}'].append(task) + task_level = task["task_level"] + task.pop("task_level") + grouped_tasks[f"Level {task_level}"].append(task) grouped_tasks_dict = dict(grouped_tasks) - return CustomResponse( - response=grouped_tasks_dict - ).get_success_response() + return CustomResponse(response=grouped_tasks_dict).get_success_response() class AddMemberAPI(APIView): def post(self, request, circle_id): - muid = request.data.get('muid') + muid = request.data.get("muid") user = User.objects.filter(muid=muid).first() if not user: - return CustomResponse( - general_message="invalid user" - ).get_failure_response() + return CustomResponse(general_message="invalid user").get_failure_response() serializer = AddMemberSerializer( data=request.data, context={ - 'user': user, - 'muid': muid, - 'circle_id': circle_id, - } + "user": user, + "muid": muid, + "circle_id": circle_id, + }, ) if serializer.is_valid(): serializer.save() return CustomResponse( - general_message='user added successfully' + general_message="user added successfully" ).get_success_response() - return CustomResponse( - message=serializer.errors - ).get_failure_response() + return CustomResponse(message=serializer.errors).get_failure_response() class ValidateUserMeetCreateAPI(APIView): @@ -768,12 +670,12 @@ def get(self, request, circle_id): if not is_valid_learning_circle(circle_id): return CustomResponse( - general_message='invalid learning circle' + general_message="invalid learning circle" ).get_failure_response() if not is_learning_circle_member(user_id, circle_id): return CustomResponse( - general_message='unauthorized access' + general_message="unauthorized access" ).get_failure_response() today_date_time = DateTimeUtils.get_current_utc_time() @@ -782,27 +684,20 @@ def get(self, request, circle_id): start_of_week, end_of_week = get_week_start_end(today_date_time) if CircleMeetingLog.objects.filter( - circle_id=circle_id, - meet_time__range=( - start_of_day, - end_of_day - ) + circle_id=circle_id, meet_time__range=(start_of_day, end_of_day) ).exists(): return CustomResponse( - general_message=f'Another meet already scheduled on {today_date_time.date()}' + general_message=f"Another meet already scheduled on {today_date_time.date()}" ).get_failure_response() - if CircleMeetingLog.objects.filter( - circle_id=circle_id, - meet_time__range=( - start_of_week, - end_of_week - ) - ).count() >= 5: + if ( + CircleMeetingLog.objects.filter( + circle_id=circle_id, meet_time__range=(start_of_week, end_of_week) + ).count() + >= 5 + ): return CustomResponse( - general_message='you can create only 5 meeting in a week' + general_message="you can create only 5 meeting in a week" ).get_failure_response() - return CustomResponse( - general_message='success' - ).get_success_response() + return CustomResponse(general_message="success").get_success_response() diff --git a/api/dashboard/profile/profile_view.py b/api/dashboard/profile/profile_view.py index 7ee05fe7..ccee93e2 100644 --- a/api/dashboard/profile/profile_view.py +++ b/api/dashboard/profile/profile_view.py @@ -1,13 +1,13 @@ from io import BytesIO -import decouple import qrcode import requests -from PIL import Image +from django.conf import settings from django.contrib.auth.hashers import make_password from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db.models import Prefetch +from PIL import Image from rest_framework.views import APIView from db.organization import UserOrganizationLink @@ -16,7 +16,8 @@ from utils.permission import CustomizePermission, JWTUtils from utils.response import CustomResponse from utils.types import WebHookActions, WebHookCategory -from utils.utils import DateTimeUtils, DiscordWebhooks +from utils.utils import DiscordWebhooks + from . import profile_serializer from .profile_serializer import LinkSocials @@ -61,7 +62,7 @@ def patch(self, request): def delete(self, request): user_id = JWTUtils.fetch_user_id(request) user = User.objects.get(id=user_id).delete() - + return CustomResponse( general_message="User deleted successfully" ).get_success_response() @@ -202,7 +203,6 @@ def put(self, request): # function for generating profile qr code def get(self, request, uuid=None): - base_url = decouple.config("FR_DOMAIN_NAME") fs = FileSystemStorage() if uuid is not None: user = User.objects.filter(id=uuid).first() @@ -218,64 +218,63 @@ def get(self, request, uuid=None): return CustomResponse( general_message="Private Profile" ).get_failure_response() - else: - user_uuid = JWTUtils.fetch_user_id(request) - data = f"{base_url}/profile/{user_uuid}" - - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_H, - box_size=10, - border=4, - ) - qr.add_data(data) - qr.make(fit=True) - - img = qr.make_image(fill_color="black", back_color="white") - - logo_url = f"{base_url}/favicon.ico/" # Replace with your logo URL - logo_response = requests.get(logo_url) - - if logo_response.status_code == 200: - logo_image = Image.open(BytesIO(logo_response.content)) - else: - return CustomResponse( - general_message="Failed to download the logo from the URL" - ).get_failure_response() - - logo_width, logo_height = logo_image.size - basewidth = 100 - wpercent = basewidth / float(logo_width) - hsize = int((float(logo_height) * float(wpercent))) - resized_logo = logo_image.resize((basewidth, hsize)) - - QRcode = qrcode.QRCode( - error_correction=qrcode.constants.ERROR_CORRECT_H - ) - - QRcode.add_data(data) - QRcolor = "black" - QRimg = QRcode.make_image( - fill_color=QRcolor, back_color="white" - ).convert("RGB") - - pos = ( - (QRimg.size[0] - resized_logo.size[0]) // 2, - (QRimg.size[1] - resized_logo.size[1]) // 2, - ) - # image = Image.open(BytesIO("image_response.content")) - QRimg.paste(resized_logo, pos) - image_io = BytesIO() - QRimg.save(image_io, format="PNG") - image_io.seek(0) - image_data: bytes = image_io.getvalue() - file_path = f"user/qr/{user_uuid}.png" - fs.exists(file_path) and fs.delete(file_path) - file = fs.save(file_path, ContentFile(image_io.read())) + user_uuid = JWTUtils.fetch_user_id(request) + data = f"{settings.FR_DOMAIN_NAME}/profile/{user_uuid}" + + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=10, + border=4, + ) + qr.add_data(data) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + logo_url = ( + f"{settings.FR_DOMAIN_NAME}/favicon.ico/" # Replace with your logo URL + ) + logo_response = requests.get(logo_url) + + if logo_response.status_code == 200: + logo_image = Image.open(BytesIO(logo_response.content)) + else: return CustomResponse( - general_message="QR code image with logo saved locally" - ).get_success_response() + general_message="Failed to download the logo from the URL" + ).get_failure_response() + + logo_width, logo_height = logo_image.size + basewidth = 100 + wpercent = basewidth / float(logo_width) + hsize = int((float(logo_height) * float(wpercent))) + resized_logo = logo_image.resize((basewidth, hsize)) + + QRcode = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H) + + QRcode.add_data(data) + QRcolor = "black" + QRimg = QRcode.make_image(fill_color=QRcolor, back_color="white").convert( + "RGB" + ) + + pos = ( + (QRimg.size[0] - resized_logo.size[0]) // 2, + (QRimg.size[1] - resized_logo.size[1]) // 2, + ) + # image = Image.open(BytesIO("image_response.content")) + QRimg.paste(resized_logo, pos) + image_io = BytesIO() + QRimg.save(image_io, format="PNG") + image_io.seek(0) + image_data: bytes = image_io.getvalue() + file_path = f"user/qr/{user_uuid}.png" + fs.exists(file_path) and fs.delete(file_path) + file = fs.save(file_path, ContentFile(image_io.read())) + + return CustomResponse( + general_message="QR code image with logo saved locally" + ).get_success_response() class UserLevelsAPI(APIView): diff --git a/api/dashboard/roles/dash_roles_views.py b/api/dashboard/roles/dash_roles_views.py index d8782432..c2dc8291 100644 --- a/api/dashboard/roles/dash_roles_views.py +++ b/api/dashboard/roles/dash_roles_views.py @@ -144,13 +144,13 @@ def get(self, request, role_id): {"muid": "muid", "full_name": "full_name"}, ) - serializer = dash_roles_serializer.UserRoleSearchSerializer( + serialized_data = dash_roles_serializer.UserRoleSearchSerializer( paginated_queryset.get("queryset"), many=True ).data return CustomResponse( response={ - "data": serializer, + "data": serialized_data, "pagination": paginated_queryset.get("pagination"), } ).get_success_response() @@ -161,7 +161,7 @@ class UserRoleLinkManagement(APIView): """ This API is creates an interface to help manage the user and role link by providing support for - - Listing all users with the given role + - Listing all users with the given role (max 10 users) - Giving a lot of users a specific role """ @@ -170,9 +170,9 @@ def get(self, request, role_id): """ Lists all the users with a given role """ - users = ( - User.objects.filter(user_role_link_user__role__pk=role_id).distinct().all() - ) + users = User.objects.filter(user_role_link_user__role__pk=role_id).distinct() + users = self.filter_users(request, users) + serialized_users = dash_roles_serializer.UserRoleLinkManagementSerializer( users, many=True ) @@ -184,11 +184,11 @@ def put(self, request, role_id): Lists all the users without a given role; used to assign roles """ - users = ( - User.objects.filter(~Q(user_role_link_user__role__pk=role_id)) - .distinct() - .all() - ) + users = User.objects.filter( + ~Q(user_role_link_user__role__pk=role_id) + ).distinct() + users = self.filter_users(request, users) + serialized_users = dash_roles_serializer.UserRoleLinkManagementSerializer( users, many=True ) @@ -242,6 +242,24 @@ def patch(self, request, role_id): except Role.DoesNotExist as e: return CustomResponse(general_message=str(e)).get_failure_response() + def filter_users(self, request, users): + """ + Filter users based on search parameters. + + Args: + request: The HTTP request object. + users: The queryset of users to be filtered. + + Returns: + QuerySet: The filtered queryset of users. + """ + if search_param := request.query_params.get("search", None): + users = users.filter( + Q(muid__icontains=search_param) | Q(full_name__icontains=search_param) + ) + return users[:10] + + class UserRole(APIView): authentication_classes = [CustomizePermission] diff --git a/db/apps.py b/db/apps.py index 554ab03f..39863731 100644 --- a/db/apps.py +++ b/db/apps.py @@ -17,6 +17,7 @@ def ready(self) -> None: @classmethod def check_system_user_exists(cls): + from db.organization import District as _ from db.user import User if not User.objects.filter(id=config("SYSTEM_ADMIN_ID")).exists(): raise SystemUserNotFoundError( diff --git a/mulearnbackend/middlewares.py b/mulearnbackend/middlewares.py index a752aaa0..70297744 100644 --- a/mulearnbackend/middlewares.py +++ b/mulearnbackend/middlewares.py @@ -97,7 +97,6 @@ def __call__(self, request): _ = request.body return self.get_response(request) - def log_exception(self, request, exception): """ Log the exception and prints the information in CLI. @@ -117,9 +116,9 @@ def log_exception(self, request, exception): with suppress(json.JSONDecodeError): auth = json.dumps(auth, indent=4) - - exception_id = self.generate_error_id(exception) - + + exception_id = self.generate_error_id(exception, request) + request_info = ( f"EXCEPTION INFO:\n" f"ID: {exception_id}\n" @@ -134,9 +133,9 @@ def log_exception(self, request, exception): logger.error(request_info) print(request_info) - - def generate_error_id(self, exception): - error_info = f"{type(exception).__name__}: {str(exception)}" + + def generate_error_id(self, exception, request): + error_info = f"{type(exception).__name__}: {str(exception)}: {request.method}: {request.path}" hash_object = hashlib.sha256(error_info.encode()) return hash_object.hexdigest() diff --git a/mulearnbackend/settings.py b/mulearnbackend/settings.py index 3f9d6d39..be8c7207 100644 --- a/mulearnbackend/settings.py +++ b/mulearnbackend/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ + import os from pathlib import Path @@ -29,7 +30,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = decouple_config("DEBUG", default=False, cast=bool) -ALLOWED_HOSTS = decouple_config("ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(",")]) +ALLOWED_HOSTS = decouple_config( + "ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(",")] +) # System admin applied to rows when their parent user instance is deleted SYSTEM_ADMIN_ID = decouple_config("SYSTEM_ADMIN_ID") @@ -37,7 +40,7 @@ # Application definition INSTALLED_APPS = [ - 'daphne', + "daphne", # 'django.contrib.admin', "django.contrib.auth", "django.contrib.contenttypes", @@ -50,7 +53,7 @@ "utils.apps.UtilsConfig", "api.apps.ApiConfig", "corsheaders", - 'db', + "db", ] MIDDLEWARE = [ @@ -66,7 +69,9 @@ ROOT_URLCONF = "mulearnbackend.urls" CORS_ALLOW_ALL_ORIGINS = True -REST_FRAMEWORK = {"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",)} +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",) +} # paginator settings PAGE_SIZE = 10 TEMPLATES = [ @@ -93,7 +98,7 @@ "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { - "hosts": [(decouple_config('REDIS_HOST'), decouple_config('REDIS_PORT'))], + "hosts": [(decouple_config("REDIS_HOST"), decouple_config("REDIS_PORT"))], }, }, } @@ -130,61 +135,63 @@ }, ] +LOG_PATH = decouple_config("LOGGER_DIR_PATH") + LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'request_log': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': decouple_config("LOGGER_DIR_PATH") + '/request.log', - 'formatter': 'verbose', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "request_log": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": f"{LOG_PATH}/request.log", + "formatter": "verbose", }, - 'error_log': { - 'level': 'ERROR', - 'class': 'logging.FileHandler', - 'filename': decouple_config("LOGGER_DIR_PATH") + '/error.log', - 'formatter': 'verbose', + "error_log": { + "level": "ERROR", + "class": "logging.FileHandler", + "filename": f"{LOG_PATH}/error.log", + "formatter": "verbose", }, - 'sql_log': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': decouple_config("LOGGER_DIR_PATH") + '/sql.log', - 'formatter': 'verbose', + "sql_log": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": f"{LOG_PATH}/sql.log", + "formatter": "verbose", }, - 'root_log': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': decouple_config("LOGGER_DIR_PATH") + '/root.log', - 'formatter': 'verbose', + "root_log": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": f"{LOG_PATH}/root.log", + "formatter": "verbose", }, }, - 'loggers': { - 'django.request': { - 'handlers': ['request_log'], - 'level': 'INFO', - 'propagate': True, + "loggers": { + "django.request": { + "handlers": ["request_log"], + "level": "INFO", + "propagate": True, }, - 'django': { - 'handlers': ['error_log'], - 'level': 'ERROR', - 'propagate': True, + "django": { + "handlers": ["error_log"], + "level": "ERROR", + "propagate": True, }, - 'django.db.backends': { - 'handlers': ['sql_log'], - 'level': 'DEBUG', - 'propagate': True, + "django.db.backends": { + "handlers": ["sql_log"], + "level": "DEBUG", + "propagate": True, }, - '': { - 'handlers': ['root_log'], - 'level': 'DEBUG', - 'propagate': True, + "": { + "handlers": ["root_log"], + "level": "DEBUG", + "propagate": True, }, }, - 'formatters': { - 'verbose': { - 'format': '{asctime} {levelname} {message}', - 'style': '{', + "formatters": { + "verbose": { + "format": "{asctime} {levelname} {message}", + "style": "{", }, }, } @@ -203,14 +210,12 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static') -] -STATIC_ROOT = os.path.join(BASE_DIR, 'assets') -STATIC_URL = '/muback-static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] +STATIC_ROOT = os.path.join(BASE_DIR, "assets") +STATIC_URL = "/muback-static/" -MEDIA_URL = '/muback-media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = "/muback-media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field @@ -224,11 +229,12 @@ EMAIL_HOST_PASSWORD = decouple_config("EMAIL_HOST_PASSWORD") EMAIL_PORT = decouple_config("EMAIL_PORT") EMAIL_USE_TLS = decouple_config("EMAIL_USE_TLS") -from_mail = decouple_config('FROM_MAIL') +FROM_MAIL = decouple_config("FROM_MAIL") +FR_DOMAIN_NAME = decouple_config("FR_DOMAIN_NAME") -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) -INTERNAL_IPS = [f'{ip[:-1]}1' for ip in ips] + ['127.0.0.1', '10.0.2.2'] +INTERNAL_IPS = [f"{ip[:-1]}1" for ip in ips] + ["127.0.0.1", "10.0.2.2"] diff --git a/utils/types.py b/utils/types.py index 36a424dd..b3ff4405 100644 --- a/utils/types.py +++ b/utils/types.py @@ -46,6 +46,14 @@ class RoleType(Enum): CAMPUS_ACTIVATION_TEAM = "Campus Activation Team" LEAD_ENABLER = "Lead Enabler" + @classmethod + def ig_campus_lead_role(cls,ig_code:str): + return f"{ig_code} CampusLead" + + @classmethod + def get_ig_lead_role(cls,ig_code:str): + return f"{ig_code} IGLead" + class OrganizationType(Enum): COLLEGE = 'College' diff --git a/utils/utils.py b/utils/utils.py index 65168bdf..8e679f48 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -4,14 +4,13 @@ import io from datetime import timedelta -import decouple import openpyxl import pytz import requests from decouple import config -from django.core.mail import EmailMessage -from django.core.mail import send_mail -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.conf import settings +from django.core.mail import EmailMessage, send_mail +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q from django.db.models.query import QuerySet from django.http import HttpResponse @@ -20,7 +19,7 @@ class CommonUtils: @staticmethod - def get_paginated_queryset( + def get_paginated_queryset( queryset: QuerySet, request, search_fields, @@ -195,37 +194,31 @@ def send_template_mail( and used as the content of the email attachment: The Attachment That send to the user """ - - from_mail = decouple.config("FROM_MAIL") - - base_url = decouple.config("FR_DOMAIN_NAME") status = None email_content = render_to_string( - f"mails/{'/'.join(map(str, address))}", {"user": context, "base_url": base_url} + f"mails/{'/'.join(map(str, address))}", {"user": context, "base_url": settings.FR_DOMAIN_NAME} ) if not (mail := getattr(context, "email", None)): mail = context["email"] if attachment is None: - status = send_mail( + return send_mail( subject=subject, message=email_content, - from_email=from_mail, + from_email=settings.FROM_MAIL, recipient_list=[mail], html_message=email_content, fail_silently=False, ) - else: - email = EmailMessage( - subject=subject, - body=email_content, - from_email=from_mail, - to=[context["email"]], - ) - email.attach(attachment) - email.content_subtype = "html" - status = email.send() - - return status + email = EmailMessage( + subject=subject, + body=email_content, + from_email=settings.FROM_MAIL, + to=[context["email"]], + ) + + email.attach(attachment) + email.content_subtype = "html" + return email.send()