Skip to content

Commit

Permalink
Base of #251
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonLovesDoggo committed Jan 31, 2024
1 parent c058cb5 commit 378aa9d
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 2 deletions.
3 changes: 3 additions & 0 deletions core/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
),
path("v3/staff", staff, name="api_staff3"),
path("v3/feeds", feeds, name="api_feeds3"),
path(
"v3/obj/user/<int:id>/delete", UserDeleteView.as_view(), name="api_user_delete3"
),
path("v3/obj/<str:type>", ObjectList.as_view(), name="api_object_list3"),
path("v3/obj/<str:type>/new", ObjectNew.as_view(), name="api_object_new3"),
path(
Expand Down
9 changes: 8 additions & 1 deletion core/api/views/objects/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
enum=get_providers_by_operation("list"),
description="Which object provider to use",
),
# OpenApiParameter(
# name="lookup", location="query", type=int, description="Lookup field", required=False, default="id"),
],
)
class ObjectList(
Expand All @@ -42,6 +44,7 @@ class ObjectList(
"""
Endpoint for listing objects with various filters.
"""

mutate = False
detail = False
kind = "list"
Expand Down Expand Up @@ -183,6 +186,7 @@ class ObjectNew(ObjectAPIView, LookupField, generics.CreateAPIView):
"""
Endpoint for creating new objects.
"""

mutate = True
detail = None
kind = "new"
Expand All @@ -209,7 +213,6 @@ def post(self, *args, **kwargs):
],
)
class ObjectRetrieve(

ObjectAPIView,
LookupField,
generics.RetrieveAPIView,
Expand All @@ -219,6 +222,7 @@ class ObjectRetrieve(
"""
Endpoint for retrieving objects with various lookups.
"""

mutate = False
detail = True
kind = "retrieve"
Expand Down Expand Up @@ -260,6 +264,7 @@ class ObjectSingle(
"""
Endpoint for editing objects with support for lookups.
"""

mutate = True
detail = None
kind = "single"
Expand All @@ -273,6 +278,8 @@ def check_allow_single(self):
def delete(self, *args, **kwargs):
if x := self.check_allow_single():
return x
if getattr(self.provider, "delete", None):
return self.provider.delete(self, *args, **kwargs)
return super().delete(*args, **kwargs)

def put(self, *args, **kwargs):
Expand Down
15 changes: 14 additions & 1 deletion core/api/views/objects/user.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import base64
import hashlib

from django.http import JsonResponse
from rest_framework.response import Response

from core.api.serializers.custom import UserOrganizationField
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes.models import ContentType
from rest_framework import permissions, serializers, validators
from rest_framework import permissions, serializers, validators, status

from .base import BaseProvider
from ...utils.gravatar import gravatar_url
Expand Down Expand Up @@ -212,3 +215,13 @@ def get_last_modified_queryset():
.latest("action_time")
.action_time
)

@staticmethod
def delete(request, *args, **kwargs):
"""Respond with correct endpoint for deleting a user."""
return JsonResponse(
status=status.HTTP_304_NOT_MODIFIED,
data={
"detail": "Not allowed, Please read the docs for the correct endpoint."
},
)
12 changes: 12 additions & 0 deletions core/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .. import serializers, utils
from ... import models
from core.models import User


class UserDetail(generics.RetrieveAPIView):
Expand Down Expand Up @@ -74,3 +75,14 @@ def get(self, request, format=None):

serializer = serializers.TimetableSerializer(current_timetable)
return Response(serializer.data)


class UserDeleteView(APIView):
permission_classes = [permissions.IsAuthenticated]

def post(self, id):
user: User = models.User.objects.filter(id=id).first()
if user is None:
return Response(status=status.HTTP_404_NOT_FOUND)
user.mark_deleted()
return Response(status=status.HTTP_204_NO_CONTENT)
27 changes: 27 additions & 0 deletions core/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Q, CharField
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone

from . import timezone_choices, graduating_year_choices
from .course import Term
from .post import Announcement
from ..utils.choices import calculate_years
from ..utils.fields import SetField, ChoiceArrayField
from ..utils.mail import send_mail


# Create your models here.
Expand Down Expand Up @@ -102,6 +105,30 @@ def can_edit(self, obj):
def can_approve(self, obj):
return obj.approvable(user=self)

def mark_deleted(self):
self.is_active = False
self.last_login = timezone.now()
self.save()
email_template_context = {
"user": self,
"time_deleted": timezone.now(),
"restore_link": settings.SITE_URL + reverse("restore", args=(self.id,)),
}

send_mail( # todo: frontend needs to make a page for this
f"[ACTION REQUIRED] Your account has been marked for deletion.",
render_to_string(
"core/email/restore_deleted_user.txt",
email_template_context,
),
None,
[self.email],
html_message=render_to_string(
"core/email/restore_deleted_user.html",
email_template_context,
),
)

@classmethod
def all(cls):
return cls.objects.filter(is_active=True)
Expand Down
26 changes: 26 additions & 0 deletions core/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from celery.utils.log import get_task_logger
from django.conf import settings
from django.db.models import Value, JSONField, Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _l
from django.utils.translation import ngettext
from exponent_server_sdk import (
Expand All @@ -19,6 +20,7 @@
from requests.exceptions import ConnectionError, HTTPError

from core.models import Announcement, User, Event, BlogPost
from core.utils.tasks import get_random_username
from metropolis.celery import app

logger = get_task_logger(__name__)
Expand Down Expand Up @@ -47,13 +49,37 @@ def users_with_token():

@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(crontab(hour=0, minute=0), delete_expired_users)
sender.add_periodic_task(crontab(hour=18, minute=0), notif_events_singleday)
sender.add_periodic_task(crontab(day_of_month=1), run_group_migrations)
sender.add_periodic_task(
crontab(hour=1, minute=0), clear_expired
) # Delete expired oauth2 tokens from db everyday at 1am


@app.task
def delete_expired_users():
"""Scrub user data from inactive accounts that have not logged in for 14 days. (marked deleted)"""
queryset = User.objects.filter(
is_active=False, last_login__lt=dt.datetime.now() - dt.timedelta(days=14)
)
queryset.update( # We need to object to not break posts or comments
first_name="Deleted",
last_name="User",
username=get_random_username(),
bio="",
timezone="",
graduating_year=None,
is_teacher=False,
organizations=[],
tags_following=[],
qltrs=None,
saved_blogs=[],
saved_announcements=[],
expo_notif_tokens={},
)


@app.task
def run_group_migrations():
from scripts.migrations import migrate_groups
Expand Down
20 changes: 20 additions & 0 deletions core/utils/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
One off random tasks that need to be run on the server.
"""


import datetime as dt

from django.utils.crypto import get_random_string

from core.models import User


def get_random_username():
"""
Generate a random username that is not already taken.
"""
username = "deleted-" + get_random_string(length=6)
if User.objects.filter(username=username).exists():
return get_random_username()
return username

0 comments on commit 378aa9d

Please sign in to comment.