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

various admin updates #420

Open
wants to merge 35 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a175ca5
use project_id for POST assets/ if available, otherwise determine pro…
hburgund Dec 20, 2021
4cbba61
allow audiolength, start_time and end_time params to be passed in POS…
hburgund Dec 20, 2021
6b0d553
expand/correct optional audio time param input handling
hburgund Dec 20, 2021
9eece16
clean up events/ response to include correct types for latitude/longi…
hburgund Feb 3, 2022
b071cd4
file paths to work both for local testing as well as production
hburgund Feb 25, 2022
ce5e352
store mp3 version file path in speaker object regardless of format of…
hburgund Feb 25, 2022
6e41c23
ensure that speaker audio files are available in mp3 and m4a as well …
hburgund Feb 25, 2022
9d92d71
convert results conditionally in serializer to avoid NoneType errors
hburgund Mar 2, 2022
a114ab7
handle tag_ids param in list format in addition to comma-separated st…
hburgund Mar 2, 2022
5ce1a51
use tag_ids instead of tags wherever possible
hburgund Mar 2, 2022
5989b7e
ensure tag_ids passed in request before manipulating it
hburgund Mar 2, 2022
4d3a308
this attempt at reducing variable confusion causes issues, so undoing...
hburgund Mar 2, 2022
7139c3c
filter listening history items by session_id, asset_id and project_id…
hburgund Mar 5, 2022
1eb2b8c
convert audio uploads to 48KHz mp3 and m4a regardless of wav, mp3 or …
hburgund Mar 5, 2022
2c188c2
make POST and PATCH listenevents/ params more consistent with output …
hburgund Mar 5, 2022
2255b35
handle speaker audio uploads that contain spaces in filename
hburgund May 13, 2022
b6ee7af
add project-description as localized field for admin access to all av…
hburgund Jun 11, 2022
7b06cea
allow POST assets/ to include "filename" param of file already existi…
hburgund Jun 25, 2022
e77cb62
handle event.tags parsing properly to ensure it results in an array o…
hburgund Jul 5, 2022
a468ff7
make asset.description field significantly longer to store full trans…
hburgund Nov 14, 2022
e292a99
add new uigroup.selection_method field (including migration) to enabl…
hburgund Nov 14, 2022
900f899
change new uigroup field name to uiitem_filter and modify option name…
hburgund Nov 15, 2022
6a45943
simplify uiitem_filter to remove strict validation to make more flexi…
hburgund Nov 17, 2022
840c42e
apparently syntax has changed for updating ManyToMany related fields …
hburgund Dec 9, 2022
875116a
fix incorrect reference to local database for running migrations and …
hburgund Dec 12, 2022
53e75bf
add delete_binary param to DELETE assets/ request to allow for option…
hburgund Dec 16, 2022
2a7c26b
add custom OR string filter for username, first/last name and email v…
hburgund Dec 16, 2022
0d98257
add delete_binary param to DELETE speakers/ request to allow for opti…
hburgund Dec 16, 2022
9e79def
update max length for asset.description in serializer
hburgund Dec 16, 2022
c2c1b64
change response codes for binary deletion to proper 204 from 400
hburgund Dec 29, 2022
5f5ac61
delete all variations (mp3, m4a, wav etc) of binaries when deleting a…
hburgund Jan 20, 2023
03113e1
add assets/count/ endpoint to retrieve filterable count of assets wit…
hburgund Mar 24, 2023
4b562fd
add count/ endpoints for listenevents and sessions
hburgund Mar 25, 2023
2379562
add optional pagination to GET listenevents/ request
hburgund Mar 25, 2023
43397ed
allow speakers to be created and modified by app users without admin …
hburgund Nov 6, 2024
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
2 changes: 2 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ if [ "$FOUND_VAGRANT" = true ]; then
USERNAME="vagrant"
fi

cp $SOURCE_PATH/files/home-user-profile /home/$USERNAME/.profile

# Set paths/directories
WWW_PATH="/var/www/roundware"
CODE_PATH="$WWW_PATH/source"
Expand Down
3 changes: 2 additions & 1 deletion files/home-user-profile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ source /var/www/roundware/bin/activate
RWPATH=`readlink ~/roundware-server`
if [ -n "$RWPATH" ]; then
# If ~/roundware-server is a symbolic link, set it to the dereferenced path.
export PYTHONPATH=$RWPATH
export DJANGO_SETTINGS_MODULE=roundware_production
export PYTHONPATH="$RWPATH:/var/www/roundware/source/roundware:/var/www/roundware/settings"
else
# If ~/roundware-server is not a symbolic link, set it.
export PYTHONPATH=~/roundware-server
Expand Down
19 changes: 16 additions & 3 deletions roundware/api2/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ def filter(self, qs, value):
Q(**{'user__username__'+ self.lookup_expr: value}))
return qs

class NameEmailUserFilter(django_filters.CharFilter):
def filter(self, qs, value):
if value:
return qs.filter(Q(**{'first_name__'+ self.lookup_expr: value}) |
Q(**{'last_name__'+ self.lookup_expr: value}) |
Q(**{'email__'+ self.lookup_expr: value}) |
Q(**{'username__'+ self.lookup_expr: value}))
return qs


class AssetFilterSet(django_filters.FilterSet):
session_id = django_filters.NumberFilter()
Expand Down Expand Up @@ -175,12 +184,15 @@ class ListeningHistoryItemFilterSet(django_filters.FilterSet):
start_time__lte = django_filters.DateTimeFilter(field_name='starttime', lookup_expr='lte')
start_time__range = django_filters.DateRangeFilter(field_name='starttime')
project_id = django_filters.NumberFilter(field_name='session_id__project_id')
asset_id = django_filters.NumberFilter()
session_id = django_filters.NumberFilter()

class Meta:
model = ListeningHistoryItem
fields = ['starttime',
'session',
'asset',
'session_id',
'asset_id',
'project_id',
'duration']


Expand Down Expand Up @@ -312,7 +324,7 @@ class UIGroupFilterSet(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='startswith')
ui_mode = django_filters.TypedChoiceFilter(choices=UIGroup.UI_MODES)
tag_category_id = django_filters.NumberFilter()
select = django_filters.TypedChoiceFilter(choices=UIGroup.SELECT_METHODS)
select = django_filters.TypedChoiceFilter(choices=UIGroup.SELECT_OPTIONS)
active = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool)
index = django_filters.NumberFilter()
project_id = django_filters.NumberFilter()
Expand Down Expand Up @@ -340,6 +352,7 @@ class UserFilterSet(django_filters.FilterSet):
first_name = django_filters.CharFilter(lookup_expr='icontains')
last_name = django_filters.CharFilter(lookup_expr='icontains')
email = django_filters.CharFilter(lookup_expr='icontains')
search_str = NameEmailUserFilter(lookup_expr='icontains')

class Meta:
model = User
Expand Down
18 changes: 14 additions & 4 deletions roundware/api2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def get_fields(self):

class AssetSerializer(AdminLocaleStringSerializerMixin, serializers.ModelSerializer):
audiolength_in_seconds = serializers.FloatField(required=False)
description = serializers.CharField(max_length=2048, default="", allow_blank=True)
description = serializers.CharField(max_length=8192, default="", allow_blank=True)

class Meta:
model = Asset
Expand Down Expand Up @@ -171,8 +171,17 @@ def to_representation(self, obj):
result = super(EventSerializer, self).to_representation(obj)
result["session_id"] = result["session"]
del result["session"]
result["tag_ids"] = result["tags"]
del result["tags"]
if result["tags"]:
result["tag_ids"] = result["tags"].split(",")
del result["tags"]
newList = [i for i in result["tag_ids"] if i.isnumeric()]
result["tag_ids"] = newList
for i in range(0, len(result["tag_ids"])):
result["tag_ids"][i] = int(result["tag_ids"][i])
if result["latitude"]:
result["latitude"] = float(result["latitude"])
if result["longitude"]:
result["longitude"] = float(result["longitude"])
return result


Expand Down Expand Up @@ -236,7 +245,8 @@ class Meta:
model = Project
fields = "__all__"
localized_fields = ['demo_stream_message_loc', 'legal_agreement_loc',
'sharing_message_loc', 'out_of_range_message_loc']
'sharing_message_loc', 'out_of_range_message_loc',
'description_loc']

def to_representation(self, obj):
result = super(ProjectSerializer, self).to_representation(obj)
Expand Down
140 changes: 133 additions & 7 deletions roundware/api2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from roundware.lib.api import (get_project_tags_new as get_project_tags,
add_asset_to_envelope,
save_asset_from_request, vote_asset, get_projects_by_location,
vote_count_by_asset, log_event, save_speaker_from_request)
vote_count_by_asset, log_event, save_speaker_from_request,
delete_binary_from_server)
from roundware.api2.permissions import AuthenticatedReadAdminWrite
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated, DjangoObjectPermissions
Expand Down Expand Up @@ -96,6 +97,7 @@ class AssetViewSet(viewsets.GenericViewSet, AssetPaginationMixin,):
api/2/assets/:id/votes/
api/2/assets/random/
api/2/assets/blocked/
api/2/assets/count/
"""

# TODO: Implement DjangoObjectPermissions
Expand Down Expand Up @@ -142,8 +144,11 @@ def create(self, request):
"""
POST api/2/assets/ - Create a new Asset
"""
if "file" not in request.data:
raise ParseError("Must supply file for asset content")
# ensure one and only one of "file" and "filename" params are passed
if ("filename" in request.data and "file" in request.data) or \
("filename" not in request.data and "file" not in request.data):
return Response({"detail": "Request must include either 'file' or 'filename', but not both."},
status.HTTP_400_BAD_REQUEST)
if not request.data["envelope_ids"].isdigit():
raise ParseError("Must provide a single envelope_id in envelope_ids parameter for POST. "
"You can add more envelope_ids in subsequent PATCH calls")
Expand Down Expand Up @@ -179,7 +184,7 @@ def partial_update(self, request, pk):
request.data['loc_alt_text'] = request.data['alt_text_loc_ids']
del request.data['alt_text_loc_ids']
if 'envelope_ids' in request.data:
request.data['envelope_set'] = request.data['envelope_ids']
request.data['envelope'] = request.data['envelope_ids']
del request.data['envelope_ids']
if 'user_id' in request.data:
request.data['user'] = request.data['user_id']
Expand All @@ -202,6 +207,14 @@ def destroy(self, request, pk=None):
except Asset.DoesNotExist:
raise Http404("Asset not found; cannot delete!")
asset.delete()
if ('delete_binary' in request.query_params and request.query_params.get('delete_binary')=='true'):
delete_binary_from_server(asset.filename)
elif ('delete_binary' not in request.query_params or request.query_params.get('delete_binary')=='false'):
return Response({"detail": "Asset deleted but binary deletion not requested, so binary file will remain on server."},
status.HTTP_204_NO_CONTENT)
else:
return Response({"detail": "No binary associated with specified asset found so binary not deleted!"},
status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)

@action(methods=['post', 'get'], detail=True)
Expand Down Expand Up @@ -306,6 +319,17 @@ def assets_by_user(self, user_id):

return user_asset_ids

@action(methods=['get'], detail=False)
def count(self, request, pk=None):
"""
GET api/2/assets/count/ - retrieve count of Assets filtered by parameters
"""
assets = AssetFilterSet(request.query_params).qs.values_list('id', flat=True)
asset_count = len(assets)

result = OrderedDict()
result['count'] = asset_count
return Response(result)


class AudiotrackViewSet(viewsets.ViewSet):
Expand Down Expand Up @@ -508,6 +532,9 @@ def create(self, request):
raise ParseError("a session_id is required for this operation")
if 'event_type' not in request.data:
raise ParseError("an event_type is required for this operation")
if ('tag_ids' in request.data and isinstance(request.data['tag_ids'], list)):
string_list = [str(i) for i in request.data['tag_ids']]
request.data['tag_ids'] = ",".join(string_list)
try:
e = log_event(request.data['event_type'], request.data['session_id'], request.data)
except Exception as e:
Expand Down Expand Up @@ -577,15 +604,56 @@ def destroy(self, request, pk=None):
return Response(status=status.HTTP_204_NO_CONTENT)


class ListenEventViewSet(viewsets.ViewSet):
class ListenEventPaginationMixin(object):

@property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator

def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination
is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(
queryset, self.request, view=self)

def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given
output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)


class ListenEventPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 10000


class ListenEventViewSet(viewsets.ViewSet, ListenEventPaginationMixin,):
"""
API V2: api/2/listenevents/
api/2/listenevents/:id/
api/2/listenevents/count/
"""

# TODO: Rename ListeningHistoryItem model to ListenEvent.
queryset = ListeningHistoryItem.objects.all()
permission_classes = (IsAuthenticated,)
pagination_class = ListenEventPagination

def get_object(self, pk):
try:
Expand All @@ -598,7 +666,19 @@ def list(self, request):
GET api/2/listenevents/ - Get ListenEvents by filtering parameters
"""
events = ListeningHistoryItemFilterSet(request.query_params).qs
if "paginate" in request.query_params:
paginate = strtobool(request.query_params['paginate'])
else:
paginate = False

page = self.paginate_queryset(events)
if page is not None and paginate:
# serializer = self.get_serializer(page, context={"admin": "admin" in request.query_params}, many=True)
serializer = serializers.ListenEventSerializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = serializers.ListenEventSerializer(events, many=True)
# serializer = self.get_serializer(events, context={"admin": "admin" in request.query_params}, many=True)
return Response(serializer.data)

def retrieve(self, request, pk=None):
Expand All @@ -620,6 +700,12 @@ def create(self, request):
if 'duration_in_seconds' in request.data:
request.data['duration'] = float(request.data['duration_in_seconds']) * float(1000000000)
del request.data['duration_in_seconds']
if 'session_id' in request.data:
request.data['session'] = request.data['session_id']
del request.data['session_id']
if 'asset_id' in request.data:
request.data['asset'] = request.data['asset_id']
del request.data['asset_id']

serializer = serializers.ListenEventSerializer(data=request.data)
if serializer.is_valid():
Expand All @@ -636,7 +722,13 @@ def partial_update(self, request, pk):
if 'duration_in_seconds' in request.data:
request.data['duration'] = float(request.data['duration_in_seconds']) * float(1000000000)
del request.data['duration_in_seconds']

if 'session_id' in request.data:
request.data['session'] = request.data['session_id']
del request.data['session_id']
if 'asset_id' in request.data:
request.data['asset'] = request.data['asset_id']
del request.data['asset_id']

listen_event = self.get_object(pk)
serializer = serializers.ListenEventSerializer(listen_event, data=request.data, partial=True)
if serializer.is_valid():
Expand All @@ -652,6 +744,18 @@ def destroy(self, request, pk=None):
listen_event.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

@action(methods=['get'], detail=False)
def count(self, request, pk=None):
"""
GET api/2/listenevents/count/ - retrieve count of Listen Events filtered by parameters
"""
listen_events = ListeningHistoryItemFilterSet(request.query_params).qs.values_list('id', flat=True)
listen_event_count = len(listen_events)

result = OrderedDict()
result['count'] = listen_event_count
return Response(result)


class LocalizedStringViewSet(viewsets.ViewSet):
"""
Expand Down Expand Up @@ -1023,6 +1127,7 @@ def projects(self, request, pk=None):
class SessionViewSet(viewsets.ViewSet):
"""
API V2: api/2/sessions/
api/2/sessions/count/
"""
queryset = Session.objects.all()
permission_classes = (IsAuthenticated,)
Expand Down Expand Up @@ -1092,14 +1197,26 @@ def create(self, request):
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@action(methods=['get'], detail=False)
def count(self, request, pk=None):
"""
GET api/2/sessions/count/ - retrieve count of Sessions filtered by parameters
"""
sessions = SessionFilterSet(request.query_params).qs.values_list('id', flat=True)
session_count = len(sessions)

result = OrderedDict()
result['count'] = session_count
return Response(result)


class SpeakerViewSet(viewsets.ViewSet):
"""
API V2: api/2/speakers/
api/2/speakers/:id/
"""
queryset = Speaker.objects.all()
permission_classes = (IsAuthenticated, AuthenticatedReadAdminWrite)
permission_classes = (IsAuthenticated,)

def get_object(self, pk):
try:
Expand Down Expand Up @@ -1182,6 +1299,15 @@ def destroy(self, request, pk=None):
"""
speaker = self.get_object(pk)
speaker.delete()
if ('delete_binary' in request.query_params and request.query_params.get('delete_binary')=='true'):
speaker_binary_filename = speaker.uri.split("rwmedia/",1)[1]
delete_binary_from_server(speaker_binary_filename)
elif ('delete_binary' not in request.query_params or request.query_params.get('delete_binary')=='false'):
return Response({"detail": "Speaker deleted but binary deletion not requested, so binary file will remain on server."},
status.HTTP_204_NO_CONTENT)
else:
return Response({"detail": "No binary associated with specified Speaker found so binary not deleted!"},
status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down
Loading
Loading