Skip to content

Commit d4688a5

Browse files
authored
Merge pull request #99 from LamArt/feature/SCHOOL-344/safedelete_integration
Make models soft-deletable or undeletable
2 parents d42f6ad + a3bcd8c commit d4688a5

File tree

78 files changed

+1214
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1214
-188
lines changed

config/django/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
'phonenumber_field',
6767
'rules.apps.AutodiscoverRulesConfig',
6868
'storages',
69+
'safedelete'
6970
]
7071

7172
INSTALLED_APPS = [
@@ -181,7 +182,7 @@
181182
),
182183
'DEFAULT_AUTHENTICATION_CLASSES': [],
183184
'DEFAULT_PERMISSION_CLASSES': [
184-
'rest_framework.permissions.AllowAny',
185+
'rest_framework.permissions.AllowAny',
185186
],
186187
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
187188
}
@@ -206,5 +207,5 @@
206207
from config.settings.sessions import * # noqa
207208
from config.settings.celery import * # noqa
208209
from config.settings.sentry import * # noqa
209-
from config.settings.geo_django import * # noqa
210-
from config.settings.object_storage import * # noqa
210+
from config.settings.geo_django import * # noqa
211+
from config.settings.object_storage import * # noqa

open_schools_platform/common/admin.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1+
from typing import Type, Sequence, Union, Callable, Any
2+
13
from django.contrib import admin
4+
from safedelete.admin import SafeDeleteAdmin, SafeDeleteAdminFilter
5+
6+
7+
class BaseAdmin(SafeDeleteAdmin):
8+
list_display = ("highlight_deleted_field",) # type: Sequence[Union[str, Callable[[Any], Any]]]
9+
list_filter = (SafeDeleteAdminFilter,) + SafeDeleteAdmin.list_filter # type: ignore[operator]
10+
field_to_highlight = "name"
11+
12+
class Meta:
13+
abstract = True
214

315

416
class InputFilter(admin.SimpleListFilter):
@@ -15,3 +27,19 @@ def choices(self, changelist):
1527
if k != self.parameter_name
1628
)
1729
yield all_choice
30+
31+
32+
def admin_wrapper(admin_model: Type[BaseAdmin]):
33+
admin_model.highlight_deleted_field.short_description = admin_model.field_to_highlight # type: ignore[attr-defined]
34+
35+
if admin_model.list_display:
36+
admin_model.list_display = BaseAdmin.list_display + admin_model.list_display # type: ignore[operator]
37+
else:
38+
admin_model.list_display = BaseAdmin.list_display
39+
40+
if admin_model.list_filter:
41+
admin_model.list_filter = BaseAdmin.list_filter + admin_model.list_filter
42+
else:
43+
admin_model.list_filter = BaseAdmin.list_filter
44+
45+
return admin_model

open_schools_platform/common/filters.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
from enum import Enum
12
from typing import List, Type
23

34
import django_filters
4-
from django.db.models import Q
5+
from django.db.models import Q, QuerySet
56
from django_filters import CharFilter, BaseInFilter, UUIDFilter
67
from django_filters.rest_framework import DjangoFilterBackend
78
from rest_framework.exceptions import ValidationError
9+
from safedelete.config import DELETED_ONLY_VISIBLE, DELETED_VISIBLE
10+
11+
12+
class SoftCondition(Enum):
13+
"""
14+
This Enum determines which objects will be filtered regarding their soft deleting state
15+
* it is used in SafeDeleteManager.all
16+
"""
17+
NOT_DELETED = None
18+
DELETED_ONLY = DELETED_ONLY_VISIBLE
19+
ALL = DELETED_VISIBLE
820

921

1022
class CustomDjangoFilterBackend(DjangoFilterBackend):
@@ -46,20 +58,26 @@ class BaseFilterSet(django_filters.FilterSet):
4658
4759
1. Opportunity to use special field OR_SEARCH_FIELD that will search
4860
for all char fields and combine results
61+
* Your filter should contain this attribute: 'search = CharFilter(field_name="search", method="OR")'
4962
* This feature works only if your input dictionary has pair [OR_SEARCH_FIELD: some_value]
5063
2. Will raise ValidationError when filter get not valid data
5164
3. Will order result by "-created_at" field
5265
* To use this class your input model type should inherit BaseModel
5366
otherwise you can redefine ORDER_FIELD
5467
* To disable this feature write ORDER_FIELD=None
5568
* Note: symbol '-' is the reverse trigger
69+
4. SoftCondition by default is NOT_DELETED
5670
"""
5771
OR_SEARCH_FIELD = "search"
5872
ORDER_FIELD = "-created_at"
5973

60-
def __init__(self, *args, **kwargs):
74+
def __init__(self, data: dict = None, queryset: QuerySet = None, **kwargs):
6175
self.search_value = None
62-
super().__init__(*args, **kwargs)
76+
self.force_visibility = SoftCondition.NOT_DELETED
77+
if data is not None:
78+
self.force_visibility = data.get('DELETED', SoftCondition.NOT_DELETED)
79+
80+
super().__init__(data, queryset, **kwargs)
6381
if not self.is_valid():
6482
raise ValidationError(self.errors)
6583

@@ -72,7 +90,8 @@ def OR(self, queryset, field_name, value):
7290

7391
@property
7492
def qs(self):
75-
base_queryset = super().qs
93+
base_queryset = super().qs.all(force_visibility=self.force_visibility.value)
94+
7695
if self.ORDER_FIELD:
7796
base_queryset = base_queryset.order_by(self.ORDER_FIELD)
7897

@@ -122,4 +141,5 @@ def filter_by_object_ids(object_name: str):
122141
def func(queryset, name, value):
123142
values = value.split(',')
124143
return queryset.filter(**{"{object_name}__in".format(object_name=object_name): values})
144+
125145
return func

open_schools_platform/common/models.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
import safedelete
12
from django.db import models
23
from django.utils import timezone
34
from rules.contrib.models import RulesModelMixin, RulesModelBase
5+
from safedelete.managers import SafeDeleteManager
6+
from safedelete.models import SafeDeleteModel
47

58

6-
class BaseModel(RulesModelMixin, models.Model, metaclass=RulesModelBase):
9+
class BaseManager(SafeDeleteManager):
10+
pass
11+
12+
13+
class BaseModel(RulesModelMixin, SafeDeleteModel, metaclass=RulesModelBase):
14+
_safedelete_policy = safedelete.config.NO_DELETE
715
created_at = models.DateTimeField(db_index=True, default=timezone.now)
816
updated_at = models.DateTimeField(auto_now=True)
917

open_schools_platform/common/selectors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ def wrapper(*, filters=None, user: User = None, empty_exception: bool = False,
1414
if empty_exception and not qs:
1515
raise NotFound(empty_message)
1616
return qs
17+
1718
return wrapper

open_schools_platform/common/types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from django.db import models
33
from django.views.generic.base import View
44

5-
65
# Generic type for a Django model
76
# Reference: https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-type-of-class-objects
87

open_schools_platform/organization_management/circles/admin.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from .models import Circle
55
from .selectors import get_circles
6-
from ...common.admin import InputFilter
6+
from ...common.admin import InputFilter, BaseAdmin, admin_wrapper
77
from django.utils.translation import gettext_lazy as _
88

99

@@ -28,8 +28,9 @@ def queryset(self, request, queryset):
2828
return get_circles(filters={"address": address})
2929

3030

31-
class CircleAdmin(LeafletGeoAdmin):
32-
list_display = ("name", "organization", "address", "capacity", "location", "id")
31+
@admin_wrapper
32+
class CircleAdmin(BaseAdmin, LeafletGeoAdmin):
33+
list_display = ("organization", "address", "capacity", "location", "id")
3334
search_fields = ("name",)
3435
list_filter = (OrganizationFilter, AddressFilter)
3536

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 3.2.12 on 2022-10-29 10:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('circles', '0004_reverse_location'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='circle',
15+
name='deleted',
16+
field=models.DateTimeField(db_index=True, editable=False, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='circle',
20+
name='deleted_by_cascade',
21+
field=models.BooleanField(default=False, editable=False),
22+
),
23+
]

open_schools_platform/organization_management/circles/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import uuid
22
from typing import Any
33

4+
import safedelete.models
45
from django.contrib.gis.geos import Point
56
from django.core.validators import MinValueValidator
67
from django.contrib.gis.db import models
7-
from open_schools_platform.common.models import BaseModel
8+
9+
from open_schools_platform.common.models import BaseModel, BaseManager
810
from open_schools_platform.organization_management.organizations.models import Organization
911

1012

11-
class CircleManager(models.Manager):
13+
class CircleManager(BaseManager):
1214
def create_circle(self, *args: Any, **kwargs: Any):
1315
circle = self.model(
1416
*args,
@@ -21,6 +23,7 @@ def create_circle(self, *args: Any, **kwargs: Any):
2123

2224

2325
class Circle(BaseModel):
26+
_safedelete_policy = safedelete.config.SOFT_DELETE_CASCADE
2427
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
2528
name = models.CharField(max_length=200)
2629
organization = models.ForeignKey(Organization, related_name="circles", on_delete=models.CASCADE)

open_schools_platform/organization_management/circles/selectors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def get_circle(*, filters=None, user: User = None) -> Circle:
2727
qs = Circle.objects.all()
2828
circle = CircleFilter(filters, qs).qs.first()
2929

30-
if user and not user.has_perm("circles.circle_access", circle):
30+
if user and circle and not user.has_perm("circles.circle_access", circle):
3131
raise PermissionDenied
3232

3333
return circle

0 commit comments

Comments
 (0)