diff --git a/general/app/_files/Templates_downloaded_on_login.png b/general/app/_files/Templates_downloaded_on_login.png deleted file mode 100644 index 00489b96e7..0000000000 Binary files a/general/app/_files/Templates_downloaded_on_login.png and /dev/null differ diff --git a/general/app/_files/Templates_downloaded_when_requested.png b/general/app/_files/Templates_downloaded_when_requested.png deleted file mode 100644 index e5654c71d7..0000000000 Binary files a/general/app/_files/Templates_downloaded_when_requested.png and /dev/null differ diff --git a/general/app/_files/dynamic_templates.jpg b/general/app/_files/dynamic_templates.jpg new file mode 100644 index 0000000000..08630430bf Binary files /dev/null and b/general/app/_files/dynamic_templates.jpg differ diff --git a/general/app/_files/static_templates.jpg b/general/app/_files/static_templates.jpg new file mode 100644 index 0000000000..441ab0278e Binary files /dev/null and b/general/app/_files/static_templates.jpg differ diff --git a/general/app/development/development-guide.md b/general/app/development/development-guide.md index bd0838e493..c075a332c1 100644 --- a/general/app/development/development-guide.md +++ b/general/app/development/development-guide.md @@ -284,15 +284,29 @@ Learn more about unit tests in the [Testing](#testing) section. ## Routing -All core features and addons can define their own routes, and we can do that in their main module. However, those are loaded when the application starts up, and that won't be desirable in most cases. In those situations, we can use [lazy loading](https://angular.io/guide/lazy-loading-ngmodules) to defer it until necessary. To encapsulate lazy functionality, we can define a [Routed Module](https://angular.io/guide/module-types#routed) named `{feature-name}LazyModule`. For example, the *login* core feature defines both a `CoreLoginModule` and a `CoreLoginLazyModule`. +All core features and addons can define their own routes, and we can do that in their main module. However, those are loaded when the application starts up, and that won't be desirable in most cases. We can use [lazy loading](https://angular.io/guide/lazy-loading-ngmodules) to defer loading routes until they are necessary. To encapsulate lazy functionality, we can define a [Routed Module](https://angular.io/guide/module-types#routed) named `{feature-name}LazyModule`. For example, the *login* core feature defines both a `CoreLoginModule` (for routes that are loaded when the application starts up) and a `CoreLoginLazyModule` (for routes that are loaded only when necessary). + +### Dynamic Routes With the [folders structure](#folders-structure) we're using, it is often the case where different core features or addons need to define routes depending on each other. For example, the *mainmenu* feature defines the layout and routes for the tabs that are always present at the bottom of the UI. But the home tab is defined in the *home* feature. In this scenario, it would be possible to just import the pages from the *home* module within the *mainmenu*, since both are core features and are allowed to know each other. But that approach can become messy, and what happens if an addon also needs to define a tab (like *privatefiles*)? -As described in the [addons/ folder documentation](#addons), the answer to this situation is using the dependency inversion pattern. Instead of the *mainmenu* depending on anything rendering a tab (*home*, *privatefiles*, etc.), we can make those depend on *mainmenu*. And we can do that using Angular's container. +As described in the [addons/ folder documentation](#addons), the answer to this situation is using the dependency inversion principle. Instead of the *mainmenu* depending on anything rendering a tab (*home*, *privatefiles*, etc.), we can make those depend on *mainmenu*. And we can do that using Angular's container. + +In order to allow injecting routes from other modules, we create a separated [Routing Module](https://angular.io/guide/module-types#routing-ngmodules). This is the only situation where we'll have a dedicated module for routing. Any routes that are not meant to be injected can be defined directly on their main or lazy module. + +It is often the case that modules using injected routes use a [RouterOutlet](https://angular.io/api/router/RouterOutlet). For that reason, injected routes can be defined either as children or siblings of the main route. The difference between those is that a child will be rendered within the outlet, whilst a sibling will replace the entire page. In order to make this distinction, routing modules accept either an array of routes to use as siblings or an object indicating both types of routes. + +Finally, since these routes are defined dynamically, they cannot be imported statically when defining parent routes. They will need to be encapsulated on a builder function, taking an `injector` as an argument to resolve all the injected routes. You can see an example of this in the `buildTabMainRoutes`, and how it's used across the app. + +### Split View Routes -In order to allow injecting routes from other modules, we create a separated [Routing Module](https://angular.io/guide/module-types#routing-ngmodules). This is the only situation where we'll have a dedicated module for routing, in order to reduce the amount of module files in a feature root folder. Any routes that are not injected can be defined directly on their main or lazy module. +Some pages in the app use a split-view pattern that consists of a navigation menu on the left, and the main content in the right (in LTR interfaces). It is typically used to display a list of items in the menu, and display the contents of the selected item in the content. For example, showing a list of settings on the left with their content on the right. -It is often the case that modules using injected routes have a [RouterOutlet](https://angular.io/api/router/RouterOutlet). For that reason, injected routes can be defined either as children or siblings of the main route. The difference between those is that a child will be rendered within the outlet, whilst a sibling will replace the entire page. In order to make this distinction, routing modules accept either an array of routes to use as siblings or an object indicating both types of routes. +This pattern is used in large screens (such as tablets), and logically is made up of two pages: one used for the menu and one for the content. The one with the menu defines an outlet for the content page, and in smaller devices (such as mobile phones), the outlet is hidden and navigating to items will override the entire page instead of populating the outlet. This is achieved by the styles and markup of the `` component. + +In order for the different behaviour to take place, routes are defined twice. Once where the content is a children of the menu, and again where the content is a sibling of the menu. These two definitions would clash in normal situations, but they are defined with a conditional that toggles them depending on the active breakpoint. You can find an example looking at the route definitions in the `CoreSettingsLazyModule`, which correspond with the routes that you can visit from the Main Menu > More > App Settings. + +The navigation between these routes is often encapsulated within a `CoreListItemsManager` instance, that takes care of discerning the current active item and updating the route when selected items change. This manager will obtain the items from a `CoreRoutedItemsManagerSource`, which is necessary to enable [swipe navigation](#navigating-using-swipe-gestures). ### Navigating between routes @@ -310,6 +324,14 @@ Other than navigation, this service also contains some helpers that are not avai Make sure to [check out the full api](https://github.com/moodlehq/moodleapp/blob/main/src/core/services/navigator.ts) to learn more about the `CoreNavigator` service. +### Navigating using swipe gestures + +Most pages that use a split-view in tablets can be navigated using swipe gestures in mobile devices. The navigation is often encapsulated within a `CoreSwipeNavigationItemsManager` instance. + +As mentioned in the [split-view section](#split-view-routes), the items used by the manager are obtained from a `CoreRoutedItemsManagerSource`. This source will be reused between menu and content pages in mobile as well, so that swipe navigation respects any filters that have been applied in the menu page. In order to make sure that the same instance is reused, instead of creating a new one, these can be instantiated using the `CoreRoutedItemsManagerSourcesTracker.getOrCreateSource()` method. It will reuse instances that are still active, and when passed to managers the references will be cleared up when the managers are destroyed. + +You can find an example of this pattern in `CoreUserParticipantsPage`, where participants can be filtered and the swipe navigation will respect the filtered results. + ## Singletons The application relies heavily on the [Singleton design pattern](https://en.wikipedia.org/wiki/Singleton_pattern), and there are two types of singletons used throughout the application: Pure singletons and Service singletons. @@ -382,6 +404,26 @@ All the nomenclature can be a bit confusing, so let's do a recap: - Service Singleton: An instance of a Singleton Service. - Singleton Proxy: An object that relays method calls to a Service Singleton instance. +## Database + +Most of the persistent data in the application is stored in SQLite databases. In particular, there is one database for global app configuration, and one for each site. Reading and writing data is encapsulated in the `CoreDatabaseTable` class. Each table can be configured to use one of the following caching strategies: + +- Eager Caching: When the table is initialised, it will query all the records and store them in memory. This improves performance for data that is read very often, because reads will happen in-memory without touching the database. But it shouldn't be used for tables with a lot of records, to reduce memory consumption. +- Lazy Caching: Lazy caching works similar to eager caching, but instead of querying all the records upfront it'll remember records after reading them for the first time. This strategy is more appropriate for tables that are read often but have too many records to cache completely in memory. +- No Caching: Finally, for tables that are written more often than they are read, it is possible to disable caching altogether. + +Something else important to note is that not all these tables are instantiated when the application is initialized, so for example even though a table may have Eager loading; it could be itself initialized lazily. + +### Schema migrations + +Table schemas are declared using `CoreAppSchema`, `CoreSiteSchema`, and `SQLiteDBTableSchema` interfaces; and invoked using `CoreApp.createTablesFromSchema()` and `CoreSitesProvider.applySiteSchemas()`. In the case of site tables, these can be registered with the `CORE_SITE_SCHEMAS` injection token and they'll be invoked automatically when a new site is created. + +In order to make some changes in existing schemas, it'll be necessary to change the `version` number and implement the `migrate` method to perform any operations necessary during the migration. + +### Legacy + +Ideally, all interactions with the database would go through a `CoreDatabaseTable` instance. However, there is still some code using the previous approach through the `SQLiteDB` class. This should be avoided for new code, and eventually migrated to use the new approach to take advantage of caching. + ## Application Lifecycle When the application is launched, the contents of [index.html](#indexhtml) are rendered on screen. This file is intentionally concise because all the flare is added by JavaScript, and the splash screen will be covering the application UI until it has fully started. If you are developing in the browser, this will be a blank screen for you given that the splash screen is not available on the web. We are not targeting browsers in production, so it's acceptable to have this behaviour during development. @@ -420,7 +462,6 @@ You can learn more about this in the [Acceptance testing for the Moodle App](./t ## See also -- [Moodle App Scripts: gulp push](./scripts/gulp-push.md) - [Moodle App Accessibility](./accessibility.md) - [Moodle App Link Handling](./link-handling/app-links.md) - [Moodle App Translation](https://docs.moodle.org/en/Moodle_App_Translation) diff --git a/general/app/development/link-handling/app-links.md b/general/app/development/link-handling/app-links.md index 7f8e1d635b..1fcddb584d 100644 --- a/general/app/development/link-handling/app-links.md +++ b/general/app/development/link-handling/app-links.md @@ -17,7 +17,7 @@ When a user presses a link in the Moodle app, the behaviour changes depending on ## Extending the list of supported URLs -The app has a defined list of supported URLs. If you have a plugin adapted to work in the app and you want to support links to your plugin you will need to create a Link Handler. For more information and examples about this, please see the [Link handlers](../plugins-development-guide/index.md#link-handlers) documentation. +The app has a defined list of supported URLs. If you have a plugin adapted to work in the app and you want to support links to your plugin you will need to create a Link Handler. For more information and examples about this, please see the [CoreContentLinksDelegate](../plugins-development-guide/api-reference.md#corecontentlinksdelegate) documentation. ## Opening links in an embedded browser diff --git a/general/app/development/link-handling/deep-linking.md b/general/app/development/link-handling/deep-linking.md index e93557b58b..b16800c025 100644 --- a/general/app/development/link-handling/deep-linking.md +++ b/general/app/development/link-handling/deep-linking.md @@ -1,6 +1,6 @@ --- title: Moodle App Deep Linking -sidebar_label: Deep Linking +sidebar_label: Deep linking sidebar_position: 2 tags: - Moodle App @@ -78,16 +78,6 @@ The redirect parameter can be a relative URL based on the base URL. The example moodlemobile://https://domain.com?redirect=/course/view.php?id=2 ``` -## Before 3.7 - -Deep linking was introduced in version 3.6.1, but it had a different format that was updated in 3.7.0 to the one we use today. - -This is an example of the previous format: - -```text -moodlemobile://link=https://mysite.es/mod/choice/view.php?id=8 -``` - ## See also - [Custom URL Scheme Cordova plugin used by the app](https://github.com/EddyVerbruggen/Custom-URL-scheme). diff --git a/general/app/development/link-handling/notification-links.md b/general/app/development/link-handling/notification-links.md new file mode 100644 index 0000000000..2b9fce4514 --- /dev/null +++ b/general/app/development/link-handling/notification-links.md @@ -0,0 +1,19 @@ +--- +title: Moodle App Notification Links +sidebar_label: Notification links +sidebar_position: 3 +tags: + - Moodle App +--- + +Push notifications will open the app when clicked, and you can customize which page is open by default. + +The easiest way to achieve it is to include a `contexturl` property in your notification. Notice that using this property, the url will also be displayed when you see the notification in the web. You can override this behaviour in the app using `appurl` within `customdata`: + +```php +$notification->customdata = [ + 'appurl' => $myurl->out(), +]; +``` + +The url will be handled using the default [link handling](./app-links.md) in the app. If you want to implement some custom behaviour when opening notifications, you can achieve it with a Site Plugin implementing a [CorePushNotificationsDelegate](../plugins-development-guide/api-reference.md#corepushnotificationsdelegate) handler. diff --git a/general/app/development/network-debug.md b/general/app/development/network-debug.md index 140c48a34b..74f71c1b82 100644 --- a/general/app/development/network-debug.md +++ b/general/app/development/network-debug.md @@ -72,12 +72,12 @@ Here's how to debug web service errors: You can execute the following in the JavaScript console: -```javascript +```js window.handleOpenURL("moodlemobile://URL?token=WSTOKEN"); ``` You can also launch a normal authentication process (allowing the authentication popup) and capture the redirect to `moodlemobile://...` created by the `admin/tool/mobile/launch.php` script and then execute the following in the console: -```javascript +```js window.handleOpenURL("moodlemobile://token=ABCxNGUxMD........="); ``` diff --git a/general/app/development/plugins-development-guide/api-reference.md b/general/app/development/plugins-development-guide/api-reference.md new file mode 100644 index 0000000000..4a67d4ced1 --- /dev/null +++ b/general/app/development/plugins-development-guide/api-reference.md @@ -0,0 +1,971 @@ +--- +title: Moodle App Plugins API Reference +sidebar_label: API Reference +sidebar_position: 2 +tags: + - Moodle App +--- + +## Content responses + +Methods defined in the `classes/output/mobile.php` class will be called through the `tool_mobile_get_content` Web Service, and should have the following structure in their response: + +| Name | Default | Description | +|--------------------|-------------|-------------| +| `templates` | - | An array of templates used by the handler.

For `method` responses, the first one will be used to render the page content and all of them will be available in JavaScript at `this.CONTENT_TEMPLATES`.

For `init` responses, they will be available in JavaScript at `this.INIT_TEMPLATES`.

You can learn more about this in [Rendering UI](./index.md#rendering-ui).| +| `templates[].id` | Required | ID of the template. | +| `templates[].html` | Required | HTML code. This HTML will be rendered in the app, so you should use Ionic and custom app components rather than Bootstrap. | +| `javascript` | - | JavaScript code to execute immediately after the Web Service call returns. This may happen before the template is rendered in the DOM, if you need to make sure that it has been rendered you should put your code inside of a `setTimeout` callback. | +| `otherdata` | - | Object with arbitrary data to use in the template and JavaScript, supporting 2-way data-binding.

For `method` responses, it will be available in JavaScript at `this.CONTENT_OTHERDATA`.

For `init` responses, it will be available in JavaScript at `this.INIT_OTHERDATA`.

Note that sending nested arrays will cause an error, you should serialize any complex values with `json_encode` (they'll still be available as proper arrays or objects in JavaScript). | +| `files` | - | A list of files that the app should be able to download (mostly for offline usage). | +| `restrict` | - | Object with conditions to restrict the handler depending on the context.

This is only used in `init` responses. | +| `restrict.users` | All users | List of allowed user IDs. | +| `restrict.courses` | All courses | List of allowed course IDs. | +| `disabled` | `false` | Whether to disable the handler entirely.

This is only used in `init` responses. | + +### Functions + +The JavaScript returned by content responses, as well as the JavaScript executed in Angular within templates, has access to the following functions: + +- `openContent(...)`: Open a new page to display some new content. Takes the following arguments: + - `title: string`: Title to display with the new content. + - `args: Record`: Arguments for the new content request. + - `component?: string`: Component for the new content request. If not provided, the current component will be used. + - `method?: string`: Method for the new content request. If not provided, the current method will be used. + - `jsData?: Record | boolean`: Variables to use in the JavaScript of the new content response. If true is supplied instead of an object, all initial variables from current page will be used. + - `preSets?: CoreSiteWSPreSets`: Presets for the Web Service call of the new content request. + - `ptrEnabled?: boolean`: Whether PTR (pull-to-refresh) should be enabled in the new page. Defaults to true. +- `refreshContent(showSpinner: boolean = true)`: Refresh the current content. +- `updateContent(...)`: Refresh the current page with new content. Takes the following arguments: + - `args?: Record`: Arguments for the new content request. + - `component?: string`: Component for the new content request. If not provided, the current component will be used. + - `method?: string`: Method for the new content request. If not provided, the current method will be used. + - `jsData?: Record`: Variables to use in the JavaScript of the new content response. + - `preSets?: CoreSiteWSPreSets`: Presets for the Web Service call of the new content request. +- `updateModuleCourseContent(cmId: number, alreadyFetched?: boolean)`: Update the content for a module in the course page. Only works if that module is a site plugin using `coursepagemethod`. +- `updateCachedContent()`: Update cached content for the page without reloading the UI. This is useful to update subsequent views. Only available in 4.4+. + +You can learn how to use these functions in the [Group Selector example](./examples/groups-selector.md#loading-new-content). + +### Lifecycle + +For content used to render pages, it is possible to hook into [Ionic Life Cycle Hooks](https://ionicframework.com/docs/api/router-outlet#life-cycle-hooks) in JavaScript. + +Additionally, you can also define a `canLeave` method that will be used in the `canDeactive` guard of the [Angular route definition](https://angular.io/api/router/Route). + +```js +this.ionViewWillLeave = function() { + // ... +}; + +this.canLeave = function() { + // ... +}; +``` + +## Handlers + +Handlers are configured under the `handlers` property in `mobile.php` using an associative array with the name of the handler and configuration options. The handler name should be an alphanumeric string. + +These are the configuration options common to most handlers, you can find specific options depending on the delegate below. + +| Name | Default | Description | +|-----------------------------|-------------|-------------| +| `delegate` | - | Name of the delegate to register the handler in. See the following sections for available delegates. | +| `method` | - | Name of the PHP method used to retrieve the page content. | +| `init` | - | Name of the PHP method used during [JavaScript initialisation](./index.md#javascript-initialisation). | +| `styles` | - | An associative array with configuration options for CSS styles. | +| `styles.url` | Required | URL pointing to a CSS file, either using an absolute URL or a relative URL. The CSS will be downloaded and applied to the whole app, so it's recommended to include styles scoped to your plugin templates. | +| `styles.version` | Required | Version number used to determine if the file needs to be downloaded again. You should change the version number every time you change the contents of the CSS file. | +| `moodlecomponent` | Plugin name | Name of the component implemented by the handler.

Most of the time, this can be ignored because mobile support is usually included in the same plugin where custom components are defined, but it may be different in some cases. For example, imagine a local plugin called `local_myactivitymobile` is implementing mobile support for a `mod_myactivity` component. In that case, you would set this option to `"mod_myactivity"`. | +| `restricttocurrentuser` | `false` | Restricts the handler to appear only for the current user. For more advanced restrictions, you can use the `restrict` and `disabled` properties returned during [JavaScript initialisation](./index.md#javascript-initialisation). | +| `restricttoenrolledcourses` | `false` | Restricts the handler to appear only for courses the user is enrolled in. For more advanced restrictions, you can use the `restrict` and `disabled` properties returned during [JavaScript initialisation](./index.md#javascript-initialisation). | + +### CoreMainMenuDelegate + +Adds a new item to the main menu. Main Menu handlers are always displayed in the More menu (the three dots), they cannot be displayed as tabs in the main navigation bar. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|----------|-------------| +| `displaydata` | Required | An associative array with configuration options for the main menu item. | +| `displaydata.title` | Required | Language string identifier to use in the main menu item. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.icon` | Required | The icon to use in the main menu item. See the [ion-icon](#ion-icon) documentation for available values. | +| `displaydata.class` | - | A CSS class to add in the main menu item. | +| `priority` | `0` | Priority of the handler, higher priority items are displayed first. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### CoreMainMenuHomeDelegate + +Adds new tabs in the home page. By default, the app is displaying the "Dashboard" and "Site home" tabs. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|----------|-------------| +| `displaydata` | Required | An associative array with configuration options for the tab. | +| `displaydata.title` | Required | Language string identifier to use in the tab. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.class` | - | A CSS class to add in the tab. | +| `priority` | `0` | Priority of the handler, higher priority tabs are displayed first. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### CoreCourseOptionsDelegate + +Add new option in the course page, either as a tab or in the course summary. For example, the tab will appear alongside Participants and Grades. And the course summary can be opened using the info icon in the header. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|----------|-------------| +| `displaydata` | Required | An associative array with configuration options for the tab. | +| `displaydata.title` | Required | Language string identifier to use in the tab. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.class` | - | A CSS class to add in the tab. | +| `priority` | `0` | Priority of the handler, higher priority tabs are displayed first. | +| `ismenuhandler` | `false` | Whether to show the option in the course summary. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### CoreCourseModuleDelegate + +Add support for activity modules or resources. + +The following functions can be declared in the object evaluated in the last statement of the [JavaScript initialisation](./index.md#javascript-initialisation) in order to implement additional functionality: + +- `supportsFeature(feature: string): any`: Check whether the module supports a given feature. +- `manualCompletionAlwaysShown(module: CoreCourseModuleData): Promise`: Check whether to show the manual completion regardless of the course's `showcompletionconditions` setting. + +You can learn more about this in the [Course Modules example](./examples/course-modules). + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|------------------------|----------|-------------| +| `method` | - | Name of the PHP method used to render the page content.

When this option is missing, the module won't be clickable; regardless of `FEATURE_NO_VIEW_LINK` feature support. | +| `coursepagemethod` | - | Name of the PHP method used to render the module in the course page. The rendered HTML should not contain directives or components, only plain HTML.

When this option is present, the module won't be clickable; regardless of `FEATURE_NO_VIEW_LINK` feature support. | +| `displaydata` | - | An associative array with configuration options for the module icon. | +| `displaydata.icon` | - | The icon to use for the module. See the [ion-icon](#ion-icon) documentation for available values. | +| `displaydata.class` | - | A CSS class to add in the module icon. | +| `offlinefunctions` | `[]` | Associative array where the keys are functions to call when prefetching the module, and the values are lists of parameters sent by the app. These functions can be either PHP method names from the `output/mobile.php` class, or Web Services.

In case of using PHP method names, the parameters array will be ignored and the default parameters will be sent: `courseid`, `cmid`, and `userid`. With Web Services, an empty parameters array will indicate sending the default parameters; but using specific values it is possible to customize which are sent. This can include some additional parameters not present in the defaults, such as `courseids` (a list of the courses the user is enrolled in) and `{component}id` (for example, for `mod_certificate` this would be `certificateid`).

Prefetching the module will also download all the files returned by the methods in these offline functions (in the `files` array).

Note that if your functions use additional custom parameters using this option won't work. For example, if you implement multiple pages within a module's view function using a page parameter, the app won't know which page to send. In situations where you need to prefetch more complex data, you should use [Prefetch Handlers](./examples/prefetch-handlers.md) instead.

You can find some [examples below](#example-updatesnames-values). | +| `downloadbutton` | `true` | Whether to display download button in the module.

Only used if there is any offline function. | +| `isresource` | `false` | Whether the module is a resource. If the handler relies on the module contents, this should be `true`.

Only used if there is any offline function. | +| `updatesnames` | `/.*/` | A regular expression to check which module updates are considered to mark it as outdated in the app. In particular, this regular expression will be matched against the field names returned by the `core_course_check_updates` Web Service. If none of the updated fields match the regular expression, they will be ignored and the module won't need to be prefetched again.

Only used if there is any offline function.

You can find some [examples below](#example-updatesnames-values). | +| `displayopeninbrowser` | `true` | Whether the module should display the "Open in browser" option in the top-right menu (only for teachers).

Before 4.4, this was displayed to everyone, not just teachers.

This can also be configured in JavaScript with `this.displayOpenInBrowser = false;`. | +| `displaydescription` | `true` | Whether the module should display the "Description" option in the top-right menu.

This can also be configured in JavaScript with `this.displayDescription = false;`. | +| `displayrefresh` | `true` | Whether the module should display the "Refresh" option in the top-right menu.

This can also be configured in JavaScript with `this.displayRefresh = false;`. | +| `displayprefetch` | `true` | Whether the module should display the download option in the top-right menu.

This can also be configured in JavaScript with `this.displayPrefetch = false;`. | +| `displaysize` | `true` | Whether the module should display the downloaded size in the top-right menu.

This can also be configured in JavaScript with `this.displaySize = false;`. | +| `supportedfeatures` | `[]` | Associative array with configuration for the features supported in the plugin. If you need to calculate these dynamically, you can implement the `supportsFeature` function in the JavaScript.

Some features are not available in the app and they will be ignored. The available features are `FEATURE_MOD_ARCHETYPE`, `FEATURE_MOD_PURPOSE`, `FEATURE_GRADE_HAS_GRADE`, `FEATURE_SHOW_DESCRIPTION`, and `FEATURE_NO_VIEW_LINK`.

You can find some [examples below](#example-supportedfeatures-values). | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +#### Example `offlinefunctions` values + +Using PHP method names: + +```php +[ + 'mobile_course_view' => [], + 'mobile_issues_view' => [], +] +``` + +Using Web Services: + +```php +[ + 'mod_certificate_view_course' => [], // Will receive default parameters: courseid, cmid, and userid. + 'mod_certificate_view_certificate' => ['courseid', 'certificateid'], // Will receive courseid and certificateid. +] +``` + +#### Example `updatesnames` values + +The following regular expression would only consider the "grades" and "gradeitems" fields in module updates to consider a module outdated: + +```php +'/^grades$|^gradeitems$/' +``` + +#### Example `supportedfeatures` values + +```php +[ + FEATURE_NO_VIEW_LINK => true, + FEATURE_MOD_PURPOSE => MOD_PURPOSE_ASSESSMENT, +] +``` + +#### Example `supportedFeatures()` JavaScript definition + +```js +var result = { + supportsFeature: function(feature) { + if (feature === 'viewlink') { + return true; + } + + if (feature === 'mod_purpose') { + return 'assessment'; + } + }, +}; + +result; +``` + +### CoreUserDelegate + +Add new option in the user profile page. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|--------------|-------------| +| `displaydata` | Required | An associative array with configuration options for the option. | +| `displaydata.title` | Required | Language string identifier to use in the option. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.icon` | Required | The icon to use in the option. See the [ion-icon](#ion-icon) documentation for available values. | +| `displaydata.class` | - | A CSS class to add in the option. | +| `type` | `'listitem'` | Visual representation of the option, accepted values are `listitem` and `button`.

Before 4.4, the accepted values were `newpage` and `communication` (`newpage` was the default). | +| `priority` | `0` | Priority of the handler, higher priority options are displayed first. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### CoreCourseFormatDelegate + +Add support for a custom course format. The template returned by this handler also has access to the following properties: + +- `coreCourseFormatComponent`: Course format component instance (see [CoreCourseFormatComponent component](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/course/components/course-format/course-format.ts) for details). +- `course`: Course data (see [CoreCourseAnyCourseData](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/courses/services/courses.ts#L1733) type for details). +- `sections`: Course sections array (see [CoreCourseSectionToDisplay](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/course/components/course-format/course-format.ts#L690..L692) for details). +- `initialSectionId`: Initial section ID. +- `initialSectionNumber`: Initial section number. +- `moduleId`: Module id. + +You can learn more about this in the [Course Formats example](./examples/course-formats). + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|--------------------------|-------------|-------------| +| `canviewallsections` | `true` | Whether the course format allows seeing all sections in a single page.| +| `displaycourseindex` | `true` | Whether the course index should be displayed. | + +### CoreSettingsDelegate + +Add new option in the settings page. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|-------------|-------------| +| `displaydata` | Required | An associative array with configuration options for the option. | +| `displaydata.title` | Required | Language string identifier to use in the option. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.icon` | Required | The icon to use in the option. See the [ion-icon](#ion-icon) documentation for available values. | +| `displaydata.class` | - | A CSS class to add in the option. | +| `priority` | `0` | Priority of the handler, higher priority options are displayed first. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### AddonMessageOutputDelegate + +Add support for custom user notification settings. This will add a new option in the actions menu of the user notifications page (User menu > Preferences > Notifications). + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|-------------|-------------| +| `displaydata` | Required | An associative array with configuration options for the option. | +| `displaydata.title` | Required | Language string identifier to use in the option. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.icon` | Required | The icon to use in the option. See the [ion-icon](#ion-icon) documentation for available values. | +| `priority` | `0` | Priority of the handler, higher priority options are displayed first. | +| `ptrenabled` | `true` | Whether to enable the PTR (pull-to-refresh) gesture in the page. | + +### CoreBlockDelegate + +Add support for a custom block. Blocks can be displayed in Site Home, Dashboard and Course page. + +**Template type:** [Dynamic](./index.md#dynamic-templates)
+**JavaScript overrides:** None
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|---------------------|------------------------|-------------| +| `displaydata` | - | An associative array with configuration options for the block. | +| `displaydata.title` | Required | Language string identifier to use in the block. See the [localisation](./index.md#localisation) documentation to learn more. | +| `displaydata.class` | `'block_{block-name}'` | A CSS class to add in the block. | +| `displaydata.type` | - | Predefined type of block to render, available options are `title` and `prerendered`.

`title` blocks will render a single button with the name of the block, and they will open a page rendered using the `method`.

`prerendered` blocks will use the pre-rendered content and footer returned by the Web Services (like `core_block_get_course_blocks`).

When this option is missing, the block will be rendered using `method` or `fallback`. | +| `fallback` | - | Name of another block to use in order to render the block in the app. This can be useful for custom blocks that have the same graphical interface as other block, even if they are technically different blocks. | + +### CoreQuestionDelegate + +Add support for a custom question type. + +You can learn more about this in the [Question Types example](./examples/question-types.md). + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [CoreQuestionHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/question/services/question-delegate.ts#L26..L209)
+**JavaScript component:** [CoreSitePluginsQuestionComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/question/question.ts) + +*This handler does not have any specific options.* + +### CoreQuestionBehaviourDelegate + +Add support for a custom question behaviour. + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [CoreQuestionBehaviourHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/question/services/behaviour-delegate.ts#L26..L60)
+**JavaScript component:** [CoreSitePluginsQuestionBehaviourComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/question-behaviour/question-behaviour.ts) + +*This handler does not have any specific options.* + +### CoreUserProfileFieldDelegate + +Add support for a custom user profile field. + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [CoreUserProfileFieldHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/user/services/user-profile-field-delegate.ts#L26..L55)
+**JavaScript component:** [CoreSitePluginsUserProfileFieldComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/user-profile-field/user-profile-field.ts) + +*This handler does not have any specific options.* + +### AddonModQuizAccessRuleDelegate + +Add support for a custom quiz access rule. + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [AddonModQuizAccessRuleHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/addons/mod/quiz/services/access-rules-delegate.ts#L27..L122)
+**JavaScript component:** [CoreSitePluginsQuizAccessRuleComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/quiz-access-rule/quiz-access-rule.ts) + +*This handler does not have any specific options.* + +### AddonModAssignSubmissionDelegate + +Add support for a custom assign submission. + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [AddonModAssignSubmissionHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/addons/mod/assign/services/submission-delegate.ts#L30..L269)
+**JavaScript component:** [CoreSitePluginsAssignSubmissionComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/assign-submission/assign-submission.ts) + +*This handler does not have any specific options.* + +### AddonModAssignFeedbackDelegate + +Add support for a custom assign feedback. + +**Template type:** [Static](index.md#static-templates)
+**JavaScript overrides:** [AddonModAssignFeedbackHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/addons/mod/assign/services/feedback-delegate.ts#L30..L177)
+**JavaScript component:** [CoreSitePluginsAssignFeedbackComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/assign-feedback/assign-feedback.ts) + +*This handler does not have any specific options.* + +### AddonWorkshopAssessmentStrategyDelegate + +Add support for a custom workshop assessment strategy. + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [AddonWorkshopAssessmentStrategyHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/addons/mod/workshop/services/assessment-strategy-delegate.ts#L26..L76).* + +### CoreContentLinksDelegate + +Allows you to intercept what happens when links are clicked. For example, you can open a plugin page when a link is clicked. The Moodle app automatically creates some link handlers for module plugins, you can learn more about this and how to use link handlers in the [Link Handlers example](./examples/link-handlers.md). + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CoreContentLinksHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/contentlinks/services/contentlinks-delegate.ts#L27..L95).* + +### CorePushNotificationsDelegate + +Allows you to intercept what happens when push notifications are clicked. + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CorePushNotificationsClickHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/pushnotifications/services/push-delegate.ts#L27..L59).* + +### CoreCourseModulePrefetchDelegate + +Allows you to implement custom logic to prefetch module content. You can learn more about this in the [Prefetch Handlers example](./examples/prefetch-handlers.md). + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CoreCourseModulePrefetchHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/course/services/module-prefetch-delegate.ts#L1410..L1568).* + +### CoreFileUploaderDelegate + +Add new option in the upload file picker. + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CoreFileUploaderHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/fileuploader/services/fileuploader-delegate.ts#L26..L46).* + +### CorePluginFileDelegate + +Perform operations with files such as listening to file events (download, deletion, etc.) or using a different URL when downloading. + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CorePluginFileHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/services/plugin-file-delegate.ts#L296..L389).* + +### CoreFilterDelegate + +Add support for a custom filter. Note that you'll only need this if you have to manipulate the content with JavaScript, PHP filters are applied in the content before sending the HTML to the app. + +*This handler can only be registered using [JavaScript initialisation](./index.md#javascript-initialisation), you can find more about it in the [CoreFilterHandler interface declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/filter/services/filter-delegate.ts#L28..L76).* + +### CoreEnrolDelegate `4.3+` + +Add support for custom enrolment methods. You can see an example of customizing the default behaviour using JavaScript in the [Self Enrolment example](./examples/self-enrolment.md). + +This delegate was introduced in version 4.3 of the app. + +**Template type:** None
+**JavaScript overrides:** [CoreEnrolHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/enrol/services/enrol-delegate.ts#L33..L59) (only when `enrolmentAction` is `self` or `guest`)
+**JavaScript component:** None + +#### Options + +| Name | Default | Description | +|-------------------------|-------------|-------------| +| `enrolmentAction` | `'browser'` | Action performed by the handler. Possible values are: `browser`, `self`, and `guest`.

`browser` opens the browser to perform the enrolment in the LMS, outside of the app.

`self` requires implementing the `enrol` function in JavaScript. Also, the PHP class extending `enrol_plugin` must return some data in the `get_enrol_info` method.

`guest` allows users to enter the course as guests. It requires implementing the `canAccess` and `validateAccess` functions in the [JavaScript initialisation](./index.md#javascript-initialisation) JavaScript. Also, the PHP class extending `enrol_plugin` must return some data in the `get_enrol_info` method. | +| `infoIcons` | `[]` | Array of icons related to enrolment to display next to the course. These can also be calculated dynamically in JavaScript using course information. | +| `infoIcons[].icon` | Required | The icon to use. See the [ion-icon](#ion-icon) documentation for available values. | +| `infoIcons[].label` | Required | Language string identifier to use in the aria-label of the icon. See the [localisation](./index.md#localisation) documentation to learn more. | +| `infoIcons[].className` | - | A CSS class to add in the icon. | + +## Components + +In addition to built-in Angular and [Ionic Components](https://ionicframework.com/docs/components), the following are also available in the Moodle App. + +Please note that this list is not exhaustive, you can find all the components available in the app under [src/core/components](https://github.com/moodlehq/moodleapp/tree/latest/src/core/components). + +### core-format-text + +Formats the text and adds functionality specific for the app. For example, it adds [core-link](#core-link) to links, [core-external-content](#core-external-content) to embedded media, applies [filters](https://docs.moodle.org/en/Filters), etc. + +#### Attributes + +| Name | Default | Description | +|---------------------|-----------------|-------------| +| `text` | Required | Text to format. | +| `siteId` | Current site ID | Site ID for contextual functionality, such as downloads. | +| `component` | - | Component for contextual functionality, such as downloads. | +| `componentId` | - | Component ID for contextual functionality, such as downloads. | +| `adaptImg` | `true` | Whether to adapt images to screen width. | +| `clean` | `false` | Whether to treat the contents as plain text and remove all the HTML tags. | +| `singleLine` | `false` | Whether to remove new lines and display the text in a single line.

Only used if `clean` is `true`. | +| `highlight` | - | Text to highlight. | +| `filter` | - | Whether to apply filters text contents. If not defined, it will be `true` when `contextLevel` and `instanceId` are set. | +| `contextLevel` | `'system'` | Context level of the text. Possible values are `system`, `user`, `coursecat`, `course`, `module`, and `block`. | +| `contextInstanceId` | - | Instance ID related to the context. | +| `courseId` | - | Course ID related to the context. It can be useful to improve performance with filters. | +| `wsNotFiltered` | `false` | Used to indicate that the Web Services didn't filter the text for some reason. | +| `captureLinks` | `true` | Whether links should be open inside the app if possible. | +| `openLinksInApp` | `false` | Whether links should be opened in InAppBrowser. | +| `hideIfEmpty` | `false` | Whether to hide the element when the text is empty. | +| `disabled` | `false` | Whether to disable elements with autoplay. | + +#### Examples + +```html ng2 + + +``` + +### core-navbar-buttons + +Adds buttons to the header of the current page. + +#### Attributes + +| Name | Default | Description | +|-----------|---------|-------------| +| `slot` | - | When this attribute is present, buttons will only be added if the header has some buttons in that position. Otherwise, they will be added to the first `` found in the header. | +| `hidden` | - | When this attribute is present, with any value, all the buttons are hidden. | +| `prepend` | - | When this attribute is present, with any value, the buttons will be added to the beginning. Otherwise, they are added at the end. | + +#### Examples + +```html ng2 + + + + + +``` + +You can also use it to add options to the context menu: + +```html ng2 + + + + + + +``` + +### core-file + +Handles a remote file. It shows the file name, icon (depending on mime type), and a button to download or refresh it. Users can identify if the file is downloaded or not based on the button. + +#### Attributes + +| Name | Default | Description | +|---------------------|----------|-------------| +| `file` | Required | Object with data about the file to download. | +| `file.filename` | Required | Name of the file to download. | +| `file.url` | - | Url of the file to download.

Required if `file.fileurl` is missing. | +| `file.fileurl` | - | Url of the file to download.

Required if `file.url` is missing. | +| `file.timemodified` | - | Time modified of the file, used to find out if the file needs to be downloaded again in the app. | +| `file.filesize` | - | File size, used to request confirmation before downloading large files. | +| `component` | - | Component related with the file. | +| `componentId` | - | ID to use in conjunction with the component. | +| `canDelete` | `false` | Whether the file can be deleted. | +| `alwaysDownload` | `false` | Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they're outdated or not. | +| `canDownload` | `true` | Whether file can be downloaded. | +| `showSize` | `true` | Whether show the file size. | +| `showTime` | `true` | Whether show the time modified. | +| `onDelete` | - | Listener for the file being deleted. | + +#### Examples + +```html ng2 + + +``` + +### core-rich-text-editor + +Text editor to write rich content including formatting text, inserting links and images, uploading files, etc. + +Using this component may require setting up a `FormControl`, you can learn more about this in the [Forms example](./examples/forms.md#using-core-rich-text-editor). + +#### Attributes + +| Name | Default | Description | +|---------------------|---------------------------|-------------| +| `placeholder` | - | Placeholder to show when the editor content is empty. | +| `control` | - | Form control. Learn more about this in the [Forms example](./examples/forms.md#using-core-rich-text-editor). | +| `name` | `'core-rich-text-editor'` | Form input name. | +| `component` | - | Component to link uploaded files. | +| `componentId` | - | ID to use in conjunction with the component. | +| `autoSave` | `true` | Whether to auto-save the contents in a draft. | +| `contextLevel` | `'system'` | Context level of the text. Possible values are `system`, `user`, `coursecat`, `course`, `module`, and `block`. | +| `contextInstanceId` | `0` | Instance ID related to the context. | +| `elementId` | - | ID to set to the element. | +| `draftExtraParams` | - | Extra parameters to identify the draft. | + +### ion-icon + +Even though [ion-icon](https://ionicframework.com/docs/api/icon) is a built-in Ionic component, it has additional functionality in the Moodle App. In particular, it's possible to use more icons besides [Ionicons](https://ionic.io/ionicons). + +This can be achieved using different prefixes in the `name` attribute: + +- `fas-` or `fa-` will use [Font Awesome 6.3 solid library](https://fontawesome.com/search?o=r&m=free&s=solid). +- `far-` will use [Font Awesome 6.3 regular library](https://fontawesome.com/search?o=r&m=free&s=regular). +- `fab-` will use [Font Awesome 6.3 brands library](https://fontawesome.com/search?o=r&m=free&f=brands). Note that only a few are supported, and their use is discouraged. +- `moodle-` will use svg icons [imported from Moodle LMS](https://github.com/moodlehq/moodleapp/tree/latest/src/assets/fonts/moodle/moodle). +- `fam-` will use [some custom icons only available in the app](https://github.com/moodlehq/moodleapp/tree/latest/src/assets/fonts/moodle/font-awesome). +- Otherwise, Ionicons icons will be used. + +We encourage the use of Font Awesome icons to match the appearance of the LMS website. + +#### Examples + +Show the "pizza-slice" icon from Font Awesome regular library: + +```html + +``` + +## Directives + +In addition to built-in [Angular Directives](https://angular.dev/guide/directives), the following are also available in the Moodle App. + +Please note that this list is not exhaustive, you can find all the components available in the app under [src/core/directives](https://github.com/moodlehq/moodleapp/tree/latest/src/core/directives). + +### collapsible-item + +Makes an element collapsible. + +This directive takes a single argument, which is optional, to indicate the max height to render the content box. The minimum accepted value is 56. Using the argument will force `display: block` to calculate the height better. If you want to avoid this, use `class="inline"` at the same time to use `display: inline-block`. + +#### Examples + +```html +
+ ... +
+``` + +### core-link + +Performs several checks upon link navigation, like launching a browser instead of overriding the app. + +This directive is applied automatically to all the links and media inside of [core-format-text](#core-format-text) components. + +#### Attributes + +| Name | Default | Description | +|----------------------|-----------|-------------| +| `capture` | `false` | Whether the link should be captured by the app instead of opening a browser. See [CoreContentLinksDelegate](#corecontentlinksdelegate) to learn more. | +| `inApp` | `false` | Whether to open in an embedded browser rather than the system browser. | +| `autoLogin` | `'check'` | Whether to open the link with auto-login. Possible values are `yes`, `no`, and `check` (Auto-login only if the link belongs to the current site) | +| `showBrowserWarning` | `true` | Whether to show a warning before opening browser. | + +#### Examples + +```html ng2 +Open +``` + +### core-external-content + +Handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. + +If a file is downloaded, its URL will be replaced by the local file URL. + +This directive is applied automatically to all the links and media inside of [core-format-text](#core-format-text) components. + +#### Attributes + +| Name | Default | Description | +|---------------|-----------------|-------------| +| `siteId` | Current site ID | Site ID to use. | +| `component` | - | Component to use when downloading embedded files. | +| `componentId` | - | ID to use in conjunction with the component. | + +#### Examples + +```html ng2 + +``` + +### core-user-link + +Open user profile when clicked. + +#### Attributes + +| Name | Default | Description | +|------------|----------|-------------| +| `userId` | Required | User ID to open the profile. | +| `courseId` | - | Course id to show the user info related to that course. | + +#### Examples + +```html ng2 +Open user profile +``` + +### core-download-file + +Download and open a file when clicked. + +For most cases, it is recommended to use the [core-file](#core-file) component instead because it will display some useful information about the file. + +#### Attributes + +| Name | Default | Description | +|-----------------------------------|----------|-------------| +| `core-download-file` | Required | Object with data about the file to download. | +| `core-download-file.url` | - | Url of the file to download.

Required if `core-download-file.fileurl` is missing. | +| `core-download-file.fileurl` | - | Url of the file to download.

Required if `core-download-file.url` is missing. | +| `core-download-file.timemodified` | - | Time modified of the file, used to find out if the file needs to be downloaded again in the app. | +| `core-download-file.filesize` | - | File size, used to request confirmation before downloading large files. | +| `component` | - | Component related with the file. | +| `componentId` | - | ID to use in conjunction with the component. | + +#### Examples + +```html ng2 + + {{ 'plugin.mod_certificate.download | translate }} + +``` + +### core-course-download-module-main-file + +Download and open the main file of a module when clicked. This directive is intended for modules like `mod_resource`. + +#### Attributes + +| Name | Default | Description | +|---------------|-------------------|-------------| +| `module` | | Module object.

Required if `moduleId` is missing. | +| `moduleId` | | ID of the module to open.

Required if `module` is missing. | +| `courseId` | | Course ID. | +| `component` | - | Component related with the file. | +| `componentId` | - | ID to use in conjunction with the component. | +| `files` | `module.contents` | List of files of the module. | + +#### Examples + +```html ng2 + + {{ 'plugin.mod_certificate.getcertificate' | translate }} + +``` + +### core-site-plugins-new-content + +Open new content when clicked. This content can be displayed in a new page or in the current page, if the current page is already displaying plugin content. This directive is typically used to navigate through plugin pages. + +#### Attributes + +| Name | Default | Description | +|----------------|----------|-------------| +| `component` | Required | Component for the new content. | +| `method` | Required | Name of the PHP method for the new page request. | +| `args` | - | Parameters to include in the new page request. | +| `preSets` | - | Additional configuration for the Web Service request. You can find the available options in the [CoreSiteWSPreSets type declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/classes/sites/authenticated-site.ts#L1625..L1746). | +| `samePage` | `false` | Whether to display content in the same page instead of a new one. This will only work for pages already rendered by plugins, otherwise a new page will be open regardless of this value. | +| `title` | - | Language string identifier to use in the new page. See the [localisation](./index.md#localisation) documentation to learn more.

Only used if `samePage` is `false`. | +| `useOtherData` | - | Whether to include `otherdata` from the [content response](#content-responses) in the arguments for the new page request. If this attribute is undefined or not supplied, `otherdata` will not be included. If this attribute is an array of strings, it'll be used to include only the indicated properties. For any other value (including falsy values like `false`, `null`, or an empty string), the entire `otherdata` will be sent. Additionally, any nested arrays or object will be sent as a JSON-string. | +| `form` | - | ID or name to identify a form in the template, that will be obtained from `document.forms`. The form data will be included in the arguments for the new page request.

You can learn more about forms in the [Forms example](./examples/forms.md). | +| `jsData` | - | JavaScript variables to pass to the new page so they can be used in the template or JavaScript. This attribute can also be set to `true`, in which case all initial variables from current page will be used. | +| `ptrEnabled` | `true` | Whether to enable PTR (pull-to-refresh) in the new page. | + +#### Examples + +A button that loads content in a new page: + +```html ng2 + + {{ 'plugin.mod_certificate.viewissued' | translate }} + +``` + +A button that loads content in the current page, and includes `userid` from `otherdata` in the request: + +```html ng2 + + {{ 'plugin.mod_certificate.viewissued' | translate }} + +``` + +### core-site-plugins-call-ws + +Calls a Web Service when clicked. Depending on the response, the current page will be refreshed, a message will be displayed, or the application will navigate back to the previous page. + +If you'd rather load new content when the Web Service request is done, use [core-site-plugins-call-ws-new-content](#core-site-plugins-call-ws-new-content) instead. + +You can see this directive in use in the [Forms example](./examples/forms.md). + +#### Attributes + +| Name | Default | Description | +|---------------------|----------|-------------| +| `name` | Required | The name of the Web Service to call. | +| `params` | - | Parameters for the Web Service request. | +| `preSets` | - | Additional configuration for the Web Service request. You can find the available options in the [CoreSiteWSPreSets type declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/classes/sites/authenticated-site.ts#L1625..L1746). | +| `useOtherDataForWS` | - | Whether to include `otherdata` from the [content response](#content-responses) in the arguments for the new page request. If this attribute is undefined or not supplied, `otherdata` will not be included. If this attribute is an array of strings, it'll be used to include only the indicated properties. For any other value (including falsy values like `false`, `null`, or an empty string), the entire `otherdata` will be sent. Additionally, any nested arrays or object will be sent as a JSON-string. | +| `form` | - | ID or name to identify a form in the template, that will be obtained from `document.forms`. The form data will be included in the arguments for the new page request.

You can learn more about forms in the [Forms example](./examples/forms.md). | +| `confirmMessage` | - | Message to confirm the action when the user clicks the element. If this attribute is supplied with an empty string, "Are you sure?" will be used. | +| `showError` | `true` | Whether to show an error message if the WS call fails. | +| `successMessage` | - | Message to show on success. If not supplied, no message. If this attribute is supplied with an empty string, "Success" will be used. | +| `goBackOnSuccess` | `false` | Whether to go back if the Web Service call is successful. | +| `refreshOnSuccess` | `false` | Whether to refresh the current page if the Web Service call is successful. | +| `onSuccess` | - | A function to call when the Web Service call is successful (HTTP call successful and no exception returned). | +| `onError` | - | A function to call when the Web Service call fails (HTTP call fails or an exception is returned). | +| `onDone` | - | A function to call when the Web Service call finishes (either success or fail). | + +#### Examples + +A button to send some data to the server without using cache, displaying default messages and refreshing on success: + +```html ng2 + + {{ 'plugin.mod_certificate.senddata' | translate }} + +``` + +A button to send some data to the server using cache without confirming, going back on success and using `userid` from `otherdata`: + +```html ng2 + + {{ 'plugin.mod_certificate.senddata' | translate }} + +``` + +Same as the previous example, but implementing custom JS code to run on success: + +```html ng2 title="Template" + + {{ 'plugin.mod_certificate.senddata' | translate }} + +``` + +```js title="JavaScript" +this.certificateViewed = function(result) { + // Code to run when the WS call is successful. +}; +``` + +### core-site-plugins-call-ws-new-content + +Calls a Web Service when clicked and loads new content passing the Web Service result as arguments. This content can be displayed in a new page or in the current page, if the current page is already displaying plugin content. + +If you don't need to load new content, use [core-site-plugins-call-ws](#core-site-plugins-call-ws) instead. + +#### Attributes + +| Name | Default | Description | +|---------------------|----------|-------------| +| `name` | Required | The name of the Web Service to call. | +| `params` | - | Parameters for the Web Service request. | +| `preSets` | - | Additional configuration for the Web Service request. You can find the available options in the [CoreSiteWSPreSets type declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/classes/sites/authenticated-site.ts#L1625..L1746). | +| `useOtherDataForWS` | - | Whether to include `otherdata` from the [content response](#content-responses) in the arguments for the new page request. If this attribute is undefined or not supplied, `otherdata` will not be included. If this attribute is an array of strings, it'll be used to include only the indicated properties. For any other value (including falsy values like `false`, `null`, or an empty string), the entire `otherdata` will be sent. Additionally, any nested arrays or object will be sent as a JSON-string. | +| `form` | - | ID or name to identify a form in the template, that will be obtained from `document.forms`. The form data will be included in the arguments for the new page request.

You can learn more about forms in the [Forms example](./examples/forms.md). | +| `confirmMessage` | - | Message to confirm the action when the user clicks the element. If this attribute is supplied with an empty string, "Are you sure?" will be used. | +| `showError` | `true` | Whether to show an error message if the WS call fails. | +| `component` | Required | Component for the new content. | +| `method` | Required | Name of the PHP method for the new page request. | +| `args` | - | Parameters to include in the new page request. | +| `samePage` | `false` | Whether to display content in the same page instead of a new one. This will only work for pages already rendered by plugins, otherwise a new page will be open regardless of this value. | +| `title` | - | Language string identifier to use in the new page. See the [localisation](./index.md#localisation) documentation to learn more.

Only used if `samePage` is `false`. | +| `useOtherData` | - | Whether to include `otherdata` from the [content response](#content-responses) in the arguments for the new page request. If this attribute is undefined or not supplied, `otherdata` will not be included. If this attribute is an array of strings, it'll be used to include only the indicated properties. For any other value (including falsy values like `false`, `null`, or an empty string), the entire `otherdata` will be sent. Additionally, any nested arrays or object will be sent as a JSON-string. | +| `jsData` | - | JavaScript variables to pass to the new page so they can be used in the template or JavaScript. This attribute can also be set to `true`, in which case all initial variables from current page will be used. | +| `newContentPreSets` | - | Additional configuration for the Web Service request for the new page. You can find the available options in the [CoreSiteWSPreSets type declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/classes/sites/authenticated-site.ts#L1625..L1746). | +| `onSuccess` | - | A function to call when the Web Service call is successful (HTTP call successful and no exception returned). | +| `onError` | - | A function to call when the Web Service call fails (HTTP call fails or an exception is returned). | +| `onDone` | - | A function to call when the Web Service call finishes (either success or fail). | +| `ptrEnabled` | `true` | Whether to enable PTR (pull-to-refresh) in the new page. | + +#### Examples + +A button to get some data from the server without using cache, showing default confirm and displaying a new page: + +```html ng2 + + {{ 'plugin.mod_certificate.getissued' | translate }} + +``` + +A button to get some data from the server using cache, without confirm, displaying new content in same page and using `userid` from `otherdata`: + +```html ng2 + + {{ 'plugin.mod_certificate.getissued' | translate }} + +``` + +Same as the previous example, but implementing a custom JavaScript code to run on success: + +```html ng2 title="Template" + + {{ 'plugin.mod_certificate.getissued' | translate }} + +``` + +```js title="JavaScript" +this.callDone = function(result) { + // Code to run when the WS call is successful. +}; +``` + +### core-site-plugins-call-ws-on-load + +Call a Web Service as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services. + +If you want to call a Web Service when the user clicks on a certain element, use [core-site-plugins-call-ws](#core-site-plugins-call-ws) instead. + +#### Attributes + +| Name | Default | Description | +|-------------------|----------|-------------| +| name | Required | The name of the Web Service to call. | +| params | - | Parameters for the Web Service request. | +| preSets | - | Additional configuration for the Web Service request. You can find the available options in the [CoreSiteWSPreSets type declaration](https://github.com/moodlehq/moodleapp/blob/latest/src/core/classes/sites/authenticated-site.ts#L1625..L1746). | +| useOtherDataForWS | - | Whether to include `otherdata` from the [content response](#content-responses) in the arguments for the new page request. If this attribute is undefined or not supplied, `otherdata` will not be included. If this attribute is an array of strings, it'll be used to include only the indicated properties. For any other value (including falsy values like `false`, `null`, or an empty string), the entire `otherdata` will be sent. Additionally, any nested arrays or object will be sent as a JSON-string. | +| form | - | ID or name to identify a form in the template, that will be obtained from `document.forms`. The form data will be included in the arguments for the new page request.

You can learn more about forms in the [Forms example](./examples/forms.md). | +| onSuccess | - | A function to call when the Web Service call is successful (HTTP call successful and no exception returned). | +| onError | - | A function to call when the Web Service call fails (HTTP call fails or an exception is returned). | +| onDone | - | A function to call when the Web Service call finishes (either success or fail). | + +#### Examples + +```html ng2 title="Template" + + +``` + +```js title="JavaScript" +this.callDone = function(result) { + // Code to run when the WS call is successful. +}; +``` + +## Services + +When writing JavaScript in plugins, many of the app services are available as well. + +Given the magnitude of the codebase, we're not going to document all the services here. Instead, we suggest that you take a look at the source code directly. The application is written with TypeScript and has extensive documentation blocks, so it shouldn't be hard to navigate the APIs available. You can start looking where the services are injected to the plugins runtime, in [src/core/features/compile/services/compile.ts](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/compile/services/compile.ts#L338..L388). diff --git a/general/app/development/plugins-development-guide/examples/course-formats.md b/general/app/development/plugins-development-guide/examples/course-formats.md new file mode 100644 index 0000000000..30057074a2 --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/course-formats.md @@ -0,0 +1,199 @@ +--- +title: Course Formats +tags: + - Moodle App +--- + +You can implement custom course formats using the [CoreCourseFormatDelegate](../api-reference.md#corecourseformatdelegate): + +```php title="db/mobile.php" +$addons = [ + 'format_myformat' => [ + 'handlers' => [ + 'myformat' => [ + 'delegate' => 'CoreCourseFormatDelegate', + 'method' => 'mobile_course_view', + 'styles' => [ + 'url' => $CFG->wwwroot . '/course/format/myformat/mobile.css', + 'version' => 2019041000, + ], + 'init' => 'myformat_init', + ], + ], + ], +]; +``` + +```php title="classes/output/mobile.php" +class mobile { + + public static function mobile_course_view($args) { + global $OUTPUT, $CFG; + + $course = get_course($args['courseid']); + require_login($course); + $html = $OUTPUT->render_from_template('format_myformat/mobile_course', []); + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $html, + ], + ], + 'otherdata' => [ + // ... + ], + ]; + } + +} +``` + +```html handlebars title="templates/mobile_course.mustache" +{{=<% %>=}} + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Note that in this case, the template is not rendering any dynamic data with mustache; it's simply a static Angular template. + +With this, you will have something similar to the core format plugin: a list of sections with headers, each containing its list of course modules. From here, you can make customisations to support your course format's features. + +To find more about the specific properties and configuration options, make sure to read the [CoreCourseFormatDelegate](../api-reference.md#corecourseformatdelegate) reference. + +## Filtering sections + +When your course page loads, the sections array contains all of the sections on the course. However, you might not want to display all of these sections on one page. +You can achieve this by returning the list of sections to display along with the template in the rendering method: + +```php +class mobile { + + public static function mobile_course_view($args) { + // ... + + $displaysections = myformat\helper::get_list_of_section_ids($courseid); + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $html, + ], + ], + 'otherdata' => [ + 'displaysections' => json_encode($displaysections), + ], + ]; + } + +} +``` + +Then filter the list of sections in your template: + +```html + + + + + +``` + +## Using JavaScript + +You can also register custom formats using [JavaScript initialisation](../index.md#javascript-initialisation). + +For example, you can implement a single activity format returning the following data: + +```html ng2 title="template with 'main' ID" + +``` + +```js title="JavaScript" +const that = this; + +class AddonSingleActivityFormatComponent { + + constructor() { + this.data = {}; + } + + ngOnChanges(changes) { + const self = this; + + if (this.course && this.sections?.length) { + const module = this.sections[0]?.modules?.[0]; + + if (module && !this.componentClass) { + that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) => { + self.componentClass = component ?? that.CoreCourseUnsupportedModuleComponent; + }); + } + + this.data.courseId = this.course.id; + this.data.module = module; + } + } + + async doRefresh(refresher, done) { + await this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done]); + } + +} + +class AddonSingleActivityFormatHandler { + + constructor() { + this.name = 'singleactivity'; + } + + isEnabled() { + return true; + } + + canViewAllSections() { + return false; + } + + getCourseTitle(course, sections) { + return sections?.[0]?.modules?.[0] + ?? course.fullname + ?? ''; + } + + displayCourseIndex() { + return false; + } + + getCourseFormatComponent() { + return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES['main'], AddonSingleActivityFormatComponent); + } + +} + +this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler()); +``` diff --git a/general/app/development/plugins-development-guide/examples/course-modules.md b/general/app/development/plugins-development-guide/examples/course-modules.md new file mode 100644 index 0000000000..931aa2e47c --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/course-modules.md @@ -0,0 +1,263 @@ +--- +title: Course Modules +tags: + - Moodle App +--- + +You can implement custom course modules using the [CoreCourseModuleDelegate](../api-reference.md#corecoursemoduledelegate). + +In this example, we're going to implement a simple activity module that displays a certificate issued for the current user along with the list of previously issued certificates. It also stores view events in the course log, and it works offline. When the user downloads the course or activity, the data is pre-fetched for offline use. + +We'll start by declaring the handler in `db/mobile.php`: + +```php +$addons = [ + 'mod_certificate' => [ + 'handlers' => [ + 'coursecertificate' => [ + 'displaydata' => [ + 'icon' => $CFG->wwwroot . '/mod/certificate/pix/icon.gif', + 'class' => '', + ], + 'delegate' => 'CoreCourseModuleDelegate', + 'method' => 'mobile_course_view', + 'offlinefunctions' => [ + 'mobile_course_view' => [], + 'mobile_issues_view' => [], + ], + ], + ], + 'lang' => [ + ['pluginname', 'certificate'], + ['summaryofattempts', 'certificate'], + ['getcertificate', 'certificate'], + ['requiredtimenotmet', 'certificate'], + ['viewcertificateviews', 'certificate'], + ], + ], +]; +``` + +In this file, we declare the `mobile_course_view` and `mobile_issues_view` Web Services for offline use. When these are called, the parameters will include the current `userid` and `courseid`. Once they have been called, the result will be cached in the app and these pages will work offline. + +Of course, this only includes viewing the pages; but any interaction that requires sending data to the server will not work. + +Now, let's implement the [content response](../api-reference.md#content-responses) in `classes/output/mobile.php`: + +```php +class mobile { + + public static function mobile_course_view($args) { + global $OUTPUT, $USER, $DB; + + $args = (object) $args; + $cm = get_coursemodule_from_id('certificate', $args->cmid); + + // Capabilities check. + require_login($args->courseid, false, $cm, true, true); + + $context = context_module::instance($cm->id); + + require_capability('mod/certificate:view', $context); + + if ($args->userid !== $USER->id) { + require_capability('mod/certificate:manage', $context); + } + + $certificate = $DB->get_record('certificate', ['id' => $cm->instance]); + + // Get certificates from external (taking care of exceptions). + try { + $issued = mod_certificate_external::issue_certificate($cm->instance); + $certificates = mod_certificate_external::get_issued_certificates($cm->instance); + $issues = array_values($certificates['issues']); // Make it mustache compatible. + } catch (Exception $e) { + $issues = []; + } + + // Set timemodified for each certificate. + foreach ($issues as $issue) { + if (!empty($issue->timemodified)) { + continue; + } + + $issue->timemodified = $issue->timecreated; + } + + $showget = !$certificate->requiredtime || + has_capability('mod/certificate:manage', $context) || + certificate_get_course_time($certificate->course) >= ($certificate->requiredtime * 60); + + $certificate->name = format_string($certificate->name); + [$certificate->intro, $certificate->introformat] = external_format_text( + $certificate->intro, + $certificate->introformat, + $context->id, + 'mod_certificate', + 'intro' + ); + + $data = [ + 'certificate' => $certificate, + 'showget' => $showget && count($issues) > 0, + 'issues' => $issues, + 'issue' => $issues[0], + 'numissues' => count($issues), + 'cmid' => $cm->id, + 'courseid' => $args->courseid, + ]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data), + ], + ], + 'files' => $issues, + ]; + } + +} +``` + +In the first part of the function, we check permissions and capabilities (like a view.php script would normally do). Then, we retrieve the certificate information that's necessary to display the template. + +We also return a list of files to prefetch for offline use. + +Finally, let's implement the mustache template in `templates/mobile_view_page.mustache`: + +```html handlebars +{{=<% %>=}} +
+ + + + + +

{{ 'plugin.mod_certificate.summaryofattempts' | translate }}

+
+ + <%#issues%> + + + + {{ 'plugin.mod_certificate.viewcertificateviews' | translate: {$a: <% numissues %>} }} + + + + <%/issues%> + + <%#showget%> + + + + + {{ 'plugin.mod_certificate.getcertificate' | translate }} + + + + <%/showget%> + + <%^showget%> + + + {{ 'plugin.mod_certificate.requiredtimenotmet' | translate }} + + + <%/showget%> + + + + +
+
+``` + +Here's some things to pay attention to: + +1. We are showing the module description using `core-course-module-description`. +2. We are showing the certificate information with a list of elements, adding a header on top. +3. We are using translate pipe to show the `summaryofattempts` string. We could've used mustache translation, but it is usually better to delegate the localization to the app. +4. We added a button to transition to another page when there are certificates issued. The [`core-site-plugins-new-content` directive](../api-reference.md#core-site-plugins-new-content) indicates that if the user clicks the button, we need to call the `mobile_issues_view` function; passing as arguments the `cmid` and `courseid`. The content returned by this function will be displayed in a new page (we'll implement this shortly). +5. We added a button to download an issued certificate. The [`core-course-download-module-main-file` directive](../api-reference.md#core-course-download-module-main-file) indicates that clicking this button will download the whole activity and open the main file. This means that, using this button, we can prefetch the whole activity to be available offline. +6. We are using the [`core-site-plugins-call-ws-on-load` directive](../api-reference.md#core-site-plugins-call-ws-on-load) to indicate that once the page is loaded, we need to call a Web Service function. In this case, we are calling the `mod_certificate_view_certificate` to log that the user viewed this page. + +Now, let's implement the page to view the individual certificates: + +```php title="classes/output/mobile.php" +public static function mobile_issues_view($args) { + global $OUTPUT, $USER, $DB; + + $args = (object) $args; + $cm = get_coursemodule_from_id('certificate', $args->cmid); + + // Capabilities check. + require_login($args->courseid, false, $cm, true, true); + + $context = context_module::instance($cm->id); + + require_capability ('mod/certificate:view', $context); + + if ($args->userid != $USER->id) { + require_capability('mod/certificate:manage', $context); + } + + $certificate = $DB->get_record('certificate', ['id' => $cm->instance]); + + // Get certificates from external (taking care of exceptions). + try { + $issued = mod_certificate_external::issue_certificate($cm->instance); + $certificates = mod_certificate_external::get_issued_certificates($cm->instance); + $issues = array_values($certificates['issues']); // Make it mustache compatible. + } catch (Exception $e) { + $issues = []; + } + + $data = ['issues' => $issues]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_issues', $data), + ], + ], + ]; +} +``` + +```html handlebars title="templates/mobile_view_issues.mustache" +{{=<% %>=}} +
+ + <%#issues%> + + +

{{ <%timecreated%> * 1000 | coreFormatDate: 'dffulldate' }}

+

<%grade%>

+
+
+ <%/issues%> +
+
+``` + +The method for the new page is very similar to the first one: we check the capabilities, retrieve the information required for the template, and return the rendered template. + +The template is a simple Ionic list that will display the issued certificates. We are showing their creation date using the `coreFormatDate` pipe, and their grade. + +## Other examples + +- [Custom certificate module](https://github.com/mdjnelson/moodle-mod_customcert) +- [Group choice module](https://github.com/ndunand/moodle-mod_choicegroup) +- [Attendance module](https://github.com/danmarsden/moodle-mod_attendance) +- [ForumNG module](https://github.com/moodleou/moodle-mod_forumng) diff --git a/general/app/development/plugins-development-guide/examples/create-course-formats.md b/general/app/development/plugins-development-guide/examples/create-course-formats.md deleted file mode 100644 index b947028dd6..0000000000 --- a/general/app/development/plugins-development-guide/examples/create-course-formats.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: Creating mobile course formats -tags: - - Moodle App ---- - -Course format plugins can be supported in the app using the CoreCourseFormatDelegate. - -To begin, add a handler for this delegate to your course format's db/mobile.php file: - -```php title="db/mobile.php" -$addons = [ - 'format_myformat' => [ - 'handlers' => [ // Different places where the plugin will display content. - 'myformat' => [ // Handler unique name (alphanumeric). - 'delegate' => 'CoreCourseFormatDelegate', // Delegate (where to display the link to the plugin) - 'method' => 'mobile_course_view', // Main function in \format_myformat\output\mobile. - 'styles' => [ - 'url' => $CFG->wwwroot . '/course/format/myformat/mobile.css', - 'version' => 2019041000 - ], - 'displaysectionselector' => true, // Set to false to disable the default section selector. - 'displayenabledownload' => true, // Set to false to hide the "Enable download" option in the course context menu. - 'init' => 'myformat_init' - ] - ] -]; -``` - -As with other plugins, you then use a function in your plugin's classes/output/mobile.php to return a template: - -```php title="classes/output/mobile.php" -class mobile { - - /** - * Main course page. - * - * @param array $args Standard mobile web service arguments - * @return array - */ - public static function mobile_course_view($args) { - global $OUTPUT, $CFG; - - $course = get_course($args['courseid']); - require_login($course); - $html = $OUTPUT->render_from_template('format_myformat/mobile_course', []); - - return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $html - ] - ], - 'otherdata' => [ - ... - ] - ]; - } -} -``` - -Then your templates/mobile_course.mustache file will contain the angular template to display your page - -```html handlebars title="templates/mobile_course.mustache" -{{=<% %>=}} - - - - - -
- - - - - - {{section.count}} / {{section.total}} - - -
-
- - - - - - - - - - - -
-
-``` - -You don't have to use a mustache template, you can just use a static angular template. - -This will get you to a stage where you have something similar to the core format plugin - a list of sections with headers, each containing its list of course modules. From here, you can make customisations to support your course format's features. - -Note that there are a few objects available to your template without you having to do anything: - -- `sections` - this is an array of all the sections on the course, which includes all of the modules within that course. -- `course` - this contains the basic course data -- `downloadEnabled` - This is set using the option in the context menu, if `displayenabledownload` is used in your db.php - -## Example: only display certain sections - -When your course page loads, the sections array contains all of the sections on the course. However, you might not want to display all of these sections on one page. -You can achieve this by returning the list of sections to display along with the template in classes/output/mobile.php: - -```php title="classes/output/mobile.php" -class mobile { - public static function mobile_course_view($args) { - ... - $displaysections = myformat\helper::get_list_of_section_ids($courseid); - - return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $html - ] - ], - 'otherdata' => [ - 'displaysections' => json_encode($displaysections); - ] - ]; - } -} - -``` - -Then filter the list of sections in your template: - -```html - - - - - -``` diff --git a/general/app/development/plugins-development-guide/examples/dynamic-names.md b/general/app/development/plugins-development-guide/examples/dynamic-names.md deleted file mode 100644 index 142c93c807..0000000000 --- a/general/app/development/plugins-development-guide/examples/dynamic-names.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Accepting dynamic names in a Web Service -tags: - - Moodle App ---- - -We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new Web Service that we have created. - -The first issue we find is that the Web Service needs to define the names of the parameters received, but in this case they're dynamic. The solution is to accept an array of objects with name and value. So in the `\_parameters()` function of our new Web Service, we will add this parameter: - -```php -'data' => new external_multiple_structure( - new external_single_structure( - [ - 'name' => new external_value(PARAM_RAW, 'data name'), - 'value' => new external_value(PARAM_RAW, 'data value'), - ] - ), - 'The data to be saved', VALUE_DEFAULT, [] -) -``` - -Now we need to adapt our form to send the data as the Web Service requires it. In our template, we have a button with the `core-site-plugins-call-ws` directive that will send the form data to our Web Service. To make this work we will have to pass the parameters manually, without using the `form` attribute, because we need to format the data before it is sent. - -Since we will send the parameters manually and we want it all to be sent in the same array, we will use `ngModel` to store the input data into a variable that we'll call `data`, but you can use the name you want. This variable will be an object that will hold the input data with the format "name->value". For example, if I have an input with name "a1" and value "My answer", the data object will be: - -```javascript -{a1: 'My answer'} -``` - -So we need to add `ngModel` to all the inputs whose values need to be sent to the `data` WS param. Please notice that `ngModel` requires the element to have a name, so if you add `ngModel` to a certain element you need to add a name too. For example: - -```html ng2 -) -``` - -As you can see, we're using `CONTENT_OTHERDATA` to store the data. We do it like this because we'll use `otherdata` to initialise the form, setting the values the user has already stored. If you don't need to initialise the form, then you can use the `dataObject` variable, an empty object that the mobile app creates for you: - -```html ng2 -[(ngModel)]="dataObject['<% name %>']" -``` - -The app has a function that allows you to convert this data object into an array like the one the WS expects: `objectToArrayOfObjects`. So in our button we'll use this function to format the data before it's sent: - -```html ng2 - -``` - -As you can see in the example above, we're specifying that the keys of the `data` object need to be stored in a property called "name", and the values need to be stored in a property called "value". If your Web Service expects different names you need to change the parameters of the `objectToArrayOfObjects` function. - -If you open your plugin now in the app it will display an error in the JavaScript console. The reason is that the `data` variable doesn't exist inside `CONTENT_OTHERDATA`. As it is explained in previous sections, `CONTENT_OTHERDATA` holds the data that you return in `otherdata` for your method. We'll use `otherdata` to initialise the values to be displayed in the form. - -If the user hasn't answered the form yet, we can initialise the `data` object as an empty object. Please remember that we cannot return arrays or objects in `otherdata`, so we'll return a JSON string. - -```php -'otherdata' => ['data' => '{}'], -``` - -With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this: - -```php -$userdata = get_user_responses(); // It will held the data in a format name->value. Example: ['a1' => 'My value']. - -// ... - -'otherdata' => ['data' => json_encode($userdata)], -``` - -Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our Web Service in array format. diff --git a/general/app/development/plugins-development-guide/examples/forms.md b/general/app/development/plugins-development-guide/examples/forms.md new file mode 100644 index 0000000000..25437d291d --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/forms.md @@ -0,0 +1,218 @@ +--- +title: Forms +tags: + - Moodle App +--- + +In this example, we are going to see how to display a form and send the data to a Web Service when it's submitted. + +First, we return the initial values of the form in `otherdata`: + +```php +class mobile { + + public static function view_form($args) { + global $OUTPUT; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('local_hello/form', []), + ], + ], + 'otherdata' => [ + 'name' => 'Clark Ken', + ], + ]; + } + +} +``` + +In the template, we use it referencing `CONTENT_OTHERDATA`: + +```html ng2 +{{=<% %>=}} + + + + + + + {{ 'plugin.local_hello.submit | translate }} + + + +``` + +In the template, we are creating an input text and using `[(ngModel)]` to set the value in `name` as the initial value. In this case, the input will be set to "Clark Ken". And if the user changes the value, these changes will be applied to the `name` variable. This is called 2-way data binding in Angular. + +We also add a button to send this data to a Web Service, and for that we use the [`core-site-plugins-call-ws` directive](../api-reference.md#core-site-plugins-call-ws). We use the `useOtherDataForWS` attribute to specify which variable from `otherdata` we want to include in the Web Service request. If you change the input to "Louis Lane" and press the button, you will call the Web Service `local_hello_submit` with the following parameters: `['name' => 'Louis Lane']`. + +This is how you would declare the Web Service: + +```php title="db/services.php" +$functions = [ + + 'local_hello_submit' => [ + 'classname' => 'local_hello\external\submit', + 'methodname' => 'execute', + 'classpath' => 'local/hello/classes/external/submit.php', + 'description' => 'Submit a form', + 'type' => 'write', + 'ajax' => true, + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + +]; +``` + +```php title="classes/external/submit.php" +class submit extends external_api { + + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'name' => new external_value(PARAM_RAW_TRIMMED, 'Name'), + ]); + } + + public static function execute($name) { + $validatedparams = self::validate_parameters(self::execute_parameters(), compact('name')); + [$name] = array_values($validatedparams); + + return ['message' => "Hello, $name!"]; + } + + public static function execute_returns(): external_description { + return new external_single_structure([ + 'message' => new external_value(PARAM_RAW, 'Message'), + ]); + } + +} +``` + +## Using `params` + +We can also achieve the same result using the `params` attribute of the `core-site-plugins-call-ws` directive instead of using `useOtherDataForWS`: + +```html ng2 + + {{ 'plugin.local_hello.submit | translate }} + +``` + +## Using `form` + +This could also be done without using `otherdata`, with the `form` attribute for the `core-site-plugins-call-ws` directive. However, keep in mind that you'd still need to use `otherdata` to set the initial value of the inputs. + +In this example, we're not using `otherdata` at all, so the input will be empty by default: + +```html ng2 +{{=<% %>=}} +
+ + + + + + + {{ 'plugin.local_hello.submit | translate }} + + + +
+``` + +## Using `core-rich-text-editor` + +In some forms, you may want to use the [`core-rich-text-editor` component](../api-reference.md#core-rich-text-editor) for writing formatted content. However, you should be aware of some intricacies. + +First of all, using the `[(ngModel)]` approach will not work. Instead, you have to define a [FormControl](https://angular.io/api/forms/FormControl): + +```js title="JavaScript" +this.nameControl = this.FormBuilder.control(this.CONTENT_OTHERDATA.name); +``` + +```html ng2 title="Template" + + + +... + + + {{ 'plugin.local_hello.submit | translate }} + +``` + +Notice how we're passing `nameControl.value` in `params`, rather than using `useOtherDataForWS`. In this case, the value of `CONTENT_OTHERDATA.name` is only used to initialize the content in the editor, but it won't be updated when the text changes. + +If you are using the second approach, including inputs in a `
` and using the `form` attribute, it should work as expected: + +```html ng2 + + +``` + +## Sending dynamic data + +In some situations, you may need to submit dynamic data. This means that field names won't always be the same, this happens for example in quiz forms. + +One challenge with this approach is that Moodle Web Services don't accept dynamic parameters, so we'll need to send them in an array of objects with the field names and values: + +```php title="classes/external/submit.php" +public static function execute_parameters(): external_function_parameters { + return new external_single_structure([ + 'data' => new external_multiple_structure( + new external_single_structure( + [ + 'name' => new external_value(PARAM_RAW, 'data name'), + 'value' => new external_value(PARAM_RAW, 'data value'), + ], + ), + 'Form data', + VALUE_DEFAULT, + [], + ), + ]); +} +``` + +Once we've done that, we'll need to change our form to use the new data structure in submissions. The only way to do that is using the `params` attribute. One helper you may find useful is `CoreUtilsProvider.objectToArrayOfObjects`, which will transform an object with name/value pairs into an array of objects using the format we declared in the Web Service: + +```html ng2 + + {{ 'plugin.local_hello.submit | translate }} + +``` + +As you can see, the second and third arguments for the `CoreUtilsProvider.objectToArrayOfObjects` method indicate the field names for the objects array. + +This can now be used to send any fields that you want, even if they are not declared beforehand in the Web Service. diff --git a/general/app/development/plugins-development-guide/examples/groups-selector.md b/general/app/development/plugins-development-guide/examples/groups-selector.md new file mode 100644 index 0000000000..ff237b865e --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/groups-selector.md @@ -0,0 +1,121 @@ +--- +title: Groups Selector +tags: + - Moodle App +--- + +If you have an activity that uses groups, you may want to implement a group selector to filter the rendered content. There are many ways to achieve this, and we're going to outline some of them in this example. + +However, before looking at the solutions, we're going to simplify the use-case. Normally, you'd use functions like `groups_get_activity_allowed_groups` to get the list of groups; and filter content according to each group's permissions. But in this example, we're going to rely on a `$groups` variable containing a list of items for each group: + +```php +$groups = [ + [ + 'name' => 'One', + 'items' => [ + ['name' => '1.1'], + ['name' => '1.2'], + ], + ], + [ + 'name' => 'Two', + 'items' => [ + ['name' => '2.1'], + ['name' => '2.2'], + ['name' => '2.3'], + ], + ], +]; +``` + +The UI we're going to implement will show a groups selector with the list of items below. With this, you should learn the intricacies of App development without getting into the weeds of group management. + +## Using Angular + +One approach is to send all the relevant data through `otherdata`, and filter the content using Angular: + +```php title="Content response" +return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('local_sample/groups', []), + ], + ], + 'otherdata' => [ + 'groups' => json_encode($groups), + 'selectedGroup' => 0, + ], +]; +``` + +```html ng2 title="Template" +{{=<% %>=}} + + + + + {{ group.name }} + + + + + {{ item.name }} + + +``` + +One advantage of this approach is that it can work completely offline, once the template has been downloaded it can be cached and the selector will work without requesting new information. However, it will require performing a PTR (Pull-To-Refresh) to get the latest information. + +## Loading new content + +If we want to render new content every time the selector is changed, we can take advantage of the [functions available in templates](../api-reference.md#functions): + +```php title="Content response" +$group = $args['group'] ?? 'One'; +$groupnames = array_map(fn ($group) => $group['name'], $groups); +$selectedgroup = array_search($group, $groupnames) || 0; + +return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('local_sample/groups', [ + 'groups' => $groups, + 'selectedgroup' => $groups[$selectedgroup], + ]), + ], + ], +]; +``` + +```html ng2 title="Template" +{{=<% %>=}} + + + + <%# groups %> + + <% name %> + + <%/ groups %> + + + <%# selectedgroup.items %> + + <% name %> + + <%/ selectedgroup.items %> + +``` + +In this case, we're only sending the rendered UI and all the filtering happens in the server. Using the `ionChange` listener, we can call the `updateContent` function to reload the content with a different group selected. We got the value of the selected option using the special `$event` variable that references the change event. + +Also, make sure to set the `value` attribute in the `ion-select` element, so that the initial group is the correct one. diff --git a/general/app/development/plugins-development-guide/examples/index.md b/general/app/development/plugins-development-guide/examples/index.md index a84eec4990..4b894c6d15 100644 --- a/general/app/development/plugins-development-guide/examples/index.md +++ b/general/app/development/plugins-development-guide/examples/index.md @@ -1,5 +1,6 @@ --- title: Examples +sidebar_position: 1 --- diff --git a/general/app/development/plugins-development-guide/examples/link-handlers.md b/general/app/development/plugins-development-guide/examples/link-handlers.md new file mode 100644 index 0000000000..680c76e502 --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/link-handlers.md @@ -0,0 +1,125 @@ +--- +title: Link Handlers +tags: + - Moodle App +--- + +You can create a link handler to intercept what happens when links are clicked using the [CoreContentLinksDelegate](../api-reference.md#corecontentlinksdelegate). + +The Moodle app automatically creates two link handlers for module plugins, so you don't need to create them in your plugin's JavaScript code anymore: + +- A handler to treat links to `mod/pluginname/view.php?id=X`. When this link is clicked, it will open your module in the app. +- A handler to treat links to `mod/pluginname/index.php?id=X`. When this link is clicked, it will open a page in the app listing all the modules of your type inside a certain course. + +If these aren't sufficient, link handlers have some advanced features that allow you to change how links behave under different conditions. + +In order to implement a custom link handler, you should register an object implementing the [CoreContentLinksHandler interface](https://github.com/moodlehq/moodleapp/blob/main/src/core/features/contentlinks/services/contentlinks-delegate.ts#L27..L95) in the delegate. You can also achieve this extending the [CoreContentLinksHandlerBase class](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/contentlinks/classes/base-handler.ts): + +```js +class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {} + +this.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler()); +``` + +## Using patterns + +You'll most likely need to match only certain links. You can define a Regular Expression pattern to filter clicks: + +```js +class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase { + + constructor() { + super(); + + this.pattern = RegExp('\/mod\/foo\/specialpage.php'); + } + +} +``` + +## Setting a priority + +Multiple link handlers may apply to a given link. You can define the order of precedence by setting the priority; the handler with the highest priority will be used. + +Handlers have a priority default priority of 0, so 1 or higher will override the default: + +```js +class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase { + + constructor() { + super(); + + this.priority = 1; + } + +} +``` + +## Performing multiple actions + +Once a link has been matched, the handler's `getActions()` method determines what the link should do. This method has access to the URL and its parameters. + +Different actions can be returned depending on different conditions: + +```js +class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase { + + getActions(siteIds, url, params) { + return [ + { + action: function(siteId, navCtrl) { + // The actual behaviour of the link goes here. + }, + sites: [ + // ... + ], + }, + { + // ... + }, + ]; + } + +} +``` + +Once handlers have been matched for a link, the actions will be fetched for all them and sorted by priority. The first valid action will be used to open the link. + +If your handler is matched with a link, but a condition assessed in the `getActions()` method determines that you want to skip it, you can invalidate it by setting its sites property to an empty array. + +## Complex example + +This will match all URLs containing `/mod/foo/`, and force those with an id parameter that's not in the `supportedModFoos` array to open in the user's browser, rather than the app: + +```js +const that = this; +const supportedModFoos = [...]; + +class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase { + + constructor() { + super(); + + this.pattern = new RegExp('\/mod\/foo\/'); + this.name = 'AddonModFooLinkHandler'; + this.priority = 1; + } + + getActions(siteIds, url, params) { + const action = { + action() { + that.CoreUtilsProvider.openInBrowser(url); + }, + }; + + if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) { + action.sites = []; + } + + return [action]; + } + +} + +this.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler()); +``` diff --git a/general/app/development/plugins-development-guide/examples/prefetch-handlers.md b/general/app/development/plugins-development-guide/examples/prefetch-handlers.md new file mode 100644 index 0000000000..d1954f5dfa --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/prefetch-handlers.md @@ -0,0 +1,82 @@ +--- +title: Prefetch Handlers +tags: + - Moodle App +--- + +[CoreCourseModuleDelegate](../api-reference.md#corecoursemoduledelegate) handlers allow you to define a list of offline functions to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain Web Service calls (passing the result of a Web Service request to another), and this cannot be done using offline functions. + +In order to implement a custom prefetch handler, you should register an object implementing the [CoreCourseModulePrefetchHandler interface](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/course/services/module-prefetch-delegate.ts#L1410..L1568) in the [CoreCourseModulePrefetchDelegate](../api-reference.md#corecoursemoduleprefetchdelegate) delegate. You can also achieve this extending the [CoreCourseActivityPrefetchHandlerBase class](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/course/classes/activity-prefetch-handler.ts): + +```js +class AddonModCertificateModulePrefetchHandler extends this.CoreCourseActivityPrefetchHandlerBase { + + constructor() { + super(); + + this.name = 'AddonModCertificateModulePrefetchHandler'; + this.modName = 'certificate'; + + // This must match the plugin identifier from db/mobile.php, + // otherwise the download link in the context menu will not update correctly. + this.component = 'mod_certificate'; + this.updatesNames = /^configuration$|^.*files$/; + } + + // Override the prefetch call. + prefetch(module, courseId, single, dirPath) { + return this.prefetchPackage(module, courseId, single, prefetchCertificate); + } + +} + +async function prefetchCertificate(module, courseId, single, siteId) { + // Perform all the WS calls. + // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call(). +} + +this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler()); +``` + +One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same `cmid`. The app will not automatically work in this situation — it will call the offline function with the standard arguments only — so you won't be able to prefetch all the possible parameters. + +To deal with this, you need to implement a Web Service in your Moodle component that returns the list of possible extra arguments, and then you can call this Web Service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module with multiple values for a custom `section` parameter in the mobile function `mobile_document_view`. We're only showing the actual prefetch function, the rest of the code is the same as above: + +```js +async function prefetchOucontent(module, courseId, single, siteId) { + const component = 'mod_oucontent'; + + // Get the site, first. + const site = await that.CoreSitesProvider.getSite(siteId); + + // Read the list of pages in this document using a web service. + const pages = site.read('mod_oucontent_get_page_list', {'cmid': module.id}); + + // For each page, read and process the page - this is a copy of logic in the app at + // siteplugins.ts (prefetchFunctions), but modified to add the custom argument. + await Promise.all(pages.map(async (page) => { + const args = { + courseid: courseId, + cmid: module.id, + userid: site.getUserId() + }; + + if (page !== '') { + args.section = page; + } + + const result = await that.CoreSitePluginsProvider.getContent(component, 'mobile_document_view', args); + + if (result.files?.length) { + await that.CoreFilepoolProvider.downloadOrPrefetchFiles( + site.id, + result.files, + true, + false, + component, + module.id, + ); + } + })); +} +``` diff --git a/general/app/development/plugins-development-guide/examples/question-types.md b/general/app/development/plugins-development-guide/examples/question-types.md new file mode 100644 index 0000000000..6638cf1d1c --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/question-types.md @@ -0,0 +1,138 @@ +--- +title: Question Types +tags: + - Moodle App +--- + +You can implement custom question types using the [CoreQuestionDelegate](../api-reference.md#corequestiondelegate): + +```php title="db/mobile.php" +$addons = [ + "qtype_YOURQTYPENAME" => [ + "handlers" => [ + 'YOURQTYPENAME' => [ + 'displaydata' => [ + 'title' => 'YOURQTYPENAME question', + 'icon' => '/question/type/YOURQTYPENAME/pix/icon.gif', + 'class' => '', + ], + 'delegate' => 'CoreQuestionDelegate', + 'method' => 'mobile_get_YOURQTYPENAME', + 'offlinefunctions' => [ + 'mobile_get_YOURQTYPENAME' => [], + ], + 'styles' => [ + 'url' => '/question/type/YOURQTYPENAME/mobile/styles_app.css', + 'version' => '1.00', + ], + ], + ], + 'lang' => [ + ['pluginname', 'qtype_YOURQTYPENAME'], + ], + ], +]; +``` + +```php title="classes/output/mobile.php" +class mobile { + + public static function mobile_get_YOURQTYPENAME() { + global $OUTPUT, $CFG; + + $html = $OUTPUT->render_from_template('qtype_YOURQTYPENAME/mobile', []); + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $html, + ], + ], + 'javascript' => file_get_contents($CFG->dirroot . '/question/type/YOURQTYPENAME/mobile/mobile.js'), + ]; + } + +} +``` + +```html handlebars title="templates/mobile.mustache" +
+ + + + + + +
+``` + +```js title="mobile/mobile.js" +const that = this; +const result = { + componentInit() { + if (!this.question) { + console.warn('Aborting because of no question received.'); + + return that.CoreQuestionHelperProvider.showComponentError(that.onAbort); + } + + const div = document.createElement('div'); + + div.innerHTML = this.question.html; + + // Get question questiontext. + const questiontext = div.querySelector('.qtext'); + + // Replace Moodle's correct/incorrect and feedback classes with our own. + // Only do this if you want to use the standard classes. + this.CoreQuestionHelperProvider.replaceCorrectnessClasses(div); + this.CoreQuestionHelperProvider.replaceFeedbackClasses(div); + + // Treat the correct/incorrect icons. + this.CoreQuestionHelperProvider.treatCorrectnessIcons(div); + + if (div.querySelector('.readonly') !== null) { + this.question.readonly = true; + } + + if (div.querySelector('.feedback') !== null) { + this.question.feedback = div.querySelector('.feedback'); + this.question.feedbackHTML = true; + } + + this.question.text = this.CoreDomUtilsProvider.getContentsOfElement(div, '.qtext'); + + if (typeof this.question.text === 'undefined') { + this.logger.warn('Aborting because of an error parsing question.', this.question.name); + + return this.CoreQuestionHelperProvider.showComponentError(this.onAbort); + } + + // Called by the reference in html to (afterRender)="questionRendered()". + this.questionRendered = function questionRendered() { + // Do stuff that needs the question rendered before it can run. + }; + + // Wait for the DOM to be rendered. + setTimeout(() => { + // Put stuff here that will be pulled from the rendered question. + }); + + return true; + } +}; + +result; +``` + +## Other examples + +- [Question type plugin template](https://github.com/marcusgreen/moodle-qtype_TEMPLATE) +- [Gapfill question type](https://github.com/marcusgreen/moodle-qtype_gapfill) +- [Wordselect question type](https://github.com/marcusgreen/moodle-qtype_wordselect) +- [RegExp question type](https://github.com/rezeau/moodle-qtype_regexp) diff --git a/general/app/development/plugins-development-guide/examples/self-enrolment.md b/general/app/development/plugins-development-guide/examples/self-enrolment.md new file mode 100644 index 0000000000..89bbdf605f --- /dev/null +++ b/general/app/development/plugins-development-guide/examples/self-enrolment.md @@ -0,0 +1,58 @@ +--- +title: Self Enrolment +tags: + - Moodle App +--- + +Using [CoreEnrolDelegate](../api-reference.md#coreenroldelegate-43) handlers you can support enrolment plugins in the app. In this example, we can see how to support a self enrol plugin. You'll need to register a handler in `db/mobile.php` and return the following JavaScript from the `method` implementation: + +```js +const getEnrolmentInfo = (id) => { + // Get enrolment info for the enrol instance. + // Used internally, you can use any name, parameters and return data in here. +}; + +const selfEnrol = (method, info) => { + // Self enrol the user in the course. + // Used internally, you can use any name, parameters and return data in here. +}; + +var result = { + getInfoIcons: (courseId) => { + return this.CoreEnrolService.getSupportedCourseEnrolmentMethods(courseId, 'selftest').then(enrolments => { + if (!enrolments.length) { + return []; + } + + // Since this code is for testing purposes just use the first one. + return getEnrolmentInfo(enrolments[0].id).then(info => { + if (!info.enrolpassword) { + return [{ + label: 'plugin.enrol_selftest.pluginname', + icon: 'fas-right-to-bracket', + }]; + } else { + return [{ + label: 'plugin.enrol_selftest.pluginname', + icon: 'fas-key', + }]; + } + }); + }); + }, + enrol: (method) => { + return getEnrolmentInfo(method.id).then(info => { + return selfEnrol(method, info); + }); + }, + invalidate: (method) => { + // Invalidate WS data. + }, +}; + +result; +``` + +## Other examples + +You can find more examples about this type of plugins in [MOBILE-4323](https://tracker.moodle.org/browse/MOBILE-4323). diff --git a/general/app/development/plugins-development-guide/index.md b/general/app/development/plugins-development-guide/index.md index b4576d7047..0e8f02a2a2 100644 --- a/general/app/development/plugins-development-guide/index.md +++ b/general/app/development/plugins-development-guide/index.md @@ -6,29 +6,31 @@ tags: - Moodle App --- -If you want to add mobile support to your Moodle plugin, you can achieve it by extending different areas of the app using **just PHP server side code** and providing templates written with [Ionic](https://ionicframework.com/docs/components) and custom components. +If you want add mobile support to your plugins, **you only need to write PHP code**. You can also use JavaScript for advanced functionality, but mobile plugins are written the same way as any other Moodle plugin: using PHP and Mustache templates. -You will have to: +In this guide we'll go over everything you need to know, but we recommend having read the [Moodle App Overview](../../overview.md) before starting. -1. Create a `db/mobile.php` file in your plugin. In this file, you will be able to indicate which areas of the app you want to extend. For example, adding a new option in the main menu, implementing support for a new activity module, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document. -2. Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered as html. This html should use Ionic components so that it looks native, but it can be generated using mustache templates. +In order to get your mobile plugin, you'll need to do the following: + +1. Create a `db/mobile.php` file. This file indicates which areas of the app the plugin extends. +2. Create new functions in a reserved namespace to return app templates. This content will be rendered as html, but using the application UI and components rather than core. That means that instead of Bootstrap components and CSS, you'll need to use Ionic. Let's clarify some points: - You don't need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace. -- Those functions will be exported via the Web Service function `tool_mobile_get_content`. -- As arguments of your functions you will always receive the `userid`, some relevant details of the app (like the app version or the current language in the app), and some specific data depending on the type of plugin (`courseid`, `cmid`, ...). -- The mobile app also implements a list of custom Ionic components and directives that provide dynamic behaviour; like indicating that you are linking a file that can be downloaded, allowing a transition to new pages into the app calling a specific function in the server, submitting form data to the server, etc. +- These functions are exported via the `tool_mobile_get_content` Web Service. +- These functions will receive arguments such as `userid` and other relevant details of the app like app version or current language. Depending on the type of plugin, they will also receive some contextual data like `courseid` or `cmid`. +- In addition to [built-in Ionic components](https://ionicframework.com/docs/components), the app also implements some [custom components](./api-reference.md#components) and [directives](./api-reference.md#components) specific to Moodle. ## Getting started If you only want to write a plugin, it is not necessary that you set up your environment to work with the Moodle App. In fact, you don't even need to compile it. You can just [use a Chromium-based browser](./setup/app-in-browser) to add mobile support to your plugins! -You can use the app from one of the hosted versions on [latest.apps.moodledemo.net](https://latest.apps.moodledemo.net) (the latest stable version) and [main.apps.moodledemo.net](https://main.apps.moodledemo.net) (the latest development version). If you need any specific environment (hosted versions are deployed with a **production** environment), you can also use [Docker images](./setup/docker-images). And if you need to test your plugin in a native device, you can always use [Moodle HQ's application](https://download.moodle.org/mobile). +You can use the app from one of the hosted versions on [latest.apps.moodledemo.net](https://latest.apps.moodledemo.net) (the latest stable version) and [main.apps.moodledemo.net](https://main.apps.moodledemo.net) (the latest development version). If you need any specific environment (hosted versions are deployed with a production environment), you can also use [Docker images](./setup/docker-images). And if you need to test your plugin in a native device, you can always use [Moodle HQ's application](https://download.moodle.org/mobile). This should suffice for developing plugins. However, if you are working on advanced functionality and you need to run the application from the source code, you can find more information in the [Moodle App Development guide](./development-guide). -### Development workflow +### Your first plugin Before getting into the specifics of your plugin, we recommend that you start adding a simple "Hello World" button in the app to see that everything works properly. @@ -37,6 +39,8 @@ Let's say your plugin is called `local_hello`, you can start by adding the follo ```php title="db/mobile.php" [ 'handlers' => [ @@ -83,1815 +87,288 @@ class mobile { render_from_template`. However, keep in mind that the `{{ }}` syntax is also used for interpolating values in Angular. We recommend switching Mustache's interpolation syntax in mobile templates to `<% %>`. This can be achieved by adding `{{=<% %>=}}` at the beginning of the file. Here's an example: -### Step 1. Update the `db/mobile.php` file +```php title="method in classes/output/mobile.php" +public static function mobile_course_view($args) { + global $OUTPUT; -In this case, we are updating an existing file. For new plugins, you should create this new file. - -```php title="db/mobile.php" -$addons = [ - 'mod_certificate' => [ // Plugin identifier - 'handlers' => [ // Different places where the plugin will display content. - 'coursecertificate' => [ // Handler unique name (alphanumeric). - 'displaydata' => [ - 'icon' => $CFG->wwwroot . '/mod/certificate/pix/icon.gif', - 'class' => '', - ], - - 'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the plugin) - 'method' => 'mobile_course_view', // Main function in \mod_certificate\output\mobile - 'offlinefunctions' => [ - 'mobile_course_view' => [], - 'mobile_issues_view' => [], - ], // Function that needs to be downloaded for offline. + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('local_hello/greeting', ['name' => 'John']), ], ], - 'lang' => [ // Language strings that are used in all the handlers. - ['pluginname', 'certificate'], - ['summaryofattempts', 'certificate'], - ['getcertificate', 'certificate'], - ['requiredtimenotmet', 'certificate'], - ['viewcertificateviews', 'certificate'], + 'otherdata' => [ + 'surname' => 'Doe', ], - ], -]; -``` - -**Plugin identifier** - -A unique name for the plugin, it can be anything (there's no need to match the module name). - -**Handlers (Different places where the plugin will display content)** - -A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric). - -**Display data** - -This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields). In this case, we are indicating the module icon. - -**Delegate** - -Where to display the link to the plugin, see the [Delegates](#delegates) section for all the possible options. - -**Method** - -This is the method in the Moodle `{component-name}\output\mobile` class to be executed the first time the user clicks in the new option displayed in the app. - -**Offline functions** - -This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the `mobile.php` file. - -In our example, downloading for offline access will mean that we'll execute the functions for getting the certificate and issued certificates passing as parameters the current `userid` (and `courseid` when we are using the mod or course delegate). If we have the result of those functions stored in the app, we'll be able to display the certificate information even if the user is offline. - -Offline functions will be mostly used to display information for final users, any further interaction with the view won't be supported offline (for example, trying to send information when the user is offline). - -You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently `userid` and `courseid`). - -Prefetching the module will also download all the files returned by the methods in these offline functions (in the `files` array). - -Note that if your functions use additional custom parameters (for example, if you implement multiple pages within a module's view function by using a `page` parameter in addition to the usual `cmid`, `courseid`, and `userid`) then the app will not know which additional parameters to supply. In this case, do not list the function in `offlinefunctions`; instead, you will need to manually implement a [module prefetch handler](#module-prefetch-handler). - -**Lang** - -The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here, like `['cancel', 'moodle']`. If you do this, be warned that in the app you will then need to refer to that string as `{{ 'plugin.myplugin.cancel' | translate }}` (not `{{ 'plugin.moodle.cancel' | translate }}`). - -Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful. - -There are additional attributes supported by the `mobile.php` list, you can find about them in the [Mobile.php supported options](#mobilephp-supported-options) section. - -### Step 2. Creating the main function - -The main function displays the current issued certificate (or several warnings if it's not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates. - -All the functions must be created in the plugin or subsystem `classes/output` directory, the name of the class must be `mobile`. - -For this example, the namespace name will be `mod_certificate\output`. - -```php title="mod/certificate/classes/output/mobile.php" -cmid); - - // Capabilities check. - require_login($args->courseid, false, $cm, true, true); - - $context = \context_module::instance($cm->id); - - require_capability('mod/certificate:view', $context); - if ($args->userid != $USER->id) { - require_capability('mod/certificate:manage', $context); - } - $certificate = $DB->get_record('certificate', ['id' => $cm->instance]); - - // Get certificates from external (taking care of exceptions). - try { - $issued = \mod_certificate_external::issue_certificate($cm->instance); - $certificates = \mod_certificate_external::get_issued_certificates($cm->instance); - $issues = array_values($certificates['issues']); // Make it mustache compatible. - } catch (Exception $e) { - $issues = []; - } - - // Set timemodified for each certificate. - foreach ($issues as $issue) { - if (empty($issue->timemodified)) { - $issue->timemodified = $issue->timecreated; - } - } - - $showget = true; - if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) { - if (certificate_get_course_time($certificate->course) < ($certificate->requiredtime * 60)) { - $showget = false; - } - } - - $certificate->name = format_string($certificate->name); - [$certificate->intro, $certificate->introformat] = - external_format_text($certificate->intro, $certificate->introformat, $context->id, 'mod_certificate', 'intro'); - $data = [ - 'certificate' => $certificate, - 'showget' => $showget && count($issues) > 0, - 'issues' => $issues, - 'issue' => $issues[0], - 'numissues' => count($issues), - 'cmid' => $cm->id, - 'courseid' => $args->courseid, - ]; - - return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data), - ], - ], - 'javascript' => '', - 'otherdata' => '', - 'files' => $issues, - ]; - } + ]; } ``` -**Function declaration** - -The function name is the same as the one used in the `mobile.php` file (`method` field). There is only one argument, `$args`, which is an array containing all the information sent by the mobile app (the `courseid`, `userid`, `appid`, `appversionname`, `appversioncode`, `applang`, `appcustomurlscheme`, ...). - -**Function implementation** - -In the first part of the function, we check permissions and capabilities (like a `view.php` script would do normally). Then we retrieve the certificate information that's necessary to display the template. - -**Function return** - -- `templates` — The rendered template (notice that we could return more than one template, but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some JavaScript code. -- `javascript` — Empty, because we don't need any in this case. -- `otherdata` — Empty as well, because we don't need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way data-binding in the template. -- `files` — A list of files that the app should be able to download (for offline usage mostly). - -### Step 3. Creating the template for the main function - -This is the most important part of your plugin because it contains the code that will be rendered on the mobile app. - -In this template we'll be using Ionic, together with directives and components specific to the Moodle App. - -All the HTML elements starting with `ion-` are ionic components. Most of the time, the component name is self-explanatory but you may refer to a detailed guide here: [https://ionicframework.com/docs/components/](https://ionicframework.com/docs/components/). - -All the HTML elements starting with `core-` are custom components of the Moodle App. - -```html title="mod/certificate/templates/mobile_view_page.mustache" +```html handlebars title="template in templates/greeting.mustache" {{=<% %>=}} -
- - - - - -

{{ 'plugin.mod_certificate.summaryofattempts' | translate }}

-
- - <%#issues%> - - - {{ 'plugin.mod_certificate.viewcertificateviews' | translate: {$a: <% numissues %>} }} - - - <%/issues%> - - <%#showget%> - - - - - - {{ 'plugin.mod_certificate.getcertificate' | translate }} - - - - <%/showget%> - - <%^showget%> - -

{{ 'plugin.mod_certificate.requiredtimenotmet' | translate }}

-
- <%/showget%> - - - - -
-
+ +

Hello, <% name %> {{ CONTENT_OTHERDATA.surname }}

``` -In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). +You can find the type of template for each handler in the [API Reference](./api-reference.md#handlers). -Then we display the module description using `core-course-module-description`, which is a component used to include the course module description. +### Dynamic templates -For displaying the certificate information we create a list of elements, adding a header on top. +These templates are generated each time they are used in the app. This means that the PHP function will receive some contextual parameters, and the template can be rendered dynamically in the server. -The following line using the `translate` filter indicates that the app will translate the `summaryofattempts` string id (here we could've used mustache translation but it is usually better to delegate the strings translations to the app). The string id has the following format: +For example, if you're developing a [course module plugin](./api-reference.md#corecoursemoduledelegate) you will receive the `courseid` and the `cmid`. -```text -plugin.{plugin-identifier}.{string-id} -``` - -Where `{plugin-identifier}` is taken from `mobile.php` and `{string-id}` must be indicated in the `lang` field in `mobile.php`. - -Then, we display a button to transition to another page if there are certificates issued. The attribute (directive) `core-site-plugins-new-content` indicates that if the user clicks the button, we need to call the `mobile_issues_view` function in the `mod_certificate` component; passing as arguments the `cmid` and `courseid`. The content returned by this function will be displayed in a new page (read the following section to see the code of this new page). - -Just after this button, we display another one but this time for downloading an issued certificate. The `core-course-download-module-main-file` directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available offline. +![](../../_files/dynamic_templates.jpg) -Finally, just before the `ion-list` is closed, we use the `core-site-plugins-call-ws-on-load` directive to indicate that once the page is loaded, we need to call a Web Service function in the server, in this case we are calling the `mod_certificate_view_certificate` that will log that the user viewed this page. +### Static templates -As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes. +These templates are generated once when the user logs in and cached in the device. This means that the PHP function will not receive any contextual parameters, and it must return a generic template. But it doesn't mean that the UI has to be completely static in the app; it can still use JavaScript to render dynamic elements. -### Step 4. Adding an additional page +![](../../_files/static_templates.jpg) -Add the following method to `mod/certificate/classes/output/mobile.php`: +## Localisation -```php title="mod/certificate/classes/output/mobile.php" -/** - * Returns the certificate issues view for the mobile app. - * @param array $args Arguments from tool_mobile_get_content WS. - * - * @return array HTML, JS and other data. - */ -public static function mobile_issues_view($args) { - global $OUTPUT, $USER, $DB; +You can declare the language strings used in your plugin in the `lang` property of the configuration. Normally these will be strings from your own plugin, but you can list any strings. For example, you could include `moodle.cancel` from Moodle core. - $args = (object) $args; - $cm = get_coursemodule_from_id('certificate', $args->cmid); - - // Capabilities check. - require_login($args->courseid, false, $cm, true, true); - - $context = context_module::instance($cm->id); - - require_capability ('mod/certificate:view', $context); - if ($args->userid != $USER->id) { - require_capability('mod/certificate:manage', $context); - } - $certificate = $DB->get_record('certificate', ['id' => $cm->instance]); - - // Get certificates from external (taking care of exceptions). - try { - $issued = mod_certificate_external::issue_certificate($cm->instance); - $certificates = mod_certificate_external::get_issued_certificates($cm->instance); - $issues = array_values($certificates['issues']); // Make it mustache compatible. - } catch (Exception $e) { - $issues = []; - } +Strings can be used in the templates using `plugin.{plugin_name}.{string_identifier}`, where `{plugin_name}` will be the name of your plugin and `{string_identifier}` the unscoped string identifier. Note that even strings from different namespaces will use the plugin name, not their original namespace. - $data = ['issues' => $issues]; +Here's an example for `local_hello`: - return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_issues', $data), - ], +```php title="db/mobile.php" +$addons = [ + 'local_hello' => [ + 'handlers' => [ + // ... ], - 'javascript' => '', - 'otherdata' => '', - ]; -} + 'lang' => [ + ['hello', 'local_hello'], + ['cancel', 'moodle'], + ], + ], +]; ``` -This method for the new page was added just after `mobile_course_view`, the code is quite similar: checks the capabilities, retrieves the information required for the template, and returns the template rendered. - -The code of the mustache template is also very simple. - -```html handlebars title="mod/certificate/templates/mobile_view_issues.mustache" -{{=<% %>=}} -
- - <%#issues%> - - -

{{ <%timecreated%> | coreToLocaleString }}

-

<%grade%>

-
-
- <%/issues%> -
-
+```html ng2 title="Template" +{{ 'plugin.local_hello.hello' | translate }} +{{ 'plugin.local_hello.cancel' | translate }} ``` -As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). +Make sure to only include the strings you actually need. The Web Service that returns the plugin information will include the translations for every language, and this will be cached in the device. So listing too many strings can be wasteful. -Here we are creating an Ionic list that will display a new item in the list per each issued certificated. +### Dynamic strings -For the issued certificated we'll display the time when it was created (using the app filter `coreToLocaleString`). We are also displaying the grade displayed in the certificate (if any). +If you wish to have an element that displays a localised string based on value from your template you can do something like this: -### Step 5. Plugin webservices, if included - -If your plugin uses its own web services, they will also need to be enabled for mobile access in your `db/services.php` file. - -The following line should be included in each webservice definition: - -```php title="db/services.php" -'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'], +```html handlebars + + + {{ 'plugin.mod_myactivity.<% status %>' | translate }} + + ``` -```php title="mod/certificate/db/services.php" - [ - 'classname' => 'mod_certificate_external', - 'methodname' => 'get_certificates_by_courses', - 'description' => 'Returns a list of certificate instances...', - 'type' => 'read', - 'capabilities' => 'mod/certificate:view', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'], - ], -]; +```html handlebars + + + <%#isedting%>{{ 'plugin.mod_myactivity.editing' | translate }}<%/isediting%> + <%#isopen%>{{ 'plugin.mod_myactivity.open' | translate }}<%/isopen%> + <%#isclosed%>{{ 'plugin.mod_myactivity.closed' | translate }}<%/isclosed%> + + ``` -## Mobile.php supported options - -In the previous section, we learned about some of the existing options for handlers configuration. This is the full list of supported options. - -### Common options - -- `delegate` (mandatory) — Name of the delegate to register the handler in. -- `method` (mandatory) — The method to call to retrieve the main page content. -- `init` (optional) — A method to call to retrieve the initialisation JS and the restrictions to apply to the whole handler. It can also return templates that can be used from JavaScript. You can learn more about this in the [Initialisation](#initialisation) section. -- `restricttocurrentuser` (optional) — Only used if the delegate has a `isEnabledForUser` function. If true, the handler will only be shown for the current user. For more info about displaying the plugin only for certain users, please see [Display the plugin only if certain conditions are met](#display-the-plugin-only-if-certain-conditions-are-met). -- `restricttoenrolledcourses` (optional) — Only used if the delegate has a `isEnabledForCourse` function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [Display the plugin only if certain conditions are met](#display-the-plugin-only-if-certain-conditions-are-met). -- `styles` (optional) — An array with two properties: `url` and `version`. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It's recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number every time you change the CSS file. -- `moodlecomponent` (optional) — If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be `local_whatever`, but in `moodlecomponent` you can specify that this handler will implement `format_whatever` or `mod_whatever`. This property was introduced in the version 3.6.1 of the app. - -### Options only for CoreMainMenuDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `icon` — The name of an ionic icon. [See icons section](#using-font-icons-with-ion-icon). - - `class` — A CSS class. -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the More tab, they cannot be displayed as tabs in the bottom bar. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreMainMenuHomeDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `class` — A CSS class. -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreCourseOptionsDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `class` — A CSS class. -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. -- `ismenuhandler` (optional) — Supported from the 3.7.1 version of the app. Set it to `true` if you want your plugin to be displayed in the contextual menu of the course instead of in the top tabs. The contextual menu is displayed when you click in the 3-dots button at the top right of the course. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreCourseModuleDelegate - -- `displaydata` (mandatory): - - `icon` — Path to the module icon. After Moodle app 4.0, this icon is only used as a fallback, the app will always try to use the theme icon so themes can override icons in the app. - - `class` — A CSS class. -- `method` (optional) — The function to call to retrieve the main page content. In this delegate the method is optional. If the method is not set, the module won't be clickable. -- `offlinefunctions` (optional) — List of functions to call when prefetching the module. It can be a `get_content` method or a WS. You can filter the params received by the WS. By default, WS will receive these params: `courseid`, `cmid`, `userid`. Other valid values that will be added if they are present in the list of params: `courseids` (it will receive a list with the courses the user is enrolled in), `{component}id` (For example, `certificateid`). -- `downloadbutton` (optional) — Whether to display download button in the module. If not defined, the button will be shown if there is any `offlinefunctions`. -- `isresource` (optional) — Whether the module is a resource or an activity. Only used if there is any offline function. If your module relies on the `contents` field, then it should be `true`. -- `updatesnames` (optional) — Only used if there is any offline function. A regular expression to check if there's any update in the module. It will be compared to the result of `core_course_check_updates`. -- `displayopeninbrowser` (optional) — Whether the module should display the "Open in browser" option in the top-right menu. This can be done in JavaScript too: `this.displayOpenInBrowser = false;`. Supported from the 3.6 version of the app. -- `displaydescription` (optional) — Whether the module should display the "Description" option in the top-right menu. This can be done in JavaScript too: `this.displayDescription = false;`. Supported from the 3.6 version of the app. -- `displayrefresh` (optional) — Whether the module should display the "Refresh" option in the top-right menu. This can be done in JavaScript too: `this.displayRefresh = false;`. Supported from the 3.6 version of the app. -- `displayprefetch` (optional) — Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: `this.displayPrefetch = false;`. Supported from the 3.6 version of the app. -- `displaysize` (optional) — Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: `this.displaySize = false;`. Supported from the 3.6 version of the app. -- `supportedfeatures` (optional) — It can be used to specify the supported features of the plugin. Please notice that some features are not supported by the app, unsupported features will be ignored. It should be an array with features as keys (For example, `FEATURE_NO_VIEW_LINK => true`). If you need to calculate this dynamically please see [Module plugins: dynamically determine if a feature is supported](#module-plugins-dynamically-determine-if-a-feature-is-supported). Supported from the 3.6 version of the app. -- `coursepagemethod` (optional) — If set, this method will be called when the course is rendered and the HTML returned will be displayed in the course page for the module. Please notice the HTML returned should not contain directives or components, only default HTML. Supported from the 3.8 version of the app. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreCourseFormatDelegate - -- `canviewallsections` (optional) — Whether the course format allows seeing all sections in a single page. Defaults to `true`. -- `displayenabledownload` (optional) — Deprecated in the 4.0 app, it's no longer used. -- `displaysectionselector` (optional) — Deprecated in the 4.0 app, use `displaycourseindex` instead. -- `displaycourseindex` (optional) — Whether the default course index should be displayed. Defaults to `true`. - -### Options only for CoreUserDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `icon` — The name of an ionic icon. [See icons section](#using-font-icons-with-ion-icon). - - `class` — A CSS class. -- `type` — The type of the addon. The values accepted are `'newpage'` (default) and `'communication'`. -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreSettingsDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `icon` — The name of an ionic icon. [See icons section](#using-font-icons-with-ion-icon). - - `class` — A CSS class. -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for AddonMessageOutputDelegate - -- `displaydata` (mandatory): - - `title` — A language string identifier that was included in the `lang` section. - - `icon` — The name of an ionic icon. [See icons section](#using-font-icons-with-ion-icon). -- `priority` (optional) — Priority of the handler. Higher priority is displayed first. -- `ptrenabled` (optional) — Whether to enable pull-to-refresh gesture to refresh page content. - -### Options only for CoreBlockDelegate - -- `displaydata` (optional): - - `title` — A language string identifier that was included in the `lang` section. If this is not supplied, it will default to `'plugins.block\_{block-name}.pluginname'`, where `{block-name}` is the name of the block. - - `class` — A CSS class. If this is not supplied, it will default to `block\_{block-name}`, where `{block-name}` is the name of the block. - - `type` — Possible values are: - - `"title"` — Your block will only display the block title, and when it's clicked it will open a new page to display the block contents (the template returned by the block's method). - - `"prerendered"` — Your block will display the content and footer returned by the WebService to get the blocks (for example, `core_block_get_course_blocks`), so your block's method will never be called. - - Any other value — Your block will immediately call the method specified in `mobile.php` and it will use the template to render the block. -- `fallback` (optional) — This option allows you to specify a block to use in the app instead of your block. For example, you can make the app display the "My overview" block instead of your block in the app by setting `'fallback' => 'myoverview'`. The fallback will only be used if you don't specify a `method` and the `type` is different to `'title'` or `'prerendered'`. Supported from the 3.9.0 version of the app. - -### Options only for CoreEnrolDelegate - -- `enrolmentAction` (optional): The type of action done by the enrolment plugin. Defaults to 'browser'. Possible values: - - `browser` — When the user clicks to enrol, open a browser to perform the enrol in the browser. It doesn't require any JavaScript to work in the app. - - `self` — The user can self enrol in the app. Requires implementing the 'enrol' function in your JavaScript code. Also, your PHP class extending `enrol_plugin` should return some data instead of null in the `get_enrol_info` function, otherwise it won't work in the app. - - `guest` — Allows the user to enter the course as guest in the app. Requires implementing the 'canAccess' and 'validateAccess' functions in your JavaScript code. Also, your PHP class extending `enrol_plugin` should return some data instead of null in the `get_enrol_info` function, otherwise it won't work in the app. -- `infoIcons` (optional) — Icons related to the enrolment to display next to the course. If the icons need to be calculated dynamically based on the course you need to implement the function 'getInfoIcons' in your JavaScript code. Properties for each icon: - - `icon` (required) — The icon name. E.g. 'fas-credit-card'. - - `label` (required) — The label of the icon (for accessibility). - - `className` (optional) — A CSS class to add to the icon element. - -## Delegates - -Delegates can be classified by type of plugin. For more info about type of plugins, please see the [Types of plugins](#types-of-plugins) section. - -### Templates generated and downloaded when the user opens the plugins - -#### `CoreMainMenuDelegate` - -You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). - -#### `CoreMainMenuHomeDelegate` - -You must use this delegate when you want to add new tabs in the home page (by default the app is displaying the "Dashboard" and "Site home" tabs). - -#### `CoreCourseOptionsDelegate` - -You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate). - -#### `CoreCourseModuleDelegate` - -You must use this delegate for supporting activity modules or resources. - -#### `CoreUserDelegate` - -You must use this delegate when you want to add additional options in the user profile page in the app. - -#### `CoreCourseFormatDelegate` - -You must use this delegate for supporting course formats. When you open a course from the course list in the mobile app, it will check if there is a `CoreCourseFormatDelegate` handler for the format that site uses. If so, it will display the course using that handler. Otherwise, it will use the default app course format. - -You can learn more about this at the [Creating mobile course formats](./plugins-development-guide/examples/create-course-formats) page. - -#### `CoreSettingsDelegate` - -You must use this delegate to add a new option in the settings page. - -#### `AddonMessageOutputDelegate` - -You must use this delegate to support a message output plugin. - -#### `CoreBlockDelegate` - -You must use this delegate to support a block. For example, blocks can be displayed in Site Home, Dashboard and the Course page. - -### Templates downloaded on login and rendered using JS data - -#### `CoreQuestionDelegate` - -You must use this delegate for supporting question types. - -You can learn more about this at the [Creating mobile question types](https://docs.moodle.org/dev/Creating_mobile_question_types) page. - -#### `CoreQuestionBehaviourDelegate` - -You must use this delegate for supporting question behaviours. - -#### `CoreUserProfileFieldDelegate` - -You must use this delegate for supporting user profile fields. - -#### `AddonModQuizAccessRuleDelegate` - -You must use this delegate to support a quiz access rule. - -#### `AddonModAssignSubmissionDelegate` and `AddonModAssignFeedbackDelegate` - -You must use these delegates to support assign submission or feedback plugins. - -#### `AddonWorkshopAssessmentStrategyDelegate` - -You must use this delegate to support a workshop assessment strategy plugin. - -### Pure JavaScript plugins - -These delegates require JavaScript to be supported. See [Initialisation](#initialisation) for more information. - -- `CoreContentLinksDelegate` -- `CoreCourseModulePrefetchDelegate` -- `CoreFileUploaderDelegate` -- `CorePluginFileDelegate` -- `CoreFilterDelegate` -- `CoreEnrolDelegate` (added in 4.3 version of the app) - -## Available components and directives +### Date strings -### Difference between components and directives +If you need to include a formatted date in a string, you can achieve it using `coreFormatDate`: -A directive is usually represented as an HTML attribute, allows you to extend a piece of HTML with additional information or functionality. Example of directives are: `core-auto-focus`, `\*ngIf`, and `ng-repeat`. - -Components are also directives, but they are usually represented as an HTML tag and they are used to add custom elements to the app. Example of components are `ion-list`, `ion-item`, and `core-search-box`. - -Components and directives are Angular concepts; you can learn more about them and the components come out of the box with Ionic in the following links: - -- [Angular directives documentation](https://angular.io/guide/built-in-directives) -- [Ionic components](https://ionicframework.com/docs/components) - -### Custom core components and directives - -These are some useful custom components and directives that are only available in the Moodle App. Please note that this isn't the full list of custom components and directives, it's just an extract of the most common ones. - -You can find a full list of components and directives in the source code of the app, within [`src/core/components`](https://github.com/moodlehq/moodleapp/tree/latest/src/core/components) and [`src/core/directives`](https://github.com/moodlehq/moodleapp/tree/latest/src/core/directives). - -#### `core-format-text` - -This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive. - -This directive automatically applies `core-external-content` and `core-link` to all the links and embedded media. - -Data that can be passed to the directive: - -- `text` (string) — The text to format. -- `siteId` (string) — Optional. Site ID to use. If not defined, it will use the id of the current site. -- `component` (string) — Optional. Component to use when downloading embedded files. -- `componentId` (string|number) — Optional. ID to use in conjunction with the component. -- `adaptImg` (boolean) — Optional, defaults to `true`. Whether to adapt images to screen width. -- `clean` (boolean) — Optional, defaults to `false`. Whether all HTML tags should be removed. -- `singleLine` (boolean) — Optional, defaults to `false`. Whether new lines should be removed to display all the text in single line. Only if `clean` is `true`. -- `maxHeight` (number) — Optional. Max height in pixels to render the content box. The minimum accepted value is 50. Using this parameter will force `display: block` to calculate the height better. If you want to avoid this, use `class="inline"` at the same time to use `display: inline-block`. - -```html ng2 title="Example of usage" - +```php title="String definition" +$string['start'] = 'Activity starts on {$a}.'; ``` -#### `core-link` - -Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app). - -This directive is automatically applied to all the links and media inside `core-format-text`. - -Data that can be passed to the directive: - -- `capture` (boolean) — Optional, defaults to `false`. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser). -- `inApp` (boolean) — Optional, defaults to `false`. Whether to open in an embedded browser within the app or in the system browser. -- `autoLogin` (string) — Optional, defaults to `"check"`. If the link should be open with auto-login. Accepts the following values: - - `"yes"` — Always auto-login. - - `"no"` — Never auto-login. - - `"check"` — Auto-login only if it points to the current site. - -```html ng2 title="Example of usage" - +```html handlebars title="Template" +{{ + 'plugin.mod_myactivity.start' | translate: { + $a: <% timestamp %> * 1000 | coreFormatDate: 'dffulldate' + } +}} ``` -#### `core-external-content` +Make sure that you are passing milliseconds. Unix timestamp are usually expressed in seconds, so you'll usually need to multiply them by 1000 in the app. -Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. +The following formats are available (expressed in [Moment.js format](https://momentjs.com/docs/#/displaying/format/)): -If a file is downloaded, its URL will be replaced by the local file URL. +:::note Notice +These formats can change depending on the language in the app. You can find the translations in [AMOS](https://lang.moodle.org). +::: -This directive is automatically applied to all the links and media inside `core-format-text`. +| Name | Value (English) | Example | +|-------------------------|-----------------------------------------|----------------------------------| +| `dfdaymonthyear` | `MM-DD-YYYY` | 04-25-2024 | +| `dfdayweekmonth` | `ddd, D MMM` | Thu, 25 Apr | +| `dffulldate` | `dddd, D MMMM YYYY h[:]mm A` | Thursday, 25 April 2024 4:28 PM | +| `dflastweekdate` | `ddd` | Thu | +| `dfmediumdate` | `LLL` | April 25, 2024 4:28 PM | +| `dftimedate` | `h[:]mm A` | 4:28 PM | +| `strftimedate` | `D[ ]MMMM[ ]YYYY[ ]` | 25 April 2024 | +| `strftimedatefullshort` | `D[/]MM[/]YY` | 25/04/24 | +| `strftimedateshort` | `D[ ]MMMM` | 25 April | +| `strftimedatetime` | `D[ ]MMMM[ ]YYYY[, ]h[:]mm[ ]A` | 25 April 2024, 4:28 PM | +| `strftimedatetimeshort` | `D[/]MM[/]YY[, ]HH[:]mm` | 25/04/24, 16:28 | +| `strftimedaydate` | `dddd[, ]D[ ]MMMM[ ]YYYY` | Thursday, 25 April 2024 | +| `strftimedaydatetime` | `dddd[, ]D[ ]MMMM[ ]YYYY[, ]h[:]mm[ ]A` | Thursday, 25 April 2024, 4:28 PM | +| `strftimedayshort` | `dddd[, ]D[ ]MMMM` | Thursday, 25 April | +| `strftimedaytime` | `ddd[, ]HH[:]mm` | Thu, 16:28 | +| `strftimemonthyear` | `MMMM[ ]YYYY` | April 2024 | +| `strftimerecent` | `D[ ]MMM[, ]HH[:]mm` | 25 Apr, 16:28 | +| `strftimerecentfull` | `ddd[, ]D[ ]MMM[ ]YYYY[, ]h[:]mm[ ]A` | Thu, 25 Apr 2024, 4:28 PM | +| `strftimetime` | `h[:]mm[ ]A` | 4:28 PM | +| `strftimetime12` | `h[:]mm[ ]A` | 4:28 PM | +| `strftimetime24` | `HH[:]mm` | 16:28 | -Data that can be passed to the directive: +## JavaScript initialisation -- `siteId` (string) — Optional. Site ID to use. If not defined, it will use the id of the current site. -- `component` (string) — Optional. Component to use when downloading embedded files. -- `componentId` (string|number) — Optional. ID to use in conjunction with the component. +All handlers can specify an `init` method in their configuration, and the JavaScript from the [content response](./api-reference.md#content-responses) will be executed as soon as the plugin is retrieved. The templates can be accessible in all JavaScript scripts of the handler at `this.INIT_TEMPLATES`, and the `otherdata` object at `this.INIT_OTHERDATA`. -```html ng2 title="Example of usage" - -``` +This JavaScript can be used to manually register handlers in delegates, without having to rely on the default handlers built based on the `mobile.php` data. Some handlers, such as [CoreContentLinksDelegate](./api-reference.md#corecontentlinksdelegate) handlers, can only be registered this way. However, keep in mind that handlers registered using JavaScript won't respect the `restrict` and `disable` configuration from the content response, so make sure to make the checks yourself in JavaScript. -#### `core-user-link` +Finally, the last statement in the JavaScript code will be used to evaluate an object with properties passed to all the JavaScript code of the handler. -Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened. +You can find which APIs are available in JavaScript in the [Services](./api-reference.md#services) documentation. -Data that can be passed to the directive: +Here's an example using `templates`, `otherdata`, and custom properties: -- `userId` (number) — User id to open the profile. -- `courseId` (number) — Optional. Course id to show the user info related to that course. +```php title="PHP init method" +public static function my_init() { + return [ + 'templates' => [ + [ + 'id' => 'greeting', + 'html' => '

Hello!

', + ], + ], + 'otherdata' => ['foo' => 'bar'], + 'javascript' => " + var result = { lorem: 'ipsum' }; -```html ng2 title="Example of usage" -
+ result; + ", + ]; +} ``` -#### `core-file` - -Component to handle a remote file. It shows the file name, icon (depending on mime type) and a button to download or refresh it. The user can identify if the file is downloaded or not based on the button. - -Data that can be passed to the directive: - -- `file` (object) — The file. Must have a `filename` property and either `fileurl` or `url`. -- `component` (string) — Optional. Component the file belongs to. -- `componentId` (string|number) — Optional. ID to use in conjunction with the component. -- `canDelete` (boolean) — Optional. Whether the file can be deleted. -- `alwaysDownload` (boolean) — Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they're outdated or not. -- `canDownload` (boolean) — Optional, defaults to `true`. Whether file can be downloaded. - -```html ng2 title="Example of usage" - - +```js title="any JavaScript in the handler, not just the initialisation script" +console.log(this.INIT_TEMPLATES.greeting); // "

Hello!

" +console.log(this.INIT_OTHERDATA.foo); // "bar" +console.log(this.lorem); // "ipsum" ``` -#### `core-download-file` - -Directive to allow downloading and opening a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened. - -It is usually recommended to use the `core-file` component since it also displays the state of the file. +## JavaScript overrides -Data that can be passed to the directive: +Handlers using [Static templates](#static-templates) can override the default handler methods using JavaScript. This also works for [CoreEnrolDelegate](./api-reference.md#coreenroldelegate-43) when using `self` or `guest` enrolment action. -- `core-download-file` (object) — The file to download. -- `component` (string) — Optional. Component to link the file to. -- `componentId` (string|number) — Optional. Component ID to use in conjunction with the component. +In order to do this, the last statement in the JavaScript returned from the [content response](./api-reference.md#content-responses) will be evaluated for method overrides. -```html ng2 title="Example of usage (a button to download a file)" - - {{ 'plugin.mod_certificate.download | translate }} - -``` - -#### `core-course-download-module-main-file` +Additionally, you can also implement a `componentInit` function. This function will be bound to the scope of the Angular component, which can be the one provided by the app or a custom component returned by one of the handler methods (such as `getComponent()`). -Directive to allow downloading and opening the main file of a module. +You can find more details about the specific methods and components available in the [API Reference](./api-reference.md). You can also find all the APIs are available in JavaScript in the [Services](./api-reference.md#services) documentation. -When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like `mod_resource`. +For example, this is how you would override the `getData` method for a [CoreUserProfileFieldDelegate](./api-reference.md#coreuserprofilefielddelegate) handler; and hook into the component initialization for a [CoreSitePluginsUserProfileFieldComponent](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/siteplugins/components/user-profile-field/user-profile-field.ts) component: -This directive must receive either a `module` or a `moduleId`. If no files are provided, it will use `module.contents`. - -Data that can be passed to the directive: +```js +var that = this; -- `module` (object) — Optional, required if module is not supplied. The module object. -- `moduleId` (number) — Optional, required if module is not supplied. The module ID. -- `courseId` (number) — The course ID the module belongs to. -- `component` (string) — Optional. Component to link the file to. -- `componentId` (string|number) — Optional, defaults to the same value as `moduleId`. Component ID to use in conjunction with the component. -- `files` (object[])— Optional. List of files of the module. If not provided, uses `module.contents`. +var result = { + componentInit: function() { + if (this.field && this.edit && this.form) { + this.field.modelName = 'profile_field_' + this.field.shortname; -```html ng2 title="Example of usage" - - {{ 'plugin.mod_certificate.getcertificate' | translate }} - -``` + if (this.field.param2) { + this.field.maxlength = parseInt(this.field.param2, 10) || ''; + } -#### `core-navbar-buttons` + this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? 'password' : 'text'; -Component to add buttons to the app's header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page. + var formData = { + value: this.field.defaultdata, + disabled: this.disabled, + }; -If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first `` found in the header. + this.form.addControl(this.field.modelName, + that.FormBuilder.control(formData, this.field.required && !this.field.locked ? that.Validators.required : null)); + } + }, + getData: function(field, signup, registerAuth, formValues) { + var name = 'profile_field_' + field.shortname; -You can use the `[hidden]` input to hide all the inner buttons if a certain condition is met. + return { + type: "text", + name: name, + value: that.CoreTextUtilsProvider.cleanTags(formValues[name]), + }; + } +}; -```html ng2 title="Example of usage" - - - - - +result; ``` -You can also use this to add options to the context menu, for example: - -```html ng2 - - - - - - -``` +## Web Services -#### Using 'font' icons with `ion-icon` +If you need to use some Web Services in your plugin, make sure to include `MOODLE_OFFICIAL_MOBILE_SERVICE` in their declaration. -Font icons are widely used on the app and Moodle LMS website. In order to support [Font Awesome 6.3 icons](https://fontawesome.com/search?o=r&m=free). We've added a directive that uses prefixes on the `name` attribute to use different font icons. +You can learn how to use Web Services in mobile plugins in the [Forms example](./examples/forms.md). -- Name prefixed with `fas-` or `fa-` will use [Font Awesome solid library](https://fontawesome.com/search?o=r&m=free&s=solid). -- Name prefixed with `far-` will use [Font Awesome regular library](https://fontawesome.com/search?o=r&m=free&s=regular). -- Name prefixed with `fab-` will use [Font Awesome brands library](https://fontawesome.com/search?o=r&m=free&f=brands) (But only a few are supported and we discourage to use them). -- Name prefixed with `moodle-` will use some svg icons [imported from Moodle LMS](https://github.com/moodlehq/moodleapp/tree/main/src/assets/fonts/moodle/moodle). -- Name prefixed with `fam-` will use [customized Font Awesome icons](https://github.com/moodlehq/moodleapp/tree/main/src/assets/fonts/moodle/font-awesome). -- If the prefix is not found or not valid, the app will search the icon name on the [Ionicons library](https://ionic.io/ionicons). +## Testing -```html title="Example of usage to show icon "pizza-slice" from Font Awesome regular library" - -``` +You can also write automated tests for your plugin using Behat, you can read more about it on the [Acceptance testing for the Moodle App](./testing/acceptance-testing) page. -We encourage the use of Font Awesome 6.3 icons to match the appearance from the LMS website version. +## Moodle plugins with mobile support -### Specific component and directives for plugins +If you want to see some real examples, you can find plugins with mobile support in the plugins database: -These are component and directives created specifically for supporting Moodle plugins. +- [Custom certificate module](https://github.com/mdjnelson/moodle-mod_customcert) +- [Group choice module](https://github.com/ndunand/moodle-mod_choicegroup) +- [Gapfill question type](https://github.com/marcusgreen/moodle-qtype_gapfill) +- [Wordselect question type](https://github.com/marcusgreen/moodle-qtype_wordselect) +- [RegExp question type](https://github.com/rezeau/moodle-qtype_regexp) +- [Attendance module](https://github.com/danmarsden/moodle-mod_attendance) +- [ForumNG module](https://github.com/moodleou/moodle-mod_forumng) +- [News block](https://github.com/moodleou/moodle-block_news) -#### `core-site-plugins-new-content` - -Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content). - -Data that can be passed to the directive: - -- `component` (string) — The component of the new content. -- `method` (string) — The method to get the new content. -- `args` (object) — The params to get the new content. -- `preSets` (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0. -- `title` (string) — The title to display with the new content. Only if `samePage` is `false`. -- `samePage` (boolean) — Optional, defaults to `false`. Whether to display the content in same page or open a new one. -- `useOtherData` (any) — Whether to include `otherdata` (from the `get_content` WS call) in the arguments for the new `get_content` call. If not supplied, no other data will be added. If supplied but empty (`null`, `false` or an empty string) all the `otherdata` will be added. If it's an array, it will only copy the properties whose names are in the array. Please notice that doing `[useOtherData]=""` is the same as not supplying it, so nothing will be copied. Also, objects or arrays in `otherdata` will be converted to a JSON encoded string. -- `form` (string) — ID or name to identify a form in the template. The form will be obtained from `document.forms`. If supplied and a form is found, the form data will be retrieved and sent to the new `get_content` WS call. If your form contains an `ion-radio`, `ion-checkbox` or `ion-select`, please see [Values of \`ion-radio\`, \`ion-checkbox\` or \`ion-select\` aren't sent to my WS](./troubleshooting.md#values-of-ion-radio-ion-checkbox-or-ion-select-arent-sent-to-my-ws). - -Let's see some examples. - -```html ng2 title="A button to go to a new content page" - - {{ 'plugin.mod_certificate.viewissued' | translate }} - -``` - -```html ng2 title="A button to load new content in current page using userid from otherdata" - - {{ 'plugin.mod_certificate.viewissued' | translate }} - -``` - -#### `core-site-plugins-call-ws` - -Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view. - -If you want to load a new content when the WS call is done, please see [core-site-plugins-call-ws-new-content](#core-site-plugins-call-ws-new-content). - -Data that can be passed to the directive: - -- `name` (string) — The name of the WS to call. -- `params` (object) — The params for the WS call. -- `preSets` (object) — Extra options for the WS call: whether to use cache or not, etc. -- `useOtherDataForWS` (any) — Whether to include `otherdata` (from the `get_content` WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (`null`, `false` or an empty string) all the `otherdata` will be added. If it's an array, it will only copy the properties whose names are in the array.Please notice that `[useOtherDataForWS]=""` is the same as not supplying it, so nothing will be copied. Also, objects or arrays in `otherdata` will be converted to a JSON encoded string. -- `form` (string) — ID or name to identify a form in the template. The form will be obtained from `document.forms`. If supplied and a form is found, the form data will be retrieved and sent to the new `get_content` WS call. If your form contains an `ion-radio`, `ion-checkbox` or `ion-select`, please see [Values of \`ion-radio\`, \`ion-checkbox\` or \`ion-select\` aren't sent to my WS](./troubleshooting.md#values-of-ion-radio-ion-checkbox-or-ion-select-arent-sent-to-my-ws). -- `confirmMessage` (string) — Message to confirm the action when the user clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used. -- `showError` (boolean) — Optional, defaults to `true`. Whether to show an error message if the WS call fails. This field was added in 3.5.2. -- `successMessage` (string) — Message to show on success. If not supplied, no message. If supplied but empty, defaults to "Success". -- `goBackOnSuccess` (boolean) — Whether to go back if the WS call is successful. -- `refreshOnSuccess` (boolean) — Whether to refresh the current view if the WS call is successful. -- `onSuccess` (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2. -- `onError` (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2. -- `onDone` (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2. - -Let's see some examples. - -```html ng2 title="A button to send some data to the server without using cache, displaying default messages and refreshing on success" - - {{ 'plugin.mod_certificate.senddata' | translate }} - -``` - -```html ng2 title="A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata" - - {{ 'plugin.mod_certificate.senddata' | translate }} - -``` - -```html ng2 title="Same as the previous example, but implementing custom JS code to run on success" - - {{ 'plugin.mod_certificate.senddata' | translate }} - -``` - -In the JavaScript side, you would do: - -```javascript -this.certificateViewed = function(result) { - // Code to run when the WS call is successful. -}; -``` - -#### `core-site-plugins-call-ws-new-content` - -Directive to call a WS when the element is clicked and load a new content passing the WS result as arguments. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content). - -If you don't need to load some new content when done, please see [core-site-plugins-call-ws](#core-site-plugins-call-ws). - -- `name` (string) — The name of the WS to call. -- `params` (object) — The parameters for the WS call. -- `preSets` (object) — Extra options for the WS call: whether to use cache or not, etc. -- `useOtherDataForWS` (any) — Whether to include `otherdata` (from the `get_content` WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (`null`, `false` or an empty string) all the `otherdata` will be added. If it's an array, it will only copy the properties whose names are in the array. Please notice that `[useOtherDataForWS]=""` is the same as not supplying it, so nothing will be copied. Also, objects or arrays in `otherdata` will be converted to a JSON encoded string. -- `form` (string) — ID or name to identify a form in the template. The form will be obtained from `document.forms`. If supplied and a form is found, the form data will be retrieved and sent to the new `get_content` WS call. If your form contains an `ion-radio`, `ion-checkbox` or `ion-select`, please see [Values of \`ion-radio\`, \`ion-checkbox\` or \`ion-select\` aren't sent to my WS](./troubleshooting.md#values-of-ion-radio-ion-checkbox-or-ion-select-arent-sent-to-my-ws). -- `confirmMessage` (string) — Message to confirm the action when the user clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used. -- `showError` (boolean) — Optional, defaults to `true`. Whether to show an error message if the WS call fails. This field was added in 3.5.2. -- `component` (string) — The component of the new content. -- `method` (string) — The method to get the new content. -- `args` (object) — The parameters to get the new content. -- `title` (string) — The title to display with the new content. Only if `samePage` is `false`. -- `samePage` (boolean) — Optional, defaults to `false`. Whether to display the content in the same page or open a new one. -- `useOtherData` (any) — Whether to include `otherdata` (from the `get_content` WS call) in the arguments for the new `get_content` call. The format is the same as in `useOtherDataForWS`. -- `jsData` (any) — JS variables to pass to the new page so they can be used in the template or JS. If `true` is supplied instead of an object, all initial variables from current page will be copied. This field was added in 3.5.2. -- `newContentPreSets` (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in 3.6.0. -- `onSuccess` (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2. -- `onError` (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2. -- `onDone` (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2. - -Let's see some examples. - -```html ng2 title="A button to get some data from the server without using cache, showing default confirm and displaying a new page" - - {{ 'plugin.mod_certificate.getissued' | translate }} - -``` - -```html ng2 title="A button to get some data from the server using cache, without confirm, displaying new content in same page and using userid from otherdata" - - {{ 'plugin.mod_certificate.getissued' | translate }} - -``` - -```html ng2 title="Same as the previous example, but implementing a custom JS code to run on success" - - {{ 'plugin.mod_certificate.getissued' | translate }} - -``` - -In the JavaScript side, you would do: - -```javascript -this.callDone = function(result) { - // Code to run when the WS call is successful. -}; -``` - -#### `core-site-plugins-call-ws-on-load` - -Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services. - -If you want to call a WS when the user clicks on a certain element, please see [core-site-plugins-call-ws](#core-site-plugins-call-ws). - -- `name` (string) — The name of the WS to call. -- `params` (object) — The parameters for the WS call. -- `preSets` (object) — Extra options for the WS call: whether to use cache or not, etc. -- `useOtherDataForWS` (any) — Whether to include `otherdata` (from the `get_content` WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (`null`, `false` or an empty string) all the `otherdata` will be added. If it's an array, it will only copy the properties whose names are in the array. Please notice that `[useOtherDataForWS]=""` is the same as not supplying it, so nothing will be copied. Also, objects or arrays in `otherdata` will be converted to a JSON encoded string. -- `form` (string) — ID or name to identify a form in the template. The form will be obtained from `document.forms`. If supplied and a form is found, the form data will be retrieved and sent to the new `get_content` WS call. If your form contains an `ion-radio`, `ion-checkbox` or `ion-select`, please see [Values of \`ion-radio\`, \`ion-checkbox\` or \`ion-select\` aren't sent to my WS](./troubleshooting.md#values-of-ion-radio-ion-checkbox-or-ion-select-arent-sent-to-my-ws). -- `onSuccess` (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2. -- `onError` (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2. -- `onDone` (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2. - -```html ng2 title="Example of usage" - - -``` - -In the JavaScript side, you would do: - -```javascript -this.callDone = function(result) { - // Code to run when the WS call is successful. -}; -``` - -## Advanced features - -### Display the plugin only if certain conditions are met - -You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the initialisation method (for more info, please see the [Initialisation](#initialisation) section ahead) - -All initialisation methods are called as soon as your plugin is retrieved. If you don't want your plugin to be displayed for the current user, then you should return the following in the initialisation method (only for Moodle site 3.8 and onwards): - -```php -return ['disabled' => true]; -``` - -If the Moodle site is older than 3.8, then the initialisation method should return this instead: - -```php -return ['javascript' => 'this.HANDLER_DISABLED']; -``` - -On the other hand, you might want to display a plugin only for certain courses (`CoreCourseOptionsDelegate`) or only if the user is viewing certain users' profiles (`CoreUserDelegate`). This can be achieved with the initialisation method too. - -In the initialisation method you can return a `restrict` property with two fields in it: `courses` and `users`. If you return a list of courses IDs in this property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users' profiles. - -### Using `otherdata` - -The values returned by the functions in `otherdata` are added to a variable so they can be used both in JavaScript and in templates. The `otherdata` returned by an initialisation call is added to a variable named `INIT_OTHERDATA`, while the `otherdata` returned by a `get_content` WS call is added to a variable named `CONTENT_OTHERDATA`. - -The `otherdata` returned by an initialisation call will be passed to the JS and template of all the `get_content` calls in that handler. The `otherdata` returned by a `get_content` call will only be passed to the JS and template returned by that `get_content` call. - -This means that, in your JavaScript, you can access and use the data like this: - -```javascript -this.CONTENT_OTHERDATA.myVar; -``` - -And in the template you could use it like this: - -```html ng2 -{{ CONTENT_OTHERDATA.myVar }} -``` - -`myVar` is the name we put to one of our variables, it can be any name that you want. In the example above, this is the `otherdata` returned by the PHP method: - -```php -['myVar' => 'Initial value'] -``` - -#### Example - -In our plugin, we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain Web Service. This can be done using `otherdata`. - -We will return the initial value of the input in the `otherdata` of our PHP method: - -```php -'otherdata' => ['myVar' => 'My initial value'], -``` - -Then in the template we will use it like this: - -```html ng2 - - {{ 'plugin.mod_certificate.textlabel | translate }} - - - - - {{ 'plugin.mod_certificate.send | translate }} - - -``` - -In the example above, we are creating an input text and we use `[(ngModel)]` to use the value in `myVar` as the initial value and to store the changes in the same `myVar` variable. This means that the initial value of the input will be "My initial value", and if the user changes the value of the input these changes will be applied to the `myVar` variable. This is called 2-way data binding in Angular. - -Then we add a button to send this data to a WS, and for that we use the `core-site-plugins-call-ws` directive. We use the `useOtherDataForWS` attribute to specify which variable from `otherdata` we want to send to our WebService. So if the user enters "A new value" in the input and then clicks the button, it will call the WebService `mod_certificate_my_webservice` and will send as a parameter `['myVar' => 'A new value']`. - -We can also achieve the same result using the `params` attribute of the `core-site-plugins-call-ws` directive instead of using `useOtherDataForWS`: - -```html ng2 - - {{ 'plugin.mod_certificate.send | translate }} - -``` - -The Web Service call will be exactly the same with both versions. - -Notice that this example could be done without using `otherdata` too, using the `form` input of the `core-site-plugins-call-ws` directive. - -### Running JS code after a content template has loaded - -When you return JavaScript code from a handler function using the `javascript` array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. - -If your code needs to run after the DOM has been updated, you can use `setTimeout` to call it. For example: - -```php -return [ - 'templates' => [ - // ... - ], - 'javascript' => 'setTimeout(function() { console.log("DOM is available now"); });', - 'otherdata' => '', - 'files' => [], -]; -``` - -Notice that if you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an initialisation template, so that it does not get loaded again with each page of content. - -### JS functions visible in the templates - -The app provides some JavaScript functions that can be used from the templates to update, refresh or view content. These are the functions: - -- `openContent(title: string, args: any, component?: string, method?: string)` — Open a new page to display some new content. You need to specify the `title` of the new page and the `args` to send to the method. If `component` and `method` aren't provided, it will use the same as in the current page. -- `refreshContent(showSpinner = true)` — Refresh the current content. By default, it will display a spinner while refreshing. If you don't want it to be displayed, you should pass `false` as a parameter. -- `updateContent(args: any, component?: string, method?: string)` — Refresh the current content using different parameters. You need to specify the `args` to send to the method. If `component` and `method` aren't provided, it will use the same as in the current page. - -#### Examples - -##### Group selector - -Imagine we have an activity that uses groups and we want to let the user select which group they want to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting. - -To do so, we'll use a drop down to select the group. When the user selects a group using this drop down, we'll update the page content to display the new group. - -The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular's `ngModel`. - -###### Using plain HTML - -We need to add a `selected` attribute to the option that needs to be selected. To do so, we need to pre-calculate the selected option in the PHP code: - -```php -$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0. -$groups = groups_get_activity_allowed_groups($cm, $user->id); - -// Detect which group is selected. -foreach ($groups as $gid=>$group) { - $group->selected = $gid === $groupid; -} - -$data = [ - 'cmid' => $cm->id, - 'courseid' => $args->courseid, - 'groups' => $groups, -]; - -return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data), - ], - ], -]; -``` - -In the code above, we're retrieving the groups the user can see and then we're adding a `selected` boolean to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template. - -In the template, we display the drop down like this: - -```html handlebars - - <%#groups%> - selected<%/selected%> ><% name %> - <%/groups%> - -``` - -The `ionChange` function will be called every time the user selects a different group with the drop down. We're using the `updateContent` function to update the current view using the new group. `$event` is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work. - -###### Using `ngModel` - -`ngModel` is an Angular directive that allows storing the value of a certain input or select in a JavaScript variable, and also the opposite way: tell the input or select which value to set. The main problem is that we cannot initialise a JavaScript variable from the template, so we'll use `otherdata`. - -In the PHP function we'll return the group that needs to be selected in the `otherdata` array: - -```php -$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0. -$groups = groups_get_activity_allowed_groups($cm, $user->id); - -// ... - -return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data), - ], - ], - 'otherdata' => [ - 'group' => $groupid, - ], -]; -``` - -In the example above we don't need to iterate over the groups array like in the plain HTML example. However, now we're returning the group id in the `otherdata` array. As it's explained in the [Using \`otherdata\`](#using-otherdata) section, this `otherdata` is visible in the templates inside a variable named `CONTENT_OTHERDATA`. So in the template we'll use this variable like this: - -```html handlebars - - <%#groups%> - <% name %> - <%/groups%> - -``` - -### Use the rich text editor - -The rich text editor included in the app requires a `FormControl` to work. You can use the `FormBuilder` library to create this control (or to create a whole `FormGroup` if you prefer). - -With the following JavaScript you'll be able to create a `FormControl`: - -```javascript -this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte); -``` - -In the example above we're using a value returned in `OTHERDATA` as the initial value of the rich text editor, but you can use whatever you want. - -Then you need to pass this control to the rich text editor in your template: - -```html ng2 - - - - -``` - -Finally, there are several ways to send the value in the rich text editor to a Web Service to save it. This is one of the simplest options: - -```html ng2 - -``` - -This template is returned by the initialisation method. And this is the JavaScript code returned: - -```javascript -var that = this; - -class AddonSingleActivityFormatComponent { - - constructor() { - this.data = {}; - } - - ngOnChanges(changes) { - var self = this; - - if (this.course && this.sections && this.sections.length) { - var module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0]; - if (module && !this.componentClass) { - that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) => { - self.componentClass = component || that.CoreCourseUnsupportedModuleComponent; - }); - } - - this.data.courseId = this.course.id; - this.data.module = module; - } - } - - doRefresh(refresher, done) { - return Promise.resolve(this.dynamicComponent.callComponentFunction("doRefresh", [refresher, done])); - } - -} - -class AddonSingleActivityFormatHandler { - - constructor() { - this.name = 'singleactivity'; - } - - isEnabled() { - return true; - } - - canViewAllSections() { - return false; - } - - getCourseTitle(course, sections) { - if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) { - return sections[0].modules[0].name; - } - - return course.fullname || ''; - } - - displayEnableDownload() { - return false; - } - - displaySectionSelector() { - return false; - } - - getCourseFormatComponent() { - return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES['main'], AddonSingleActivityFormatComponent); - } - -} - -this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler()); -``` - -##### Self enrol plugin - -The `CoreEnrolDelegate` handler allows you to support enrolment plugins in the app. This example will show how to support a self enrol plugin, you can find an example of each type of plugin in the issue [MOBILE-4323](https://tracker.moodle.org/browse/MOBILE-4323). - -Here's an example on how to create a prefetch handler using the JS returned by the main method: - -```javascript -const getEnrolmentInfo = (id) => { - // Get enrolment info for the enrol instance. - // Used internally, you can use any name, parameters and return data in here. -}; - -const selfEnrol = (method, info) => { - // Self enrol the user in the course. - // Used internally, you can use any name, parameters and return data in here. -}; - -var result = { - getInfoIcons: (courseId) => { - return this.CoreEnrolService.getSupportedCourseEnrolmentMethods(courseId, 'selftest').then(enrolments => { - if (!enrolments.length) { - return []; - } - - // Since this code is for testing purposes just use the first one. - return getEnrolmentInfo(enrolments[0].id).then(info => { - if (!info.enrolpassword) { - return [{ - label: 'plugin.enrol_selftest.pluginname', - icon: 'fas-right-to-bracket', - }]; - } else { - return [{ - label: 'plugin.enrol_selftest.pluginname', - icon: 'fas-key', - }]; - } - }); - }); - }, - enrol: (method) => { - return getEnrolmentInfo(method.id).then(info => { - return selfEnrol(method, info); - }); - }, - invalidate: (method) => { - // Invalidate WS data. - }, -}; - -result; -``` - -### Using the JavaScript API - -The JavaScript API is only supported by the delegates specified in the [Templates downloaded on login and rendered using JS data](#templates-downloaded-on-login-and-rendered-using-js-data) section. This API allows you to override any of the functions of the default handler. - -The `method` specified in a handler registered in the `CoreUserProfileFieldDelegate` will be called immediately after the initialisation method, and the JavaScript returned by this method will be run. If this JavaScript code returns an object with certain functions, these functions will override the ones in the default handler. - -For example, if the JavaScript returned by the method returns something like this: - -```javascript -var result = { - getData: function(field, signup, registerAuth, formValues) { - // ... - } -}; -result; -``` - -The the `getData` function of the default handler will be overridden by the returned `getData` function. - -The default handler for `CoreUserProfileFieldDelegate` only has 2 functions: `getComponent` and `getData`. In addition, the JavaScript code can return an extra function named `componentInit` that will be executed when the component returned by `getComponent` is initialised. - -Here's an example on how to support the text user profile field using this API: - -```javascript -var that = this; - -var result = { - componentInit: function() { - if (this.field && this.edit && this.form) { - this.field.modelName = 'profile_field_' + this.field.shortname; - - if (this.field.param2) { - this.field.maxlength = parseInt(this.field.param2, 10) || ''; - } - - this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? 'password' : 'text'; - - var formData = { - value: this.field.defaultdata, - disabled: this.disabled, - }; - - this.form.addControl(this.field.modelName, - that.FormBuilder.control(formData, this.field.required && !this.field.locked ? that.Validators.required : null)); - } - }, - getData: function(field, signup, registerAuth, formValues) { - var name = 'profile_field_' + field.shortname; - - return { - type: "text", - name: name, - value: that.CoreTextUtilsProvider.cleanTags(formValues[name]), - }; - } -}; - -result; -``` - -### Translate dynamic strings - -If you wish to have an element that displays a localised string based on value from your template you can doing something like: - -```html handlebars - - - {{ 'plugin.mod_myactivity.<% status %>' | translate }} - - -``` - -This could save you from having to write something like when only one value should be displayed: - -```html handlebars - - - <%#isedting%>{{ 'plugin.mod_myactivity.editing' | translate }}<%/isediting%> - <%#isopen%>{{ 'plugin.mod_myactivity.open' | translate }}<%/isopen%> - <%#isclosed%>{{ 'plugin.mod_myactivity.closed' | translate }}<%/isclosed%> - - -``` - -### Using strings with dates - -If you have a string that you wish to pass a formatted date, for example in the Moodle language file you have: - -```php -$string['strwithdate'] = 'This string includes a date of {$a->date} in the middle of it.'; -``` - -You can localise the string correctly in your template using something like the following: - -```html handlebars -{{ 'plugin.mod_myactivity.strwithdate' | translate: {$a: { date: <% timestamp %> * 1000 | coreFormatDate: "dffulldate" } } }} -``` - -A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, whereas Unix timestamps are in seconds. - -### Support push notification clicks - -If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this. - -The easiest way is to include a `contexturl` in your notification. When the notification is clicked, the app will try to open the `contexturl`. - -Please notice that the `contexturl` will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a `customdata` array that contains an `appurl` property: - -```php -$notification->customdata = [ - 'appurl' => $myurl->out(), -]; -``` - -In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see [how to create an advanced link handler](#link-handlers). - -If you want to do something that only happens when the notification is clicked, not when the link is clicked, you'll have to implement a push click handler yourself. The way to create it is similar to [creating an advanced link handler](#link-handlers), but you'll have to use `CorePushNotificationsDelegate` and your handler will have to implement the properties and functions defined in the [CorePushNotificationsClickHandler](https://github.com/moodlehq/moodleapp/blob/latest/src/core/features/pushnotifications/services/push-delegate.ts#L27) interface. - -### Implement a module similar to mod_label - -In Moodle 3.8 or higher, if your plugin doesn't support `FEATURE_NO_VIEW_LINK` and you don't specify a `coursepagemethod` then the module will only display the module description in the course page and it won't be clickable in the app, just like `mod_label`. You can decide if you want the module icon to be displayed or not (if you don't want it to be displayed, then don't define it in `displaydata`). - -However, if your plugin needs to work in previous versions of Moodle or you want to display something different than the description then you need a different approach. - -If your plugin wants to render something in the course page instead of just the module name and description you should specify the `coursepagemethod` property in `mobile.php`. The template returned by this method will be rendered in the course page. Please notice the HTML returned should not contain directives or components, only plain HTML. - -If you don't want your module to be clickable then you just need to remove `method` from `mobile.php`. With these 2 changes you can have a module that behaves like `mod_label` in the app. - -### Use Ionic navigation lifecycle functions - -Ionic let pages define some functions that will be called when certain navigation lifecycle events happen. For more info about these functions, see [Ionic's documentation](https://ionicframework.com/docs/api/router-outlet). - -You can define these functions in your plugin JavaScript: - -```javascript -this.ionViewWillLeave = function() { - // ... -}; -``` - -In addition to that, you can also implement `canLeave` to use Angular route guards: - -```javascript -this.canLeave = function() { - // ... -}; -``` - -So for example you can make your plugin ask for confirmation if the user tries to leave the page when he has some unsaved data. - -### Module plugins: dynamically determine if a feature is supported - -In Moodle you can specify if your plugin supports a certain feature, like `FEATURE_NO_VIEW_LINK`. If your plugin will always support or not a certain feature, then you can use the `supportedfeatures` property in `mobile.php`to specify it ([see more documentation about this](#options-only-for-corecoursemoduledelegate)). But if you need to calculate it dynamically then you will have to create a function to calculate it. - -This can be achieved using the initialisation method (for more info, please see the [Initialisation](#initialisation) section above). The JavaScript returned by your initialisation method will need to define a function named `supportsFeature` that will receive the name of the feature: - -```javascript -var result = { - supportsFeature: function(featureName) { - // ... - } -}; -result; -``` - -Currently the app only uses `FEATURE_MOD_ARCHETYPE` and `FEATURE_NO_VIEW_LINK`. - -## Testing - -You can also write automated tests for your plugin using Behat, you can read more about it on the [Acceptance testing for the Moodle App](./testing/acceptance-testing) page. - -## Upgrading plugins from an older version - -If you added mobile support to your plugin for the Ionic 3 version of the app (previous to the 3.9.5 release), you will probably need to make some changes to make it compatible with Ionic 5. - -Learn more at the [Moodle App Plugins upgrade guide](../upgrading/plugins-upgrade-guide). - -## Moodle plugins with mobile support - -- Group choice: [Moodle plugins directory entry](https://moodle.org/plugins/mod_choicegroup) and [code in github](https://github.com/ndunand/moodle-mod_choicegroup). -- Custom certificate: [Moodle plugins directory entry](https://moodle.org/plugins/mod_customcert) and [code in github](https://github.com/markn86/moodle-mod_customcert). -- Gapfill question type: [Moodle plugins directory entry](https://moodle.org/plugins/qtype_gapfill) and [in github](https://github.com/marcusgreen/moodle-qtype_gapfill). -- Wordselect question type: [Moodle plugins directory entry](https://moodle.org/plugins/qtype_wordselect) and [in github](https://github.com/marcusgreen/moodle-qtype_wordselect). -- RegExp question type: [Moodle plugins directory entry](https://moodle.org/plugins/qtype_regexp) and [in github](https://github.com/rezeau/moodle-qtype_regexp). -- Certificate: [Moodle plugins directory entry](https://moodle.org/plugins/mod_certificate) and [in github](https://github.com/markn86/moodle-mod_certificate). -- Attendance [Moodle plugins directory entry](https://moodle.org/plugins/mod_attendance) and [in github](https://github.com/danmarsden/moodle-mod_attendance). -- ForumNG (unfinished support) [Moodle plugins directory entry](https://moodle.org/plugins/mod_forumng) and [in github](https://github.com/moodleou/moodle-mod_forumng). -- News block [in github](https://github.com/moodleou/moodle-block_news). -- H5P activity module [Moodle plugins directory entry](https://moodle.org/plugins/mod_hvp) and [in github](https://github.com/h5p/h5p-moodle-plugin). - -See the complete list in [the plugins database](https://moodle.org/plugins/browse.php?list=award&id=6) (it may contain some outdated plugins). +See [the complete list](https://moodle.org/plugins/browse.php?list=award&id=6) to find more, but keep in mind that it may contain some outdated plugins. ### Mobile app support award -If you want your plugin to be awarded in the plugins directory and marked as supporting the mobile app, please feel encouraged to contact us via email at [mobile@moodle.com](mailto:mobile@moodle.com). - -Don't forget to include a link to your plugin page and the location of its code repository. - -See [the list of awarded plugins](https://moodle.org/plugins/?q=award:mobile-app) in the plugins directory. +In order to recognise plugin developers that have added mobile support, we give a Mobile app support award in the plugins directory. If you've developed a plugin and want to receive this recognition, please contact us at [mobile@moodle.com](mailto:mobile@moodle.com). You'll need to send us a link to your plugin page and the location of its source code. diff --git a/general/app/development/plugins-development-guide/troubleshooting.md b/general/app/development/plugins-development-guide/troubleshooting.md index 81ca991c84..45ad291bd1 100644 --- a/general/app/development/plugins-development-guide/troubleshooting.md +++ b/general/app/development/plugins-development-guide/troubleshooting.md @@ -1,111 +1,49 @@ --- title: Troubleshooting Moodle App Plugins Development sidebar_label: Troubleshooting -sidebar_position: 2 +sidebar_position: 3 tags: - Moodle App --- -## Invalid response received - -You might receive this error when using the `core-site-plugins-call-ws` directive or similar. By default, the app expects all Web Service calls to return an object, if your Web Service returns another type (string, boolean, etc.) then you need to specify it using the `preSets` attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this: - -```html ng2 -[preSets]="{typeExpected: 'boolean'}" -``` - -In a similar way, if your Web Service returns `null` you need to tell the app not to expect any result using `preSets`: - -```html ng2 -[preSets]="{responseExpected: false}" -``` +## Plugin changes are not picked up in the app -## Values of `ion-radio`, `ion-checkbox` or `ion-select` aren't sent to my WS +Remember to go through the list of tips in the [Seeing plugin changes in the app](./index.md#seeing-plugin-changes-in-the-app) section. -Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, `ion-radio`, `ion-checkbox` and `ion-select` don't use HTML inputs, they simulate them, so the directive isn't going to find their data and so it won't be sent to the Web Service. - -There are 2 workarounds to fix this problem. +## Invalid response received -### Sending the data manually +You might get this error when using the [core-site-plugins-call-ws](./api-reference.md#core-site-plugins-call-ws) directive or similar. -The first solution is to send the missing params manually using the `params` property. We will use `ngModel` to store the input value in a variable, and this variable will be passed to the parameters. Please notice that `ngModel` requires the element to have a name, so if you add `ngModel` to a certain element you need to add a name too. +By default, the app expects all Web Service calls to return an object. If your Web Service returns another type, you need to specify it using the `preSets` attribute in the directive. -For example, if you have a template like this: +For example, if your Web Service returns a boolean you should specify it like this: ```html ng2 - - - First value - - - - - - {{ 'plugin.mycomponent.save' | translate }} + + {{ 'plugin.local_sample.submit' | translate }} ``` -Then you should modify it like this: +Similarly, if the Web Service returns `null` you need to tell the app not to expect any result using `preSets`: ```html ng2 - - - First value - - - - - - {{ 'plugin.mycomponent.save' | translate }} + + {{ 'plugin.local_sample.submit' | translate }} ``` -Basically, you need to add `ngModel` to the affected element (in this case, the `radio-group`). You can put whatever name you want as the value, we used "responses". With this, every time the user selects a radio button the value will be stored in a variable called "responses". Then, in the button we are passing this variable to the parameters of the Web Service. - -Please notice that the `form` attribute has priority over `params`, so if you have an input with `name="responses"` it will override what you're manually passing to `params`. - -### Using a hidden input - -Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use `ngModel` to synchronise your radio/checkbox/select with the new hidden input. Please notice that `ngModel` requires the element to have a name, so if you add `ngModel` to a certain element you need to add a name too. - -For example, if you have a radio button like this: - -```html ng2 -
- - First value - - -
-``` - -Then you should modify it like this: - -```html ng2 -
- - First value - - - - -
-``` - -In the example above, we're using a variable called "responses" to synchronise the data between the `radio-group` and the hidden input. You can use whatever name you want. - ## I can't return an object or array in `otherdata` -If you try to return an object or an array in any field inside `otherdata`, the Web Service call will fail with the following error: +If you try to return an object or an array in any field inside `otherdata` in [content responses](./api-reference.md#content-responses), the Web Service call will fail with the following error: ```text Scalar type expected, array or object received ``` -Each field in `otherdata` must be a string, number or boolean; it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string: +Each field in `otherdata` must be a string, number or boolean; it cannot be an object or array. If you need to send complex values, you can use `json_encode`: ```php 'otherdata' => ['data' => json_encode($data)], ``` -The app will automatically parse this JSON and convert it back into an array or object. +The app will parse the string and it will be available as an array or object. diff --git a/general/app/development/scripts/gulp-push.md b/general/app/development/scripts/gulp-push.md deleted file mode 100644 index 98d1775468..0000000000 --- a/general/app/development/scripts/gulp-push.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: 'Moodle App Scripts: gulp push' -sidebar_label: 'gulp push' -tags: - - Moodle App - - Tools ---- - -The `gulp push` command automatically pushes a branch to a git remote and then updates the corresponding Moodle Tracker (Jira) issue with the diff URL or a patch file, similar to `mdk push -t`. This script was developed using [mdk](https://github.com/FMCorz/mdk) as an example. It's meant to be used for `MOBILE` issues, so it will only update the "main" fields in the tracker. - -To run it, just go to the root of the project and run: - -```bash -gulp push -``` - -By default, running `gulp push` without any parameter will push the **current branch** to the **origin** remote. Then it will guess the issue number based on the branch name and it will update the tracker issue to include the following fields: - -- If it's a security issue, it will upload a patch file. -- Otherwise it will update the fields: "Pull from Repository", "Pull Main Branch", and "Pull Main Diff URL". - -## Parameters - -All the parameters must be passed preceded by `--`. For example: - -```bash -gulp push --branch MOBILE-1234 --remote upstream --force -``` - -- `branch` — To specify the branch you want to push. By default: current branch. -- `remote` — To specify the remote where you want to push your branch. By default: origin. -- `force` — To force the push of changes to the git remote. By default: false. -- `patch` — To upload a patch file instead of a diff URL. If the issue you're pushing is a security issue, this setting will be forced to true. By default: false. - -## Moodle Tracker data - -The script needs the following data to be able to update the tracker: tracker URL, username, and password. - -First the script will try to read the URL and password from the [config file](#config-file). If the file doesn't exist or it lacks any of those fields, it will check if `mdk` is installed and configured. If it is, then the script will use the same tracker URL and username as `mdk`. - -If none of those conditions are fulfilled, then the script will ask the user to input the URL and username and it will store them in the config file. - -We use the [`node-keytar`](https://github.com/atom/node-keytar) library to manage the password. This library uses `Keychain` on macOS, Secret Service API/`libsecret` on Linux, and Credential Vault on Windows. We use the same key as `mdk` to store and retrieve the tracker password, so if you already use `mdk` this script will automatically get the password (it will probably ask your root/admin password in the device to be able to access it). - -## Config file - -The script will use a file named `.moodleapp-dev-config` to store some configuration data in JSON format. You can also create or edit that file to configure the script's behaviour. These are the fields it accepts: - -- `upstreamRemote` — The upstream where to push the branch if the remote param isn't supplied. By default: origin. -- `tracker.url` — URL of the tracker to update. By default: [https://tracker.moodle.org/](https://tracker.moodle.org/). -- `tracker.username` — Username to use in the tracker. -- `tracker.fieldnames.repositoryurl` — Name of the tracker field where to put the repository URL. By default: "Pull from Repository". -- `tracker.fieldnames.branch` — Name of the tracker field where to put the branch name. By default: "Pull Main Branch". -- `tracker.fieldnames.diffurl` — Name of the tracker field where to put the diff URL. By default: "Pull Main Diff URL". -- `wording.branchRegex` — Regex to use to identify the issue number based on the branch name. By default: `(MOBILE)[-\_](\[0-9]+)`. If you want to use the script to handle issues that aren't `MOBILE` you'll need to update this field. For example, if you work on 2 projects: `(MOBILE|MYPROJECT)[-\_](\[0-9]+)`. -- `{PROJECTNAME}.repositoryUrl` — To specify the git URL where to push changes for a certain project (`{PROJECTNAME}` is the name of the project). This can be used if you work on different projects and you want to push changes to different remotes depending on the project. For example: `MOBILE.repositoryUrl: https://github.com/moodlehq/moodleapp`. -- `{PROJECTNAME}.diffUrlTemplate` — To specify the diff URL template to use for a certain project (`{PROJECTNAME}` is the name of the project). By default: `remoteUrl + '/compare/%headcommit%...%branch%'`. diff --git a/general/app/development/scripts/index.md b/general/app/development/scripts/index.md deleted file mode 100644 index d218b8af2f..0000000000 --- a/general/app/development/scripts/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Scripts -sidebar_position: 7 ---- - - - -import DocCardList from '@theme/DocCardList'; - - diff --git a/general/app/development/setup/app-in-browser.md b/general/app/development/setup/app-in-browser.md index 3568e83b3d..d72ea6aa0f 100644 --- a/general/app/development/setup/app-in-browser.md +++ b/general/app/development/setup/app-in-browser.md @@ -5,13 +5,10 @@ sidebar_position: 1 tags: - Moodle App --- -Browsers are not officially supported by the application, but you can use a **Chromium-based** browser older than version 119 for development if you don't need any native functionality. -:::note Notice -Please notice that it has to be a Chromium-based browser older than version 119 because the application relies on the [Web SQL Database API](https://caniuse.com/?search=websql) which isn't supported in most browsers. This is a non-standard API, but that's not a problem because this is only used in the browser. Running on a native environment, the application relies on native APIs that are well supported. +Browsers are not officially supported by the application in production, but you can use a **Chromium-based** browser for development if you don't need any native functionality. -This requirement may be dropped in future versions of the app: [MOBILE-4304](https://tracker.moodle.org/browse/MOBILE-4304) -::: +The Chromium version needs to be 102 or newer, because the app's use of the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle) during development. ## Differences between Chromium and Google Chrome @@ -24,32 +21,32 @@ Main advantages: - Faster development. - DOM inspector. - Network monitor. -- Database inspector. - Emulation options. Disadvantages: - You can't use native functionality. +- SCORM and H5P aren't supported in browsers. - If you need to use Cordova plugins, you will probably need to provide a way to emulate those APIs in the browser or avoid calling them in the browser environment. - You will always need to test in a native device prior to a production release. - You will need to verify that your CSS/layout looks the same in native devices. ## Installation and configuration -You can install the Chromium browser by downloading it from [the official download page](https://www.chromium.org/getting-involved/download-chromium) (make sure to install a version older than 119). +You can install the Chromium browser by downloading it from [the official download page](https://www.chromium.org/getting-involved/download-chromium). In order to run the Moodle App, we recommend that you launch the browser with a couple of arguments. These are necessary to disable some of the limitations that don't exist in the native application, and also prepare the development environment. ```bash title="Linux" -chromium-browser --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --user-data-dir=~/.chromium-dev-data +chromium-browser --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --ignore-certificate-errors --disable-infobars --user-data-dir=~/.chromium-dev-data ``` ```bash title="Windows" -start chrome.exe --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --user-data-dir=~/.chromium-dev-data +start chrome.exe --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --ignore-certificate-errors --disable-infobars --user-data-dir=~/.chromium-dev-data ``` ```bash title="MacOS" -open -a /Applications/Chromium.app --args --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --user-data-dir=~/.chromium-dev-data +open -a /Applications/Chromium.app --args --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --ignore-certificate-errors --disable-infobars --user-data-dir=~/.chromium-dev-data ``` If you are using other operative system, check out [how to run chromium with flags](https://www.chromium.org/developers/how-tos/run-chromium-with-flags) in other environments. @@ -64,7 +61,7 @@ We strongly recommend that you create a new shortcut and use it only for working In Linux, you can create such a shortcut by writing a script that is globally available. For example, you can create the following file in `/usr/local/bin/unsafe-chromium`: ```bash -chromium-browser --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --user-data-dir=/home/{username}/.chromium-dev-data $@ +chromium-browser --allow-file-access-from-files --disable-web-security --disable-site-isolation-trials --allow-running-insecure-content --no-referrers --unlimited-storage --auto-open-devtools-for-tabs --ignore-certificate-errors --disable-infobars --user-data-dir=/home/{username}/.chromium-dev-data $@ ``` Notice that this time we shouldn't use `~/.chromium-dev-data` to describe the user data dir. That's because this file can be called from a different shell, and `~` could not be interpreted properly (this may end up creating a folder called "~" in your project folder, and you probably don't want that). @@ -74,27 +71,50 @@ Also, remember to make this file executable by running `sudo chmod +x /usr/local For convenience, you can also define an application launch that calls this script. :::note Help wanted! -These instructions have only been tested in Linux. If you are using a different operative system, [let us know](https://github.com/moodle/devdocs/issues/76) how it went (or just [edit this page](https://github.com/moodle/devdocs/edit/main/docs/moodleapp/development/setup/app-in-browser.md)!). +These instructions have only been tested in Linux. If you are using a different operative system, [let us know](https://github.com/moodle/devdocs/issues/76) how it went (or just [edit this page](https://github.com/moodle/devdocs/edit/main/general/app/development/setup/app-in-browser.md)!). ::: ### Configuring the default browser -When you launch the application by running `npm start`, this will open a tab on your default browser. You can close this tab and open the url with your development browser, but if you want to do it automatically you can override the default browser by setting the `MOODLE_APP_BROWSER` environment variable. +When you launch the application by running `npm start`, this will open a tab in your default browser. You can close this tab and open the url with your development browser, but if you want to do it automatically you can override the default browser by setting the `MOODLE_APP_BROWSER` environment variable. For example, if you have created a shortcut like we mentioned in the previous section, you can just add the following to your `~/.bashrc` file: ```bash -export MOODLE_APP_BROWSER=unsafe-chromium +export MOODLE_APP_BROWSER=/usr/local/bin/unsafe-chromium ``` +:::caution Use absolute paths +Make sure to set this variable to an absolute path, and not just the name of the binary. Even if the program is loaded in the global PATH, it will not work unless it's an absolute path. +::: + :::note Help wanted! -These instructions have only been tested in Linux. If you are using a different operative system, [let us know](https://github.com/moodle/devdocs/issues/76) how it went (or just [edit this page](https://github.com/moodle/devdocs/edit/main/docs/moodleapp/development/setup/app-in-browser.md)!). +These instructions have only been tested in Linux. If you are using a different operative system, [let us know](https://github.com/moodle/devdocs/issues/76) how it went (or just [edit this page](https://github.com/moodle/devdocs/edit/main/general/app/development/setup/app-in-browser.md)!). ::: +## Handling deep links + +Using a browser, you'll realize it's not possible to handle deep links, for example if you're trying to log in using SSO. + +To work around that, you can simulate a deep link being pressed running the following code in the console: + +```js +handleOpenURL('moodlemobile://token=...'); +``` + ## Using the hosted versions of the app You can access your site using the hosted versions of the app in [latest.apps.moodledemo.net](https://latest.apps.moodledemo.net) (the latest stable version) and [main.apps.moodledemo.net](https://main.apps.moodledemo.net) (the current version in development). +### Special headers + +The application also relies on the [SharedArrayBuffer API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer), so in case you're hosting the app yourself you'll need to return the following headers: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + ## Tips & tricks Once you have everything set up, you should be able to develop like you would with any other front-end application. You can learn about the development tools you have available by reading the [Chrome DevTools documentation](https://developer.chrome.com/devtools/index). @@ -106,4 +126,4 @@ Here's some things we find useful to work with the Moodle App in particular: - [Console Panel](https://developer.chrome.com/docs/devtools/console/) — This panel is essential for any developer, since it will show you any errors or custom messages that you've written. You can also use the search box to filter messages seeing everything is too overwhelming. You will also see specific logs from the Moodle App, but keep in mind that they are not used in production environment. If you are not running the application yourself, you can inspect the environment by opening the /assets/env.json url. - [Elements Panel](https://developer.chrome.com/docs/devtools/dom/) — This panel is also essential for any developer, you'll be able to inspect and modify the HTML that is actually being rendered. - [Network Panel](https://developer.chrome.com/docs/devtools/network/) — This panel can be useful if you are trying to see how the Moodle App communicates with a Moodle site. You may also want to [disable the cache](https://developer.chrome.com/docs/devtools/network/reference/#disable-cache) in order to have the same behaviour after each reload. However, keep in mind that this only disables the browser cache, any Web Service calls that are cached by the Moodle App will remain cached. You can learn more about network requests in the [Debugging network requests in the Moodle App](../network-debug.md) page. -- [WebSQL Inspector](https://developer.chrome.com/docs/devtools/storage/websql/) — As mentioned before, WebSQL is a non-standard API. But since the Moodle App uses it for development, this inspector may come in handy. Keep in mind that the native application does not use WebSQL, so it is possible that you see some different behaviour in a native device. But it should be reliable for the most part. +- [OPFS Explorer](https://chromewebstore.google.com/detail/opfs-explorer/acndjpgkpaclldomagafnognkcgjignd) — This browser extension can be used to view the `.sqlite` files of the database. If you download these, you can open them in any SQLite database inspector, for example [sqliteviewer.app](https://sqliteviewer.app/). diff --git a/general/app/development/setup/docker-images.md b/general/app/development/setup/docker-images.md index 5d150ef627..e3d8f8a836 100644 --- a/general/app/development/setup/docker-images.md +++ b/general/app/development/setup/docker-images.md @@ -16,21 +16,21 @@ In order to run them, you should have [Docker](https://www.docker.com/) installe You can run the latest stable version of the application using the following command: ```bash -docker run --rm -p 8100:80 moodlehq/moodleapp +docker run --rm -p 8100:443 moodlehq/moodleapp ``` -This will launch the container running the application and expose it locally on your port **8100**. You should be able to open it on `http://localhost:8100`. +This will launch the container running the application and expose it locally on your port **8100**. You should be able to open it on `https://localhost:8100`. If you want to use a specific version, you can do it using the tag with the release number: ```bash -docker run --rm -p 8100:80 moodlehq/moodleapp:4.0.0 +docker run --rm -p 8100:443 moodlehq/moodleapp:4.4.0 ``` You can also use the development version using the `next` tag: ```bash -docker run --rm -p 8100:80 moodlehq/moodleapp:next +docker run --rm -p 8100:443 moodlehq/moodleapp:next ``` ## Using a specific environment @@ -40,28 +40,24 @@ By default, the application will be launched on a __production__ environment. If You can use images on different environments by adding their short name as a suffix. The available environments are __production__ (no suffix), __development__ (`-dev` suffix) and __testing__ (`-test` suffix): ```bash title="Using the latest stable version" -docker run --rm -p 8100:80 moodlehq/moodleapp:latest-test -docker run --rm -p 8100:80 moodlehq/moodleapp:latest-dev +docker run --rm -p 8100:443 moodlehq/moodleapp:latest-test +docker run --rm -p 8100:443 moodlehq/moodleapp:latest-dev ``` ```bash title="Using a specific version" -docker run --rm -p 8100:80 moodlehq/moodleapp:4.0.0-test -docker run --rm -p 8100:80 moodlehq/moodleapp:4.0.0-dev +docker run --rm -p 8100:443 moodlehq/moodleapp:4.0.0-test +docker run --rm -p 8100:443 moodlehq/moodleapp:4.0.0-dev ``` ```bash title="Using the latest development version" -docker run --rm -p 8100:80 moodlehq/moodleapp:next-test -docker run --rm -p 8100:80 moodlehq/moodleapp:next-dev +docker run --rm -p 8100:443 moodlehq/moodleapp:next-test +docker run --rm -p 8100:443 moodlehq/moodleapp:next-dev ``` ## Using old versions -Before version 3.9.5, images didn't support specifying the environment and they were always run as **development**. You will also notice that they take a while to launch and they are exposed in port 8100 instead, that's because these images contained the source code and they were run using Angular's development server. This has been improved since and images are easier to work with. - -If you want to use an old version, you can run the following command: +Before version 4.4.0, images didn't run on a secure context, so you'd need to access them on `http://localhost:8100` and expose port `80` instead: ```bash -docker run --rm -p 8100:8100 moodlehq/moodleapp:3.6.0 +docker run --rm -p 8100:80 moodlehq/moodleapp:4.3.0 ``` - -Tagged releases are only available from version 3.6.0 onwards. diff --git a/general/app/development/setup/index.md b/general/app/development/setup/index.md index 58f1b81c2e..981a3a2884 100644 --- a/general/app/development/setup/index.md +++ b/general/app/development/setup/index.md @@ -59,22 +59,6 @@ pod setup Please note that for compiling the app in Mac you need to open the `Moodle.xcworkspace` file, more information here: [MOBILE-1970](https://tracker.moodle.org/browse/MOBILE-1970). -### Linux only: `libsecret` - -If you are using [the gulp push script](./scripts/gulp-push), you need to have `libsecret` installed before running `npm install`. Depending on your distribution, you will need to run one of the following commands: - -```bash title="Debian/Ubuntu" -sudo apt-get install libsecret-1-dev -``` - -```bash title="Red Hat" -sudo yum install libsecret-devel -``` - -```bash title="Arch Linux" -sudo pacman -S libsecret -``` - ## Running the app in a browser You can obtain a copy of the source code by cloning the public repository: @@ -130,36 +114,6 @@ Running `npm start`, `npm run dev:android` or `npm run dev:ios` compiles using J The `npm run prod:android` and `npm run prod:ios` commands use AOT compilation because they generate production bundles. -### Using Android emulators - -Most of the time, you should be using an emulator running recent versions of Android, and it should work fine. But sometimes, you may want to use an older version to test a specific behaviour. - -If you want to run the application in an Android 5 emulator, you'll need to upgrade the system webview because emulators come with version 37 preinstalled. Your first idea may be to upgrade the webview using the Google Play store, but it will not work because the webview served by Google Play is `com.google.android.webview` whilst the system webview used in emulators is `com.android.webview`. You can do the following instead. - -Once you have [created your Android 5 virtual device](https://developer.android.com/studio/run/managing-avds), you'll need to do download [the apk for Webview 61](https://android.googlesource.com/platform/external/chromium-webview/+/refs/heads/oreo-m3-release/prebuilt/x86_64/) and run the following commands: - -```bash -# Open the folder where the "emulator" script is installed -cd $(dirname `which emulator`) - -# Boot the emulator in write mode (you can get a list of device names running "emulator -list-avds") -emulator @DeviceName -writable-system - -# In a different shell, make /system writable -adb remount - -# Uninstall the webview app manually and reboot the device -adb shell -rm -rf /data/data/com.android.webview -rm -rf /system/app/webview -reboot - -# Install the new version -adb install webview.apk -``` - -After doing this, remember to run the emulator in write mode for subsequent sessions, but you don't need to call the `remount` command every time. - ## See also - [Moodle App Coding Style](../../../development/policies/codingstyle-moodleapp.md) diff --git a/general/app/development/setup/troubleshooting.md b/general/app/development/setup/troubleshooting.md index f6a51279c1..2c7d2c0305 100644 --- a/general/app/development/setup/troubleshooting.md +++ b/general/app/development/setup/troubleshooting.md @@ -14,8 +14,10 @@ If you are stuck with an error and you can't find a way to continue, here's a li - Using git, look at the changes you have in your working directory and make sure that they aren't causing the problem. Be specially careful with changes in `package.json` and `package-lock.json`. You can see a list of the files you have modified running `git status`. - Make sure that you are using the proper node and npm versions. You can see it looking at the `engines` key in `package.json`. If you are using [nvm](https://github.com/nvm-sh/nvm), just run `nvm install`. -- Make sure that all dependencies have been installed properly. To be extra sure, run `npm ci`; this will remove the `node_modules/` folder and install all dependencies again exactly as described in your `package-lock.json`. -- If you are having issues trying to build for Android or iOS, try removing the `www/`, `platforms/` and `plugins/` folders and try again. +- Make sure that all dependencies have been installed properly. To be extra sure, run `npm ci`; this will remove the `node_modules/` folder and install all dependencies again exactly as described in your `package-lock.json`. You may want to delete `cordova-plugin-moodleapp/node_modules` as well. +- Clear Angular's cache by removing the `.angular` folder. +- If you are having issues trying to build for Android or iOS, try removing the `www/`, `platforms/`, and `plugins/` folders and try again. +- Make sure to use the scripts configured in the app repository, such as `npm start` and `npm run dev:android`. The Ionic, Angular, and Cordova CLIs have many commands you could be using, such as `ionic serve`, `cordova run android`, `ng lint`, etc. We don't recommend using them because they haven't been tested as thoroughly. If you end up using them anyways, make sure to use them through `npx`, such as `npx ionic serve`. This will ensure that you're using the proper version of the CLIs instead of relying on a globally installed dependency. - If you are using a development version, maybe the repository is broken and it's not your fault. Try checking out the `latest` branch and see if you're getting the same error. - Try cloning the repository in a new folder and run through the instructions in this page again. If you can, try doing it on a different computer to make sure that you're doing everything properly and it's not a problem in your machine. - Try creating [a blank Ionic application](https://ionicframework.com/docs/cli/commands/start) and see if you're having the same problems. Make sure that you are using the same version of the main dependencies (Angular, Cordova, Ionic CLI, etc.). @@ -35,7 +37,7 @@ To get more debug output from npm commands, see [the available configuration fla ## I can't change the language -If you're getting a network error for a url like `http://localhost:8100/assets/lang/es.json`, this probably means that you haven't installed the language packs. You can install them with `npm run lang:update-langpacks`. +If you're getting a network error for a url like `https://localhost:8100/assets/lang/es.json`, this probably means that you haven't installed the language packs. You can install them with `npm run lang:update-langpacks`. ## Error: `libsass` bindings not found. Try reinstalling node-sass? @@ -222,7 +224,3 @@ There will be a pause (a few minutes) while building everything. It should finis ` Then you can access it by running Chrome and connecting to localhost:8100. - -## `window.openDatabase` is not a function - -This error appears in browsers that don't support the [WebSQL](https://caniuse.com/?search=websql) API. We suggest using a [Chromium-based browser older than version 119](./app-in-browser.md). diff --git a/general/app/development/testing/acceptance-testing.md b/general/app/development/testing/acceptance-testing.md index c897cb0e71..e5d6f59cf7 100644 --- a/general/app/development/testing/acceptance-testing.md +++ b/general/app/development/testing/acceptance-testing.md @@ -1,6 +1,7 @@ --- title: Acceptance testing for the Moodle App sidebar_label: Acceptance testing +sidebar_position: 2 tags: - Quality Assurance - Testing @@ -23,7 +24,7 @@ The main advantages of this approach are: In order to run tests for the app, you will need to run both a Moodle site and the Moodle App. -The Moodle site should be version 3.9.7+, 3.10.4+ or newer (3.11, 4.0, etc.). You also need to install the [`local_moodleappbehat`](https://github.com/moodlehq/moodle-local_moodleappbehat/) plugin, using the version that corresponds with the version of the Moodle App that you're testing on. If you have tests for an older version, you can read [How to upgrade tests from an older version](../../upgrading/acceptance-tests-upgrade-guide.md). +You need to install the [`local_moodleappbehat`](https://github.com/moodlehq/moodle-local_moodleappbehat/) plugin, using the version that corresponds with the version of the Moodle App that you're testing on. If you have tests for an older version, you can read [How to upgrade tests from an older version](../../upgrading/acceptance-tests-upgrade-guide.md). We recommend that you use [moodle-docker](https://github.com/moodlehq/moodle-docker#use-containers-for-running-behat-tests-for-the-mobile-app), because it's configured to run mobile tests and you can skip reading this entire section. You won't even need to clone the app repository. @@ -48,13 +49,34 @@ However you set up the environment, if you change the version of the app you'll In order to enable app testing, you need to add the following configuration to your site's `config.php` file: ```php -$CFG->behat_ionic_wwwroot = 'http://localhost:8100'; +$CFG->behat_ionic_wwwroot = 'https://localhost:8100'; ``` The url you use here must be reachable by your Moodle site, and the application needs to be served at this url when running tests and also when you initialise the Behat environment. The Moodle App [only works in Chromium-based browsers](../setup/app-in-browser), so mobile tests will be ignored if you are using any other browser. You can learn how to configure the browser used in your tests in the [Running acceptance test](../../../development/tools/behat/running.md) page. +Additionally, the app must run in a secure context and will issue local certificates during development. This aren't usually trusted by browsers out of the box, so you'll need to disable some security capabilities to make it work: + +```php +$CFG->behat_profiles = [ + 'default' => [ + 'browser' => 'chrome', // Make sure it's version 102 or newer. + 'wd_host' => 'http://localhost:4444/wd/hub', + 'capabilities' => [ + 'extra_capabilities' => [ + 'chromeOptions' => [ + 'args' => [ + '--ignore-certificate-errors', + '--allow-running-insecure-content', + ], + ], + ], + ], + ], +]; +``` + If everything is configured properly, you should see "Configured app tests for version X.X.X" after running `admin/tool/behat/cli/init.php`. ## Running Behat @@ -328,7 +350,7 @@ While the test is paused, you can also carry out some of the app Behat steps man Here are some examples: -```javascript +```js // I set the field "Password" to "student2" in the app behat.setField('Password', 'student2'); @@ -366,17 +388,15 @@ Learn more about it in the [plugin documentation](https://github.com/NoelDeMarti If you are stuck with an error and you can't find a way to continue, here's a list of things you can do: -- Make sure you added `$CFG->behat_ionic_wwwroot = "http://localhost:8100";` (or equivalent) to your `config.php` file, and that url is reachable from the host where your Moodle site is running. +- Make sure you added `$CFG->behat_ionic_wwwroot = "https://localhost:8100";` (or equivalent) to your `config.php` file, and that url is reachable from the host where your Moodle site is running. - Remember when you need to re-run `admin/tool/behat/cli/init.php`, and make sure that you see "Configured app tests for version X.X.X". When in doubt, just run it again; it may fix your problem. - It is possible that your tests break if you're using an unstable version of the app. Try to use stable versions using the `latest` branch if you're working with the source code or tagged releases if you're using Docker. - Mobile Behat tests don't work well with XDebug, so if you're using it, turn it off in `php.ini` while running the tests. Also, remember to restart Apache if necessary. -### Unable to load app version from http://moodleapp:8100/config.json +### Unable to load app version from https://localhost:8100/assets/env.json This message appears when the Moodle site is not able to reach the app. Make sure that the url is available from the host you're running the Behat commands from. Also make sure that the app is actually running at the specified url. -It's ok if the actual `/config.json` url doesn't work, that's actually a remnant from legacy code. The url that Moodle is actually looking for is `/assets/env.json`. - ### The plugins required by this course could not be loaded correctly... This means either some activity on the course is not adapted to support the moodle app or there is a timeout in the request to your behat site. diff --git a/general/app/development/testing/continuous-integration.md b/general/app/development/testing/continuous-integration.md new file mode 100644 index 0000000000..0ee1effde4 --- /dev/null +++ b/general/app/development/testing/continuous-integration.md @@ -0,0 +1,40 @@ +--- +title: Continuous Integration for the Moodle App +sidebar_label: Continuous Integration +sidebar_position: 3 +tags: + - Quality Assurance + - Testing + - CI + - Continuous Integration + - Behat + - Moodle App +--- + +Tests can be useful during development, but a good test suite really shines with CI processes in place. Running tests and other checks on a regular basis can be helpful to maintain good quality assurance, and catch regressions as early as possible. + +In this page we'll discuss some of the techniques and tools you can use to test any code impacting the Moodle App. + +## Plugin tests + +For most use-cases, you should be able to use [moodle-plugin-ci](https://github.com/moodlehq/moodle-plugin-ci). There is already some extensive documentation in the official docs, and in particular you should take a look in the [Moodle App section](https://moodlehq.github.io/moodle-plugin-ci/MoodleApp.html). + +In practice, you'll most likely be able to use it out of the box by setting `MOODLE_APP=true`, and using one of the templates for [Github Actions](https://moodlehq.github.io/moodle-plugin-ci/GHAFileExplained.html) or [Travis](https://moodlehq.github.io/moodle-plugin-ci/TravisFileExplained.html). + +## Core tests + +For any new feature going into Moodle, the entire suite of app tests is run to make sure that nothing is broken. You can find the suite used for these purposes in the `ci` branch of the [local_moodleappbehat](https://github.com/moodlehq/moodle-local_moodleappbehat) plugin repository. These tests will always be run against the `latest` version of the app, which is the most recent release and should help us catch any regressions before it's too late. This happens in the Jenkins instance hosted in `ci.moodle.org` (which uses [moodle-ci-runner](https://github.com/moodlehq/moodle-ci-runner) under the hood, but you probably won't need to interact with this in any way). + +Even though the tests are mirrored into the `local_moodleappbehat` repository, their source is managed in the main Moodle App repository. These tests are also run every time anything changes in the app codebase, using Github Actions. The workflow configuration can be found in [acceptance.yml](https://github.com/moodlehq/moodleapp/blob/main/.github/workflows/acceptance.yml). + +### What can I do if my changes in core are making the app tests fail? + +You can look at the failure logs, and hopefully that will give you enough hints to understand what's happening. For more details, you can also check the Build Artifacts to find screenshots of the application when the tests failed. + +If that's not clear enough, try to use the app against your local instance to see if you can reproduce the problem (you can use the [development webapps](../network-debug.md#using-a-browser) in your computer). You can also run the app tests in your development machine [following the documentation](./acceptance-testing) (make sure to use the `ci` branch of the `local_moodleappbehat` plugin to include the tests). + +Also, make sure your moodle code is up to date with the latest changes. The application tests are working against the latest development version of the LMS, so if your fork is using an old version it's possible that the problem has been fixed upstream. + +If you're still unable to find why the tests are failing, you can reach out for help in the [developer chat](../../../../general/community/channels.md#developer-chat) or ask anyone from the [mobile developers](https://tracker.moodle.org/issues/?jql=assignee%20in%20(membersOf(mobile-developers))). + +Finally, if you've made sure that the application is working properly but the Behats tests need to change, also reach out the [mobile developers](https://tracker.moodle.org/issues/?jql=assignee%20in%20(membersOf(mobile-developers))). Tests can be skipped temporarily before an issue is integrated, and updated to conform with the new behaviour afterwards. diff --git a/general/app/development/testing/unit-testing.md b/general/app/development/testing/unit-testing.md index d2dbcc6d53..a640b6e52c 100644 --- a/general/app/development/testing/unit-testing.md +++ b/general/app/development/testing/unit-testing.md @@ -1,6 +1,7 @@ --- title: Unit testing for the Moodle App sidebar_label: Unit testing +sidebar_position: 1 tags: - Quality Assurance - Testing diff --git a/general/app/overview.md b/general/app/overview.md index b61b9a3796..55ed621117 100644 --- a/general/app/overview.md +++ b/general/app/overview.md @@ -67,7 +67,7 @@ When the application connects with a site, it will fetch information about which The Moodle App only works with Moodle sites running version 3.5 or newer. -The minimum platforms supported by the application are Android 5.1 (with Webview 61 or higher) and iOS 11. +The minimum platforms supported by the application are Android 7 (with Webview 79 or higher) and iOS 13. Browsers are not officially supported, but you can use a Chromium-based browser for development if you don't need any native functionality. However, there are [some caveats](./development/setup/app-in-browser.md) you should be aware of. diff --git a/general/app/upgrading/acceptance-tests-upgrade-guide.md b/general/app/upgrading/acceptance-tests-upgrade-guide.md index bd49561c91..d0fd48fbf8 100644 --- a/general/app/upgrading/acceptance-tests-upgrade-guide.md +++ b/general/app/upgrading/acceptance-tests-upgrade-guide.md @@ -11,6 +11,16 @@ In the following guide, you will learn how to upgrade your plugins' acceptance t Depending on which version of the app you're upgrading from, you'll need to go through multiple version upgrades. This guide is divided by version ranges, so you should be able to start with your current version and build up from there. +## 4.3 to 4.4 + +The application now needs to run in a secure context (https://). This change only affects your development environment, including acceptance tests, and it was necessary to [move on from the deprecated WebSQL API](https://tracker.moodle.org/browse/MOBILE-4304). Make sure to update the settings related to the app in the `config.php` of your Moodle's development environment. + +If you were using [moodle-docker](https://github.com/moodlehq/moodle-docker), you no longer need to use the `MOODLE_DOCKER_APP_RUNTIME` env variable; but if you do, you'll also need to change it to `ionic7`. + +## 4.2 to 4.3 + +There haven't been any relevant changes in this version, but make sure to run your tests against the latest version to check that they continue working properly. + ## 4.1 to 4.2 The default dimensions in app tests have changed from 360x720 to 500x720. If you weren't running tests in headless mode, this change won't affect you because 500 was already the minimum width for a non-headless Chrome instance. This change was made to have consistent behaviours when the tests are run in both modes. diff --git a/general/app/upgrading/plugins-upgrade-guide.md b/general/app/upgrading/plugins-upgrade-guide.md index d901451788..14ddbe72b7 100644 --- a/general/app/upgrading/plugins-upgrade-guide.md +++ b/general/app/upgrading/plugins-upgrade-guide.md @@ -18,11 +18,27 @@ Depending on which version of the app you're upgrading from, you'll need to go t Other than the changes outlined in this document, there may be smaller API changes that aren't highlighted here. Make sure to check the [upgrade.txt](https://github.com/moodlehq/moodleapp/blob/latest/upgrade.txt) file for an exhaustive list with all the changes. +## 4.3 to 4.4 + +Starting with this release, the changes listed in [upgrade.txt](https://github.com/moodlehq/moodleapp/blob/latest/upgrade.txt) will only document breaking changes for APIs exposed to site plugins. Internal changes will no longer be documented. Make sure to check out the file to learn about the changes in this version. + +Also, the Ionic version has been upgraded to v7 (from v5), make sure to check the relevant upgrade guides for [v6](https://ionicframework.com/docs/updating/6-0) and [v7](https://ionicframework.com/docs/updating/7-0). In particular, the syntax to declare input labels has been refactored. The legacy syntax will continue working for the time being, but we recommend migrating to the [modern syntax](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax) as soon as possible. + +The Angular version has also been upgraded to v17, and it comes with new features such as [a new syntax for conditionals and loops](https://angular.dev/essentials/conditionals-and-loops) and [signals](https://angular.dev/guide/signals). Signals are not available in the app yet, but most new features like the conditionals should work. In any case, always make sure to test your code with the latest version of the app before proceeding; and keep in mind that some of your users could still be using an old version of the app. So adopt these new features with caution. + +Finally, the application now needs to run in a secure context (https://). This change only affects your development environment, and it was necessary to [move on from the deprecated WebSQL API](https://tracker.moodle.org/browse/MOBILE-4304). + +## 4.2 to 4.3 + +Font Awesome icons have been updated to version 6.4.0, so make sure that all the icons you're using in your plugin are still supported. + +Other than that, there have been some changes in the APIs related to analytics. Check out [upgrade.txt](https://github.com/moodlehq/moodleapp/blob/latest/upgrade.txt) to learn about the specifics. + ## 4.1 to 4.2 Font Awesome icons have been updated to version 6.3.0, so make sure that all the icons you're using in your plugin are still supported. -Additionally, the `` component has been removed (it was deprecated in 3.9.5). If you were still using it, you should replace it with `` which now supports [using font icons](../development/plugins-development-guide#using-font-icons-with-ion-icon). +Additionally, the `` component has been removed (it was deprecated in 3.9.5). If you were still using it, you should replace it with `` which now supports [using font icons](../development/plugins-development-guide/api-reference.md#ion-icon). ## 4.0 to 4.1 @@ -136,7 +152,7 @@ Here's an example to create a subclass of `CoreContentLinksModuleIndexHandler`: -```javascript +```js function AddonModCertificateModuleLinkHandler() { that.CoreContentLinksModuleIndexHandler.call( this, @@ -152,7 +168,7 @@ AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentL AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler; ``` -```javascript +```js class AddonModCertificateModuleLinkHandler extends this.CoreContentLinksModuleIndexHandler { constructor() { @@ -170,17 +186,17 @@ class AddonModCertificateModuleLinkHandler extends this.CoreContentLinksModuleIn We've also done some changes to the code of the app. Most of these changes probably don't affect your plugin, but you should still check this out just in case: -- `` has been deprecated, please use `` instead that now supports Font Awesome icons. See [Using 'font' icons with ion-icon](../development/plugins-development-guide#using-font-icons-with-ion-icon) for more information. +- `` has been deprecated, please use `` which now supports [using font icons](../development/plugins-development-guide/api-reference.md#ion-icon). - To "cross out" an icon using `ion-icon` you need to use `class="icon-slash"` instead of `slash="true"`. - The function `syncOnSites` from `CoreSyncBaseProvider` now expects to receive a function with the parameters already bound: -```javascript -syncOnSites('events', this.syncAllEventsFunc.bind(this), [siteId); +```js +syncOnSites('events', this.syncAllEventsFunc.bind(this), [siteId]); ``` -```javascript +```js syncOnSites('events', this.syncAllEventsFunc.bind(this, force), siteId); ``` diff --git a/general/app/upgrading/remote-themes-upgrade-guide.md b/general/app/upgrading/remote-themes-upgrade-guide.md index 642c03735f..74d4d936cc 100644 --- a/general/app/upgrading/remote-themes-upgrade-guide.md +++ b/general/app/upgrading/remote-themes-upgrade-guide.md @@ -42,6 +42,14 @@ You can follow the same process that is documented in the [Moodle App Remote The Make sure to read it in order to understand how to style your application for newer versions of the app. If you're upgrading your styles, it is likely that the documentation has been updated since you read it. So we recommend taking a look even if you're already familiar with Remote Themes. +## 4.3 to 4.4 + +Ionic version has been upgraded to v7 (from v5). This shouldn't have any direct impact in remote themes; but make sure that they are still working properly. + +## 4.2 to 4.3 + +The only change to keep in mind for this release is that Font Awesome icons were upgraded to version 6.4.0. This shouldn't affect Remote Themes directly, but given that it affects the visuals aspects of the app, it could potentially be relevant. + ## 4.1 to 4.2 The only change to keep in mind for this release is that Font Awesome icons were upgraded to version 6.3.0. This shouldn't affect Remote Themes directly, but given that it affects the visuals aspects of the app, it could potentially be relevant. diff --git a/general/app_releases.md b/general/app_releases.md index 2ea27de7a2..faf51919eb 100644 --- a/general/app_releases.md +++ b/general/app_releases.md @@ -10,6 +10,7 @@ tags: | **Version name** | **Date** | |---|---| +| [Moodle App 4.4.0](./app_releases/v4/v4.4.0) | 28 June 2024 | | [Moodle App 4.3.0](./app_releases/v4/v4.3.0) | 10 November 2023 | | [Moodle App 4.2.0](./app_releases/v4/v4.2.0) | 9 June 2023 | | [Moodle App 4.1.1](./app_releases/v4/v4.1.1) | 28 February 2023 | @@ -18,6 +19,9 @@ tags: | [Moodle App 4.0.1](./app_releases/v4/v4.0.1) | 26 May 2022 | | [Moodle App 4.0.0](./app_releases/v4/v4.0.0) | 22 April 2022 | +- From 4.0.0 to 4.3.0 it was based on Ionic 5 version. +- On 4.4.0 the app was based on Ionic 7 version. + ## Moodle App 3.x Version 3 has been aligned with the Moodle LMS deployments. diff --git a/general/app_releases/v4/v4.4.0.md b/general/app_releases/v4/v4.4.0.md new file mode 100644 index 0000000000..87003cd959 --- /dev/null +++ b/general/app_releases/v4/v4.4.0.md @@ -0,0 +1,129 @@ +--- +title: Moodle App 4.4.0 release notes +sidebar_label: Moodle App 4.4.0 +tags: + - Moodle App + - Release notes +--- + +Release date: 28 June 2024 + +## New features and improvements + +- Blog entries edition support +- Sites policies can be now we accepted via the app +- New privacy and policies section in the user menu +- Users avatars do not show a default image anymore +- Support for partial grades in quiz +- Improved the visualisation of attempts in quiz +- Support for the new ordering question type +- Course activity icons redesign +- Integration with external communication tools +- Improvements in the visualisation of videos in different formats +- Accessibility improvements + +## New requirements + +- Minimum Android Version: Updated to 7 (from 5.1) +- Minimum iOS Version: Updated to 13 (from 11); although it will be only tested in versions iOS 14 onward +- Note: Android 5 and 6 users can still use version 4.3 but won't receive new updates. + +## For developers + +- The app now uses Ionic 7 and Angular 17 +- Custom language strings using the old format (those starting with mm. or MMA) won't be supported any more. The new string identifiers can be found [here](https://latest.apps.moodledemo.net/assets/lang/en.json) + +## Complete list of issues + +### Task + +- [MOBILE-3947](https://tracker.moodle.org/browse/MOBILE-3947) - Upgrade to Ionic 7 and to Angular 17 +- [MOBILE-4357](https://tracker.moodle.org/browse/MOBILE-4357) - Upgrade Cordova and Android SDK to 34, cordova-android to 12 and cordova-ios to 7 +- [MOBILE-4449](https://tracker.moodle.org/browse/MOBILE-4449) - Use Android photo picker to avoid using READ_MEDIA_IMAGES and READ_MEDIA_VIDEO +- [MOBILE-4465](https://tracker.moodle.org/browse/MOBILE-4465) - Remove deprecated 4.0 code +- [MOBILE-4492](https://tracker.moodle.org/browse/MOBILE-4492) - Upgrade cordova-plugin-file to 8.0.1, cordova-plugin-media-capture and use cordova-plugin-camera + +### New feature + +- [MOBILE-4219](https://tracker.moodle.org/browse/MOBILE-4219) - Create, delete and update blog entries +- [MOBILE-4329](https://tracker.moodle.org/browse/MOBILE-4329) - Add "Privacy and policies" section in the app to comply with app stores policies (delete account) +- [MOBILE-4457](https://tracker.moodle.org/browse/MOBILE-4457) - Add the Ordering question type (qtype_ordering) into the core Moodle code +- [MOBILE-4460](https://tracker.moodle.org/browse/MOBILE-4460) - Prevent links to the Moodle site (outlinks) to be displayed on the app +- [MOBILE-4463](https://tracker.moodle.org/browse/MOBILE-4463) - Implement option in app settings to clear cache +- [MOBILE-4573](https://tracker.moodle.org/browse/MOBILE-4573) - Forum search icon shouldn't appear with disabled search +- [MOBILE-4577](https://tracker.moodle.org/browse/MOBILE-4577) - Missing link handler for new section page + +### Improvement + +- [MOBILE-2823](https://tracker.moodle.org/browse/MOBILE-2823) - Remove coreToLocaleString Pipe +- [MOBILE-3403](https://tracker.moodle.org/browse/MOBILE-3403) - Avoid rendering external assets before core-external-content is applied +- [MOBILE-3862](https://tracker.moodle.org/browse/MOBILE-3862) - Improve mobile plugins documentation +- [MOBILE-4173](https://tracker.moodle.org/browse/MOBILE-4173) - iframe containing a pdf doesn't display in moodle mobile app +- [MOBILE-4243](https://tracker.moodle.org/browse/MOBILE-4243) - Communication tools integration (matrix link in mobile app) +- [MOBILE-4266](https://tracker.moodle.org/browse/MOBILE-4266) - Add theme class to the html +- [MOBILE-4268](https://tracker.moodle.org/browse/MOBILE-4268) - Design System - Error Accordion component +- [MOBILE-4272](https://tracker.moodle.org/browse/MOBILE-4272) - Decouple workshop code from initial bundle +- [MOBILE-4313](https://tracker.moodle.org/browse/MOBILE-4313) - Check Android notification and reminder settings, and let user enable them if disabled +- [MOBILE-4339](https://tracker.moodle.org/browse/MOBILE-4339) - Add a warning if quiz is being submitted with unanswered questions (follow-up of MDL-74996) +- [MOBILE-4446](https://tracker.moodle.org/browse/MOBILE-4446) - Add log assertions in Behat +- [MOBILE-4451](https://tracker.moodle.org/browse/MOBILE-4451) - Improve debugging tracker for tracking unexpected errors +- [MOBILE-4456](https://tracker.moodle.org/browse/MOBILE-4456) - Activity icons redesign +- [MOBILE-4459](https://tracker.moodle.org/browse/MOBILE-4459) - New demo mode for custom apps +- [MOBILE-4464](https://tracker.moodle.org/browse/MOBILE-4464) - Improve identification of types in site plugins +- [MOBILE-4469](https://tracker.moodle.org/browse/MOBILE-4469) - Refactor CoreSite class +- [MOBILE-4479](https://tracker.moodle.org/browse/MOBILE-4479) - Let site plugins use core-course-module-info +- [MOBILE-4483](https://tracker.moodle.org/browse/MOBILE-4483) - Support the new completion field "isoverallcomplete" (LMS 4.4 onward) +- [MOBILE-4485](https://tracker.moodle.org/browse/MOBILE-4485) - Improve the experience when attempting connecting to a site which doesn't support the app +- [MOBILE-4487](https://tracker.moodle.org/browse/MOBILE-4487) - Pass plugin "args" to the JavaScript of the plugin +- [MOBILE-4501](https://tracker.moodle.org/browse/MOBILE-4501) - Remove unnecessary permissions declaration (iOS and Android) added by cordova-diagnostic-plugin +- [MOBILE-4508](https://tracker.moodle.org/browse/MOBILE-4508) - Update H5P library to 1.26 +- [MOBILE-4522](https://tracker.moodle.org/browse/MOBILE-4522) - After opening a course, the WS mod_forum_get_forums_by_courses is called even if there are no forums with tracking enabled +- [MOBILE-4524](https://tracker.moodle.org/browse/MOBILE-4524) - Avoid calling the WS core_search_get_search_areas_list after login +- [MOBILE-4525](https://tracker.moodle.org/browse/MOBILE-4525) - Optimise retrieving the course categories filter as it is called per category +- [MOBILE-4526](https://tracker.moodle.org/browse/MOBILE-4526) - Use new WebService to obtain filter overrides +- [MOBILE-4528](https://tracker.moodle.org/browse/MOBILE-4528) - Decouple mod_chat code from initial bundle +- [MOBILE-4529](https://tracker.moodle.org/browse/MOBILE-4529) - Decouple mod_survey code from initial bundle +- [MOBILE-4531](https://tracker.moodle.org/browse/MOBILE-4531) - Disable remotely the new functionality included in 4.4 (policies and privacy) +- [MOBILE-4539](https://tracker.moodle.org/browse/MOBILE-4539) - Avoid calling isDownloadable in course page +- [MOBILE-4540](https://tracker.moodle.org/browse/MOBILE-4540) - Avoid calling core_course_get_contents to get URL contents in course page +- [MOBILE-4543](https://tracker.moodle.org/browse/MOBILE-4543) - Remove open course/activity in browser from the course/activity info modal for students only +- [MOBILE-4550](https://tracker.moodle.org/browse/MOBILE-4550) - Apply changes in quiz in attempts summary (review) as well as on the summary of your previous attempts (MDL-80900 and MDL-80880) +- [MOBILE-4551](https://tracker.moodle.org/browse/MOBILE-4551) - Synchronise now (in the user preferences) should display a toast/message when the process has completed +- [MOBILE-4553](https://tracker.moodle.org/browse/MOBILE-4553) - Apply multiple total grades in quiz (MDL-74610) +- [MOBILE-4565](https://tracker.moodle.org/browse/MOBILE-4565) - Accessibility improvements for version 4.4 +- [MOBILE-4579](https://tracker.moodle.org/browse/MOBILE-4579) - Improvements for user avatars +- [MOBILE-4600](https://tracker.moodle.org/browse/MOBILE-4600) - Adapt new default templates of databases + +### Bug + +- [MOBILE-2768](https://tracker.moodle.org/browse/MOBILE-2768) - "Optional acceptance" policies cannot be accepted via the app +- [MOBILE-3622](https://tracker.moodle.org/browse/MOBILE-3622) - Customize H5P styles via Moodle theme doesn't work in the app +- [MOBILE-3790](https://tracker.moodle.org/browse/MOBILE-3790) - Text size does not change in iOS when using the existing app setting +- [MOBILE-4026](https://tracker.moodle.org/browse/MOBILE-4026) - Android: sometimes videos don't load +- [MOBILE-4304](https://tracker.moodle.org/browse/MOBILE-4304) - WebSQL Deprecation +- [MOBILE-4350](https://tracker.moodle.org/browse/MOBILE-4350) - Quiz with sequential navigation doesn't work properly +- [MOBILE-4400](https://tracker.moodle.org/browse/MOBILE-4400) - Signup: Invalid parameter value if username contains spaces +- [MOBILE-4404](https://tracker.moodle.org/browse/MOBILE-4404) - Android multiple audio files in page breaks +- [MOBILE-4430](https://tracker.moodle.org/browse/MOBILE-4430) - Course Progress bar inconsistency +- [MOBILE-4437](https://tracker.moodle.org/browse/MOBILE-4437) - Incorrect purpose color for activity module plugins +- [MOBILE-4467](https://tracker.moodle.org/browse/MOBILE-4467) - Teachers feedback for annotated assignment submissions display files that should be ignored +- [MOBILE-4478](https://tracker.moodle.org/browse/MOBILE-4478) - Some encoded links are not working properly on iOS +- [MOBILE-4481](https://tracker.moodle.org/browse/MOBILE-4481) - Use mime type of the requested file to set file extension +- [MOBILE-4490](https://tracker.moodle.org/browse/MOBILE-4490) - Fix crash in Android 5.1 and 6 due to StatusBar plugin +- [MOBILE-4491](https://tracker.moodle.org/browse/MOBILE-4491) - Fix BBB behats to work with the new "require registration" behaviour +- [MOBILE-4497](https://tracker.moodle.org/browse/MOBILE-4497) - REST exception handler: Competencies are not enabled. +- [MOBILE-4498](https://tracker.moodle.org/browse/MOBILE-4498) - Completion, comments and tags advanced features settings are not used to check the functionality availability +- [MOBILE-4499](https://tracker.moodle.org/browse/MOBILE-4499) - Local calendar events are not deleted when the user deletes a site +- [MOBILE-4502](https://tracker.moodle.org/browse/MOBILE-4502) - Font awesome icons created via editor do not display in app +- [MOBILE-4510](https://tracker.moodle.org/browse/MOBILE-4510) - H5P: wrong value stored instead of hash in the cached assets DB table +- [MOBILE-4521](https://tracker.moodle.org/browse/MOBILE-4521) - iOS: SCORM sometimes show a blank page when changing the SCO +- [MOBILE-4527](https://tracker.moodle.org/browse/MOBILE-4527) - Site Plugins don't work with AOT enabled +- [MOBILE-4530](https://tracker.moodle.org/browse/MOBILE-4530) - Fix branded attribute on event page (MDL-81089) +- [MOBILE-4544](https://tracker.moodle.org/browse/MOBILE-4544) - Hidden timer in quiz follow-ups (MDL-80630) +- [MOBILE-4560](https://tracker.moodle.org/browse/MOBILE-4560) - Question partial credit display on quiz +- [MOBILE-4563](https://tracker.moodle.org/browse/MOBILE-4563) - Update cordova-ios to 7.1.0 and Firebase iOS to 10.23.0 to comply Apple policy +- [MOBILE-4566](https://tracker.moodle.org/browse/MOBILE-4566) - VideoJS not playing .ogv files in Android devices (follow up MDL-81393) +- [MOBILE-4569](https://tracker.moodle.org/browse/MOBILE-4569) - Grade analysis opens a localhost (user teacher) +- [MOBILE-4572](https://tracker.moodle.org/browse/MOBILE-4572) - Android: Race condition when using SSO login in browser and a fixed site +- [MOBILE-4589](https://tracker.moodle.org/browse/MOBILE-4589) - Quiz - Matching question - Answer field - '<' and '>' symbols displaying as &lt; &gt; +- [MOBILE-4604](https://tracker.moodle.org/browse/MOBILE-4604) - Non-partitioned cookies are set when retrieving the site logo breaking session when trying to embed iframes diff --git a/general/development/process-moodleapp/release.md b/general/development/process-moodleapp/release.md index afeed0f5fb..6ee4a2d271 100644 --- a/general/development/process-moodleapp/release.md +++ b/general/development/process-moodleapp/release.md @@ -17,7 +17,7 @@ tags: | 4. | Add the release notes in the release issue created (search for the [release_notes tag](https://tracker.moodle.org/issues/?jql=project%20%3D%20MOBILE%20AND%20labels%20%3D%20release_notes)). Ask someone from the documentation team to review the release notes. | Developer | | 5. | Contact the marketing team announcing the new release and highlights. | Team Lead | | 6. | Add new QA tests to the `apps_test` site. New QA tests should be labeled with [qa_test_required](https://tracker.moodle.org/issues/?jql=project%20%3D%20MOBILE%20AND%20resolution%20in%20(Unresolved%2C%20Fixed)%20AND%20labels%20%3D%20qa_test_required%20ORDER%20BY%20priority%20DESC%2C%20updated%20DESC), remove that label once they are added to the site. | Tester | -| 7. | Complete all TODOs related with the upcoming release, which are marked in code with a comment starting with `@todo [version-number]` (for example, before releasing 4.1 we'd search for comments starting with `@todo [4.1]`) | Developer | +| 7. | Complete all TODOs related with the upcoming release, which are marked in code with a comment starting with `@todo {version-number}` (for example, before releasing 4.1 we'd search for comments starting with `@todo 4.1`) | Developer | | 8. | Update npm dependencies in the `main` branch, and run `npm audit` to ensure all the dependencies are OK. Also check github vulnerabilities report. | Developer | | 9. | **Start testing** | Tester | @@ -31,9 +31,10 @@ tags: | 4. | Send the applications to the stores for review. | Team Lead | | 5. | Check TAG/Release have been created in github ([moodlehq/moodleapp](https://github.com/moodlehq/moodleapp/releases)) with the version number. | Developer | | 6. | Update the `ci` branch in the behat tests plugin ([moodlehq/moodle-local_moodleappbehat](https://github.com/moodlehq/moodle-local_moodleappbehat/)) with the version number. | Developer | -| 7. | Open PR with release documentation updates (from [moodlemobile/devdocs:app-docs](https://github.com/moodlemobile/devdocs/tree/app-docs) to [moodle/devdocs](https://github.com/moodle/devdocs)). | Developer | -| 8. | Mark the issue and the [version](https://tracker.moodle.org/projects/MOBILE?selectedItem=com.atlassian.jira.jira-projects-plugin:release-page) as released in the tracker. | Team Lead | -| 9. | Update [release notes](../../app_releases.md). | Team Lead | +| 7. | Update [upgrade guides](../../app/upgrading). | Developer | +| 8. | Open PR with release documentation updates (from [moodlemobile/devdocs:app-docs](https://github.com/moodlemobile/devdocs/tree/app-docs) to [moodle/devdocs](https://github.com/moodle/devdocs)). | Developer | +| 9. | Mark the issue and the [version](https://tracker.moodle.org/projects/MOBILE?selectedItem=com.atlassian.jira.jira-projects-plugin:release-page) as released in the tracker. | Team Lead | +| 10. | Update [release notes](../../app_releases.md). | Team Lead | ## The following days @@ -52,6 +53,7 @@ tags: | 11. | Review the new features/improvements specs/shaping documents to ensure that they clearly reflect the actual implementation of the feature. | All the team | | 12. | Review that all the minor issues found during the QA testing have a related and triaged MOBILE issue in the tracker. | All the team | | 13. | Make sure that tests are passing with all the supported versions in [ci.moodle.org](https://ci.moodle.org). | Developer | +| 14. | Update APK in [download.moodle.org/mobile](https://download.moodle.org/mobile). | Team Lead | ## See also diff --git a/project-words.txt b/project-words.txt index 21015e05c7..5bd72af2c9 100644 --- a/project-words.txt +++ b/project-words.txt @@ -57,6 +57,7 @@ Nextcloud Notas OCI8 OIDC +OPFS OWASP Oudtshoorn Packagist @@ -92,6 +93,7 @@ allowcaching assignfeedback assignsubmission behat +behats behatsnapshots bigbluebuttonbn bitand @@ -187,6 +189,7 @@ intval invalidcoursemodule isempty isnotempty +isoverallcomplete lable langindex lastaccess @@ -233,6 +236,7 @@ oldmoduleid onlinetext opcache otherfields +outlinks pactivity passwordunmask pdftoppm @@ -287,6 +291,7 @@ sitepolicynotagreed superglobals smallmessage splitview +sqliteviewer stepslib strftimedate strikethrough @@ -327,6 +332,7 @@ usertimezone varchar versionname videojs +webapps webdav webserver webservice