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

Double down on hooks, remove app plugin in favour of individual hooks for individual capabilities #12879

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 6 additions & 13 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -296,24 +296,17 @@ and in the second terminal, start the webpack build process for frontend assets:
Running in App Mode
~~~~~~~~~~~~~~~~~~~

Some of Kolibri's functionality will differ when being run as a mobile app. In order to run the development server in that "app mode" context, you can use the following commands.
Some of Kolibri's functionality will differ when being run as a mobile app. In order to access the development server in that "app mode" context, open Kolibri using the URL logged in the terminal.

.. code-block:: bash

# run the Python "app mode" server and the frontend server together:
yarn run app-devserver

# you may also run the python "app mode" server by itself
# this will require you to run the frontend server in a separate terminal
yarn run app-python-devserver
When the development server is started, you will see a message with a particular URL that you will need to use in order to initialize your browser session properly. Once your browser session has been initialized for use in the app mode, your browser session will remain in this mode until you clear your cookies.

This will run the script located at ``integration_testing/scripts/run_kolibri_app_mode.py``. There you may change the port, register app capabilities (ie, ``os_user``) and make adjustments to meet your needs.
.. code-block:: bash

When the app development server is started, you will see a message with a particular URL that you will need to use in order to initialize your browser session properly. Once your browser session has been initialized for use in the app mode, your browser session will remain in this mode until you clear your cookies, even if you've started your normal Kolibri development server.
Open this URL to activate app mode: http://127.0.0.1:8000/app/api/initialize/<token>

.. code-block:: bash
Where `<token>` will be a 32-digit hex string. This token is used to authenticate the app mode session.

[app-python-devserver] Kolibri running at: http://127.0.0.1:8000/app/api/initialize/6b91ec2b697042c2b360235894ad2632
To tweak the behaviour, the plugin that controls the app integrations can be edited in `integration_testing/development_plugin`.


Editor configuration
Expand Down
14 changes: 6 additions & 8 deletions docs/manual_testing/app_plugin/index.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
Testing Kolibri with app plugin enabled
=======================================
Testing Kolibri in app mode
===========================

The Kolibri app plugin
----------------------
App mode
--------

The Kolibri app plugin is designed to provide features and behavior with the mobile app user in mind. In order to test or develop Kolibri in this mode, there are commands that can be used to initialize Kolibri as needed.
Kolibri app mode is designed to provide features and behavior with the mobile app user in mind. In order to test or develop Kolibri in this mode, a browser session can be set to app mode.

By running the command: `yarn app-python-devserver` you will start Kolibri in development mode. You can also run `yarn app-devserver` to run the frontend devserver in parallel.

When you start the server with these commands, you will see a message with a URL pointing to `http://127.0.0.1:8000/app/api/initialize/<some token>` - visiting this URL will set your browser so that it can interact with Kolibri as it runs with the app plugin. **You will only have to do this once unless you clear your browser storage.**
When you start the server, you will see a message with a URL pointing to `http://127.0.0.1:8000/app/api/initialize/<some token>` - visiting this URL will set your browser so that it can interact with Kolibri in app mode. **You will only have to do this once unless you clear your browser storage.**
90 changes: 90 additions & 0 deletions integration_testing/development_plugin/kolibri_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import logging
import os

from magicbus.plugins import SimplePlugin
from magicbus.plugins.tasks import Autoreloader

from kolibri.core.content.hooks import ShareFileHook
from kolibri.core.device.hooks import CheckIsMeteredHook
from kolibri.core.device.hooks import GetOSUserHook
from kolibri.core.tasks.hooks import JobHook
from kolibri.plugins import KolibriPluginBase
from kolibri.plugins.hooks import register_hook
from kolibri.utils.server.hooks import KolibriProcessHook

logger = logging.getLogger(__name__)


class ExampleAppPlugin(KolibriPluginBase):
pass


@register_hook
class ExampleAppGetOSUserHook(GetOSUserHook):
def get_os_user(self, auth_token):
return "os_user", True


@register_hook
class ExampleAppCheckIsMeteredHook(CheckIsMeteredHook):
def check_is_metered(self):
return True


@register_hook
class ExampleAppShareFileHook(ShareFileHook):
def share_file(self, file_path, message):
logger.debug(f"Sharing file {file_path} with message {message}")


@register_hook
class ExampleAppJobHook(JobHook):
def schedule(self, job, orm_job):
logger.debug(f"Scheduling job {job} with ORM job {orm_job}")

def update(self, job, orm_job, state=None, **kwargs):
from kolibri.core.tasks.job import log_status

log_status(job, orm_job, state=state, **kwargs)

def clear(self, job, orm_job):
logger.debug(f"Clearing job {job} with ORM job {orm_job}")


class AppUrlLoggerPlugin(SimplePlugin):
def SERVING(self, port):
self.port = port

def RUN(self):
from kolibri.core.device.utils import app_initialize_url

start_url = "http://127.0.0.1:{port}".format(
port=self.port
) + app_initialize_url(auth_token="1234")
# Use warning to make sure this message stands out in the console
logger.warning(
"Open this URL to activate app mode: {start_url}".format(
start_url=start_url
)
)


@register_hook
class DeveloperAppUrlLogger(KolibriProcessHook):
MagicBusPluginClass = AppUrlLoggerPlugin


class KolibriAutoReloader(Autoreloader):
def __init__(self, bus):
super().__init__(bus)
from kolibri.utils import conf

plugins = os.path.join(conf.KOLIBRI_HOME, "plugins.json")
options = os.path.join(conf.KOLIBRI_HOME, "options.ini")
self.files.add(plugins)
self.files.add(options)


@register_hook
class KolibriAutoReloadHook(KolibriProcessHook):
MagicBusPluginClass = KolibriAutoReloader
65 changes: 0 additions & 65 deletions integration_testing/scripts/run_kolibri_app_mode.py

This file was deleted.

1 change: 0 additions & 1 deletion kolibri/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
#: Define it here to avoid introspection malarkey, and to allow for
#: import in setup.py for creating a list of plugin entry points.
INTERNAL_PLUGINS = [
"kolibri.plugins.app",
"kolibri.plugins.coach",
"kolibri.plugins.context_translation",
"kolibri.plugins.default_theme",
Expand Down
9 changes: 3 additions & 6 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@
from kolibri.core.serializers import HexOnlyUUIDField
from kolibri.core.utils.pagination import ValuesViewsetPageNumberPagination
from kolibri.core.utils.urls import reverse_path
from kolibri.plugins.app.utils import interface
from kolibri.utils.urls import validator

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -928,18 +927,16 @@ def create(self, request):
facility_id = request.data.get("facility", None)

# Only enforce this when running in an app
if (
interface.enabled
and not allow_other_browsers_to_connect()
and not valid_app_key_on_request(request)
if not allow_other_browsers_to_connect() and not valid_app_key_on_request(
request
):
return Response(
[{"id": error_constants.INVALID_CREDENTIALS, "metadata": {}}],
status=status.HTTP_401_UNAUTHORIZED,
)

user = None
if interface.enabled and valid_app_key_on_request(request):
if valid_app_key_on_request(request):
# If we are in app context, then try to get the automatically created OS User
# if it matches the username, without needing a password.
user = self._check_os_user(request, username)
Expand Down
4 changes: 2 additions & 2 deletions kolibri/core/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from kolibri.core.auth.constants.demographics import NOT_SPECIFIED
from kolibri.core.auth.constants.demographics import UniqueIdsValidator
from kolibri.core.auth.constants.morango_sync import ScopeDefinitions
from kolibri.core.device.hooks import GetOSUserHook
from kolibri.core.device.utils import device_provisioned
from kolibri.core.device.utils import get_device_setting
from kolibri.core.device.utils import is_full_facility_import
Expand All @@ -79,7 +80,6 @@
from kolibri.core.fields import DateTimeTzField
from kolibri.core.fields import JSONField
from kolibri.core.utils.validators import JSON_Schema_Validator
from kolibri.plugins.app.utils import interface
from kolibri.utils.time_utils import local_now

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -683,7 +683,7 @@ def get_or_create_os_user(self, auth_token, facility=None):
If the user does not exist in the database, it is created.
"""
try:
os_username, is_superuser = interface.get_os_user(auth_token)
os_username, is_superuser = GetOSUserHook.retrieve_os_user(auth_token)
except NotImplementedError:
return None
if not os_username:
Expand Down
39 changes: 39 additions & 0 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.serializers import CharField
from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.serializers import Serializer
from rest_framework.views import APIView

from kolibri.core.api import BaseValuesViewset
from kolibri.core.api import CreateModelMixin
Expand All @@ -55,6 +59,7 @@
from kolibri.core.bookmarks.models import Bookmark
from kolibri.core.content import models
from kolibri.core.content import serializers
from kolibri.core.content.hooks import ShareFileHook
from kolibri.core.content.models import ContentDownloadRequest
from kolibri.core.content.models import ContentRemovalRequest
from kolibri.core.content.models import ContentRequestReason
Expand All @@ -75,11 +80,13 @@
get_channel_stats_from_studio,
)
from kolibri.core.content.utils.paths import get_channel_lookup_url
from kolibri.core.content.utils.paths import get_content_storage_file_path
from kolibri.core.content.utils.paths import get_local_content_storage_file_url
from kolibri.core.content.utils.search import get_available_metadata_labels
from kolibri.core.content.utils.stopwords import stopwords_set
from kolibri.core.decorators import query_params_required
from kolibri.core.device.models import ContentCacheKey
from kolibri.core.device.permissions import FromAppContextPermission
from kolibri.core.discovery.utils.network.client import NetworkClient
from kolibri.core.discovery.utils.network.errors import NetworkLocationConnectionFailure
from kolibri.core.discovery.utils.network.errors import NetworkLocationNotFound
Expand Down Expand Up @@ -1955,3 +1962,35 @@ def kolibri_studio_status(self, request, **kwargs):
NetworkLocationNotFound,
):
return Response({"status": "offline", "available": False})


class ShareFileSerializer(Serializer):
content_node = PrimaryKeyRelatedField(
queryset=models.ContentNode.objects.filter(available=True).exclude(
kind__in=[content_kinds.TOPIC, content_kinds.EXERCISE]
)
)
message = CharField(max_length=1000)


class ShareFileView(APIView):
permission_classes = (IsAuthenticated, FromAppContextPermission)

def post(self, request):
serializer = ShareFileSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
content_node = serializer.validated_data["content_node"]
message = serializer.validated_data["message"]
# Rely on default priority ordering
default_file = content_node.files.first()
if not default_file:
return Response(
{"error": "No files found for content node"},
status=status.HTTP_400_BAD_REQUEST,
)
filepath = get_content_storage_file_path(default_file.local_file.get_filename())
try:
ShareFileHook.execute_file_share(filepath, message)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_201_CREATED)
2 changes: 2 additions & 0 deletions kolibri/core/content/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .api import ContentRequestViewset
from .api import FileViewset
from .api import RemoteChannelViewSet
from .api import ShareFileView
from .api import UserContentNodeViewset

router = routers.SimpleRouter()
Expand Down Expand Up @@ -54,5 +55,6 @@
ChannelThumbnailView.as_view(),
name="channel-thumbnail",
),
path("sharefile/", ShareFileView.as_view(), name="sharefile"),
re_path(r"^", include(router.urls)),
]
16 changes: 14 additions & 2 deletions kolibri/core/content/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""
import json
from abc import abstractmethod
from abc import abstractproperty

from django.core.serializers.json import DjangoJSONEncoder
from django.utils.safestring import mark_safe
Expand All @@ -25,7 +24,8 @@ class ContentRendererHook(WebpackBundleHook, WebpackInclusionMixin):
"""

#: Set tuple of format presets that this content renderer can handle
@abstractproperty
@property
@abstractmethod
def presets(self):
pass

Expand Down Expand Up @@ -78,3 +78,15 @@ class ContentNodeDisplayHook(KolibriHook):
@abstractmethod
def node_url(self, content_node):
pass


@define_hook
class ShareFileHook(KolibriHook):
@abstractmethod
def share_file(self, filename, message):
pass

@classmethod
def execute_file_share(cls, filename, message):
for hook in cls.registered_hooks:
hook.share_file(filename, message)
rtibbles marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading