UiL-OTS-labs / web-experiment-datastore

Other
0 stars 0 forks source link

Participant sessions #28

Closed bbonf closed 2 years ago

tymees commented 2 years ago

Overall I think it looks good. But in my usual fashion, I managed to find some things I'd like to see different. Keep in mind that it is just my opinion, you're absolutely free to disagree in which case I will yield ;) (Unless I really really really feel very strongly about it, but that almost never happens :P)

There is one more general issue I'd like to bring up. I think it might be better to split up the UploadView into separate views with a common base class for both the session and the non-session endpoints. The current view is more complicated due to the two scenario's, so splitting it up would make the code path that is being taken more straightforward (and thus easier to understand).

I've made a small example, which needs to be cleaned up a bit still. If you agree, I can finish it properly and commit it to this branch if you want.

class BaseUploadView(ApiExperimentView):
    """This view is used to upload data into an experiment.

    It only accepts plain text content, as otherwise the Django Rest
    Framework tries to force the data into a Python format. Not only is this
    fault intolerant, we want to store the data as raw as possible.

    Raw data doesn't lose resolution, and allows for more flexibility later on.

    This view also checks several things:
    - A valid access key should be used
    - The experiment should be approved
    - The experiment should have the 'OPEN' state

    If any of these conditions are not met, an error should be returned.
    """
    parser_classes = [PlainTextParser]

    def _validate_request(self, payload):
        # Error if no data was sent
        if not payload:
            return Response({
                "result":  ResultCodes.ERR_NO_DATA,
                "message": "No data was provided"
            },
                status=400  # Bad request
            )

        # The experiment should be approved and open.
        if not self.experiment.is_open():
            return Response({
                "result":  ResultCodes.ERR_NOT_OPEN,
                "message": "The experiment is not open to new uploads"
            },
                status=403  # Forbidden
            )

        return None

    def _save_data_point(self, payload):
        dp = DataPoint()
        dp.experiment = self.experiment
        dp.data = payload

        dp.save()

        return dp

class UploadView(BaseUploadView):

    def post(self, request, access_key):
        payload = request.data

        # If the validate method returns a value, it's an error response we want
        # to sent back
        if ret := self._validate_request(payload):
            return ret

        if self.experiment.has_groups():
            # return an error, as this is the non-session class
            pass # TODO: that error ;)

        # Create the new datapoint
        self._save_data_point(payload)

        # Return that everything went OK
        return Response({
            "result":  ResultCodes.OK,
            "message": "Upload successful"
        })

class SessionUploadView(BaseUploadView):

    def post(self, request, access_key, participant_id):
        payload = request.data

        # If the validate method returns a value, it's an error response we want
        # to sent back
        if ret := self._validate_request(payload):
            return ret

        if not self.experiment.has_groups():
            # return an error, as this is the session class
            pass  # TODO: that error ;)

        try:
            participant = self.experiment.participantsession_set.get(
                uuid=participant_id
            )
        except Experiment.DoesNotExist:
            return Response({
                "result":  ResultCodes.ERR_NO_SESSION,
                "message": "Bad or missing participant session id"
            }, status=403)

        # Create the new datapoint
        dp = self._save_data_point(payload)
        dp.session = participant
        dp.save()

        participant.complete()

        # Return that everything went OK
        return Response({
            "result":  ResultCodes.OK,
            "message": "Upload successful"
        })

PS: Dr. Guido, or: how I learned to stop worrying and love the walrus operator

tymees commented 2 years ago

I like the new upload views; it's better than my mockup!

One last point; we might want to look into auto-closing the experiment if all groups are full. That way, the jspsych-utils' code that checks if the experiment is closed will trigger correctly.