Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced API #162

Merged
merged 50 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e307964
- Added separate serializers for full and partial object views.
filiptypjeu Jul 9, 2023
eb4f183
Fixed how initials are handled for combined names. This resolves issu…
filiptypjeu Jul 10, 2023
09cd5a3
Fixed pep8 tests.
filiptypjeu Jul 10, 2023
ea5e219
Converting given_names to initials in the API when necessary, and add…
filiptypjeu Jul 10, 2023
9423b48
Added a search filter to the Members API.
filiptypjeu Jul 10, 2023
1477a37
Added support for ordering in the Members API.
filiptypjeu Jul 10, 2023
b106f82
Updated Django and installed django-filters.
filiptypjeu Jul 11, 2023
be33644
Added support for filters on the Member API, and added tests for them…
filiptypjeu Jul 11, 2023
32c71cd
Moved log file path to .env. This resolves issue #124.
filiptypjeu Jul 11, 2023
0b1bc98
Removed criteria of being a valid member for being a 'public' member.
filiptypjeu Jul 11, 2023
16b55f2
Added tests for api filters.
filiptypjeu Aug 3, 2023
b9a5ef5
Created more serializer to be able to format the API return values be…
filiptypjeu Aug 3, 2023
a3cb9da
Moved Member API filter class to a separate file.
filiptypjeu Aug 4, 2023
d6ba97d
Added better tests for testing the structure of API JSON responses.
filiptypjeu Aug 4, 2023
9b5b641
Refactored staff-only filter handling into a separate base class, and…
filiptypjeu Aug 4, 2023
2e92b7c
Added Filters to MemberType and Applicant APIs as well.
filiptypjeu Aug 4, 2023
83674a5
Removed all forms from the API.
filiptypjeu Aug 4, 2023
59e28b8
Added Search and Order filters to API.
filiptypjeu Aug 4, 2023
4228ce3
Enhanced Applicant API filters.
filiptypjeu Aug 4, 2023
8e622f9
Minor fixes and comments.
filiptypjeu Aug 4, 2023
7956538
Fixed API dump endpoints.
filiptypjeu Aug 4, 2023
7d3bcf5
Added CSV renderers to all rest_framework endpoints.
filiptypjeu Aug 4, 2023
3f45f53
Added tests for API endpoints used by BILL and Generikey.
filiptypjeu Aug 4, 2023
8956316
Introduced a helper method to some model Managers that automatically …
filiptypjeu Aug 4, 2023
af3d9bb
Fixed and enhanced dump API enpoints (prefetching data, fixing errors…
filiptypjeu Aug 4, 2023
bd3c8a7
Removed two unnecessary dump endpoints.
filiptypjeu Aug 4, 2023
19e02fb
Added tests for all dump API endpoints.
filiptypjeu Aug 4, 2023
9be0d9e
Fixed broken tests, using defaultdict where applicable, and minor fix…
filiptypjeu Aug 4, 2023
2832796
Changed title of API root page.
filiptypjeu Aug 4, 2023
0773f8d
Added buttons to the Member admin page that lead to the corresponding…
filiptypjeu Aug 4, 2023
ba2f4ef
Removed unused files.
filiptypjeu Aug 5, 2023
3b2ef2e
Added pagination to all API list endpoints.
filiptypjeu Aug 5, 2023
f463bb2
Combined a few small serializers since they were doing the same thing.
filiptypjeu Aug 5, 2023
6598c6e
Enhanced APIs to contain more subobjects (GroupType including all its…
filiptypjeu Aug 5, 2023
5ca9f92
Added API filters for the new subobject count fields.
filiptypjeu Aug 5, 2023
6faafc9
Merged all 'public' serializers with the 'admin' ones.
filiptypjeu Aug 5, 2023
2f33b86
Parameter name change to match naming in parent class.
filiptypjeu Aug 5, 2023
eb21ff8
Added missing checks in the API tests.
filiptypjeu Aug 6, 2023
13c4788
Added tests for MemberType and Applicant APIs as well.
filiptypjeu Aug 6, 2023
a6ed59d
Simplified API serializers.
filiptypjeu Aug 6, 2023
277979c
Added missing 'groups' field from the GroupType API detail endpoint.
filiptypjeu Aug 6, 2023
e876166
Fixed performance issues with some of the API endpoints, and added su…
filiptypjeu Aug 6, 2023
d0a9782
Merge branch 'develop' into feature/enhanced-apis
filiptypjeu Aug 6, 2023
13362e4
Made API root page accessible to all logged in users.
filiptypjeu Aug 6, 2023
8b2adf8
Fixed typos.
filiptypjeu Aug 6, 2023
f5baef7
Updated .env.example with missing STATIC_ROOT field.
filiptypjeu Aug 6, 2023
a5b0061
Made a few more Member fields hidable.
filiptypjeu Aug 10, 2023
f04a6fe
Fixed bug where the Members in the list API were not serialized prope…
filiptypjeu Aug 10, 2023
fb37967
Made the filters in the Members API respect the new hidden fields.
filiptypjeu Aug 11, 2023
f944d07
Changed default null label for bool filters in the API.
filiptypjeu Aug 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
dj-database-url~=0.5.0
Django~=3.1.2
Django~=3.2.20
django-ajax-selects~=2.0.0
django-auth-ldap~=2.2.0
django-countries~=6.1.3
django-countries~=7.5.1
django-dotenv~=1.4.2
django-filter~=23.2
django-getenv~=1.3.2
django-bootstrap4~=2.3.1
djangorestframework~=3.12.1
Expand Down
6 changes: 6 additions & 0 deletions teknologr/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ SECRET_KEY=SomeRandomSecretKey
# Should debug be enabled? True/False
DEBUG=False

# Log file path
LOG_FILE=/var/log/teknologr/info.log

# Directory where to collect all static files
STATIC_ROOT="/var/www/teknologr/static"

# Static collection for ajax-select
#AJAX_SELECT_INLINES = 'inline'
AJAX_SELECT_INLINES = 'staticfiles'
Expand Down
3 changes: 0 additions & 3 deletions teknologr/api/admin.py

This file was deleted.

328 changes: 328 additions & 0 deletions teknologr/api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
import django_filters
from django.db.models import Count
from members.models import *
from functools import reduce
from operator import and_

class BooleanFilter(django_filters.BooleanFilter):
'''
The defualt null label is for some reason 'unknown', but I want it to be ''
'''
def __init__(self, *args, **kwargs):
kwargs['widget'] = django_filters.widgets.BooleanWidget
super().__init__(*args, **kwargs)

class BaseFilter(django_filters.rest_framework.FilterSet):
'''
Base filter class that takes care of normal users from using staff-only filters.
'''
STAFF_ONLY = []
created = django_filters.DateFromToRangeFilter(label='Skapad mellan')
modified = django_filters.DateFromToRangeFilter(label='Modifierad mellan')

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
always_last = []
# Inherited classes can remove these fields, so need to check for that
if not hasattr(self, 'created') or self.created:
always_last.append('created')
if not hasattr(self, 'modified') or self.modified:
always_last.append('modified')

# Handle thie special case STAFF_ONLY = '__all__'
if self.STAFF_ONLY == '__all__':
self.STAFF_ONLY = [k for k in list(self.filters.keys()) if k not in always_last]
self.STAFF_ONLY = self.STAFF_ONLY + always_last

# If normal user, remove all staff-only filters, otherwise move them last and add a prefix to their labels
is_staff = self.is_staff
for key in self.STAFF_ONLY:
f = self.filters.pop(key)
if is_staff:
f.label = f'[Staff only] {f.label}'
self.filters[key] = f

@property
def is_staff(self):
''' Helper method for checking if the user making the request is staff '''
user = self.request.user
return user and user.is_staff

def filter_count(self, queryset, value, field_name):
name = f'n_{field_name}'
queryset = queryset.annotate(**{name: Count(field_name, distinct=True)})
min = value.start
max = value.stop
if min is not None:
queryset = queryset.exclude(**{f'{name}__lt': min})
if max is not None:
queryset = queryset.exclude(**{f'{name}__gt': max})
return queryset


class MemberFilter(BaseFilter):
'''
A custom FilterSet class that set up filters for Members. Some filters are ignored if the requesting user is not staff.
'''

# Public filters
name = django_filters.CharFilter(
# NOTE: Custom field name
# NOTE: given_names is semi-public, handling of hidden Members is done in the method
method='filter_name',
label='Namnet innehåller',
)
n_functionaries = django_filters.RangeFilter(
method='filter_n_functionaries',
label='Antalet poster är mellan',
)
n_groups = django_filters.RangeFilter(
method='filter_n_groups',
label='Antalet grupper är mellan',
)
n_decorations = django_filters.RangeFilter(
method='filter_n_decorations',
label='Antalet betygelser är mellan',
)

# Public but hidable fields (hidden Members are not included)
HIDABLE = Member.HIDABLE_FIELDS + ['address']
address = django_filters.CharFilter(
# NOTE: Custom field name, but added manually to HIDABLE
method='filter_address',
label='Adressen innehåller',
)
email = django_filters.CharFilter(
lookup_expr='icontains',
label='E-postadressen innehåller',
)
degree_programme = django_filters.CharFilter(
lookup_expr='icontains',
label='Studieprogrammet innehåller',
)
enrolment_year = django_filters.RangeFilter(
label='Inskrivingsåret är mellan',
)
graduated = BooleanFilter(
method='filter_graduated',
label='Utexaminerad?',
)
graduated_year = django_filters.RangeFilter(
label='Utexamineringsåret är mellan',
)

# Staff only filters
STAFF_ONLY = Member.STAFF_ONLY_FIELDS
birth_date = django_filters.DateFromToRangeFilter(
label='Född mellan',
)
student_id = django_filters.CharFilter(
label='Studienummer',
)
dead = BooleanFilter(
label='Avliden?',
)
subscribed_to_modulen = BooleanFilter(
label='Prenumererar på Modulen?',
)
allow_studentbladet = BooleanFilter(
label='Prenumererar på Studentbladet?',
)
allow_publish_info = BooleanFilter(
label='Tillåter publicering av kontaktinformation?',
)
comment = django_filters.CharFilter(
lookup_expr='icontains',
label='Kommentaren innehåller',
)
username = django_filters.CharFilter(
label='Användarnamn',
)
bill_code = django_filters.CharFilter(
label='BILL-konto',
)

def includes_hidable_field(self):
''' Check if the current query includes filtering on any hidable Member fields '''
for name, value in self.data.items():
# XXX: This wrongly identifies invalid filter values as being set. For example, providing an invalid string to a BooleanFilter field will be caught here, even though the field will not be filtered... Maybe this is good enough?
if name in self.HIDABLE and any(value):
return True
return False

def filter_queryset(self, queryset):
''' Remove hidden Members for normal users if filters on any hidden field is used. '''
if not self.is_staff and self.includes_hidable_field():
queryset = queryset.filter(Member.get_show_info_Q())
return super().filter_queryset(queryset)

def filter_name(self, queryset, name, value):
filiptypjeu marked this conversation as resolved.
Show resolved Hide resolved
'''
The given_names field is semi-public so can not filter on that for hidden Members.
'''
is_staff = self.is_staff

# Split the filter value and compare all individual values against all name columns
queries = []
for v in value.split():
q = Q(preferred_name__icontains=v) | Q(surname__icontains=v)
# Non-staff users only get to filter on 'given_names' if the Member has allowed publishing of info
if is_staff:
q |= Q(given_names__icontains=v)
else:
q |= (Q(given_names__icontains=v) & Member.get_show_info_Q())
queries.append(q)
return queryset.filter(reduce(and_, queries))

def filter_address(self, queryset, name, value):
# Split the filter value and compare all individual values against all address columns
queries = []
for v in value.split():
queries.append(
Q(street_address__icontains=v) |
Q(postal_code__icontains=v) |
Q(city__icontains=v) |
Q(country__icontains=v)
)
return queryset.filter(reduce(and_, queries))

def filter_graduated(self, queryset, name, value):
'''
Modifying the graduated flag whan adding a graduated year can sometimes be forgotten, so taking that into consideration for this filter query.
'''
assert value in [True, False, None]

if value:
# Graduated or graduated year is set
return queryset.filter(Q(graduated=True) | Q(graduated_year__isnull=False))
# Not graduated and graduated year is not set
return queryset.filter(Q(graduated=False) & Q(graduated_year__isnull=True))

def filter_n_functionaries(self, queryset, name, value):
return self.filter_count(queryset, value, 'functionaries')

def filter_n_groups(self, queryset, name, value):
return self.filter_count(queryset, value, 'group_memberships')

def filter_n_decorations(self, queryset, name, value):
return self.filter_count(queryset, value, 'decoration_ownerships')

class DecorationFilter(BaseFilter):
name = django_filters.CharFilter(
lookup_expr='icontains',
label='Betygelsens namn innehåller',
)
n_ownerships = django_filters.RangeFilter(
method='filter_n_ownerships',
label='Antalet innehavare är mellan',
)

def filter_n_ownerships(self, queryset, name, value):
return self.filter_count(queryset, value, 'ownerships')

class DecorationOwnershipFilter(BaseFilter):
decoration__id = django_filters.NumberFilter(label='Betygelsens id')
decoration__name = django_filters.CharFilter(
lookup_expr='icontains',
label='Betygelsens namn innehåller',
)
member__id = django_filters.NumberFilter(label='Medlemmens id')
acquired = django_filters.DateFromToRangeFilter(label='Tilldelat mellan')


class FunctionaryTypeFilter(BaseFilter):
name = django_filters.CharFilter(
lookup_expr='icontains',
label='Postens namn innehåller',
)
n_functionaries_total = django_filters.RangeFilter(
method='filter_n_functionaries_total',
label='Totala antalet postinnehavare är mellan',
)
# XXX: What about filtering on unique functionaries?

def filter_n_functionaries_total(self, queryset, name, value):
return self.filter_count(queryset, value, 'functionaries')

class FunctionaryFilter(BaseFilter):
functionarytype__id = django_filters.NumberFilter(label='Postens id')
functionarytype__name = django_filters.CharFilter(
lookup_expr='icontains',
label='Postens namn innehåller',
)
member__id = django_filters.NumberFilter(label='Medlemmens id')
begin_date = django_filters.DateFromToRangeFilter(label='Startdatumet är mellan')
end_date = django_filters.DateFromToRangeFilter(label='Slutdatumet är mellan')


class GroupTypeFilter(BaseFilter):
name = django_filters.CharFilter(
lookup_expr='icontains',
label='Gruppens namn innehåller',
)
n_groups = django_filters.RangeFilter(
method='filter_n_groups',
label='Antalet undergrupper är mellan',
)
# XXX: What about filtering on amount of members (total and unique)?

def filter_n_groups(self, queryset, name, value):
return self.filter_count(queryset, value, 'groups')

class GroupFilter(BaseFilter):
begin_date = django_filters.DateFromToRangeFilter(label='Startdatumet är mellan')
end_date = django_filters.DateFromToRangeFilter(label='Slutdatumet är mellan')
grouptype__id = django_filters.NumberFilter(label='Gruppens id')
grouptype__name = django_filters.CharFilter(
lookup_expr='icontains',
label='Gruppens namn innehåller',
)
n_members = django_filters.RangeFilter(
method='filter_n_members',
label='Antalet gruppmedlemmar är mellan',
)

def filter_n_members(self, queryset, name, value):
return self.filter_count(queryset, value, 'memberships')

class GroupMembershipFilter(BaseFilter):
group__id = django_filters.NumberFilter(label='Undergruppens id')
group__begin_date = django_filters.DateFromToRangeFilter(label='Undergruppens startdatum är mellan')
group__end_date = django_filters.DateFromToRangeFilter(label='Undergruppens slutdatum är mellan')
group__grouptype__id = django_filters.NumberFilter(label='Gruppens id')
group__grouptype__name = django_filters.CharFilter(
lookup_expr='icontains',
label='Gruppens namn innehåller',
)
member__id = django_filters.NumberFilter(label='Medlemmens id')


class MemberTypeFilter(BaseFilter):
STAFF_ONLY = '__all__'
begin_date = django_filters.DateFromToRangeFilter(label='Startdatumet är mellan')
end_date = django_filters.DateFromToRangeFilter(label='Slutdatumet är mellan')
type = django_filters.ChoiceFilter(choices=MemberType.TYPES, label='Medlemstyp')
member__id = django_filters.NumberFilter(label='Medlemmens id')


class ApplicantFilter(MemberFilter):
STAFF_ONLY = '__all__'

# Need to remove all Member fields that do not exist on Applicant
graduated = None
graduated_year = None
comment = None
dead = None
bill_code = None
created = None

# Add all extra fields
motivation = django_filters.CharFilter(
lookup_expr='icontains',
label='Motiveringen innehåller',
)
mother_tongue = django_filters.CharFilter(
lookup_expr='icontains',
label='Modersmålet innehåller',
)
created_at = django_filters.DateFromToRangeFilter(label='Ansökningsdatum är mellan')
3 changes: 0 additions & 3 deletions teknologr/api/models.py

This file was deleted.

Loading