Skip to content

Commit

Permalink
Merge pull request #1801 from gtech-mulearn:dev
Browse files Browse the repository at this point in the history
[FEAT] User delete, bulk role, error log
  • Loading branch information
MZaFaRM authored Dec 12, 2023
2 parents c538f7b + 34e4b1d commit 15b4f13
Show file tree
Hide file tree
Showing 20 changed files with 201 additions and 148 deletions.
11 changes: 11 additions & 0 deletions api/dashboard/error_log/error_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os

from decouple import config
Expand Down Expand Up @@ -81,6 +82,8 @@ def post(self, request, log_name):


class LoggerAPI(APIView):
authentication_classes = [CustomizePermission]

@role_required(
[RoleType.ADMIN.value, RoleType.FELLOW.value, RoleType.TECH_TEAM.value]
)
Expand All @@ -95,3 +98,11 @@ def get(self, request):
log_handler = logHandler()
formatted_errors = log_handler.parse_logs(log_data)
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):
logger = logging.getLogger("django")
logger.error(f"PATCHED : {error_id}")
return CustomResponse(response="Updated patch list").get_success_response()
52 changes: 38 additions & 14 deletions api/dashboard/error_log/log_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ def parse_logs(self, log_data: str) -> list[dict]:
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)

# Extract all logs in string format
matches = reversed(re.findall(self.log_pattern, log_data, re.DOTALL))
formatted_errors = {}
Expand All @@ -43,6 +48,12 @@ def parse_logs(self, log_data: str) -> list[dict]:

return formatted_errors.values()

def extract_patches(self, log_data):
return {
patch[2]: self.get_formatted_time(patch[1])
for patch in re.finditer(self.patch_pattern, log_data)
}

def extract_log_entry(self, error: str) -> dict:
"""fetch the value from the details of how to
find it provided by the regex
Expand All @@ -58,6 +69,7 @@ def extract_log_entry(self, error: str) -> dict:

for key, value in values.items():
entry_type = self.log_entries[key]["type"]

if entry_type == datetime:
result_dict[key] = self.get_formatted_time(value)
elif entry_type == dict and value:
Expand Down Expand Up @@ -116,23 +128,35 @@ def get_patterns(self) -> list:
values = self.log_entries.values()
return [value["regex"] for value in values]

def aggregate_log_entry(self, formatted_errors: list[dict], log_entry: dict) -> None:
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
"""
return (
log_entry["id"] in self.patched_errors
and log_entry["timestamp"] < self.patched_errors[log_entry["id"]]
)

def aggregate_log_entry(
self, formatted_errors: list[dict], log_entry: dict
) -> None:
"""combines all fetched error into one
Args:
formatted_errors (list[dict]): the list to add everything into
log_entry (dict): current log entry
"""
log_id = log_entry["id"]
log_keys = self.log_entries.keys()
if log_id not in formatted_errors:
formatted_errors[log_id] = {
key: [] if key != "id" else log_id for key in log_keys
}
for key in log_keys:
if (
key != "id"
and log_entry[key]
and log_entry[key] not in formatted_errors[log_id][key]
):
formatted_errors[log_id][key].append(log_entry[key])
if not self.already_patched(log_entry):
log_id = log_entry["id"]
log_keys = self.log_entries.keys()
if log_id not in formatted_errors:
formatted_errors[log_id] = {
key: [] if key != "id" else log_id for key in log_keys
}
for key in log_keys:
if (
key != "id"
and log_entry[key]
and log_entry[key] not in formatted_errors[log_id][key]
):
formatted_errors[log_id][key].append(log_entry[key])
1 change: 1 addition & 0 deletions api/dashboard/error_log/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

urlpatterns = [
path('', error_view.LoggerAPI.as_view()),
path('patch/<str:error_id>/', error_view.LoggerAPI.as_view()),
path('<str:log_name>/', error_view.DownloadErrorLogAPI.as_view()),
path('view/<str:log_name>/', error_view.ViewErrorLogAPI.as_view()),
path('clear/<str:log_name>/', error_view.ClearErrorLogAPI.as_view()),
Expand Down
7 changes: 2 additions & 5 deletions api/dashboard/profile/profile_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,8 @@ def patch(self, request):

def delete(self, request):
user_id = JWTUtils.fetch_user_id(request)
user = User.objects.get(id=user_id)
user.deleted_by = user
user.deleted_at = DateTimeUtils.get_current_utc_time()
user.save()

user = User.objects.get(id=user_id).delete()

return CustomResponse(
general_message="User deleted successfully"
).get_success_response()
Expand Down
8 changes: 4 additions & 4 deletions api/dashboard/roles/dash_roles_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ def validate(self, attrs):
)
if users.count() != len(attrs):
raise serializers.ValidationError("One or more user IDs are invalid.")
else:
data["users"] = users
return data

data["users"] = users
return data

def create(self, validated_data):
users = validated_data.pop("users")
Expand All @@ -57,7 +57,7 @@ def create(self, validated_data):
validated_data["role"].title,
",".join(list(users.values_list("id", flat=True))),
)
return user_roles_to_create
return user_roles_to_create, validated_data["role"]


class RoleDashboardSerializer(serializers.ModelSerializer):
Expand Down
43 changes: 33 additions & 10 deletions api/dashboard/roles/dash_roles_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,7 @@ 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()
User.objects.filter(user_role_link_user__role__pk=role_id).distinct().all()
)
serialized_users = dash_roles_serializer.UserRoleLinkManagementSerializer(
users, many=True
Expand Down Expand Up @@ -204,19 +202,44 @@ def patch(self, request):
Assigns a large bunch of users a certain role
"""
request_data = request.data.copy()
request_data[
"created_by"
] = JWTUtils.fetch_user_id(request)
request_data["created_by"] = JWTUtils.fetch_user_id(request)
serialized_users = dash_roles_serializer.RoleAssignmentSerializer(
data=request_data
)
if serialized_users.is_valid():
serialized_users.save()
users, role = serialized_users.save()
return CustomResponse(
general_message="Successfully gave all users the requested role"
general_message=f"Successfully gave {len(users)} users '{role.title}' role"
).get_success_response()
return CustomResponse(response=serialized_users.errors).get_failure_response()

@role_required([RoleType.ADMIN.value])
def delete(self, request):
"""
Removes a role from a large bunch of users
"""
role = Role.objects.get(pk=request.data.get("role"))
user_role_links = UserRoleLink.objects.filter(
user__pk__in=request.data.get("users"),
role=role,
)
number = user_role_links.count()

DiscordWebhooks.general_updates(
WebHookCategory.BULK_ROLE.value,
WebHookActions.DELETE.value,
role.title,
",".join(list(user_role_links.values_list("user_id", flat=True))),
)

user_role_links.delete()

return CustomResponse(
general_message=(
f"Successfully removed the '{role.title}' role from {number} users"
)
).get_success_response()


class UserRole(APIView):
authentication_classes = [CustomizePermission]
Expand Down Expand Up @@ -421,12 +444,12 @@ def post(self, request):
else:
error_rows.append(user_roles_serializer.errors)

for role,user_set in users_by_role.items():
for role, user_set in users_by_role.items():
DiscordWebhooks.general_updates(
WebHookCategory.BULK_ROLE.value,
WebHookActions.UPDATE.value,
role,
",".join(user_set)
",".join(user_set),
)

return CustomResponse(
Expand Down
6 changes: 2 additions & 4 deletions api/dashboard/user/dash_user_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ def delete(self, request, user_id):
return CustomResponse(
general_message="User Not Available"
).get_failure_response()

user.deleted_by = User.objects.get(pk=JWTUtils.fetch_user_id(request))
user.deleted_at = DateTimeUtils.get_current_utc_time()
user.save()

user.delete()

return CustomResponse(
general_message="User deleted successfully"
Expand Down
20 changes: 1 addition & 19 deletions api/register/register_helper.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
import decouple
import requests

from db.user import User
from utils.exception import CustomException
from utils.response import CustomResponse


def get_full_name(first_name, last_name):
return f"{first_name}{last_name or ''}".replace(" ", "").lower()[:85]


def generate_muid(first_name, last_name):
full_name = get_full_name(first_name, last_name)
muid = f"{full_name}@mulearn"

counter = 0
while User.objects.filter(muid=muid).exists():
counter += 1
muid = f"{full_name}-{counter}@mulearn"

return muid


def get_auth_token(muid, password):
Expand All @@ -32,4 +14,4 @@ def get_auth_token(muid, password):
if response.get("statusCode") != 200:
raise CustomException(response.get("message"))

return response.get("response")
return response.get("response")
3 changes: 0 additions & 3 deletions api/register/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,6 @@ class UserSerializer(serializers.ModelSerializer):
def create(self, validated_data):
role = validated_data.pop("role", None)

validated_data["muid"] = register_helper.generate_muid(
validated_data["first_name"], validated_data.get('last_name', '')
)
password = validated_data.pop("password")
hashed_password = make_password(password)
validated_data["password"] = hashed_password
Expand Down
17 changes: 9 additions & 8 deletions db/hackathon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from db.organization import Organization, District
from db.user import User
from decouple import config as decouple_config

# fmt: off
# noinspection PyPep8
Expand All @@ -25,9 +26,9 @@ class Hackathon(models.Model):
event_start = models.DateTimeField(blank=True, null=True)
event_end = models.DateTimeField(blank=True, null=True)
status = models.CharField(max_length=20, blank=True, null=True)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='hackathon_updated_by')
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='updated_by', related_name='hackathon_updated_by')
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='hackathon_created_by')
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='created_by', related_name='hackathon_created_by')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
Expand All @@ -41,9 +42,9 @@ class HackathonForm(models.Model):
field_name = models.CharField(max_length=255)
field_type = models.CharField(max_length=50)
is_required = models.BooleanField(default=False)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='hackathon_form_updated_by')
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='updated_by', related_name='hackathon_form_updated_by')
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='hackathon_form_created_by')
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='created_by', related_name='hackathon_form_created_by')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
Expand All @@ -55,9 +56,9 @@ class HackathonOrganiserLink(models.Model):
id = models.CharField(primary_key=True, max_length=36)
organiser = models.ForeignKey(User, on_delete=models.CASCADE)
hackathon = models.ForeignKey(Hackathon, on_delete=models.CASCADE)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='hackathon_organiser_link_updated_by')
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='updated_by', related_name='hackathon_organiser_link_updated_by')
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='hackathon_organiser_link_created_by')
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='created_by', related_name='hackathon_organiser_link_created_by')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
Expand All @@ -69,9 +70,9 @@ class HackathonUserSubmission(models.Model):
id = models.CharField(primary_key=True, max_length=36)
user = models.ForeignKey(User, on_delete=models.CASCADE)
hackathon = models.ForeignKey(Hackathon, on_delete=models.CASCADE)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='hackathon_submission_updated_by')
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='updated_by', related_name='hackathon_submission_updated_by')
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='hackathon_submission_created_by')
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='created_by', related_name='hackathon_submission_created_by')
created_at = models.DateTimeField(auto_now_add=True)
data = models.JSONField(max_length=2000, blank=True, null=True)

Expand Down
1 change: 1 addition & 0 deletions db/integrations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid

from decouple import config as decouple_config
from django.db import models

from db.user import User
Expand Down
9 changes: 5 additions & 4 deletions db/learning_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from db.task import InterestGroup, Organization
from db.user import User
from decouple import config as decouple_config


# fmt: off
Expand All @@ -21,10 +22,10 @@ class LearningCircle(models.Model):
meet_time = models.CharField(max_length=10, blank=True, null=True)
day = models.CharField(max_length=20, blank=True, null=True)
note = models.CharField(max_length=500, blank=True, null=True)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column="updated_by",
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("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.CASCADE, db_column="created_by",
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column="created_by",
related_name="learning_circle_created_by")
created_at = models.DateTimeField(auto_now=True)

Expand Down Expand Up @@ -58,10 +59,10 @@ class CircleMeetingLog(models.Model):
attendees = models.CharField(max_length=216)
agenda = models.CharField(max_length=2000)
images = models.ImageField(max_length=200, upload_to='lc/meet-report')
created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by',
created_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), 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.CASCADE, db_column='updated_by',
updated_by = models.ForeignKey(User, on_delete=models.SET(decouple_config("SYSTEM_ADMIN_ID")), db_column='updated_by',
related_name='circle_meeting_log_updated_by')
updated_at = models.DateTimeField(auto_now=True)

Expand Down
4 changes: 1 addition & 3 deletions db/managers/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@

class ActiveUserManager(models.Manager):
def get_queryset(self):
# This will return only the users where deleted_at, deleted_by, suspended_at, and suspended_by are NULL
# This will return only the users where suspended_at, and suspended_by are NULL
return (
super()
.get_queryset()
.filter(
deleted_at__isnull=True,
deleted_by__isnull=True,
suspended_at__isnull=True,
suspended_by__isnull=True,
)
Expand Down
Loading

0 comments on commit 15b4f13

Please sign in to comment.