Skip to content

Commit

Permalink
Fix failing tests and remove old device provisioning endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexVelezLl committed Feb 24, 2025
1 parent 4216e5b commit 27cab39
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 506 deletions.
32 changes: 0 additions & 32 deletions kolibri/core/device/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from sys import version_info

from django.conf import settings
from django.contrib.auth import login
from django.db.models import Exists
from django.db.models import F
from django.db.models import Max
Expand All @@ -13,17 +12,14 @@
from django.http import Http404
from django.http.response import HttpResponseBadRequest
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import get_language
from django.views.decorators.csrf import csrf_protect
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework import FilterSet
from django_filters.rest_framework import ModelChoiceFilter
from morango.constants import transfer_statuses
from morango.models import InstanceIDModel
from morango.models import TransferSession
from rest_framework import mixins
from rest_framework import status
from rest_framework import views
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
Expand All @@ -37,12 +33,9 @@
from .models import LearnerDeviceStatus
from .models import StatusSentiment
from .models import UserSyncStatus
from .permissions import NotProvisionedCanPost
from .permissions import UserHasAnyDevicePermissions
from .serializers import DevicePermissionsSerializer
from .serializers import DeviceProvisionSerializer
from .serializers import DeviceSettingsSerializer
from kolibri.core.analytics.tasks import schedule_ping
from kolibri.core.api import ReadOnlyValuesViewset
from kolibri.core.auth.api import KolibriAuthPermissions
from kolibri.core.auth.api import KolibriAuthPermissionsFilter
Expand Down Expand Up @@ -96,31 +89,6 @@ class DevicePermissionsViewSet(viewsets.ModelViewSet):
filter_backends = (KolibriAuthPermissionsFilter,)


@method_decorator(csrf_protect, name="dispatch")
class DeviceProvisionView(viewsets.GenericViewSet):
permission_classes = (NotProvisionedCanPost,)
serializer_class = DeviceProvisionSerializer

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.save()
if data["superuser"]:
login(request, data["superuser"])
output_serializer = self.get_serializer(data)
response_data = output_serializer.data

# Restart zeroconf before moving along when we're a SoUD
if response_data["is_soud"]:
logger.info("Updating our Kolibri instance on the Zeroconf network now")
from kolibri.utils.server import update_zeroconf_broadcast

update_zeroconf_broadcast()

schedule_ping() # Trigger telemetry pingback after we've provisioned
return Response(response_data, status=status.HTTP_201_CREATED)


class FreeSpaceView(mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (IsAuthenticated,)

Expand Down
6 changes: 0 additions & 6 deletions kolibri/core/device/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from .api import DeviceInfoView
from .api import DeviceNameView
from .api import DevicePermissionsViewSet
from .api import DeviceProvisionView
from .api import DeviceRestartView
from .api import DeviceSettingsView
from .api import DriveInfoViewSet
Expand All @@ -23,11 +22,6 @@

urlpatterns = [
re_path(r"^", include(router.urls)),
re_path(
r"^deviceprovision/",
DeviceProvisionView.as_view({"post": "create"}),
name="deviceprovision",
),
re_path(r"^freespace/", FreeSpaceView.as_view({"get": "list"}), name="freespace"),
re_path(r"^deviceinfo/", DeviceInfoView.as_view(), name="deviceinfo"),
re_path(r"^devicesettings/", DeviceSettingsView.as_view(), name="devicesettings"),
Expand Down
216 changes: 0 additions & 216 deletions kolibri/core/device/serializers.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
from django.db import transaction
from django.utils.translation import check_for_language
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ParseError

from kolibri.core.auth.constants import user_kinds
from kolibri.core.auth.constants.facility_presets import choices
from kolibri.core.auth.models import Facility
from kolibri.core.auth.models import FacilityUser
from kolibri.core.auth.serializers import FacilitySerializer
from kolibri.core.content.tasks import automatic_resource_import
from kolibri.core.content.tasks import automatic_synchronize_content_requests_and_import
from kolibri.core.device.models import DevicePermissions
from kolibri.core.device.models import DeviceSettings
from kolibri.core.device.models import OSUser
from kolibri.core.device.utils import APP_AUTH_TOKEN_COOKIE_NAME
from kolibri.core.device.utils import provision_device
from kolibri.core.device.utils import provision_single_user_device
from kolibri.core.device.utils import valid_app_key_on_request
from kolibri.plugins.app.utils import GET_OS_USER
from kolibri.plugins.app.utils import interface
from kolibri.utils.filesystem import check_is_directory
from kolibri.utils.filesystem import get_path_permission

Expand Down Expand Up @@ -49,209 +36,6 @@ def validate_language_id(self, language_id):
return language_id


class DeviceProvisionSerializer(DeviceSerializerMixin, serializers.Serializer):
facility = FacilitySerializer(required=False, allow_null=True)
facility_id = serializers.CharField(max_length=50, required=False, allow_null=True)
preset = serializers.ChoiceField(choices=choices, required=False, allow_null=True)
superuser = NoFacilityFacilityUserSerializer(required=False)
language_id = serializers.CharField(max_length=15)
device_name = serializers.CharField(max_length=50, allow_null=True)
settings = serializers.JSONField()
allow_guest_access = serializers.BooleanField(allow_null=True)
is_provisioned = serializers.BooleanField(default=True)
is_soud = serializers.BooleanField(default=True)

class Meta:
fields = (
"facility",
"facility_id",
"preset",
"superuser",
"language_id",
"device_name",
"settings",
"allow_guest_access",
"is_provisioned",
"is_soud",
)

def validate(self, data):
if (
GET_OS_USER in interface
and "request" in self.context
and valid_app_key_on_request(self.context["request"])
):
data["auth_token"] = self.context["request"].COOKIES.get(
APP_AUTH_TOKEN_COOKIE_NAME
)
elif "superuser" not in data:
raise serializers.ValidationError("Superuser is required for provisioning")

has_facility = "facility" in data
has_facility_id = "facility_id" in data

if (has_facility and has_facility_id) or (
not has_facility and not has_facility_id
):
raise serializers.ValidationError(
"Please provide one of `facility` or `facility_id`; but not both."
)

if has_facility and "preset" not in data:
raise serializers.ValidationError(
"Please provide `preset` if `facility` is specified"
)

return data

def create(self, validated_data): # noqa C901
"""
Endpoint for initial setup of a device.
Expects a value for:
default language - the default language of this Kolibri device
facility - the required fields for setting up a facility
facilitydataset - facility configuration options
superuser - the required fields for a facilityuser who will be set as the super user for this device
"""
with transaction.atomic():
if validated_data.get("facility"):
facility_data = validated_data.pop("facility")
facility_id = None
else:
facility_id = validated_data.pop("facility_id")
facility_data = None

if facility_id:
try:
# We've already imported the facility to the device before provisioning
facility = Facility.objects.get(pk=facility_id)
preset = facility.dataset.preset
facility_created = False
except Facility.DoesNotExist:
raise ParseError(
"Facility with id={0} does not exist".format(facility_id)
)
else:
try:
facility = Facility.objects.create(**facility_data)
preset = validated_data.pop("preset")
facility.dataset.preset = preset
facility.dataset.reset_to_default_settings(preset)
facility_created = True
except Exception:
raise ParseError("Please check `facility` or `preset` fields.")

custom_settings = validated_data.pop("settings")

allow_learner_download_resources = False

if facility_created:
# We only want to update things about the facility or the facility dataset in the case
# that we are creating the facility during this provisioning process.
# If it has been imported as part of a whole facility import, then we should not be
# making edits just now.
# If it has been imported as part of a learner only device import, then editing
# these things now will a) not be synced back, and b) will actively block future
# syncing of updates to the facility or facility dataset from our 'upstream'.

if "on_my_own_setup" in custom_settings:
facility.on_my_own_setup = custom_settings.pop("on_my_own_setup")
# If we are in on my own setup, then we want to allow learners to download resources
# to give them a seamless onboarding experience, without the need to use the device
# plugin to download resources en masse.
allow_learner_download_resources = True

# overwrite the settings in dataset_data with validated_data.settings
for key, value in custom_settings.items():
if value is not None:
setattr(facility.dataset, key, value)
facility.dataset.save()

auth_token = validated_data.pop("auth_token", None)

if "superuser" in validated_data:
superuser_data = validated_data["superuser"]
# We've imported a facility if the username exists
try:
superuser = FacilityUser.objects.get(
username=superuser_data["username"]
)
except FacilityUser.DoesNotExist:
try:
# Otherwise we make the superuser
superuser = FacilityUser.objects.create_superuser(
superuser_data["username"],
superuser_data["password"],
facility=facility,
full_name=superuser_data.get("full_name"),
)
except Exception:
raise ParseError(
"`username`, `password`, or `full_name` are missing in `superuser`"
)
if auth_token:
# If we have an auth token, we need to create an OSUser for the superuser
# so that we can associate the user with the OSUser
os_username, _ = interface.get_os_user(auth_token)
OSUser.objects.update_or_create(
os_username=os_username, defaults={"user": superuser}
)

elif auth_token:
superuser = FacilityUser.objects.get_or_create_os_user(
auth_token, facility=facility
)
else:
raise ParseError(
"Either `superuser` or `auth_token` must be provided for provisioning"
)

is_soud = validated_data.pop("is_soud")

if superuser:
if facility_created:
# Only do this if this is a created, not imported facility.
facility.add_role(superuser, user_kinds.ADMIN)

if DevicePermissions.objects.count() == 0:
DevicePermissions.objects.create(
user=superuser,
is_superuser=True,
can_manage_content=True,
)

# Create device settings
language_id = validated_data.pop("language_id")
allow_guest_access = validated_data.pop("allow_guest_access")

if allow_guest_access is None:
allow_guest_access = preset != "formal"

provisioning_data = {
"device_name": validated_data["device_name"],
"is_provisioned": validated_data["is_provisioned"],
"language_id": language_id,
"default_facility": facility,
"allow_guest_access": allow_guest_access,
"allow_learner_download_resources": allow_learner_download_resources,
}

if is_soud:
provision_single_user_device(superuser, **provisioning_data)
else:
provision_device(**provisioning_data)

# The API View expects these fields to be in the returned serialized data as well
provisioning_data.update(
{
"superuser": superuser,
"preset": preset,
"settings": custom_settings,
}
)
return provisioning_data


class PathListField(serializers.ListField):
def to_representation(self, data):
return [
Expand Down
9 changes: 5 additions & 4 deletions kolibri/core/device/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from kolibri.core.device.utils import valid_app_key_on_request
from kolibri.core.tasks.decorators import register_task
from kolibri.core.tasks.permissions import FirstProvisioning
from kolibri.core.tasks.utils import get_current_job
from kolibri.core.tasks.validation import JobValidator
from kolibri.plugins.app.utils import GET_OS_USER
from kolibri.plugins.app.utils import interface
Expand Down Expand Up @@ -179,7 +180,7 @@ def provisiondevice(**data): # noqa C901
"Either `superuser` or `auth_token` must be provided for provisioning"
)

is_soud = data.pop("is_soud")
is_soud = data.pop("is_soud", True)

if superuser:
if facility_created:
Expand Down Expand Up @@ -220,6 +221,6 @@ def provisiondevice(**data): # noqa C901

schedule_ping() # Trigger telemetry pingback after we've provisioned

return {
"facility_id": facility.id,
}
job = get_current_job()
if job:
job.update_metadata(facility_id=facility.id)
Loading

0 comments on commit 27cab39

Please sign in to comment.