diff --git a/scripts/functions.sh b/scripts/functions.sh index ed1472357..87e5e5c43 100644 --- a/scripts/functions.sh +++ b/scripts/functions.sh @@ -21,16 +21,19 @@ function print_success { function print_error { tput setaf 1; echo " ERROR: $1" + tput setaf 0 } function print_ok { tput setaf 2; echo " OK: $1" echo + tput setaf 0 } function print_message { tput setaf 3; echo "-------- $1" echo + tput setaf 0 } function print_title { @@ -38,4 +41,5 @@ function print_title { echo tput setaf 5; echo "$stepnumber $1" tput setaf 5; echo '==================' + tput setaf 0 } \ No newline at end of file diff --git a/scripts/get_ws_changes.php b/scripts/get_ws_changes.php new file mode 100644 index 000000000..d5b91f9f0 --- /dev/null +++ b/scripts/get_ws_changes.php @@ -0,0 +1,99 @@ +. + +/** + * Script for converting a PHP WS structure to a TS type. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the path to the folder containing the Moodle installations as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +define('CLI_SCRIPT', true); +require_once('ws_to_ts_functions.php'); + +$versions = array('master', '37', '36', '35', '34', '33', '32', '31'); + +$moodlespath = $argv[1]; +$wsname = $argv[2]; +$useparams = !!(isset($argv[3]) && $argv[3]); +$pathseparator = '/'; + +// Get the path to the script. +$index = strrpos(__FILE__, $pathseparator); +if ($index === false) { + $pathseparator = '\\'; + $index = strrpos(__FILE__, $pathseparator); +} +$scriptfolder = substr(__FILE__, 0, $index); +$scriptpath = concatenate_paths($scriptfolder, 'get_ws_structure.php', $pathseparator); + +$previousstructure = null; +$previousversion = null; +$libsloaded = false; + +foreach ($versions as $version) { + $moodlepath = concatenate_paths($moodlespath, 'stable_' . $version, $pathseparator); + + if (!$libsloaded) { + $libsloaded = true; + + require($moodlepath . '/config.php'); + require($CFG->dirroot . '/webservice/lib.php'); + } + + // Get the structure in this Moodle version. + $structure = shell_exec("php $scriptpath $moodlepath $wsname " . ($useparams ? 'true' : '')); + + if (strpos($structure, 'ERROR:') === 0) { + echo "WS not found in version $version. Stop.\n"; + break; + } + + $structure = unserialize($structure); + + if ($previousstructure != null) { + echo "*** Check changes from version $version to $previousversion ***\n"; + + $messages = detect_ws_changes($previousstructure, $structure); + + if (count($messages) > 0) { + $haschanged = true; + + foreach($messages as $message) { + echo "$message\n"; + } + } else { + echo "No changes found.\n"; + } + echo "\n"; + } + + $previousstructure = $structure; + $previousversion = $version; +} diff --git a/scripts/get_ws_structure.php b/scripts/get_ws_structure.php new file mode 100644 index 000000000..bfa975ee9 --- /dev/null +++ b/scripts/get_ws_structure.php @@ -0,0 +1,55 @@ +. + +/** + * Script for getting the PHP structure of a WS returns or params. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the Moodle path as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +$moodlepath = $argv[1]; +$wsname = $argv[2]; +$useparams = !!(isset($argv[3]) && $argv[3]); + +define('CLI_SCRIPT', true); + +require($moodlepath . '/config.php'); +require($CFG->dirroot . '/webservice/lib.php'); +require_once('ws_to_ts_functions.php'); + +$structure = get_ws_structure($wsname, $useparams); + +if ($structure === false) { + echo "ERROR: The WS wasn't found in this Moodle installation.\n"; + die(); +} + +remove_default_closures($structure); +echo serialize($structure); diff --git a/scripts/get_ws_ts.php b/scripts/get_ws_ts.php new file mode 100644 index 000000000..900a153b8 --- /dev/null +++ b/scripts/get_ws_ts.php @@ -0,0 +1,62 @@ +. + +/** + * Script for converting a PHP WS structure to a TS type. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is the name to put to the TS type. Defaults to "TypeName". + * The fourth parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the Moodle path as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +$moodlepath = $argv[1]; +$wsname = $argv[2]; +$typename = isset($argv[3]) ? $argv[3] : 'TypeName'; +$useparams = !!(isset($argv[4]) && $argv[4]); + +define('CLI_SCRIPT', true); + +require($moodlepath . '/config.php'); +require($CFG->dirroot . '/webservice/lib.php'); +require_once('ws_to_ts_functions.php'); + +$structure = get_ws_structure($wsname, $useparams); + +if ($structure === false) { + echo "ERROR: The WS wasn't found in this Moodle installation.\n"; + die(); +} + +if ($useparams) { + $description = "Params of WS $wsname."; +} else { + $description = "Result of WS $wsname."; +} + +echo get_ts_doc(null, $description, '') . "export type $typename = " . convert_to_ts(null, $structure, $useparams) . ";\n"; diff --git a/scripts/ws_to_ts_functions.php b/scripts/ws_to_ts_functions.php new file mode 100644 index 000000000..ca05d2082 --- /dev/null +++ b/scripts/ws_to_ts_functions.php @@ -0,0 +1,244 @@ +. + +/** + * Helper functions for converting a Moodle WS structure to a TS type. + */ + +/** + * Get the structure of a WS params or returns. + */ +function get_ws_structure($wsname, $useparams) { + global $DB; + + // get all the function descriptions + $functions = $DB->get_records('external_functions', array(), 'name'); + $functiondescs = array(); + foreach ($functions as $function) { + $functiondescs[$function->name] = external_api::external_function_info($function); + } + + if (!isset($functiondescs[$wsname])) { + return false; + } else if ($useparams) { + return $functiondescs[$wsname]->parameters_desc; + } else { + return $functiondescs[$wsname]->returns_desc; + } +} + +/** + * Fix a comment: make sure first letter is uppercase and add a dot at the end if needed. + */ +function fix_comment($desc) { + $desc = trim($desc); + $desc = ucfirst($desc); + + if (substr($desc, -1) !== '.') { + $desc .= '.'; + } + + return $desc; +} + +/** + * Get an inline comment based on a certain text. + */ +function get_inline_comment($desc) { + if (empty($desc)) { + return ''; + } + + return ' // ' . fix_comment($desc); +} + +/** + * Add the TS documentation of a certain element. + */ +function get_ts_doc($type, $desc, $indentation) { + if (empty($desc)) { + // If no key, it's probably in an array. We only document object properties. + return ''; + } + + return $indentation . "/**\n" . + $indentation . " * " . fix_comment($desc) . "\n" . + (!empty($type) ? ($indentation . " * @type {" . $type . "}\n") : '') . + $indentation . " */\n"; +} + +/** + * Specify a certain type, with or without a key. + */ +function convert_key_type($key, $type, $required, $indentation) { + if ($key) { + // It has a key, it's inside an object. + return $indentation . "$key" . ($required == VALUE_OPTIONAL ? '?' : '') . ": $type"; + } else { + // No key, it's probably in an array. Just include the type. + return $type; + } +} + +/** + * Convert a certain element into a TS structure. + */ +function convert_to_ts($key, $value, $boolisnumber = false, $indentation = '', $arraydesc = '') { + if ($value instanceof external_value || $value instanceof external_warnings || $value instanceof external_files) { + // It's a basic field or a pre-defined type like warnings. + $type = 'string'; + + if ($value instanceof external_warnings) { + $type = 'CoreWSExternalWarning[]'; + } else if ($value instanceof external_files) { + $type = 'CoreWSExternalFile[]'; + } else if ($value->type == PARAM_BOOL && !$boolisnumber) { + $type = 'boolean'; + } else if (($value->type == PARAM_BOOL && $boolisnumber) || $value->type == PARAM_INT || $value->type == PARAM_FLOAT || + $value->type == PARAM_LOCALISEDFLOAT || $value->type == PARAM_PERMISSION || $value->type == PARAM_INTEGER || + $value->type == PARAM_NUMBER) { + $type = 'number'; + } + + $result = convert_key_type($key, $type, $value->required, $indentation); + + return $result; + + } else if ($value instanceof external_single_structure) { + // It's an object. + $result = convert_key_type($key, '{', $value->required, $indentation); + + if ($arraydesc) { + // It's an array of objects. Print the array description now. + $result .= get_inline_comment($arraydesc); + } + + $result .= "\n"; + + foreach ($value->keys as $key => $value) { + $result .= convert_to_ts($key, $value, $boolisnumber, $indentation . ' ') . ';'; + + if (!$value instanceof external_multiple_structure || !$value->content instanceof external_single_structure) { + // Add inline comments after the field, except for arrays of objects where it's added at the start. + $result .= get_inline_comment($value->desc); + } + + $result .= "\n"; + } + + $result .= "$indentation}"; + + return $result; + + } else if ($value instanceof external_multiple_structure) { + // It's an array. + $result = convert_key_type($key, '', $value->required, $indentation); + + $result .= convert_to_ts(null, $value->content, $boolisnumber, $indentation, $value->desc); + + $result .= "[]"; + + return $result; + } else { + echo "WARNING: Unknown structure: $key " . get_class($value) . " \n"; + + return ""; + } +} + +/** + * Concatenate two paths. + */ +function concatenate_paths($left, $right, $separator = '/') { + if (!is_string($left) || $left == '') { + return $right; + } else if (!is_string($right) || $right == '') { + return $left; + } + + $lastCharLeft = substr($left, -1); + $firstCharRight = $right[0]; + + if ($lastCharLeft === $separator && $firstCharRight === $separator) { + return $left . substr($right, 1); + } else if ($lastCharLeft !== $separator && $firstCharRight !== '/') { + return $left . '/' . $right; + } else { + return $left . $right; + } +} + +/** + * Detect changes between 2 WS structures. We only detect fields that have been added or modified, not removed fields. + */ +function detect_ws_changes($new, $old, $key = '', $path = '') { + $messages = []; + + if (gettype($new) != gettype($old)) { + // The type has changed. + $messages[] = "Property '$key' has changed type, from '" . gettype($old) . "' to '" . gettype($new) . + ($path != '' ? "' inside $path." : "'."); + + } else if ($new instanceof external_value && $new->type != $old->type) { + // The type has changed. + $messages[] = "Property '$key' has changed type, from '" . $old->type . "' to '" . $new->type . + ($path != '' ? "' inside $path." : "'."); + + } else if ($new instanceof external_warnings || $new instanceof external_files) { + // Ignore these types. + + } else if ($new instanceof external_single_structure) { + // Check each subproperty. + $newpath = ($path != '' ? "$path." : '') . $key; + + foreach ($new->keys as $subkey => $value) { + if (!isset($old->keys[$subkey])) { + // New property. + $messages[] = "New property '$subkey' found" . ($newpath != '' ? " inside '$newpath'." : '.'); + } else { + $messages = array_merge($messages, detect_ws_changes($value, $old->keys[$subkey], $subkey, $newpath)); + } + } + } else if ($new instanceof external_multiple_structure) { + // Recursive call with the content. + $messages = array_merge($messages, detect_ws_changes($new->content, $old->content, $key, $path)); + } + + return $messages; +} + +/** + * Remove all closures (anonymous functions) in the default values so the object can be serialized. + */ +function remove_default_closures($value) { + if ($value instanceof external_warnings || $value instanceof external_files) { + // Ignore these types. + + } else if ($value instanceof external_value) { + if ($value->default instanceof Closure) { + $value->default = null; + } + + } else if ($value instanceof external_single_structure) { + + foreach ($value->keys as $key => $subvalue) { + remove_default_closures($subvalue); + } + + } else if ($value instanceof external_multiple_structure) { + remove_default_closures($value->content); + } +} diff --git a/src/addon/badges/pages/issued-badge/issued-badge.html b/src/addon/badges/pages/issued-badge/issued-badge.html index 111cc8a8f..0dcc1e810 100644 --- a/src/addon/badges/pages/issued-badge/issued-badge.html +++ b/src/addon/badges/pages/issued-badge/issued-badge.html @@ -1,6 +1,6 @@ - {{badge.name}} + {{badge && badge.name}} @@ -9,7 +9,7 @@ - + @@ -30,7 +30,7 @@ - +

{{ 'addon.badges.issuerdetails' | translate}}

@@ -48,7 +48,7 @@
- +

{{ 'addon.badges.badgedetails' | translate}}

@@ -99,7 +99,7 @@
- +

{{ 'addon.badges.issuancedetails' | translate}}

@@ -120,7 +120,7 @@
- +

{{ 'addon.badges.bendorsement' | translate}}

@@ -159,7 +159,7 @@
- +

{{ 'addon.badges.relatedbages' | translate}}

@@ -172,14 +172,14 @@
- +

{{ 'addon.badges.alignment' | translate}}

- -

+
+

- +

{{ 'addon.badges.noalignment' | translate}}

diff --git a/src/addon/badges/pages/issued-badge/issued-badge.ts b/src/addon/badges/pages/issued-badge/issued-badge.ts index d33990ddb..ba99f1bc2 100644 --- a/src/addon/badges/pages/issued-badge/issued-badge.ts +++ b/src/addon/badges/pages/issued-badge/issued-badge.ts @@ -19,7 +19,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { AddonBadgesProvider } from '../../providers/badges'; +import { AddonBadgesProvider, AddonBadgesUserBadge } from '../../providers/badges'; /** * Page that displays the list of calendar events. @@ -38,7 +38,7 @@ export class AddonBadgesIssuedBadgePage { user: any = {}; course: any = {}; - badge: any = {}; + badge: AddonBadgesUserBadge; badgeLoaded = false; currentTime = 0; diff --git a/src/addon/badges/pages/user-badges/user-badges.ts b/src/addon/badges/pages/user-badges/user-badges.ts index fe5ea5c1c..d25b88627 100644 --- a/src/addon/badges/pages/user-badges/user-badges.ts +++ b/src/addon/badges/pages/user-badges/user-badges.ts @@ -14,7 +14,7 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, Content, NavParams } from 'ionic-angular'; -import { AddonBadgesProvider } from '../../providers/badges'; +import { AddonBadgesProvider, AddonBadgesUserBadge } from '../../providers/badges'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSitesProvider } from '@providers/sites'; @@ -36,7 +36,7 @@ export class AddonBadgesUserBadgesPage { userId: number; badgesLoaded = false; - badges = []; + badges: AddonBadgesUserBadge[] = []; currentTime = 0; badgeHash: string; diff --git a/src/addon/badges/providers/badges.ts b/src/addon/badges/providers/badges.ts index f2b16814b..79f0598ad 100644 --- a/src/addon/badges/providers/badges.ts +++ b/src/addon/badges/providers/badges.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreWSExternalWarning } from '@providers/ws'; import { CoreSite } from '@classes/site'; /** @@ -70,7 +71,7 @@ export class AddonBadgesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise to be resolved when the badges are retrieved. */ - getUserBadges(courseId: number, userId: number, siteId?: string): Promise { + getUserBadges(courseId: number, userId: number, siteId?: string): Promise { this.logger.debug('Get badges for course ' + courseId); @@ -85,8 +86,13 @@ export class AddonBadgesProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('core_badges_get_user_badges', data, preSets).then((response) => { + return site.read('core_badges_get_user_badges', data, preSets).then((response: AddonBadgesGetUserBadgesResult): any => { if (response && response.badges) { + // In 3.7, competencies was renamed to alignment. Rename the property in 3.6 too. + response.badges.forEach((badge) => { + badge.alignment = badge.alignment || badge.competencies; + }); + return response.badges; } else { return Promise.reject(null); @@ -110,3 +116,85 @@ export class AddonBadgesProvider { }); } } + +/** + * Result of WS core_badges_get_user_badges. + */ +export type AddonBadgesGetUserBadgesResult = { + badges: AddonBadgesUserBadge[]; // List of badges. + warnings?: CoreWSExternalWarning[]; // List of warnings. +}; + +/** + * Badge data returned by WS core_badges_get_user_badges. + */ +export type AddonBadgesUserBadge = { + id?: number; // Badge id. + name: string; // Badge name. + description: string; // Badge description. + timecreated?: number; // Time created. + timemodified?: number; // Time modified. + usercreated?: number; // User created. + usermodified?: number; // User modified. + issuername: string; // Issuer name. + issuerurl: string; // Issuer URL. + issuercontact: string; // Issuer contact. + expiredate?: number; // Expire date. + expireperiod?: number; // Expire period. + type?: number; // Type. + courseid?: number; // Course id. + message?: string; // Message. + messagesubject?: string; // Message subject. + attachment?: number; // Attachment. + notification?: number; // @since 3.6. Whether to notify when badge is awarded. + nextcron?: number; // @since 3.6. Next cron. + status?: number; // Status. + issuedid?: number; // Issued id. + uniquehash: string; // Unique hash. + dateissued: number; // Date issued. + dateexpire: number; // Date expire. + visible?: number; // Visible. + email?: string; // @since 3.6. User email. + version?: string; // @since 3.6. Version. + language?: string; // @since 3.6. Language. + imageauthorname?: string; // @since 3.6. Name of the image author. + imageauthoremail?: string; // @since 3.6. Email of the image author. + imageauthorurl?: string; // @since 3.6. URL of the image author. + imagecaption?: string; // @since 3.6. Caption of the image. + badgeurl: string; // Badge URL. + endorsement?: { // @since 3.6. + id: number; // Endorsement id. + badgeid: number; // Badge id. + issuername: string; // Endorsement issuer name. + issuerurl: string; // Endorsement issuer URL. + issueremail: string; // Endorsement issuer email. + claimid: string; // Claim URL. + claimcomment: string; // Claim comment. + dateissued: number; // Date issued. + }; + alignment?: { // @since 3.7. Calculated by the app for 3.6 sites. Badge alignments. + id?: number; // Alignment id. + badgeid?: number; // Badge id. + targetName?: string; // Target name. + targetUrl?: string; // Target URL. + targetDescription?: string; // Target description. + targetFramework?: string; // Target framework. + targetCode?: string; // Target code. + }[]; + competencies?: { // @deprecated from 3.7. @since 3.6. In 3.7 it was renamed to alignment. + id?: number; // Alignment id. + badgeid?: number; // Badge id. + targetName?: string; // Target name. + targetUrl?: string; // Target URL. + targetDescription?: string; // Target description. + targetFramework?: string; // Target framework. + targetCode?: string; // Target code. + }[]; + relatedbadges?: { // @since 3.6. Related badges. + id: number; // Badge id. + name: string; // Badge name. + version?: string; // Version. + language?: string; // Language. + type?: number; // Type. + }[]; +}; diff --git a/src/addon/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts b/src/addon/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts index 6aa7b3650..4c232f39f 100644 --- a/src/addon/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts +++ b/src/addon/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts @@ -16,7 +16,9 @@ import { Component, OnInit, Injector, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component'; -import { AddonBlockRecentlyAccessedItemsProvider } from '../../providers/recentlyaccesseditems'; +import { + AddonBlockRecentlyAccessedItemsProvider, AddonBlockRecentlyAccessedItemsItem +} from '../../providers/recentlyaccesseditems'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; @@ -28,7 +30,7 @@ import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/hel templateUrl: 'addon-block-recentlyaccesseditems.html' }) export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseComponent implements OnInit { - items = []; + items: AddonBlockRecentlyAccessedItemsItem[] = []; protected fetchContentDefaultError = 'Error getting recently accessed items data.'; diff --git a/src/addon/block/recentlyaccesseditems/providers/recentlyaccesseditems.ts b/src/addon/block/recentlyaccesseditems/providers/recentlyaccesseditems.ts index a24bc035c..414b2366f 100644 --- a/src/addon/block/recentlyaccesseditems/providers/recentlyaccesseditems.ts +++ b/src/addon/block/recentlyaccesseditems/providers/recentlyaccesseditems.ts @@ -42,14 +42,16 @@ export class AddonBlockRecentlyAccessedItemsProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved when the info is retrieved. */ - getRecentItems(siteId?: string): Promise { + getRecentItems(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getRecentItemsCacheKey() }; - return site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets).then((items) => { + return site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets) + .then((items: AddonBlockRecentlyAccessedItemsItem[]) => { + return items.map((item) => { const modicon = item.icon && this.domUtils.getHTMLElementAttribute(item.icon, 'src'); item.iconUrl = this.courseProvider.getModuleIconSrc(item.modname, modicon); @@ -72,3 +74,27 @@ export class AddonBlockRecentlyAccessedItemsProvider { }); } } + +/** + * Result of WS block_recentlyaccesseditems_get_recent_items. + */ +export type AddonBlockRecentlyAccessedItemsItem = { + id: number; // Id. + courseid: number; // Courseid. + cmid: number; // Cmid. + userid: number; // Userid. + modname: string; // Modname. + name: string; // Name. + coursename: string; // Coursename. + timeaccess: number; // Timeaccess. + viewurl: string; // Viewurl. + courseviewurl: string; // Courseviewurl. + icon: string; // Icon. +} & AddonBlockRecentlyAccessedItemsItemCalculatedData; + +/** + * Calculated data for recently accessed item. + */ +export type AddonBlockRecentlyAccessedItemsItemCalculatedData = { + iconUrl: string; // Icon URL. Calculated by the app. +}; diff --git a/src/addon/block/timeline/components/timeline/timeline.ts b/src/addon/block/timeline/components/timeline/timeline.ts index 8325cbcf3..68e104bbe 100644 --- a/src/addon/block/timeline/components/timeline/timeline.ts +++ b/src/addon/block/timeline/components/timeline/timeline.ts @@ -21,6 +21,7 @@ import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component'; import { AddonBlockTimelineProvider } from '../../providers/timeline'; +import { AddonCalendarEvent } from '@addon/calendar/providers/calendar'; /** * Component to render a timeline block. @@ -34,9 +35,9 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen filter = 'next30days'; currentSite: any; timeline = { - events: [], + events: [], loaded: false, - canLoadMore: undefined + canLoadMore: undefined }; timelineCourses = { courses: [], diff --git a/src/addon/block/timeline/providers/timeline.ts b/src/addon/block/timeline/providers/timeline.ts index 6f90184e8..66d4bcd43 100644 --- a/src/addon/block/timeline/providers/timeline.ts +++ b/src/addon/block/timeline/providers/timeline.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCoursesDashboardProvider } from '@core/courses/providers/dashboard'; +import { AddonCalendarEvents, AddonCalendarEventsGroupedByCourse, AddonCalendarEvent } from '@addon/calendar/providers/calendar'; import * as moment from 'moment'; /** @@ -38,7 +39,7 @@ export class AddonBlockTimelineProvider { * @return Promise resolved when the info is retrieved. */ getActionEventsByCourse(courseId: number, afterEventId?: number, siteId?: string): - Promise<{ events: any[], canLoadMore: number }> { + Promise<{ events: AddonCalendarEvent[], canLoadMore: number }> { return this.sitesProvider.getSite(siteId).then((site) => { const time = moment().subtract(14, 'days').unix(), // Check two weeks ago. @@ -55,7 +56,9 @@ export class AddonBlockTimelineProvider { data.aftereventid = afterEventId; } - return site.read('core_calendar_get_action_events_by_course', data, preSets).then((courseEvents): any => { + return site.read('core_calendar_get_action_events_by_course', data, preSets) + .then((courseEvents: AddonCalendarEvents): any => { + if (courseEvents && courseEvents.events) { return this.treatCourseEvents(courseEvents, time); } @@ -82,8 +85,9 @@ export class AddonBlockTimelineProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved when the info is retrieved. */ - getActionEventsByCourses(courseIds: number[], siteId?: string): Promise<{ [s: string]: - { events: any[], canLoadMore: number } }> { + getActionEventsByCourses(courseIds: number[], siteId?: string): Promise<{ [courseId: string]: + { events: AddonCalendarEvent[], canLoadMore: number } }> { + return this.sitesProvider.getSite(siteId).then((site) => { const time = moment().subtract(14, 'days').unix(), // Check two weeks ago. data = { @@ -95,7 +99,9 @@ export class AddonBlockTimelineProvider { cacheKey: this.getActionEventsByCoursesCacheKey() }; - return site.read('core_calendar_get_action_events_by_courses', data, preSets).then((events): any => { + return site.read('core_calendar_get_action_events_by_courses', data, preSets) + .then((events: AddonCalendarEventsGroupedByCourse): any => { + if (events && events.groupedbycourse) { const courseEvents = {}; @@ -127,7 +133,9 @@ export class AddonBlockTimelineProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved when the info is retrieved. */ - getActionEventsByTimesort(afterEventId: number, siteId?: string): Promise<{ events: any[], canLoadMore: number }> { + getActionEventsByTimesort(afterEventId: number, siteId?: string): + Promise<{ events: AddonCalendarEvent[], canLoadMore: number }> { + return this.sitesProvider.getSite(siteId).then((site) => { const time = moment().subtract(14, 'days').unix(), // Check two weeks ago. data: any = { @@ -144,12 +152,14 @@ export class AddonBlockTimelineProvider { data.aftereventid = afterEventId; } - return site.read('core_calendar_get_action_events_by_timesort', data, preSets).then((events): any => { - if (events && events.events) { - const canLoadMore = events.events.length >= data.limitnum ? events.lastid : undefined; + return site.read('core_calendar_get_action_events_by_timesort', data, preSets) + .then((result: AddonCalendarEvents): any => { + + if (result && result.events) { + const canLoadMore = result.events.length >= data.limitnum ? result.lastid : undefined; // Filter events by time in case it uses cache. - events = events.events.filter((element) => { + const events = result.events.filter((element) => { return element.timesort >= time; }); @@ -236,7 +246,9 @@ export class AddonBlockTimelineProvider { * @param timeFrom Current time to filter events from. * @return Object with course events and last loaded event id if more can be loaded. */ - protected treatCourseEvents(course: any, timeFrom: number): { events: any[], canLoadMore: number } { + protected treatCourseEvents(course: AddonCalendarEvents, timeFrom: number): + { events: AddonCalendarEvent[], canLoadMore: number } { + const canLoadMore: number = course.events.length >= AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined; diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index 3cd10ebd6..e0e022245 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -18,7 +18,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; -import { AddonBlogProvider } from '../../providers/blog'; +import { AddonBlogProvider, AddonBlogPost } from '../../providers/blog'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreTagProvider } from '@core/tag/providers/tag'; @@ -48,7 +48,7 @@ export class AddonBlogEntriesComponent implements OnInit { loaded = false; canLoadMore = false; loadMoreError = false; - entries = []; + entries: AddonBlogPostFormatted[] = []; currentUserId: number; showMyEntriesToggle = false; onlyMyEntries = false; @@ -118,7 +118,7 @@ export class AddonBlogEntriesComponent implements OnInit { const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded; return this.blogProvider.getEntries(this.filter, loadPage).then((result) => { - const promises = result.entries.map((entry) => { + const promises = result.entries.map((entry: AddonBlogPostFormatted) => { switch (entry.publishstate) { case 'draft': entry.publishTranslated = 'publishtonoone'; @@ -237,5 +237,12 @@ export class AddonBlogEntriesComponent implements OnInit { }); }); } - } + +/** + * Blog post with some calculated data. + */ +type AddonBlogPostFormatted = AddonBlogPost & { + publishTranslated?: string; // Calculated in the app. Key of the string to translate the publish state of the post. + user?: any; // Calculated in the app. Data of the user that wrote the post. +}; diff --git a/src/addon/blog/providers/blog.ts b/src/addon/blog/providers/blog.ts index a816fd9d3..ab3cf1935 100644 --- a/src/addon/blog/providers/blog.ts +++ b/src/addon/blog/providers/blog.ts @@ -18,6 +18,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreTagItem } from '@core/tag/providers/tag'; /** * Service to handle blog entries. @@ -68,7 +70,7 @@ export class AddonBlogProvider { * @param siteId Site ID. If not defined, current site. * @return Promise to be resolved when the entries are retrieved. */ - getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise { + getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const data = { filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value'), @@ -105,7 +107,7 @@ export class AddonBlogProvider { * @param siteId Site ID. If not defined, current site. * @return Promise to be resolved when done. */ - logView(filter: any = {}, siteId?: string): Promise { + logView(filter: any = {}, siteId?: string): Promise { this.pushNotificationsProvider.logViewListEvent('blog', 'core_blog_view_entries', filter, siteId); return this.sitesProvider.getSite(siteId).then((site) => { @@ -117,3 +119,48 @@ export class AddonBlogProvider { }); } } + +/** + * Data returned by blog's post_exporter. + */ +export type AddonBlogPost = { + id: number; // Post/entry id. + module: string; // Where it was published the post (blog, blog_external...). + userid: number; // Post author. + courseid: number; // Course where the post was created. + groupid: number; // Group post was created for. + moduleid: number; // Module id where the post was created (not used anymore). + coursemoduleid: number; // Course module id where the post was created. + subject: string; // Post subject. + summary: string; // Post summary. + summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + content: string; // Post content. + uniquehash: string; // Post unique hash. + rating: number; // Post rating. + format: number; // Post content format. + attachment: string; // Post atachment. + publishstate: string; // Post publish state. + lastmodified: number; // When it was last modified. + created: number; // When it was created. + usermodified: number; // User that updated the post. + summaryfiles: CoreWSExternalFile[]; // Summaryfiles. + attachmentfiles?: CoreWSExternalFile[]; // Attachmentfiles. + tags?: CoreTagItem[]; // @since 3.7. Tags. +}; + +/** + * Result of WS core_blog_get_entries. + */ +export type AddonBlogGetEntriesResult = { + entries: AddonBlogPost[]; + totalentries: number; // The total number of entries found. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_blog_view_entries. + */ +export type AddonBlogViewEntriesResult = { + status: boolean; // Status: true if success. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/blog/providers/course-option-handler.ts b/src/addon/blog/providers/course-option-handler.ts index 34140ca2a..769a75f5f 100644 --- a/src/addon/blog/providers/course-option-handler.ts +++ b/src/addon/blog/providers/course-option-handler.ts @@ -21,6 +21,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { AddonBlogEntriesComponent } from '../components/entries/entries'; import { AddonBlogProvider } from './blog'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Course nav handler. @@ -100,7 +101,7 @@ export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler { return this.blogProvider.getEntries({courseid: course.id}).then((result) => { return result.entries.map((entry) => { - let files = []; + let files: CoreWSExternalFile[] = []; if (entry.attachmentfiles && entry.attachmentfiles.length) { files = entry.attachmentfiles; diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 391116337..cd9e1deda 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -19,7 +19,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarProvider, AddonCalendarWeek } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; @@ -44,7 +44,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest periodName: string; weekDays: any[]; - weeks: any[]; + weeks: AddonCalendarWeek[]; loaded = false; timeFormat: string; isCurrentMonth: boolean; diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html index 68f174608..c9ff2ded7 100644 --- a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -6,7 +6,7 @@ - +

diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 43489bd80..2dae78490 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -17,7 +17,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarProvider, AddonCalendarCalendarEvent } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; @@ -43,8 +43,8 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, protected categoriesRetrieved = false; protected categories = {}; protected currentSiteId: string; - protected events = []; // Events (both online and offline). - protected onlineEvents = []; + protected events: AddonCalendarCalendarEvent[] = []; // Events (both online and offline). + protected onlineEvents: AddonCalendarCalendarEvent[] = []; protected offlineEvents = []; // Offline events. protected deletedEvents = []; // Events deleted in offline. protected lookAhead: number; diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index 34335a7de..cb9102207 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -50,7 +50,7 @@ - +

diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index d87725e8f..87936754d 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -20,7 +20,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; -import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarProvider, AddonCalendarCalendarEvent } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; @@ -45,7 +45,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected day: number; protected categories = {}; protected events = []; // Events (both online and offline). - protected onlineEvents = []; + protected onlineEvents: AddonCalendarCalendarEvent[] = []; protected offlineEvents = {}; // Offline events. protected offlineEditedEventsIds = []; // IDs of events edited in offline. protected deletedEvents = []; // Events deleted in offline. @@ -287,7 +287,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { return this.calendarProvider.getDayEvents(this.year, this.month, this.day).catch((error) => { if (!this.appProvider.isOnline()) { // Allow navigating to non-cached days in offline (behave as if using emergency cache). - return Promise.resolve({ events: [] }); + return Promise.resolve({ events: [] }); } else { return Promise.reject(error); } diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index 9d79a7e5d..9eae210f8 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -134,7 +134,7 @@

{{ 'addon.calendar.repeatedevents' | translate }}

- {{ 'addon.calendar.repeateditall' | translate:{$a: event.othereventscount} }} + {{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }} diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index df1f0f8c7..a17e5cd5a 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -27,7 +27,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; -import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarProvider, AddonCalendarGetAccessInfoResult, AddonCalendarEvent } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; @@ -58,7 +58,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { courseGroupSet = false; advanced = false; errors: any; - event: any; // The event object (when editing an event). + event: AddonCalendarEvent; // The event object (when editing an event). + otherEventsCount: number; // Form variables. eventForm: FormGroup; @@ -70,7 +71,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { protected courseId: number; protected originalData: any; protected currentSite: CoreSite; - protected types: any; // Object with the supported types. + protected types: {[name: string]: boolean}; // Object with the supported types. protected showAll: boolean; protected isDestroyed = false; protected error = false; @@ -152,7 +153,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ protected fetchData(refresh?: boolean): Promise { - let accessInfo; + let accessInfo: AddonCalendarGetAccessInfoResult; this.error = false; @@ -197,7 +198,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { promises.push(this.calendarProvider.getEventById(this.eventId).then((event) => { this.event = event; if (event && event.repeatid) { - event.othereventscount = event.eventcount ? event.eventcount - 1 : ''; + this.otherEventsCount = event.eventcount ? event.eventcount - 1 : 0; } return event; @@ -489,7 +490,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { // Send the data. const modal = this.domUtils.showModalLoading('core.sending', true); - let event; + let event: AddonCalendarEvent; this.calendarProvider.submitEvent(this.eventId, data).then((result) => { event = result.event; @@ -497,7 +498,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { if (result.sent) { // Event created or edited, invalidate right days & months. const numberOfRepetitions = formData.repeat ? formData.repeats : - (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1); + (data.repeateditall && this.otherEventsCount ? this.otherEventsCount + 1 : 1); return this.calendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions).catch(() => { // Ignore errors. diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 366327f66..176250dec 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -2,7 +2,7 @@ - + @@ -32,7 +32,7 @@ - +

{{ 'addon.calendar.eventname' | translate }}

diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 3ae4f4c0d..a400360b4 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -34,7 +34,7 @@
- +

{{ event.timestart * 1000 | coreFormatDate: "strftimetime" }} diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 77b8922d1..2bb675eda 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -14,7 +14,7 @@ import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; import { IonicPage, Content, NavParams, NavController } from 'ionic-angular'; -import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarProvider, AddonCalendarGetEventsEvent } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; @@ -62,7 +62,7 @@ export class AddonCalendarListPage implements OnDestroy { protected manualSyncObserver: any; protected onlineObserver: any; protected currentSiteId: string; - protected onlineEvents = []; + protected onlineEvents: AddonCalendarGetEventsEvent[] = []; protected offlineEvents = []; protected deletedEvents = []; @@ -70,7 +70,7 @@ export class AddonCalendarListPage implements OnDestroy { eventsLoaded = false; events = []; // Events (both online and offline). notificationsEnabled = false; - filteredEvents = []; + filteredEvents: AddonCalendarGetEventsEvent[] = []; canLoadMore = false; loadMoreError = false; courseId: number; @@ -402,7 +402,7 @@ export class AddonCalendarListPage implements OnDestroy { * * @return Filtered events. */ - protected getFilteredEvents(): any[] { + protected getFilteredEvents(): AddonCalendarGetEventsEvent[] { if (!this.courseId) { // No filter, display everything. return this.events; @@ -581,7 +581,7 @@ export class AddonCalendarListPage implements OnDestroy { * @param event Event info. * @return If date has changed and should be shown. */ - protected endsSameDay(event: any): boolean { + protected endsSameDay(event: AddonCalendarGetEventsEvent): boolean { if (!event.timeduration) { // No duration. return true; diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index c518a989d..794e86796 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -31,6 +31,7 @@ import { SQLiteDB } from '@classes/sqlitedb'; import { AddonCalendarOfflineProvider } from './calendar-offline'; import { CoreUserProvider } from '@core/user/providers/user'; import { TranslateService } from '@ngx-translate/core'; +import { CoreWSExternalWarning, CoreWSDate } from '@providers/ws'; import * as moment from 'moment'; /** @@ -489,7 +490,7 @@ export class AddonCalendarProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise { + deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -535,22 +536,6 @@ export class AddonCalendarProvider { }); } - /** - * Check if event ends the same day or not. - * - * @param event Event info. - * @return If the . - */ - endsSameDay(event: any): boolean { - if (!event.timeduration) { - // No duration. - return true; - } - - // Check if day has changed. - return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day'); - } - /** * Format event time. Similar to calendar_format_event_time. * @@ -562,8 +547,8 @@ export class AddonCalendarProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the formatted event time. */ - formatEventTime(event: any, format: string, useCommonWords: boolean = true, seenDay?: number, showTime: number = 0, - siteId?: string): Promise { + formatEventTime(event: AddonCalendarAnyEvent, format: string, useCommonWords: boolean = true, seenDay?: number, + showTime: number = 0, siteId?: string): Promise { const start = event.timestart * 1000, end = (event.timestart + event.timeduration) * 1000; @@ -635,7 +620,7 @@ export class AddonCalendarProvider { * @return Promise resolved with object with access information. * @since 3.7 */ - getAccessInformation(courseId?: number, siteId?: string): Promise { + getAccessInformation(courseId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params: any = {}, preSets = { @@ -680,7 +665,7 @@ export class AddonCalendarProvider { * @return Promise resolved with an object indicating the types. * @since 3.7 */ - getAllowedEventTypes(courseId?: number, siteId?: string): Promise { + getAllowedEventTypes(courseId?: number, siteId?: string): Promise<{[name: string]: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { const params: any = {}, preSets = { @@ -691,7 +676,8 @@ export class AddonCalendarProvider { params.courseid = courseId; } - return site.read('core_calendar_get_allowed_event_types', params, preSets).then((response) => { + return site.read('core_calendar_get_allowed_event_types', params, preSets) + .then((response: AddonCalendarGetAllowedEventTypesResult) => { // Convert the array to an object. const result = {}; @@ -812,11 +798,10 @@ export class AddonCalendarProvider { * Get a calendar event. If the server request fails and data is not cached, try to get it from local DB. * * @param id Event ID. - * @param refresh True when we should update the event data. * @param siteId ID of the site. If not defined, use current site. * @return Promise resolved when the event data is retrieved. */ - getEvent(id: number, siteId?: string): Promise { + getEvent(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getEventCacheKey(id), @@ -834,7 +819,8 @@ export class AddonCalendarProvider { } }; - return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_events', data, preSets) + .then((response: AddonCalendarGetEventsResult) => { // The WebService returns all category events. Check the response to search for the event we want. const event = response.events.find((e) => { return e.id == id; }); @@ -849,12 +835,11 @@ export class AddonCalendarProvider { * Get a calendar event by ID. This function returns more data than getEvent, but it isn't available in all Moodles. * * @param id Event ID. - * @param refresh True when we should update the event data. * @param siteId ID of the site. If not defined, use current site. * @return Promise resolved when the event data is retrieved. * @since 3.4 */ - getEventById(id: number, siteId?: string): Promise { + getEventById(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getEventCacheKey(id), @@ -864,7 +849,8 @@ export class AddonCalendarProvider { eventid: id }; - return site.read('core_calendar_get_calendar_event_by_id', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_event_by_id', data, preSets) + .then((response: AddonCalendarGetEventByIdResult) => { return response.event; }).catch((error) => { return this.getEventFromLocalDb(id).catch(() => { @@ -918,7 +904,7 @@ export class AddonCalendarProvider { * @param siteId ID of the site the event belongs to. If not defined, use current site. * @return Promise resolved when the notification is updated. */ - addEventReminder(event: any, time: number, siteId?: string): Promise { + addEventReminder(event: AddonCalendarAnyEvent, time: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const reminder = { eventid: event.id, @@ -976,7 +962,7 @@ export class AddonCalendarProvider { * @return Promise resolved with the response. */ getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, - siteId?: string): Promise { + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1003,7 +989,7 @@ export class AddonCalendarProvider { preSets.emergencyCache = false; } - return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response: AddonCalendarCalendarDay) => { this.storeEventsInLocalDB(response.events, siteId); return response; @@ -1071,10 +1057,10 @@ export class AddonCalendarProvider { * @param daysToStart Number of days from now to start getting events. * @param daysInterval Number of days between timestart and timeend. * @param siteId Site to get the events from. If not defined, use current site. - * @return Promise to be resolved when the participants are retrieved. + * @return Promise to be resolved when the events are retrieved. */ getEventsList(initialTime?: number, daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL, - siteId?: string): Promise { + siteId?: string): Promise { initialTime = initialTime || this.timeUtils.timestamp(); @@ -1122,7 +1108,9 @@ export class AddonCalendarProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_events', data, preSets) + .then((response: AddonCalendarGetEventsResult) => { + if (!this.canViewMonthInSite(site)) { // Store events only in 3.1-3.3. In 3.4+ we'll use the new WS that return more info. this.storeEventsInLocalDB(response.events, siteId); @@ -1178,7 +1166,7 @@ export class AddonCalendarProvider { * @return Promise resolved with the response. */ getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string) - : Promise { + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1210,7 +1198,9 @@ export class AddonCalendarProvider { preSets.emergencyCache = false; } - return site.read('core_calendar_get_calendar_monthly_view', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_monthly_view', data, preSets) + .then((response: AddonCalendarMonth) => { + response.weeks.forEach((week) => { week.days.forEach((day) => { this.storeEventsInLocalDB(day.events, siteId); @@ -1270,7 +1260,8 @@ export class AddonCalendarProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the response. */ - getUpcomingEvents(courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string): Promise { + getUpcomingEvents(courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string) + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1293,7 +1284,7 @@ export class AddonCalendarProvider { preSets.emergencyCache = false; } - return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response) => { + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response: AddonCalendarUpcoming) => { this.storeEventsInLocalDB(response.events, siteId); return response; @@ -1604,11 +1595,14 @@ export class AddonCalendarProvider { * If local notification plugin is not enabled, resolve the promise. * * @param event Event to schedule. + * @param reminderId The reminder ID. * @param time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". * @param siteId Site ID the event belongs to. If not defined, use current site. * @return Promise resolved when the notification is scheduled. */ - protected scheduleEventNotification(event: any, reminderId: number, time: number, siteId?: string): Promise { + protected scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string) + : Promise { + if (this.localNotificationsProvider.isAvailable()) { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -1672,7 +1666,7 @@ export class AddonCalendarProvider { * @param siteId ID of the site the events belong to. If not defined, use current site. * @return Promise resolved when all the notifications have been scheduled. */ - scheduleEventsNotifications(events: any[], siteId?: string): Promise { + scheduleEventsNotifications(events: AddonCalendarAnyEvent[], siteId?: string): Promise { if (this.localNotificationsProvider.isAvailable()) { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -1803,11 +1797,10 @@ export class AddonCalendarProvider { * @param timeCreated The time the event was created. Only if modifying a new offline event. * @param forceOffline True to always save it in offline. * @param siteId Site ID. If not defined, current site. - * @return Promise resolved with the event and a boolean indicating if data was - * sent to server or stored in offline. + * @return Promise resolved with the event and a boolean indicating if data was sent to server or stored in offline. */ submitEvent(eventId: number, formData: any, timeCreated?: number, forceOffline?: boolean, siteId?: string): - Promise<{sent: boolean, event: any}> { + Promise<{sent: boolean, event: AddonCalendarEvent}> { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -1847,7 +1840,7 @@ export class AddonCalendarProvider { * @param siteId Site ID. If not provided, current site. * @return Promise resolved when done. */ - submitEventOnline(eventId: number, formData: any, siteId?: string): Promise { + submitEventOnline(eventId: number, formData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { // Add data that is "hidden" in web. formData.id = eventId || 0; @@ -1865,10 +1858,12 @@ export class AddonCalendarProvider { formdata: this.utils.objectToGetParams(formData) }; - return site.write('core_calendar_submit_create_update_form', params).then((result) => { + return site.write('core_calendar_submit_create_update_form', params) + .then((result: AddonCalendarSubmitCreateUpdateFormResult): AddonCalendarEvent => { + if (result.validationerror) { // Simulate a WS error. - return Promise.reject({ + return Promise.reject({ message: this.translate.instant('core.invalidformdata'), errorcode: 'validationerror' }); @@ -1879,3 +1874,337 @@ export class AddonCalendarProvider { }); } } + +/** + * Data returned by calendar's events_exporter. + */ +export type AddonCalendarEvents = { + events: AddonCalendarEvent[]; // Events. + firstid: number; // Firstid. + lastid: number; // Lastid. +}; + +/** + * Data returned by calendar's events_grouped_by_course_exporter. + */ +export type AddonCalendarEventsGroupedByCourse = { + groupedbycourse: AddonCalendarEventsSameCourse[]; // Groupped by course. +}; + +/** + * Data returned by calendar's events_same_course_exporter. + */ +export type AddonCalendarEventsSameCourse = AddonCalendarEvents & { + courseid: number; // Courseid. +}; + +/** + * Data returned by calendar's event_exporter_base. + */ +export type AddonCalendarEventBase = { + id: number; // Id. + name: string; // Name. + description?: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + location?: string; // @since 3.6. Location. + categoryid?: number; // Categoryid. + groupid?: number; // Groupid. + userid?: number; // Userid. + repeatid?: number; // Repeatid. + eventcount?: number; // Eventcount. + modulename?: string; // Modulename. + instance?: number; // Instance. + eventtype: string; // Eventtype. + timestart: number; // Timestart. + timeduration: number; // Timeduration. + timesort: number; // Timesort. + visible: number; // Visible. + timemodified: number; // Timemodified. + icon: { + key: string; // Key. + component: string; // Component. + alttext: string; // Alttext. + }; + category?: { + id: number; // Id. + name: string; // Name. + idnumber: string; // Idnumber. + description?: string; // Description. + parent: number; // Parent. + coursecount: number; // Coursecount. + visible: number; // Visible. + timemodified: number; // Timemodified. + depth: number; // Depth. + nestedname: string; // Nestedname. + url: string; // Url. + }; + course?: { + id: number; // Id. + fullname: string; // Fullname. + shortname: string; // Shortname. + idnumber: string; // Idnumber. + summary: string; // Summary. + summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + startdate: number; // Startdate. + enddate: number; // Enddate. + visible: boolean; // @since 3.8. Visible. + fullnamedisplay: string; // Fullnamedisplay. + viewurl: string; // Viewurl. + courseimage: string; // @since 3.6. Courseimage. + progress?: number; // @since 3.6. Progress. + hasprogress: boolean; // @since 3.6. Hasprogress. + isfavourite: boolean; // @since 3.6. Isfavourite. + hidden: boolean; // @since 3.6. Hidden. + timeaccess?: number; // @since 3.6. Timeaccess. + showshortname: boolean; // @since 3.6. Showshortname. + coursecategory: string; // @since 3.7. Coursecategory. + }; + subscription?: { + displayeventsource: boolean; // Displayeventsource. + subscriptionname?: string; // Subscriptionname. + subscriptionurl?: string; // Subscriptionurl. + }; + canedit: boolean; // Canedit. + candelete: boolean; // Candelete. + deleteurl: string; // Deleteurl. + editurl: string; // Editurl. + viewurl: string; // Viewurl. + formattedtime: string; // Formattedtime. + isactionevent: boolean; // Isactionevent. + iscourseevent: boolean; // Iscourseevent. + iscategoryevent: boolean; // Iscategoryevent. + groupname?: string; // Groupname. + normalisedeventtype: string; // @since 3.7. Normalisedeventtype. + normalisedeventtypetext: string; // @since 3.7. Normalisedeventtypetext. +}; + +/** + * Data returned by calendar's event_exporter. Don't confuse it with AddonCalendarCalendarEvent. + */ +export type AddonCalendarEvent = AddonCalendarEventBase & { + url: string; // Url. + action?: { + name: string; // Name. + url: string; // Url. + itemcount: number; // Itemcount. + actionable: boolean; // Actionable. + showitemcount: boolean; // Showitemcount. + }; +}; + +/** + * Data returned by calendar's calendar_event_exporter. Don't confuse it with AddonCalendarEvent. + */ +export type AddonCalendarCalendarEvent = AddonCalendarEventBase & { + url: string; // Url. + islastday: boolean; // Islastday. + popupname: string; // Popupname. + mindaytimestamp?: number; // Mindaytimestamp. + mindayerror?: string; // Mindayerror. + maxdaytimestamp?: number; // Maxdaytimestamp. + maxdayerror?: string; // Maxdayerror. + draggable: boolean; // Draggable. +} & AddonCalendarCalendarEventCalculatedData; + +/** + * Any of the possible types of events. + */ +export type AddonCalendarAnyEvent = AddonCalendarGetEventsEvent | AddonCalendarEvent | AddonCalendarCalendarEvent; + +/** + * Data returned by calendar's calendar_day_exporter. Don't confuse it with AddonCalendarDay. + */ +export type AddonCalendarCalendarDay = { + events: AddonCalendarCalendarEvent[]; // Events. + defaulteventcontext: number; // Defaulteventcontext. + filter_selector: string; // Filter_selector. + courseid: number; // Courseid. + categoryid?: number; // Categoryid. + neweventtimestamp: number; // Neweventtimestamp. + date: CoreWSDate; + periodname: string; // Periodname. + previousperiod: CoreWSDate; + previousperiodlink: string; // Previousperiodlink. + previousperiodname: string; // Previousperiodname. + nextperiod: CoreWSDate; + nextperiodname: string; // Nextperiodname. + nextperiodlink: string; // Nextperiodlink. + larrow: string; // Larrow. + rarrow: string; // Rarrow. +}; + +/** + * Data returned by calendar's month_exporter. + */ +export type AddonCalendarMonth = { + url: string; // Url. + courseid: number; // Courseid. + categoryid?: number; // Categoryid. + filter_selector?: string; // Filter_selector. + weeks: AddonCalendarWeek[]; // Weeks. + daynames: AddonCalendarDayName[]; // Daynames. + view: string; // View. + date: CoreWSDate; + periodname: string; // Periodname. + includenavigation: boolean; // Includenavigation. + initialeventsloaded: boolean; // @since 3.5. Initialeventsloaded. + previousperiod: CoreWSDate; + previousperiodlink: string; // Previousperiodlink. + previousperiodname: string; // Previousperiodname. + nextperiod: CoreWSDate; + nextperiodname: string; // Nextperiodname. + nextperiodlink: string; // Nextperiodlink. + larrow: string; // Larrow. + rarrow: string; // Rarrow. + defaulteventcontext: number; // Defaulteventcontext. +}; + +/** + * Data returned by calendar's week_exporter. + */ +export type AddonCalendarWeek = { + prepadding: number[]; // Prepadding. + postpadding: number[]; // Postpadding. + days: AddonCalendarWeekDay[]; // Days. +}; + +/** + * Data returned by calendar's week_day_exporter. + */ +export type AddonCalendarWeekDay = AddonCalendarDay & { + istoday: boolean; // Istoday. + isweekend: boolean; // Isweekend. + popovertitle: string; // Popovertitle. + ispast?: boolean; // Calculated in the app. Whether the day is in the past. + filteredEvents?: AddonCalendarCalendarEvent[]; // Calculated in the app. Filtered events. +}; + +/** + * Data returned by calendar's day_exporter. Don't confuse it with AddonCalendarCalendarDay. + */ +export type AddonCalendarDay = { + seconds: number; // Seconds. + minutes: number; // Minutes. + hours: number; // Hours. + mday: number; // Mday. + wday: number; // Wday. + year: number; // Year. + yday: number; // Yday. + timestamp: number; // Timestamp. + neweventtimestamp: number; // Neweventtimestamp. + viewdaylink?: string; // Viewdaylink. + events: AddonCalendarCalendarEvent[]; // Events. + hasevents: boolean; // Hasevents. + calendareventtypes: string[]; // Calendareventtypes. + previousperiod: number; // Previousperiod. + nextperiod: number; // Nextperiod. + navigation: string; // Navigation. + haslastdayofevent: boolean; // Haslastdayofevent. +}; + +/** + * Data returned by calendar's day_name_exporter. + */ +export type AddonCalendarDayName = { + dayno: number; // Dayno. + shortname: string; // Shortname. + fullname: string; // Fullname. +}; + +/** + * Data returned by calendar's calendar_upcoming_exporter. + */ +export type AddonCalendarUpcoming = { + events: AddonCalendarCalendarEvent[]; // Events. + defaulteventcontext: number; // Defaulteventcontext. + filter_selector: string; // Filter_selector. + courseid: number; // Courseid. + categoryid?: number; // Categoryid. + isloggedin: boolean; // Isloggedin. + date: CoreWSDate; // @since 3.8. Date. +}; + +/** + * Result of WS core_calendar_get_calendar_access_information. + */ +export type AddonCalendarGetAccessInfoResult = { + canmanageentries: boolean; // Whether the user can manage entries. + canmanageownentries: boolean; // Whether the user can manage its own entries. + canmanagegroupentries: boolean; // Whether the user can manage group entries. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_calendar_get_allowed_event_types. + */ +export type AddonCalendarGetAllowedEventTypesResult = { + allowedeventtypes: string[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_calendar_get_calendar_events. + */ +export type AddonCalendarGetEventsResult = { + events: AddonCalendarGetEventsEvent[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Event data returned by WS core_calendar_get_calendar_events. + */ +export type AddonCalendarGetEventsEvent = { + id: number; // Event id. + name: string; // Event name. + description?: string; // Description. + format: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + courseid: number; // Course id. + categoryid?: number; // @since 3.4. Category id (only for category events). + groupid: number; // Group id. + userid: number; // User id. + repeatid: number; // Repeat id. + modulename?: string; // Module name. + instance: number; // Instance id. + eventtype: string; // Event type. + timestart: number; // Timestart. + timeduration: number; // Time duration. + visible: number; // Visible. + uuid?: string; // Unique id of ical events. + sequence: number; // Sequence. + timemodified: number; // Time modified. + subscriptionid?: number; // Subscription id. + showDate?: boolean; // Calculated in the app. Whether date should be shown before this event. + endsSameDay?: boolean; // Calculated in the app. Whether the event finishes the same day it starts. + deleted?: boolean; // Calculated in the app. Whether it has been deleted in offline. +}; + +/** + * Result of WS core_calendar_get_calendar_event_by_id. + */ +export type AddonCalendarGetEventByIdResult = { + event: AddonCalendarEvent; // Event. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_calendar_submit_create_update_form. + */ +export type AddonCalendarSubmitCreateUpdateFormResult = { + event?: AddonCalendarEvent; // Event. + validationerror: boolean; // Invalid form data. +}; + +/** + * Calculated data for AddonCalendarCalendarEvent. + */ +export type AddonCalendarCalendarEventCalculatedData = { + eventIcon?: string; // Calculated in the app. Event icon. + moduleIcon?: string; // Calculated in the app. Module icon. + formattedType?: string; // Calculated in the app. Formatted type. + duration?: number; // Calculated in the app. Duration of offline event. + format?: number; // Calculated in the app. Format of offline event. + timedurationuntil?: number; // Calculated in the app. Time duration until of offline event. + timedurationminutes?: number; // Calculated in the app. Time duration in minutes of offline event. + deleted?: boolean; // Calculated in the app. Whether it has been deleted in offline. + ispast?: boolean; // Calculated in the app. Whether the event is in the past. +}; diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 6c2cd8a6b..515ad44fa 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { AddonCalendarProvider } from './calendar'; +import { AddonCalendarProvider, AddonCalendarCalendarEvent } from './calendar'; import { CoreConstants } from '@core/constants'; import { CoreConfigProvider } from '@providers/config'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -130,11 +130,11 @@ export class AddonCalendarHelperProvider { * * @param e Event to format. */ - formatEventData(e: any): void { - e.icon = this.EVENTICONS[e.eventtype] || false; - if (!e.icon) { - e.icon = this.courseProvider.getModuleIconSrc(e.modulename); - e.moduleIcon = e.icon; + formatEventData(e: AddonCalendarCalendarEvent): void { + e.eventIcon = this.EVENTICONS[e.eventtype] || ''; + if (!e.eventIcon) { + e.eventIcon = this.courseProvider.getModuleIconSrc(e.modulename); + e.moduleIcon = e.eventIcon; } e.formattedType = this.calendarProvider.getEventType(e); @@ -160,7 +160,7 @@ export class AddonCalendarHelperProvider { * @param eventTypes Result of getAllowedEventTypes. * @return Options. */ - getEventTypeOptions(eventTypes: any): {name: string, value: string}[] { + getEventTypeOptions(eventTypes: {[name: string]: boolean}): {name: string, value: string}[] { const options = []; if (eventTypes.user) { diff --git a/src/addon/competency/components/course/course.ts b/src/addon/competency/components/course/course.ts index c9c2a0c15..d6cfb77c9 100644 --- a/src/addon/competency/components/course/course.ts +++ b/src/addon/competency/components/course/course.ts @@ -16,7 +16,7 @@ import { Component, ViewChild, Input } from '@angular/core'; import { Content, NavController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { AddonCompetencyProvider, AddonCompetencyDataForCourseCompetenciesPageResult } from '../../providers/competency'; import { AddonCompetencyHelperProvider } from '../../providers/helper'; /** @@ -33,7 +33,7 @@ export class AddonCompetencyCourseComponent { @Input() userId: number; competenciesLoaded = false; - competencies: any; + competencies: AddonCompetencyDataForCourseCompetenciesPageResult; user: any; constructor(private navCtrl: NavController, private appProvider: CoreAppProvider, private domUtils: CoreDomUtilsProvider, diff --git a/src/addon/competency/pages/competencies/competencies.ts b/src/addon/competency/pages/competencies/competencies.ts index 40eed9b3d..c3f945bf3 100644 --- a/src/addon/competency/pages/competencies/competencies.ts +++ b/src/addon/competency/pages/competencies/competencies.ts @@ -17,7 +17,10 @@ import { IonicPage, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { + AddonCompetencyProvider, AddonCompetencyDataForCourseCompetenciesPageResult, AddonCompetencyDataForPlanPageResult, + AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForCourseCompetenciesPageCompetency +} from '../../providers/competency'; /** * Page that displays the list of competencies of a learning plan. @@ -36,7 +39,7 @@ export class AddonCompetencyCompetenciesPage { protected userId: number; competenciesLoaded = false; - competencies = []; + competencies: AddonCompetencyDataForPlanPageCompetency[] | AddonCompetencyDataForCourseCompetenciesPageCompetency[] = []; title: string; constructor(navParams: NavParams, private translate: TranslateService, private domUtils: CoreDomUtilsProvider, @@ -59,7 +62,7 @@ export class AddonCompetencyCompetenciesPage { this.fetchCompetencies().then(() => { if (!this.competencyId && this.splitviewCtrl.isOn() && this.competencies.length > 0) { // Take first and load it. - this.openCompetency(this.competencies[0].id); + this.openCompetency(this.competencies[0].competency.id); } }).finally(() => { this.competenciesLoaded = true; @@ -72,7 +75,7 @@ export class AddonCompetencyCompetenciesPage { * @return Promise resolved when done. */ protected fetchCompetencies(): Promise { - let promise; + let promise: Promise; if (this.planId) { promise = this.competencyProvider.getLearningPlan(this.planId); @@ -83,13 +86,16 @@ export class AddonCompetencyCompetenciesPage { } return promise.then((response) => { - if (response.competencycount <= 0) { - return Promise.reject(this.translate.instant('addon.competency.errornocompetenciesfound')); - } if (this.planId) { - this.title = response.plan.name; - this.userId = response.plan.userid; + const resp = response; + + if (resp.competencycount <= 0) { + return Promise.reject(this.translate.instant('addon.competency.errornocompetenciesfound')); + } + + this.title = resp.plan.name; + this.userId = resp.plan.userid; } else { this.title = this.translate.instant('addon.competency.coursecompetencies'); } diff --git a/src/addon/competency/pages/competency/competency.html b/src/addon/competency/pages/competency/competency.html index 4f66d9dc1..20b6876a2 100644 --- a/src/addon/competency/pages/competency/competency.html +++ b/src/addon/competency/pages/competency/competency.html @@ -51,22 +51,22 @@ - + {{ 'addon.competency.reviewstatus' | translate }} - {{ competency.usercompetency.statusname }} + {{ userCompetency.statusname }} {{ 'addon.competency.proficient' | translate }} - + {{ 'core.yes' | translate }} - + {{ 'core.no' | translate }} {{ 'addon.competency.rating' | translate }} - {{ competency.usercompetency.gradename }} + {{ userCompetency.gradename }} diff --git a/src/addon/competency/pages/competency/competency.ts b/src/addon/competency/pages/competency/competency.ts index a9999c740..c7265f4e2 100644 --- a/src/addon/competency/pages/competency/competency.ts +++ b/src/addon/competency/pages/competency/competency.ts @@ -18,8 +18,14 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { + AddonCompetencyProvider, AddonCompetencyUserCompetencySummary, AddonCompetencyUserCompetencySummaryInPlan, + AddonCompetencyUserCompetencySummaryInCourse, AddonCompetencyUserCompetencyPlan, + AddonCompetencyUserCompetency, AddonCompetencyUserCompetencyCourse +} from '../../providers/competency'; import { AddonCompetencyHelperProvider } from '../../providers/helper'; +import { CoreUserSummary } from '@core/user/providers/user'; +import { CoreCourseModuleSummary } from '@core/course/providers/course'; /** * Page that displays a learning plan. @@ -36,9 +42,10 @@ export class AddonCompetencyCompetencyPage { courseId: number; userId: number; planStatus: number; - coursemodules: any; - user: any; - competency: any; + coursemodules: CoreCourseModuleSummary[]; + user: CoreUserSummary; + competency: AddonCompetencyUserCompetencySummary; + userCompetency: AddonCompetencyUserCompetencyPlan | AddonCompetencyUserCompetency | AddonCompetencyUserCompetencyCourse; constructor(private navCtrl: NavController, navParams: NavParams, private translate: TranslateService, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, @@ -79,7 +86,8 @@ export class AddonCompetencyCompetencyPage { * @return Promise resolved when done. */ protected fetchCompetency(): Promise { - let promise; + let promise: Promise; + if (this.planId) { this.planStatus = null; promise = this.competencyProvider.getCompetencyInPlan(this.planId, this.competencyId); @@ -90,23 +98,21 @@ export class AddonCompetencyCompetencyPage { } return promise.then((competency) => { - competency.usercompetencysummary.usercompetency = competency.usercompetencysummary.usercompetencyplan || - competency.usercompetencysummary.usercompetency; + this.competency = competency.usercompetencysummary; + this.userCompetency = this.competency.usercompetencyplan || this.competency.usercompetency; if (this.planId) { - this.planStatus = competency.plan.status; + this.planStatus = ( competency).plan.status; this.competency.usercompetency.statusname = this.competencyHelperProvider.getCompetencyStatusName(this.competency.usercompetency.status); } else { - this.competency.usercompetency = this.competency.usercompetencycourse; - this.coursemodules = competency.coursemodules; + this.userCompetency = this.competency.usercompetencycourse; + this.coursemodules = ( competency).coursemodules; } if (this.competency.user.id != this.sitesProvider.getCurrentSiteUserId()) { - this.competency.user.profileimageurl = this.competency.user.profileimageurl || true; - - // Get the user profile image from the returned object. + // Get the user profile from the returned object. this.user = this.competency.user; } diff --git a/src/addon/competency/pages/competencysummary/competencysummary.ts b/src/addon/competency/pages/competencysummary/competencysummary.ts index fc81a80fc..27eccf836 100644 --- a/src/addon/competency/pages/competencysummary/competencysummary.ts +++ b/src/addon/competency/pages/competencysummary/competencysummary.ts @@ -16,7 +16,7 @@ import { Component, Optional } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { AddonCompetencyProvider, AddonCompetencySummary } from '../../providers/competency'; /** * Page that displays a learning plan. @@ -29,7 +29,7 @@ import { AddonCompetencyProvider } from '../../providers/competency'; export class AddonCompetencyCompetencySummaryPage { competencyLoaded = false; competencyId: number; - competency: any; + competency: AddonCompetencySummary; constructor(private navCtrl: NavController, navParams: NavParams, private domUtils: CoreDomUtilsProvider, @Optional() private svComponent: CoreSplitViewComponent, private competencyProvider: AddonCompetencyProvider) { @@ -41,8 +41,7 @@ export class AddonCompetencyCompetencySummaryPage { */ ionViewDidLoad(): void { this.fetchCompetency().then(() => { - const name = this.competency.competency && this.competency.competency.competency && - this.competency.competency.competency.shortname; + const name = this.competency.competency && this.competency.competency.shortname; this.competencyProvider.logCompetencyView(this.competencyId, name).catch(() => { // Ignore errors. diff --git a/src/addon/competency/pages/plan/plan.html b/src/addon/competency/pages/plan/plan.html index f8c5a91da..1d965ebf2 100644 --- a/src/addon/competency/pages/plan/plan.html +++ b/src/addon/competency/pages/plan/plan.html @@ -46,7 +46,8 @@

{{competency.competency.shortname}} {{competency.competency.idnumber}}

- {{ competency.usercompetency.gradename }} + {{ competency.usercompetencyplan.gradename }} + {{ competency.usercompetency.gradename }} diff --git a/src/addon/competency/pages/plan/plan.ts b/src/addon/competency/pages/plan/plan.ts index acbb1e419..a5ef30d5a 100644 --- a/src/addon/competency/pages/plan/plan.ts +++ b/src/addon/competency/pages/plan/plan.ts @@ -17,7 +17,7 @@ import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { AddonCompetencyProvider, AddonCompetencyDataForPlanPageResult } from '../../providers/competency'; import { AddonCompetencyHelperProvider } from '../../providers/helper'; /** @@ -31,7 +31,7 @@ import { AddonCompetencyHelperProvider } from '../../providers/helper'; export class AddonCompetencyPlanPage { protected planId: number; planLoaded = false; - plan: any; + plan: AddonCompetencyDataForPlanPageResult; user: any; constructor(private navCtrl: NavController, navParams: NavParams, private appProvider: CoreAppProvider, @@ -62,9 +62,6 @@ export class AddonCompetencyPlanPage { this.user = user; }); - plan.competencies.forEach((competency) => { - competency.usercompetency = competency.usercompetencyplan || competency.usercompetency; - }); this.plan = plan; }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'Error getting learning plan data.'); diff --git a/src/addon/competency/pages/planlist/planlist.ts b/src/addon/competency/pages/planlist/planlist.ts index 04994ac20..0774d66d2 100644 --- a/src/addon/competency/pages/planlist/planlist.ts +++ b/src/addon/competency/pages/planlist/planlist.ts @@ -16,7 +16,7 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider } from '../../providers/competency'; +import { AddonCompetencyProvider, AddonCompetencyPlan } from '../../providers/competency'; import { AddonCompetencyHelperProvider } from '../../providers/helper'; /** @@ -33,7 +33,7 @@ export class AddonCompetencyPlanListPage { protected userId: number; protected planId: number; plansLoaded = false; - plans = []; + plans: AddonCompetencyPlan[] = []; constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private competencyProvider: AddonCompetencyProvider, private competencyHelperProvider: AddonCompetencyHelperProvider) { @@ -66,7 +66,7 @@ export class AddonCompetencyPlanListPage { */ protected fetchLearningPlans(): Promise { return this.competencyProvider.getLearningPlans(this.userId).then((plans) => { - plans.forEach((plan) => { + plans.forEach((plan: AddonCompetencyPlanFormatted) => { plan.statusname = this.competencyHelperProvider.getPlanStatusName(plan.status); switch (plan.status) { case AddonCompetencyProvider.STATUS_ACTIVE: @@ -109,3 +109,10 @@ export class AddonCompetencyPlanListPage { this.splitviewCtrl.push('AddonCompetencyPlanPage', { planId }); } } + +/** + * Competency plan with some calculated data. + */ +type AddonCompetencyPlanFormatted = AddonCompetencyPlan & { + statuscolor?: string; // Calculated in the app. Color of the plan's status. +}; diff --git a/src/addon/competency/providers/competency.ts b/src/addon/competency/providers/competency.ts index ea0f6df89..9be1e7068 100644 --- a/src/addon/competency/providers/competency.ts +++ b/src/addon/competency/providers/competency.ts @@ -17,6 +17,9 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; import { CoreSite } from '@classes/site'; +import { CoreCommentsArea } from '@core/comments/providers/comments'; +import { CoreUserSummary } from '@core/user/providers/user'; +import { CoreCourseSummary, CoreCourseModuleSummary } from '@core/course/providers/course'; /** * Service to handle caompetency learning plans. @@ -147,7 +150,7 @@ export class AddonCompetencyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise to be resolved when the plans are retrieved. */ - getLearningPlans(userId?: number, siteId?: string): Promise { + getLearningPlans(userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -161,7 +164,9 @@ export class AddonCompetencyProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('tool_lp_data_for_plans_page', params, preSets).then((response) => { + return site.read('tool_lp_data_for_plans_page', params, preSets) + .then((response: AddonCompetencyDataForPlansPageResult): any => { + if (response.plans) { return response.plans; } @@ -176,9 +181,9 @@ export class AddonCompetencyProvider { * * @param planId ID of the plan. * @param siteId Site ID. If not defined, current site. - * @return Promise to be resolved when the plans are retrieved. + * @return Promise to be resolved when the plan is retrieved. */ - getLearningPlan(planId: number, siteId?: string): Promise { + getLearningPlan(planId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { this.logger.debug('Get plan ' + planId); @@ -191,7 +196,9 @@ export class AddonCompetencyProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('tool_lp_data_for_plan_page', params, preSets).then((response) => { + return site.read('tool_lp_data_for_plan_page', params, preSets) + .then((response: AddonCompetencyDataForPlanPageResult): any => { + if (response.plan) { return response; } @@ -207,9 +214,11 @@ export class AddonCompetencyProvider { * @param planId ID of the plan. * @param competencyId ID of the competency. * @param siteId Site ID. If not defined, current site. - * @return Promise to be resolved when the plans are retrieved. + * @return Promise to be resolved when the competency is retrieved. */ - getCompetencyInPlan(planId: number, competencyId: number, siteId?: string): Promise { + getCompetencyInPlan(planId: number, competencyId: number, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { this.logger.debug('Get competency ' + competencyId + ' in plan ' + planId); @@ -223,7 +232,9 @@ export class AddonCompetencyProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('tool_lp_data_for_user_competency_summary_in_plan', params, preSets).then((response) => { + return site.read('tool_lp_data_for_user_competency_summary_in_plan', params, preSets) + .then((response: AddonCompetencyUserCompetencySummaryInPlan): any => { + if (response.usercompetencysummary) { return response; } @@ -241,10 +252,10 @@ export class AddonCompetencyProvider { * @param userId ID of the user. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @return Promise to be resolved when the plans are retrieved. + * @return Promise to be resolved when the competency is retrieved. */ getCompetencyInCourse(courseId: number, competencyId: number, userId?: number, siteId?: string, ignoreCache?: boolean) - : Promise { + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -266,7 +277,9 @@ export class AddonCompetencyProvider { preSets.emergencyCache = false; } - return site.read('tool_lp_data_for_user_competency_summary_in_course', params, preSets).then((response) => { + return site.read('tool_lp_data_for_user_competency_summary_in_course', params, preSets) + .then((response: AddonCompetencyUserCompetencySummaryInCourse): any => { + if (response.usercompetencysummary) { return response; } @@ -283,9 +296,11 @@ export class AddonCompetencyProvider { * @param userId ID of the user. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @return Promise to be resolved when the plans are retrieved. + * @return Promise to be resolved when the competency summary is retrieved. */ - getCompetencySummary(competencyId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { + getCompetencySummary(competencyId: number, userId?: number, siteId?: string, ignoreCache?: boolean) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -305,7 +320,9 @@ export class AddonCompetencyProvider { preSets.emergencyCache = false; } - return site.read('tool_lp_data_for_user_competency_summary', params, preSets).then((response) => { + return site.read('tool_lp_data_for_user_competency_summary', params, preSets) + .then((response: AddonCompetencyUserCompetencySummary): any => { + if (response.competency) { return response.competency; } @@ -324,7 +341,9 @@ export class AddonCompetencyProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise to be resolved when the course competencies are retrieved. */ - getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { + getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { this.logger.debug('Get course competencies for course ' + courseId); @@ -342,7 +361,9 @@ export class AddonCompetencyProvider { preSets.emergencyCache = false; } - return site.read('tool_lp_data_for_course_competencies_page', params, preSets).then((response) => { + return site.read('tool_lp_data_for_course_competencies_page', params, preSets) + .then((response: AddonCompetencyDataForCourseCompetenciesPageResult): any => { + if (response.competencies) { return response; } @@ -356,11 +377,13 @@ export class AddonCompetencyProvider { return response; } - const promises = response.competencies.map((competency) => + let promises: Promise[]; + + promises = response.competencies.map((competency) => this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId) ); - return Promise.all(promises).then((responses: any[]) => { + return Promise.all(promises).then((responses: AddonCompetencyUserCompetencySummaryInCourse[]) => { responses.forEach((resp, index) => { response.competencies[index].usercompetencycourse = resp.usercompetencysummary.usercompetencycourse; }); @@ -486,7 +509,7 @@ export class AddonCompetencyProvider { * @return Promise resolved when the WS call is successful. */ logCompetencyInPlanView(planId: number, competencyId: number, planStatus: number, name?: string, userId?: number, - siteId?: string): Promise { + siteId?: string): Promise { if (planId && competencyId) { return this.sitesProvider.getSite(siteId).then((site) => { @@ -509,7 +532,11 @@ export class AddonCompetencyProvider { userid: userId }, siteId); - return site.write(wsName, params, preSets); + return site.write(wsName, params, preSets).then((success: boolean) => { + if (!success) { + return Promise.reject(null); + } + }); }); } @@ -527,7 +554,7 @@ export class AddonCompetencyProvider { * @return Promise resolved when the WS call is successful. */ logCompetencyInCourseView(courseId: number, competencyId: number, name?: string, userId?: number, siteId?: string) - : Promise { + : Promise { if (courseId && competencyId) { return this.sitesProvider.getSite(siteId).then((site) => { @@ -548,7 +575,11 @@ export class AddonCompetencyProvider { userid: userId }, siteId); - return site.write(wsName, params, preSets); + return site.write(wsName, params, preSets).then((success: boolean) => { + if (!success) { + return Promise.reject(null); + } + }); }); } @@ -563,7 +594,7 @@ export class AddonCompetencyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ - logCompetencyView(competencyId: number, name?: string, siteId?: string): Promise { + logCompetencyView(competencyId: number, name?: string, siteId?: string): Promise { if (competencyId) { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -576,10 +607,401 @@ export class AddonCompetencyProvider { this.pushNotificationsProvider.logViewEvent(competencyId, name, 'competency', wsName, {}, siteId); - return site.write('core_competency_competency_viewed', params, preSets); + return site.write(wsName, params, preSets).then((success: boolean) => { + if (!success) { + return Promise.reject(null); + } + }); }); } return Promise.reject(null); } } + +/** + * Data returned by competency's plan_exporter. + */ +export type AddonCompetencyPlan = { + name: string; // Name. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + userid: number; // Userid. + templateid: number; // Templateid. + origtemplateid: number; // Origtemplateid. + status: number; // Status. + duedate: number; // Duedate. + reviewerid: number; // Reviewerid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + statusname: string; // Statusname. + isbasedontemplate: boolean; // Isbasedontemplate. + canmanage: boolean; // Canmanage. + canrequestreview: boolean; // Canrequestreview. + canreview: boolean; // Canreview. + canbeedited: boolean; // Canbeedited. + isactive: boolean; // Isactive. + isdraft: boolean; // Isdraft. + iscompleted: boolean; // Iscompleted. + isinreview: boolean; // Isinreview. + iswaitingforreview: boolean; // Iswaitingforreview. + isreopenallowed: boolean; // Isreopenallowed. + iscompleteallowed: boolean; // Iscompleteallowed. + isunlinkallowed: boolean; // Isunlinkallowed. + isrequestreviewallowed: boolean; // Isrequestreviewallowed. + iscancelreviewrequestallowed: boolean; // Iscancelreviewrequestallowed. + isstartreviewallowed: boolean; // Isstartreviewallowed. + isstopreviewallowed: boolean; // Isstopreviewallowed. + isapproveallowed: boolean; // Isapproveallowed. + isunapproveallowed: boolean; // Isunapproveallowed. + duedateformatted: string; // Duedateformatted. + commentarea: CoreCommentsArea; + reviewer?: CoreUserSummary; + template?: AddonCompetencyTemplate; + url: string; // Url. +}; + +/** + * Data returned by competency's template_exporter. + */ +export type AddonCompetencyTemplate = { + shortname: string; // Shortname. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + duedate: number; // Duedate. + visible: boolean; // Visible. + contextid: number; // Contextid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + duedateformatted: string; // Duedateformatted. + cohortscount: number; // Cohortscount. + planscount: number; // Planscount. + canmanage: boolean; // Canmanage. + canread: boolean; // Canread. + contextname: string; // Contextname. + contextnamenoprefix: string; // Contextnamenoprefix. +}; + +/** + * Data returned by competency's competency_exporter. + */ +export type AddonCompetencyCompetency = { + shortname: string; // Shortname. + idnumber: string; // Idnumber. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + sortorder: number; // Sortorder. + parentid: number; // Parentid. + path: string; // Path. + ruleoutcome: number; // Ruleoutcome. + ruletype: string; // Ruletype. + ruleconfig: string; // Ruleconfig. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + competencyframeworkid: number; // Competencyframeworkid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Data returned by competency's competency_path_exporter. + */ +export type AddonCompetencyPath = { + ancestors: AddonCompetencyPathNode[]; // Ancestors. + framework: AddonCompetencyPathNode; + pluginbaseurl: string; // Pluginbaseurl. + pagecontextid: number; // Pagecontextid. + showlinks: boolean; // @since 3.7. Showlinks. +}; + +/** + * Data returned by competency's path_node_exporter. + */ +export type AddonCompetencyPathNode = { + id: number; // Id. + name: string; // Name. + first: boolean; // First. + last: boolean; // Last. + position: number; // Position. +}; + +/** + * Data returned by competency's user_competency_exporter. + */ +export type AddonCompetencyUserCompetency = { + userid: number; // Userid. + competencyid: number; // Competencyid. + status: number; // Status. + reviewerid: number; // Reviewerid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + canrequestreview: boolean; // Canrequestreview. + canreview: boolean; // Canreview. + gradename: string; // Gradename. + isrequestreviewallowed: boolean; // Isrequestreviewallowed. + iscancelreviewrequestallowed: boolean; // Iscancelreviewrequestallowed. + isstartreviewallowed: boolean; // Isstartreviewallowed. + isstopreviewallowed: boolean; // Isstopreviewallowed. + isstatusidle: boolean; // Isstatusidle. + isstatusinreview: boolean; // Isstatusinreview. + isstatuswaitingforreview: boolean; // Isstatuswaitingforreview. + proficiencyname: string; // Proficiencyname. + reviewer?: CoreUserSummary; + statusname: string; // Statusname. + url: string; // Url. +}; + +/** + * Data returned by competency's user_competency_plan_exporter. + */ +export type AddonCompetencyUserCompetencyPlan = { + userid: number; // Userid. + competencyid: number; // Competencyid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + planid: number; // Planid. + sortorder: number; // Sortorder. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + gradename: string; // Gradename. + proficiencyname: string; // Proficiencyname. +}; + +/** + * Data returned by competency's user_competency_summary_in_plan_exporter. + */ +export type AddonCompetencyUserCompetencySummaryInPlan = { + usercompetencysummary: AddonCompetencyUserCompetencySummary; + plan: AddonCompetencyPlan; +}; + +/** + * Data returned by competency's user_competency_summary_exporter. + */ +export type AddonCompetencyUserCompetencySummary = { + showrelatedcompetencies: boolean; // Showrelatedcompetencies. + cangrade: boolean; // Cangrade. + competency: AddonCompetencySummary; + user: CoreUserSummary; + usercompetency?: AddonCompetencyUserCompetency; + usercompetencyplan?: AddonCompetencyUserCompetencyPlan; + usercompetencycourse?: AddonCompetencyUserCompetencyCourse; + evidence: AddonCompetencyEvidence[]; // Evidence. + commentarea?: CoreCommentsArea; +}; + +/** + * Data returned by competency's competency_summary_exporter. + */ +export type AddonCompetencySummary = { + linkedcourses: CoreCourseSummary; // Linkedcourses. + relatedcompetencies: AddonCompetencyCompetency[]; // Relatedcompetencies. + competency: AddonCompetencyCompetency; + framework: AddonCompetencyFramework; + hascourses: boolean; // Hascourses. + hasrelatedcompetencies: boolean; // Hasrelatedcompetencies. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + taxonomyterm: string; // Taxonomyterm. + comppath: AddonCompetencyPath; + pluginbaseurl: string; // @since 3.7. Pluginbaseurl. +}; + +/** + * Data returned by competency's competency_framework_exporter. + */ +export type AddonCompetencyFramework = { + shortname: string; // Shortname. + idnumber: string; // Idnumber. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + visible: boolean; // Visible. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + contextid: number; // Contextid. + taxonomies: string; // Taxonomies. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + canmanage: boolean; // Canmanage. + competenciescount: number; // Competenciescount. + contextname: string; // Contextname. + contextnamenoprefix: string; // Contextnamenoprefix. +}; + +/** + * Data returned by competency's user_competency_course_exporter. + */ +export type AddonCompetencyUserCompetencyCourse = { + userid: number; // Userid. + courseid: number; // Courseid. + competencyid: number; // Competencyid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + gradename: string; // Gradename. + proficiencyname: string; // Proficiencyname. +}; + +/** + * Data returned by competency's evidence_exporter. + */ +export type AddonCompetencyEvidence = { + usercompetencyid: number; // Usercompetencyid. + contextid: number; // Contextid. + action: number; // Action. + actionuserid: number; // Actionuserid. + descidentifier: string; // Descidentifier. + desccomponent: string; // Desccomponent. + desca: string; // Desca. + url: string; // Url. + grade: number; // Grade. + note: string; // Note. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + actionuser?: CoreUserSummary; + description: string; // Description. + gradename: string; // Gradename. + userdate: string; // Userdate. + candelete: boolean; // Candelete. +}; + +/** + * Data returned by competency's user_competency_summary_in_course_exporter. + */ +export type AddonCompetencyUserCompetencySummaryInCourse = { + usercompetencysummary: AddonCompetencyUserCompetencySummary; + course: CoreCourseSummary; + coursemodules: CoreCourseModuleSummary[]; // Coursemodules. + plans: AddonCompetencyPlan[]; // @since 3.7. Plans. + pluginbaseurl: string; // @since 3.7. Pluginbaseurl. +}; + +/** + * Data returned by competency's course_competency_settings_exporter. + */ +export type AddonCompetencyCourseCompetencySettings = { + courseid: number; // Courseid. + pushratingstouserplans: boolean; // Pushratingstouserplans. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Data returned by competency's course_competency_statistics_exporter. + */ +export type AddonCompetencyCourseCompetencyStatistics = { + competencycount: number; // Competencycount. + proficientcompetencycount: number; // Proficientcompetencycount. + proficientcompetencypercentage: number; // Proficientcompetencypercentage. + proficientcompetencypercentageformatted: string; // Proficientcompetencypercentageformatted. + leastproficient: AddonCompetencyCompetency[]; // Leastproficient. + leastproficientcount: number; // Leastproficientcount. + canbegradedincourse: boolean; // Canbegradedincourse. + canmanagecoursecompetencies: boolean; // Canmanagecoursecompetencies. +}; + +/** + * Data returned by competency's course_competency_exporter. + */ +export type AddonCompetencyCourseCompetency = { + courseid: number; // Courseid. + competencyid: number; // Competencyid. + sortorder: number; // Sortorder. + ruleoutcome: number; // Ruleoutcome. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Result of WS tool_lp_data_for_plans_page. + */ +export type AddonCompetencyDataForPlansPageResult = { + userid: number; // The learning plan user id. + plans: AddonCompetencyPlan[]; + pluginbaseurl: string; // Url to the tool_lp plugin folder on this Moodle site. + navigation: string[]; + canreaduserevidence: boolean; // Can the current user view the user's evidence. + canmanageuserplans: boolean; // Can the current user manage the user's plans. +}; + +/** + * Result of WS tool_lp_data_for_plan_page. + */ +export type AddonCompetencyDataForPlanPageResult = { + plan: AddonCompetencyPlan; + contextid: number; // Context ID. + pluginbaseurl: string; // Plugin base URL. + competencies: AddonCompetencyDataForPlanPageCompetency[]; + competencycount: number; // Count of competencies. + proficientcompetencycount: number; // Count of proficientcompetencies. + proficientcompetencypercentage: number; // Percentage of competencies proficient. + proficientcompetencypercentageformatted: string; // Displayable percentage. +}; + +/** + * Competency data returned by tool_lp_data_for_plan_page. + */ +export type AddonCompetencyDataForPlanPageCompetency = { + competency: AddonCompetencyCompetency; + comppath: AddonCompetencyPath; + usercompetency?: AddonCompetencyUserCompetency; + usercompetencyplan?: AddonCompetencyUserCompetencyPlan; +}; + +/** + * Result of WS tool_lp_data_for_course_competencies_page. + */ +export type AddonCompetencyDataForCourseCompetenciesPageResult = { + courseid: number; // The current course id. + pagecontextid: number; // The current page context ID. + gradableuserid?: number; // Current user id, if the user is a gradable user. + canmanagecompetencyframeworks: boolean; // User can manage competency frameworks. + canmanagecoursecompetencies: boolean; // User can manage linked course competencies. + canconfigurecoursecompetencies: boolean; // User can configure course competency settings. + cangradecompetencies: boolean; // User can grade competencies. + settings: AddonCompetencyCourseCompetencySettings; + statistics: AddonCompetencyCourseCompetencyStatistics; + competencies: AddonCompetencyDataForCourseCompetenciesPageCompetency[]; + manageurl: string; // Url to the manage competencies page. + pluginbaseurl: string; // @since 3.6. Url to the course competencies page. +}; + +/** + * Competency data returned by tool_lp_data_for_course_competencies_page. + */ +export type AddonCompetencyDataForCourseCompetenciesPageCompetency = { + competency: AddonCompetencyCompetency; + coursecompetency: AddonCompetencyCourseCompetency; + coursemodules: CoreCourseModuleSummary[]; + usercompetencycourse?: AddonCompetencyUserCompetencyCourse; + ruleoutcomeoptions: { + value: number; // The option value. + text: string; // The name of the option. + selected: boolean; // If this is the currently selected option. + }[]; + comppath: AddonCompetencyPath; + plans: AddonCompetencyPlan[]; // @since 3.7. +}; diff --git a/src/addon/coursecompletion/components/report/addon-course-completion-report.html b/src/addon/coursecompletion/components/report/addon-course-completion-report.html index 2b4b92070..54683082d 100644 --- a/src/addon/coursecompletion/components/report/addon-course-completion-report.html +++ b/src/addon/coursecompletion/components/report/addon-course-completion-report.html @@ -6,7 +6,7 @@

{{ 'addon.coursecompletion.status' | translate }}

-

{{ completion.statusText | translate }}

+

{{ statusText | translate }}

{{ 'addon.coursecompletion.required' | translate }}

diff --git a/src/addon/coursecompletion/components/report/report.ts b/src/addon/coursecompletion/components/report/report.ts index 3b6371244..640def927 100644 --- a/src/addon/coursecompletion/components/report/report.ts +++ b/src/addon/coursecompletion/components/report/report.ts @@ -15,7 +15,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { AddonCourseCompletionProvider } from '../../providers/coursecompletion'; +import { AddonCourseCompletionProvider, AddonCourseCompletionCourseCompletionStatus } from '../../providers/coursecompletion'; /** * Component that displays the course completion report. @@ -29,9 +29,10 @@ export class AddonCourseCompletionReportComponent implements OnInit { @Input() userId: number; completionLoaded = false; - completion: any; + completion: AddonCourseCompletionCourseCompletionStatus; showSelfComplete: boolean; tracked = true; // Whether completion is tracked. + statusText: string; constructor( private sitesProvider: CoreSitesProvider, @@ -59,7 +60,7 @@ export class AddonCourseCompletionReportComponent implements OnInit { protected fetchCompletion(): Promise { return this.courseCompletionProvider.getCompletion(this.courseId, this.userId).then((completion) => { - completion.statusText = this.courseCompletionProvider.getCompletedStatusText(completion); + this.statusText = this.courseCompletionProvider.getCompletedStatusText(completion); this.completion = completion; this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion); diff --git a/src/addon/coursecompletion/providers/coursecompletion.ts b/src/addon/coursecompletion/providers/coursecompletion.ts index 64b4c6f01..f303acddb 100644 --- a/src/addon/coursecompletion/providers/coursecompletion.ts +++ b/src/addon/coursecompletion/providers/coursecompletion.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle course completion. @@ -43,7 +44,7 @@ export class AddonCourseCompletionProvider { * @param completion Course completion. * @return True if user can mark course as self completed, false otherwise. */ - canMarkSelfCompleted(userId: number, completion: any): boolean { + canMarkSelfCompleted(userId: number, completion: AddonCourseCompletionCourseCompletionStatus): boolean { let selfCompletionActive = false, alreadyMarked = false; @@ -68,7 +69,7 @@ export class AddonCourseCompletionProvider { * @param completion Course completion. * @return Language code of the text to show. */ - getCompletedStatusText(completion: any): string { + getCompletedStatusText(completion: AddonCourseCompletionCourseCompletionStatus): string { if (completion.completed) { return 'addon.coursecompletion.completed'; } else { @@ -96,7 +97,9 @@ export class AddonCourseCompletionProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise to be resolved when the completion is retrieved. */ - getCompletion(courseId: number, userId?: number, preSets?: any, siteId?: string): Promise { + getCompletion(courseId: number, userId?: number, preSets?: any, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); preSets = preSets || {}; @@ -112,7 +115,9 @@ export class AddonCourseCompletionProvider { preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES; preSets.cacheErrors = ['notenroled']; - return site.read('core_completion_get_course_completion_status', data, preSets).then((data) => { + return site.read('core_completion_get_course_completion_status', data, preSets) + .then((data: AddonCourseCompletionGetCourseCompletionStatusResult): any => { + if (data.completionstatus) { return data.completionstatus; } @@ -243,17 +248,56 @@ export class AddonCourseCompletionProvider { * Mark a course as self completed. * * @param courseId Course ID. - * @return Resolved on success. + * @return Promise resolved on success. */ - markCourseAsSelfCompleted(courseId: number): Promise { + markCourseAsSelfCompleted(courseId: number): Promise { const params = { courseid: courseId }; - return this.sitesProvider.getCurrentSite().write('core_completion_mark_course_self_completed', params).then((response) => { + return this.sitesProvider.getCurrentSite().write('core_completion_mark_course_self_completed', params) + .then((response: AddonCourseCompletionMarkCourseSelfCompletedResult) => { + if (!response.status) { return Promise.reject(null); } }); } } + +/** + * Completion status returned by core_completion_get_course_completion_status. + */ +export type AddonCourseCompletionCourseCompletionStatus = { + completed: boolean; // True if the course is complete, false otherwise. + aggregation: number; // Aggregation method 1 means all, 2 means any. + completions: { + type: number; // Completion criteria type. + title: string; // Completion criteria Title. + status: string; // Completion status (Yes/No) a % or number. + complete: boolean; // Completion status (true/false). + timecompleted: number; // Timestamp for criteria completetion. + details: { + type: string; // Type description. + criteria: string; // Criteria description. + requirement: string; // Requirement description. + status: string; // Status description, can be anything. + }; // Details. + }[]; +}; + +/** + * Result of WS core_completion_get_course_completion_status. + */ +export type AddonCourseCompletionGetCourseCompletionStatusResult = { + completionstatus: AddonCourseCompletionCourseCompletionStatus; // Course status. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_completion_mark_course_self_completed. + */ +export type AddonCourseCompletionMarkCourseSelfCompletedResult = { + status: boolean; // Status, true if success. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/files/pages/list/list.ts b/src/addon/files/pages/list/list.ts index 24aeb0390..061025ed0 100644 --- a/src/addon/files/pages/list/list.ts +++ b/src/addon/files/pages/list/list.ts @@ -20,7 +20,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { AddonFilesProvider } from '../../providers/files'; +import { AddonFilesProvider, AddonFilesFile, AddonFilesGetUserPrivateFilesInfoResult } from '../../providers/files'; import { AddonFilesHelperProvider } from '../../providers/helper'; /** @@ -40,10 +40,10 @@ export class AddonFilesListPage implements OnDestroy { root: string; // The root of the files loaded: 'my' or 'site'. path: string; // The path of the directory being loaded. If empty path it means the root is being loaded. userQuota: number; // The user quota (in bytes). - filesInfo: any; // Info about private files (size, number of files, etc.). + filesInfo: AddonFilesGetUserPrivateFilesInfoResult; // Info about private files (size, number of files, etc.). spaceUsed: string; // Space used in a readable format. userQuotaReadable: string; // User quota in a readable format. - files: any[]; // List of files. + files: AddonFilesFile[]; // List of files. component: string; // Component to link the file downloads to. filesLoaded: boolean; // Whether the files are loaded. @@ -147,7 +147,7 @@ export class AddonFilesListPage implements OnDestroy { * @return Promise resolved when done. */ protected fetchFiles(): Promise { - let promise; + let promise: Promise; if (!this.path) { // The path is unknown, the user must be requesting a root. diff --git a/src/addon/files/providers/files.ts b/src/addon/files/providers/files.ts index f9051494f..05613b992 100644 --- a/src/addon/files/providers/files.ts +++ b/src/addon/files/providers/files.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle my files and site files. @@ -73,7 +74,7 @@ export class AddonFilesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the files. */ - getFiles(params: any, siteId?: string): Promise { + getFiles(params: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { @@ -82,15 +83,15 @@ export class AddonFilesProvider { }; return site.read('core_files_get_files', params, preSets); - }).then((result) => { - const entries = []; + }).then((result: AddonFilesGetFilesResult) => { + const entries: AddonFilesFile[] = []; if (result.files) { result.files.forEach((entry) => { if (entry.isdir) { // Create a "link" to load the folder. entry.link = { - contextid: entry.contextid || '', + contextid: entry.contextid || null, component: entry.component || '', filearea: entry.filearea || '', itemid: entry.itemid || 0, @@ -135,7 +136,7 @@ export class AddonFilesProvider { * * @return Promise resolved with the files. */ - getPrivateFiles(): Promise { + getPrivateFiles(): Promise { return this.getFiles(this.getPrivateFilesRootParams()); } @@ -164,7 +165,7 @@ export class AddonFilesProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the info. */ - getPrivateFilesInfo(userId?: number, siteId?: string): Promise { + getPrivateFilesInfo(userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -204,7 +205,7 @@ export class AddonFilesProvider { * * @return Promise resolved with the files. */ - getSiteFiles(): Promise { + getSiteFiles(): Promise { return this.getFiles(this.getSiteFilesRootParams()); } @@ -388,7 +389,7 @@ export class AddonFilesProvider { * @param siteid ID of the site. If not defined, use current site. * @return Promise resolved in success, rejected otherwise. */ - moveFromDraftToPrivate(draftId: number, siteId?: string): Promise { + moveFromDraftToPrivate(draftId: number, siteId?: string): Promise { const params = { draftid: draftId }, @@ -414,3 +415,63 @@ export class AddonFilesProvider { }); } } + +/** + * File data returned by core_files_get_files. + */ +export type AddonFilesFile = { + contextid: number; + component: string; + filearea: string; + itemid: number; + filepath: string; + filename: string; + isdir: boolean; + url: string; + timemodified: number; + timecreated?: number; // Time created. + filesize?: number; // File size. + author?: string; // File owner. + license?: string; // File license. +} & AddonFilesFileCalculatedData; + +/** + * Result of WS core_files_get_files. + */ +export type AddonFilesGetFilesResult = { + parents: { + contextid: number; + component: string; + filearea: string; + itemid: number; + filepath: string; + filename: string; + }[]; + files: AddonFilesFile[]; +}; + +/** + * Result of WS core_user_get_private_files_info. + */ +export type AddonFilesGetUserPrivateFilesInfoResult = { + filecount: number; // Number of files in the area. + foldercount: number; // Number of folders in the area. + filesize: number; // Total size of the files in the area. + filesizewithoutreferences: number; // Total size of the area excluding file references. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Calculated data for AddonFilesFile. + */ +export type AddonFilesFileCalculatedData = { + link?: { // Calculated in the app. A link to open the folder. + contextid?: number; // Folder's contextid. + component?: string; // Folder's component. + filearea?: string; // Folder's filearea. + itemid?: number; // Folder's itemid. + filepath?: string; // Folder's filepath. + filename?: string; // Folder's filename. + }; + imgPath?: string; // Path to file icon's image. +}; diff --git a/src/addon/messageoutput/airnotifier/pages/devices/devices.ts b/src/addon/messageoutput/airnotifier/pages/devices/devices.ts index a9b581f37..dfe0e5403 100644 --- a/src/addon/messageoutput/airnotifier/pages/devices/devices.ts +++ b/src/addon/messageoutput/airnotifier/pages/devices/devices.ts @@ -16,7 +16,7 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; -import { AddonMessageOutputAirnotifierProvider } from '../../providers/airnotifier'; +import { AddonMessageOutputAirnotifierProvider, AddonMessageOutputAirnotifierDevice } from '../../providers/airnotifier'; /** * Page that displays the list of devices. @@ -28,7 +28,7 @@ import { AddonMessageOutputAirnotifierProvider } from '../../providers/airnotifi }) export class AddonMessageOutputAirnotifierDevicesPage implements OnDestroy { - devices = []; + devices: AddonMessageOutputAirnotifierDeviceFormatted[] = []; devicesLoaded = false; protected updateTimeout: any; @@ -54,7 +54,7 @@ export class AddonMessageOutputAirnotifierDevicesPage implements OnDestroy { const pushId = this.pushNotificationsProvider.getPushId(); // Convert enabled to boolean and search current device. - devices.forEach((device) => { + devices.forEach((device: AddonMessageOutputAirnotifierDeviceFormatted) => { device.enable = !!device.enable; device.current = pushId && pushId == device.pushid; }); @@ -110,8 +110,9 @@ export class AddonMessageOutputAirnotifierDevicesPage implements OnDestroy { * @param device The device object. * @param enable True to enable the device, false to disable it. */ - enableDevice(device: any, enable: boolean): void { + enableDevice(device: AddonMessageOutputAirnotifierDeviceFormatted, enable: boolean): void { device.updating = true; + this.airnotifierProivder.enableDevice(device.id, enable).then(() => { // Update the list of devices since it was modified. this.updateDevicesAfterDelay(); @@ -135,3 +136,11 @@ export class AddonMessageOutputAirnotifierDevicesPage implements OnDestroy { } } } + +/** + * User device with some calculated data. + */ +type AddonMessageOutputAirnotifierDeviceFormatted = AddonMessageOutputAirnotifierDevice & { + current?: boolean; // Calculated in the app. Whether it's the current device. + updating?: boolean; // Calculated in the app. Whether the device enable is being updated right now. +}; diff --git a/src/addon/messageoutput/airnotifier/providers/airnotifier.ts b/src/addon/messageoutput/airnotifier/providers/airnotifier.ts index e67e134bb..ff810dfc0 100644 --- a/src/addon/messageoutput/airnotifier/providers/airnotifier.ts +++ b/src/addon/messageoutput/airnotifier/providers/airnotifier.ts @@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle Airnotifier message output. @@ -39,14 +40,16 @@ export class AddonMessageOutputAirnotifierProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success. */ - enableDevice(deviceId: number, enable: boolean, siteId?: string): Promise { + enableDevice(deviceId: number, enable: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const data = { deviceid: deviceId, enable: enable ? 1 : 0 }; - return site.write('message_airnotifier_enable_device', data).then((result) => { + return site.write('message_airnotifier_enable_device', data) + .then((result: AddonMessageOutputAirnotifierEnableDeviceResult) => { + if (!result.success) { // Fail. Reject with warning message if any. if (result.warnings && result.warnings.length) { @@ -74,7 +77,7 @@ export class AddonMessageOutputAirnotifierProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the devices. */ - getUserDevices(siteId?: string): Promise { + getUserDevices(siteId?: string): Promise { this.logger.debug('Get user devices'); return this.sitesProvider.getSite(siteId).then((site) => { @@ -86,7 +89,8 @@ export class AddonMessageOutputAirnotifierProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('message_airnotifier_get_user_devices', data, preSets).then((data) => { + return site.read('message_airnotifier_get_user_devices', data, preSets) + .then((data: AddonMessageOutputAirnotifierGetUserDevicesResult) => { return data.devices; }); }); @@ -115,3 +119,36 @@ export class AddonMessageOutputAirnotifierProvider { this.sitesProvider.wsAvailableInCurrentSite('message_airnotifier_get_user_devices'); } } + +/** + * Device data returned by WS message_airnotifier_get_user_devices. + */ +export type AddonMessageOutputAirnotifierDevice = { + id: number; // Device id (in the message_airnotifier table). + appid: string; // The app id, something like com.moodle.moodlemobile. + name: string; // The device name, 'occam' or 'iPhone' etc. + model: string; // The device model 'Nexus4' or 'iPad1,1' etc. + platform: string; // The device platform 'iOS' or 'Android' etc. + version: string; // The device version '6.1.2' or '4.2.2' etc. + pushid: string; // The device PUSH token/key/identifier/registration id. + uuid: string; // The device UUID. + enable: number | boolean; // Whether the device is enabled or not. + timecreated: number; // Time created. + timemodified: number; // Time modified. +}; + +/** + * Result of WS message_airnotifier_enable_device. + */ +export type AddonMessageOutputAirnotifierEnableDeviceResult = { + success: boolean; // True if success. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS message_airnotifier_get_user_devices. + */ +export type AddonMessageOutputAirnotifierGetUserDevicesResult = { + devices: AddonMessageOutputAirnotifierDevice[]; // List of devices. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts b/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts index 53983afe9..b367bdee3 100644 --- a/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts +++ b/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@ import { Content } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { AddonMessagesProvider, AddonMessagesConversationMember } from '../../providers/messages'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** @@ -33,7 +33,7 @@ export class AddonMessagesConfirmedContactsComponent implements OnInit, OnDestro loaded = false; canLoadMore = false; loadMoreError = false; - contacts = []; + contacts: AddonMessagesConversationMember[] = []; selectedUserId: number; protected memberInfoObserver; diff --git a/src/addon/messages/components/contact-requests/contact-requests.ts b/src/addon/messages/components/contact-requests/contact-requests.ts index 1eb5aba36..50f1afffe 100644 --- a/src/addon/messages/components/contact-requests/contact-requests.ts +++ b/src/addon/messages/components/contact-requests/contact-requests.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@ import { Content } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { AddonMessagesProvider, AddonMessagesConversationMember } from '../../providers/messages'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** @@ -33,7 +33,7 @@ export class AddonMessagesContactRequestsComponent implements OnInit, OnDestroy loaded = false; canLoadMore = false; loadMoreError = false; - requests = []; + requests: AddonMessagesConversationMember[] = []; selectedUserId: number; protected memberInfoObserver; diff --git a/src/addon/messages/components/contacts/contacts.ts b/src/addon/messages/components/contacts/contacts.ts index 311196532..16f5a62d3 100644 --- a/src/addon/messages/components/contacts/contacts.ts +++ b/src/addon/messages/components/contacts/contacts.ts @@ -16,7 +16,9 @@ import { Component } from '@angular/core'; import { NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { + AddonMessagesProvider, AddonMessagesGetContactsResult, AddonMessagesSearchContactsContact +} from '../../providers/messages'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; @@ -42,7 +44,10 @@ export class AddonMessagesContactsComponent { searchType = 'search'; loadingMessage = ''; hasContacts = false; - contacts = { + contacts: AddonMessagesGetContactsFormatted = { + online: [], + offline: [], + strangers: [], search: [] }; searchString = ''; @@ -205,7 +210,7 @@ export class AddonMessagesContactsComponent { this.searchString = query; this.contactTypes = ['search']; - this.contacts['search'] = this.sortUsers(result); + this.contacts.search = this.sortUsers(result); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); }); @@ -234,3 +239,10 @@ export class AddonMessagesContactsComponent { this.memberInfoObserver && this.memberInfoObserver.off(); } } + +/** + * Contacts with some calculated data. + */ +export type AddonMessagesGetContactsFormatted = AddonMessagesGetContactsResult & { + search?: AddonMessagesSearchContactsContact[]; // Calculated in the app. Result of searching users. +}; diff --git a/src/addon/messages/pages/conversation-info/conversation-info.ts b/src/addon/messages/pages/conversation-info/conversation-info.ts index eb8046fc1..c3d030c6f 100644 --- a/src/addon/messages/pages/conversation-info/conversation-info.ts +++ b/src/addon/messages/pages/conversation-info/conversation-info.ts @@ -14,7 +14,9 @@ import { Component, OnInit } from '@angular/core'; import { IonicPage, NavParams, ViewController } from 'ionic-angular'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { + AddonMessagesProvider, AddonMessagesConversationFormatted, AddonMessagesConversationMember +} from '../../providers/messages'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** @@ -28,8 +30,8 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; export class AddonMessagesConversationInfoPage implements OnInit { loaded = false; - conversation: any; - members = []; + conversation: AddonMessagesConversationFormatted; + members: AddonMessagesConversationMember[] = []; canLoadMore = false; loadMoreError = false; diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index ef908e1c9..e6fed3ca2 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -17,7 +17,10 @@ import { IonicPage, NavParams, NavController, Content, ModalController } from 'i import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { + AddonMessagesProvider, AddonMessagesConversationFormatted, AddonMessagesConversationMember, AddonMessagesConversationMessage, + AddonMessagesGetMessagesMessage +} from '../../providers/messages'; import { AddonMessagesOfflineProvider } from '../../providers/messages-offline'; import { AddonMessagesSyncProvider } from '../../providers/sync'; import { CoreUserProvider } from '@core/user/providers/user'; @@ -54,7 +57,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { protected messagesBeingSent = 0; protected pagesLoaded = 1; protected lastMessage = {text: '', timecreated: 0}; - protected keepMessageMap = {}; + protected keepMessageMap: {[hash: string]: boolean} = {}; protected syncObserver: any; protected oldContentHeight = 0; protected keyboardObserver: any; @@ -64,7 +67,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { protected showLoadingModal = false; // Whether to show a loading modal while fetching data. conversationId: number; // Conversation ID. Undefined if it's a new individual conversation. - conversation: any; // The conversation object (if it exists). + conversation: AddonMessagesConversationFormatted; // The conversation object (if it exists). userId: number; // User ID you're talking to (only if group messaging not enabled or it's a new individual conversation). currentUserId: number; title: string; @@ -74,18 +77,18 @@ export class AddonMessagesDiscussionPage implements OnDestroy { showKeyboard = false; canLoadMore = false; loadMoreError = false; - messages = []; + messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[] = []; showDelete = false; canDelete = false; groupMessagingEnabled: boolean; isGroup = false; - members: any = {}; // Members that wrote a message, indexed by ID. + members: {[id: number]: AddonMessagesConversationMember} = {}; // Members that wrote a message, indexed by ID. favouriteIcon = 'fa-star'; favouriteIconSlash = false; deleteIcon = 'trash'; blockIcon = 'close-circle'; addRemoveIcon = 'person'; - otherMember: any; // Other member information (individual conversations only). + otherMember: AddonMessagesConversationMember; // Other member information (individual conversations only). footerType: 'message' | 'blocked' | 'requiresContact' | 'requestSent' | 'requestReceived' | 'unable'; requestContactSent = false; requestContactReceived = false; @@ -139,7 +142,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param message Message to be added. * @param keep If set the keep flag or not. */ - protected addMessage(message: any, keep: boolean = true): void { + protected addMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, + keep: boolean = true): void { + /* Create a hash to identify the message. The text of online messages isn't reliable because it can have random data like VideoJS ID. Try to use id and fallback to text for offline messages. */ message.hash = Md5.hashAsciiStr(String(message.id || message.text || '')) + '#' + message.timecreated + '#' + @@ -158,7 +163,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * * @param hash Hash of the message to be removed. */ - protected removeMessage(hash: any): void { + protected removeMessage(hash: string): void { if (this.keepMessageMap[hash]) { // Selected to keep it, clear the flag. this.keepMessageMap[hash] = false; @@ -261,10 +266,11 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (!this.title && this.messages.length) { // Didn't receive the fullname via argument. Try to get it from messages. // It's possible that name cannot be resolved when no messages were yet exchanged. - if (this.messages[0].useridto != this.currentUserId) { - this.title = this.messages[0].usertofullname || ''; + const firstMessage = this.messages[0]; + if (firstMessage.useridto != this.currentUserId) { + this.title = firstMessage.usertofullname || ''; } else { - this.title = this.messages[0].userfromfullname || ''; + this.title = firstMessage.userfromfullname || ''; } } }); @@ -302,7 +308,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * * @return Resolved when done. */ - protected fetchMessages(): Promise { + protected fetchMessages(): Promise { this.loadMoreError = false; if (this.messagesBeingSent > 0) { @@ -341,7 +347,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return this.getDiscussionMessages(this.pagesLoaded); }); } - }).then((messages) => { + }).then((messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) => { this.loadMessages(messages); }).finally(() => { this.fetching = false; @@ -353,7 +359,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * * @param messages Messages to load. */ - protected loadMessages(messages: any[]): void { + protected loadMessages(messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) + : void { + if (this.viewDestroyed) { return; } @@ -382,7 +390,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.messagesProvider.sortMessages(this.messages); // Calculate which messages need to display the date or user data. - this.messages.forEach((message, index): any => { + this.messages.forEach((message, index) => { message.showDate = this.showDate(message, this.messages[index - 1]); message.showUserData = this.showUserData(message, this.messages[index - 1]); message.showTail = this.showTail(message, this.messages[index + 1]); @@ -411,20 +419,22 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @return Promise resolved with a boolean: whether the conversation exists or not. */ protected getConversation(conversationId: number, userId: number): Promise { - let promise, - fallbackConversation; + let promise: Promise, + fallbackConversation: AddonMessagesConversationFormatted; // Try to get the conversationId if we don't have it. if (conversationId) { promise = Promise.resolve(conversationId); } else { + let subPromise: Promise; + if (userId == this.currentUserId && this.messagesProvider.isSelfConversationEnabled()) { - promise = this.messagesProvider.getSelfConversation(); + subPromise = this.messagesProvider.getSelfConversation(); } else { - promise = this.messagesProvider.getConversationBetweenUsers(userId, undefined, true); + subPromise = this.messagesProvider.getConversationBetweenUsers(userId, undefined, true); } - promise = promise.then((conversation) => { + promise = subPromise.then((conversation) => { fallbackConversation = conversation; return conversation.id; @@ -437,14 +447,14 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Ignore errors. }).then(() => { return this.messagesProvider.getConversation(conversationId, undefined, true); - }).catch((error) => { + }).catch((error): any => { // Get conversation failed, use the fallback one if we have it. if (fallbackConversation) { return fallbackConversation; } return Promise.reject(error); - }).then((conversation) => { + }).then((conversation: AddonMessagesConversationFormatted) => { this.conversation = conversation; if (conversation) { @@ -495,7 +505,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param offset Offset for message list. * @return Promise resolved with the list of messages. */ - protected getConversationMessages(pagesToLoad: number, offset: number = 0): Promise { + protected getConversationMessages(pagesToLoad: number, offset: number = 0) + : Promise { + const excludePending = offset > 0; return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => { @@ -535,7 +547,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @return Resolved when done. */ protected getDiscussionMessages(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, - lfSentUnread: number = 0, lfSentRead: number = 0): Promise { + lfSentUnread: number = 0, lfSentRead: number = 0): Promise { // Only get offline messages if we're loading the first "page". const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0; @@ -547,7 +559,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { pagesToLoad--; if (pagesToLoad > 0 && result.canLoadMore) { // More pages to load. Calculate new limit froms. - result.messages.forEach((message) => { + result.messages.forEach((message: AddonMessagesGetMessagesMessageFormatted) => { if (!message.pending) { if (message.useridfrom == this.userId) { if (message.read) { @@ -598,7 +610,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { for (const x in this.messages) { const message = this.messages[x]; // If an unread message is found, mark all messages as read. - if (message.useridfrom != this.currentUserId && message.read == 0) { + if (message.useridfrom != this.currentUserId && + ( message).read == 0) { messageUnreadFound = true; break; } @@ -616,7 +629,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { promise = this.messagesProvider.markAllMessagesRead(this.userId).then(() => { // Mark all messages as read. this.messages.forEach((message) => { - message.read = 1; + ( message).read = 1; }); }); } @@ -630,10 +643,10 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Mark each message as read one by one. this.messages.forEach((message) => { // If the message is unread, call this.messagesProvider.markMessageRead. - if (message.useridfrom != this.currentUserId && message.read == 0) { + if (message.useridfrom != this.currentUserId && ( message).read == 0) { promises.push(this.messagesProvider.markMessageRead(message.id).then(() => { readChanged = true; - message.read = 1; + ( message).read = 1; })); } }); @@ -703,7 +716,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (!message.pending && message.useridfrom != this.currentUserId) { found++; if (found == this.conversation.unreadcount) { - this.unreadMessageFrom = parseInt(message.id, 10); + this.unreadMessageFrom = Number(message.id); break; } } @@ -713,13 +726,13 @@ export class AddonMessagesDiscussionPage implements OnDestroy { let previousMessageRead = false; for (const x in this.messages) { - const message = this.messages[x]; + const message = this.messages[x]; if (message.useridfrom != this.currentUserId) { const unreadFrom = message.read == 0 && previousMessageRead; if (unreadFrom) { // Save where the label is placed. - this.unreadMessageFrom = parseInt(message.id, 10); + this.unreadMessageFrom = Number(message.id); break; } @@ -808,8 +821,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * * @param message Message to be copied. */ - copyMessage(message: any): void { - const text = this.textUtils.decodeHTMLEntities(message.smallmessage || message.text || ''); + copyMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted): void { + const text = this.textUtils.decodeHTMLEntities( + ( message).smallmessage || message.text || ''); this.utils.copyToClipboard(text); } @@ -819,7 +833,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param message Message object to delete. * @param index Index where the message is to delete it from the view. */ - deleteMessage(message: any, index: number): void { + deleteMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, index: number) + : void { + const canDeleteAll = this.conversation && this.conversation.candeletemessagesforallusers, langKey = message.pending || canDeleteAll || this.isSelf ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation', @@ -860,7 +876,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. * @return Resolved when done. */ - loadPrevious(infiniteComplete?: any): Promise { + loadPrevious(infiniteComplete?: any): Promise { let infiniteHeight = this.infinite ? this.infinite.getHeight() : 0; const scrollHeight = this.domUtils.getScrollHeight(this.content); @@ -962,7 +978,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param text Message text. */ sendMessage(text: string): void { - let message; + let message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted; this.hideUnreadLabel(); @@ -970,6 +986,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.scrollBottom = true; message = { + id: null, pending: true, sending: true, useridfrom: this.currentUserId, @@ -985,7 +1002,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // If there is an ongoing fetch, wait for it to finish. // Otherwise, if a message is sent while fetching it could disappear until the next fetch. this.waitForFetch().finally(() => { - let promise; + let promise: Promise<{sent: boolean, message: any}>; if (this.conversationId) { promise = this.messagesProvider.sendMessageToConversation(this.conversation, text); @@ -1050,7 +1067,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param prevMessage Previous message where to compare the date with. * @return If date has changed and should be shown. */ - showDate(message: any, prevMessage?: any): boolean { + showDate(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, + prevMessage?: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted): boolean { + if (!prevMessage) { // First message, show it. return true; @@ -1068,7 +1087,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param prevMessage Previous message. * @return Whether user data should be shown. */ - showUserData(message: any, prevMessage?: any): boolean { + showUserData(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, + prevMessage?: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted): boolean { + return this.isGroup && message.useridfrom != this.currentUserId && this.members[message.useridfrom] && (!prevMessage || prevMessage.useridfrom != message.useridfrom || message.showDate); } @@ -1080,7 +1101,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param nextMessage Next message. * @return Whether user data should be shown. */ - showTail(message: any, nextMessage?: any): boolean { + showTail(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, + nextMessage?: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted): boolean { return !nextMessage || nextMessage.useridfrom != message.useridfrom || nextMessage.showDate; } @@ -1422,3 +1444,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.viewDestroyed = true; } } + +/** + * Conversation message with some calculated data. + */ +type AddonMessagesConversationMessageFormatted = AddonMessagesConversationMessage & { + pending?: boolean; // Calculated in the app. Whether the message is pending to be sent. + sending?: boolean; // Calculated in the app. Whether the message is being sent right now. + hash?: string; // Calculated in the app. A hash to identify the message. + showDate?: boolean; // Calculated in the app. Whether to show the date before the message. + showUserData?: boolean; // Calculated in the app. Whether to show the user data in the message. + showTail?: boolean; // Calculated in the app. Whether to show a "tail" in the message. +}; + +/** + * Message with some calculated data. + */ +type AddonMessagesGetMessagesMessageFormatted = AddonMessagesGetMessagesMessage & { + sending?: boolean; // Calculated in the app. Whether the message is being sent right now. + hash?: string; // Calculated in the app. A hash to identify the message. + showDate?: boolean; // Calculated in the app. Whether to show the date before the message. + showUserData?: boolean; // Calculated in the app. Whether to show the user data in the message. + showTail?: boolean; // Calculated in the app. Whether to show a "tail" in the message. +}; diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index e35485724..007d55b8f 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -17,7 +17,9 @@ import { IonicPage, Platform, NavController, NavParams, Content } from 'ionic-an import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { + AddonMessagesProvider, AddonMessagesConversationFormatted, AddonMessagesConversationMessage +} from '../../providers/messages'; import { AddonMessagesOfflineProvider } from '../../providers/messages-offline'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -45,19 +47,19 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { selectedConversationId: number; selectedUserId: number; contactRequestsCount = 0; - favourites: any = { + favourites: AddonMessagesGroupConversationOption = { type: null, favourites: true, count: 0, - unread: 0 + unread: 0, }; - group: any = { + group: AddonMessagesGroupConversationOption = { type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, favourites: false, count: 0, unread: 0 }; - individual: any = { + individual: AddonMessagesGroupConversationOption = { type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, favourites: false, count: 0, @@ -331,7 +333,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * * @return Promise resolved when done. */ - protected fetchDataForExpandedOption(): Promise { + protected fetchDataForExpandedOption(): Promise { const expandedOption = this.getExpandedOption(); if (expandedOption) { @@ -349,12 +351,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param getCounts Whether to get counts data. * @return Promise resolved when done. */ - fetchDataForOption(option: any, loadingMore?: boolean, getCounts?: boolean): Promise { + fetchDataForOption(option: AddonMessagesGroupConversationOption, loadingMore?: boolean, getCounts?: boolean): Promise { option.loadMoreError = false; const limitFrom = loadingMore ? option.conversations.length : 0, promises = []; - let data, + let data: {conversations: AddonMessagesConversationForList[], canLoadMore: boolean}, offlineMessages; // Get the conversations and, if needed, the offline messages. Always try to get the latest data. @@ -422,7 +424,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param option The option to search in. If not defined, search in all options. * @return Conversation. */ - protected findConversation(conversationId: number, userId?: number, option?: any): any { + protected findConversation(conversationId: number, userId?: number, option?: AddonMessagesGroupConversationOption) + : AddonMessagesConversationForList { + if (conversationId) { const conversations = option ? (option.conversations || []) : ((this.favourites.conversations || []) .concat(this.group.conversations || []).concat(this.individual.conversations || [])); @@ -445,7 +449,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * * @return Option currently expanded. */ - protected getExpandedOption(): any { + protected getExpandedOption(): AddonMessagesGroupConversationOption { if (this.favourites.expanded) { return this.favourites; } else if (this.group.expanded) { @@ -495,9 +499,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * * @param option The option to fetch data for. * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. - * @return Resolved when done. + * @return Promise resolved when done. */ - loadMoreConversations(option: any, infiniteComplete?: any): Promise { + loadMoreConversations(option: AddonMessagesGroupConversationOption, infiniteComplete?: any): Promise { return this.fetchDataForOption(option, true).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); option.loadMoreError = true; @@ -513,7 +517,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param messages Offline messages. * @return Promise resolved when done. */ - protected loadOfflineMessages(option: any, messages: any[]): Promise { + protected loadOfflineMessages(option: AddonMessagesGroupConversationOption, messages: any[]): Promise { const promises = []; messages.forEach((message) => { @@ -588,7 +592,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param conversation Conversation where to put the last message. * @param message Offline message to add. */ - protected addLastOfflineMessage(conversation: any, message: any): void { + protected addLastOfflineMessage(conversation: any, message: AddonMessagesConversationMessage): void { conversation.lastmessage = message.text; conversation.lastmessagedate = message.timecreated / 1000; conversation.lastmessagepending = true; @@ -601,7 +605,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param conversation Conversation to check. * @return Option object. */ - protected getConversationOption(conversation: any): any { + protected getConversationOption(conversation: AddonMessagesConversationForList): AddonMessagesGroupConversationOption { if (conversation.isfavourite) { return this.favourites; } else if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { @@ -618,7 +622,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param refreshUnreadCounts Whether to refresh unread counts. * @return Promise resolved when done. */ - refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise { + refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise { // Don't invalidate conversations and so, they always try to get latest data. const promises = [ this.messagesProvider.invalidateContactRequestsCountCache(this.siteId) @@ -638,7 +642,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * * @param option The option to expand/collapse. */ - toggle(option: any): void { + toggle(option: AddonMessagesGroupConversationOption): void { if (option.expanded) { // Already expanded, close it. option.expanded = false; @@ -658,7 +662,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @param getCounts Whether to get counts data. * @return Promise resolved when done. */ - protected expandOption(option: any, getCounts?: boolean): Promise { + protected expandOption(option: AddonMessagesGroupConversationOption, getCounts?: boolean): Promise { // Collapse all and expand the right one. this.favourites.expanded = false; this.group.expanded = false; @@ -715,3 +719,25 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.memberInfoObserver && this.memberInfoObserver.off(); } } + +/** + * Conversation options. + */ +export type AddonMessagesGroupConversationOption = { + type: number; // Option type. + favourites: boolean; // Whether it contains favourites conversations. + count: number; // Number of conversations. + unread?: number; // Number of unread conversations. + expanded?: boolean; // Whether the option is currently expanded. + loading?: boolean; // Whether the option is being loaded. + canLoadMore?: boolean; // Whether it can load more data. + loadMoreError?: boolean; // Whether there was an error loading more conversations. + conversations?: AddonMessagesConversationForList[]; // List of conversations. +}; + +/** + * Formatted conversation with some calculated data for the list. + */ +export type AddonMessagesConversationForList = AddonMessagesConversationFormatted & { + lastmessagepending?: boolean; // Calculated in the app. Whether last message is pending to be sent. +}; diff --git a/src/addon/messages/pages/search/search.ts b/src/addon/messages/pages/search/search.ts index 54e5a74c7..9268f58f0 100644 --- a/src/addon/messages/pages/search/search.ts +++ b/src/addon/messages/pages/search/search.ts @@ -16,7 +16,7 @@ import { Component, OnDestroy, ViewChild } from '@angular/core'; import { IonicPage } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { AddonMessagesProvider, AddonMessagesConversationMember, AddonMessagesMessageAreaContact } from '../../providers/messages'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreAppProvider } from '@providers/app'; @@ -38,21 +38,21 @@ export class AddonMessagesSearchPage implements OnDestroy { contacts = { type: 'contacts', titleString: 'addon.messages.contacts', - results: [], + results: [], canLoadMore: false, loadingMore: false }; nonContacts = { type: 'noncontacts', titleString: 'addon.messages.noncontacts', - results: [], + results: [], canLoadMore: false, loadingMore: false }; messages = { type: 'messages', titleString: 'addon.messages.messages', - results: [], + results: [], canLoadMore: false, loadingMore: false, loadMoreError: false @@ -116,9 +116,9 @@ export class AddonMessagesSearchPage implements OnDestroy { this.displaySearching = !loadMore; const promises = []; - let newContacts = []; - let newNonContacts = []; - let newMessages = []; + let newContacts: AddonMessagesConversationMember[] = []; + let newNonContacts: AddonMessagesConversationMember[] = []; + let newMessages: AddonMessagesMessageAreaContact[] = []; let canLoadMoreContacts = false; let canLoadMoreNonContacts = false; let canLoadMoreMessages = false; diff --git a/src/addon/messages/pages/settings/settings.ts b/src/addon/messages/pages/settings/settings.ts index b2eed0099..1a8d9de5e 100644 --- a/src/addon/messages/pages/settings/settings.ts +++ b/src/addon/messages/pages/settings/settings.ts @@ -14,7 +14,10 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage } from 'ionic-angular'; -import { AddonMessagesProvider } from '../../providers/messages'; +import { + AddonMessagesProvider, AddonMessagesMessagePreferences, AddonMessagesMessagePreferencesNotification, + AddonMessagesMessagePreferencesNotificationProcessor +} from '../../providers/messages'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreAppProvider } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; @@ -34,7 +37,7 @@ import { CoreConstants } from '@core/constants'; export class AddonMessagesSettingsPage implements OnDestroy { protected updateTimeout: any; - preferences: any; + preferences: AddonMessagesMessagePreferences; preferencesLoaded: boolean; contactablePrivacy: number | boolean; advancedContactable = false; // Whether the site supports "advanced" contactable privacy. @@ -78,9 +81,9 @@ export class AddonMessagesSettingsPage implements OnDestroy { /** * Fetches preference data. * - * @return Resolved when done. + * @return Promise resolved when done. */ - protected fetchPreferences(): Promise { + protected fetchPreferences(): Promise { return this.messagesProvider.getMessagePreferences().then((preferences) => { if (this.groupMessagingEnabled) { // Simplify the preferences. @@ -90,11 +93,12 @@ export class AddonMessagesSettingsPage implements OnDestroy { return notification.preferencekey == AddonMessagesProvider.NOTIFICATION_PREFERENCES_KEY; }); - for (const notification of component.notifications) { - for (const processor of notification.processors) { + component.notifications.forEach((notification) => { + notification.processors.forEach( + (processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted) => { processor.checked = processor.loggedin.checked || processor.loggedoff.checked; - } - } + }); + }); } } @@ -168,14 +172,16 @@ export class AddonMessagesSettingsPage implements OnDestroy { * @param state State name, ['loggedin', 'loggedoff']. * @param processor Notification processor. */ - changePreference(notification: any, state: string, processor: any): void { + changePreference(notification: AddonMessagesMessagePreferencesNotificationFormatted, state: string, + processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted): void { + if (this.groupMessagingEnabled) { // Update both states at the same time. const valueArray = [], promises = []; let value = 'none'; - notification.processors.forEach((processor) => { + notification.processors.forEach((processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted) => { if (processor.checked) { valueArray.push(processor.name); } @@ -268,3 +274,17 @@ export class AddonMessagesSettingsPage implements OnDestroy { } } } + +/** + * Message preferences notification with some caclulated data. + */ +type AddonMessagesMessagePreferencesNotificationFormatted = AddonMessagesMessagePreferencesNotification & { + updating?: boolean | {[state: string]: boolean}; // Calculated in the app. Whether the notification is being updated. +}; + +/** + * Message preferences notification processor with some caclulated data. + */ +type AddonMessagesMessagePreferencesNotificationProcessorFormatted = AddonMessagesMessagePreferencesNotificationProcessor & { + checked?: boolean; // Calculated in the app. Whether the processor is checked either for loggedin or loggedoff. +}; diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index b39725450..f7e0547f2 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -248,7 +248,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr } const currentUserId = site.getUserId(), - message = conv.messages[0]; // Treat only the last message, is the one we're interested. + message: any = conv.messages[0]; // Treat only the last message, is the one we're interested. if (!message || message.useridfrom == currentUserId) { // No last message or not from current user. Return empty list. diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index a16d9810f..7b7d19447 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -23,6 +23,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; import { CoreEventsProvider } from '@providers/events'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle messages. @@ -89,9 +90,9 @@ export class AddonMessagesProvider { * * @param userId User ID of the person to block. * @param siteId Site ID. If not defined, use current site. - * @return Resolved when done. + * @return Promise resolved when done. */ - blockContact(userId: number, siteId?: string): Promise { + blockContact(userId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { let promise; if (site.wsAvailable('core_message_block_user')) { @@ -313,7 +314,9 @@ export class AddonMessagesProvider { * @param userId User ID viewing the conversation. * @return Formatted conversation. */ - protected formatConversation(conversation: any, userId: number): any { + protected formatConversation(conversation: AddonMessagesConversationFormatted, userId: number) + : AddonMessagesConversationFormatted { + const numMessages = conversation.messages.length, lastMessage = numMessages ? conversation.messages[numMessages - 1] : null; @@ -536,10 +539,10 @@ export class AddonMessagesProvider { * Get all the contacts of the current user. * * @param siteId Site ID. If not defined, use current site. - * @return Resolved with the WS data. + * @return Promise resolved with the WS data. * @deprecated since Moodle 3.6 */ - getAllContacts(siteId?: string): Promise { + getAllContacts(siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); return this.getContacts(siteId).then((contacts) => { @@ -562,9 +565,9 @@ export class AddonMessagesProvider { * Get all the users blocked by the current user. * * @param siteId Site ID. If not defined, use current site. - * @return Resolved with the WS data. + * @return Promise resolved with the WS data. */ - getBlockedContacts(siteId?: string): Promise { + getBlockedContacts(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const userId = site.getUserId(), params = { @@ -585,19 +588,24 @@ export class AddonMessagesProvider { * This excludes the blocked users. * * @param siteId Site ID. If not defined, use current site. - * @return Resolved with the WS data. + * @return Promise resolved with the WS data. * @deprecated since Moodle 3.6 */ - getContacts(siteId?: string): Promise { + getContacts(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const preSets = { cacheKey: this.getCacheKeyForContacts(), updateFrequency: CoreSite.FREQUENCY_OFTEN }; - return site.read('core_message_get_contacts', undefined, preSets).then((contacts) => { + return site.read('core_message_get_contacts', undefined, preSets).then((contacts: AddonMessagesGetContactsResult) => { // Filter contacts with negative ID, they are notifications. - const validContacts = {}; + const validContacts: AddonMessagesGetContactsResult = { + online: [], + offline: [], + strangers: [] + }; + for (const typeName in contacts) { if (!validContacts[typeName]) { validContacts[typeName] = []; @@ -621,11 +629,11 @@ export class AddonMessagesProvider { * @param limitFrom Position of the first contact to fetch. * @param limitNum Number of contacts to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS. * @param siteId Site ID. If not defined, use current site. - * @return Resolved with the list of user contacts. + * @return Promise resolved with the list of user contacts. * @since 3.6 */ getUserContacts(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS , siteId?: string): - Promise<{contacts: any[], canLoadMore: boolean}> { + Promise<{contacts: AddonMessagesConversationMember[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -638,7 +646,9 @@ export class AddonMessagesProvider { updateFrequency: CoreSite.FREQUENCY_OFTEN }; - return site.read('core_message_get_user_contacts', params, preSets).then((contacts) => { + return site.read('core_message_get_user_contacts', params, preSets) + .then((contacts: AddonMessagesConversationMember[]) => { + if (!contacts || !contacts.length) { return { contacts: [], canLoadMore: false }; } @@ -663,11 +673,11 @@ export class AddonMessagesProvider { * @param limitFrom Position of the first contact request to fetch. * @param limitNum Number of contact requests to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS. * @param siteId Site ID. If not defined, use current site. - * @return Resolved with the list of contact requests. + * @return Promise resolved with the list of contact requests. * @since 3.6 */ getContactRequests(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS, siteId?: string): - Promise<{requests: any[], canLoadMore: boolean}> { + Promise<{requests: AddonMessagesConversationMember[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { const data = { @@ -680,7 +690,9 @@ export class AddonMessagesProvider { updateFrequency: CoreSite.FREQUENCY_OFTEN }; - return site.read('core_message_get_contact_requests', data, preSets).then((requests) => { + return site.read('core_message_get_contact_requests', data, preSets) + .then((requests: AddonMessagesConversationMember[]) => { + if (!requests || !requests.length) { return { requests: [], canLoadMore: false }; } @@ -716,7 +728,7 @@ export class AddonMessagesProvider { typeExpected: 'number' }; - return site.read('core_message_get_received_contact_requests_count', data, preSets).then((count) => { + return site.read('core_message_get_received_contact_requests_count', data, preSets).then((count: number) => { // Notify the new count so all badges are updated. this.eventsProvider.trigger(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, { count }, site.id); @@ -745,7 +757,7 @@ export class AddonMessagesProvider { */ getConversation(conversationId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean, messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2, - newestFirst: boolean = true, siteId?: string, userId?: number): Promise { + newestFirst: boolean = true, siteId?: string, userId?: number): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -765,7 +777,7 @@ export class AddonMessagesProvider { newestmessagesfirst: newestFirst ? 1 : 0 }; - return site.read('core_message_get_conversation', params, preSets).then((conversation) => { + return site.read('core_message_get_conversation', params, preSets).then((conversation: AddonMessagesConversation) => { return this.formatConversation(conversation, userId); }); }); @@ -792,7 +804,8 @@ export class AddonMessagesProvider { */ getConversationBetweenUsers(otherUserId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean, messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2, - newestFirst: boolean = true, siteId?: string, userId?: number, preferCache?: boolean): Promise { + newestFirst: boolean = true, siteId?: string, userId?: number, preferCache?: boolean) + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -813,7 +826,8 @@ export class AddonMessagesProvider { newestmessagesfirst: newestFirst ? 1 : 0 }; - return site.read('core_message_get_conversation_between_users', params, preSets).then((conversation) => { + return site.read('core_message_get_conversation_between_users', params, preSets) + .then((conversation: AddonMessagesConversation) => { return this.formatConversation(conversation, userId); }); }); @@ -826,12 +840,11 @@ export class AddonMessagesProvider { * @param limitFrom Offset for members list. * @param limitTo Limit of members. * @param siteId Site ID. If not defined, use current site. - * @param userId User ID. If not defined, current user in the site. - * @return Promise resolved with the response. + * @param userId User ID. If not defined, current user in * @since 3.6 */ getConversationMembers(conversationId: number, limitFrom: number = 0, limitTo?: number, includeContactRequests?: boolean, - siteId?: string, userId?: number): Promise { + siteId?: string, userId?: number): Promise<{members: AddonMessagesConversationMember[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -853,18 +866,21 @@ export class AddonMessagesProvider { includeprivacyinfo: 1, }; - return site.read('core_message_get_conversation_members', params, preSets).then((members) => { - const result: any = {}; + return site.read('core_message_get_conversation_members', params, preSets) + .then((members: AddonMessagesConversationMember[]) => { if (limitTo < 1) { - result.canLoadMore = false; - result.members = members; + return { + canLoadMore: false, + members: members + }; } else { - result.canLoadMore = members.length > limitTo; - result.members = members.slice(0, limitTo); + return { + canLoadMore: members.length > limitTo, + members: members.slice(0, limitTo) + }; } - return result; }); }); } @@ -884,7 +900,8 @@ export class AddonMessagesProvider { * @since 3.6 */ getConversationMessages(conversationId: number, excludePending: boolean, limitFrom: number = 0, limitTo?: number, - newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number): Promise { + newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number) + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -913,7 +930,9 @@ export class AddonMessagesProvider { preSets['emergencyCache'] = false; } - return site.read('core_message_get_conversation_messages', params, preSets).then((result) => { + return site.read('core_message_get_conversation_messages', params, preSets) + .then((result: AddonMessagesGetConversationMessagesResult) => { + if (limitTo < 1) { result.canLoadMore = false; result.messages = result.messages; @@ -975,7 +994,8 @@ export class AddonMessagesProvider { * @since 3.6 */ getConversations(type?: number, favourites?: boolean, limitFrom: number = 0, siteId?: string, userId?: number, - forceCache?: boolean, ignoreCache?: boolean): Promise<{conversations: any[], canLoadMore: boolean}> { + forceCache?: boolean, ignoreCache?: boolean) + : Promise<{conversations: AddonMessagesConversationFormatted[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -1017,7 +1037,7 @@ export class AddonMessagesProvider { } return Promise.reject(error); - }).then((response) => { + }).then((response: AddonMessagesGetConversationsResult) => { // Format the conversations, adding some calculated fields. const conversations = response.conversations.slice(0, this.LIMIT_MESSAGES).map((conversation) => { return this.formatConversation(conversation, userId); @@ -1053,7 +1073,9 @@ export class AddonMessagesProvider { cacheKey: this.getCacheKeyForConversationCounts() }; - return site.read('core_message_get_conversation_counts', {}, preSets).then((result) => { + return site.read('core_message_get_conversation_counts', {}, preSets) + .then((result: AddonMessagesGetConversationCountsResult) => { + const counts = { favourites: result.favourites, individual: result.types[AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL], @@ -1080,10 +1102,14 @@ export class AddonMessagesProvider { * @return Promise resolved with messages and a boolean telling if can load more messages. */ getDiscussion(userId: number, excludePending: boolean, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, - lfSentUnread: number = 0, lfSentRead: number = 0, toDisplay: boolean = true, siteId?: string): Promise { + lfSentUnread: number = 0, lfSentRead: number = 0, toDisplay: boolean = true, siteId?: string) + : Promise<{messages: AddonMessagesGetMessagesMessage[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { - const result = {}, + const result = { + messages: [], + canLoadMore: false + }, preSets = { cacheKey: this.getCacheKeyForDiscussion(userId) }, @@ -1107,7 +1133,7 @@ export class AddonMessagesProvider { // Get message received by current user. return this.getRecentMessages(params, preSets, lfReceivedUnread, lfReceivedRead, toDisplay, site.getId()) .then((response) => { - result['messages'] = response; + result.messages = response; params.useridto = userId; params.useridfrom = site.getUserId(); hasReceived = response.length > 0; @@ -1115,16 +1141,16 @@ export class AddonMessagesProvider { // Get message sent by current user. return this.getRecentMessages(params, preSets, lfSentUnread, lfSentRead, toDisplay, siteId); }).then((response) => { - result['messages'] = result['messages'].concat(response); + result.messages = result.messages.concat(response); hasSent = response.length > 0; - if (result['messages'].length > this.LIMIT_MESSAGES) { + if (result.messages.length > this.LIMIT_MESSAGES) { // Sort messages and get the more recent ones. - result['canLoadMore'] = true; - result['messages'] = this.sortMessages(result['messages']); - result['messages'] = result['messages'].slice(-this.LIMIT_MESSAGES); + result.canLoadMore = true; + result.messages = this.sortMessages(result['messages']); + result.messages = result.messages.slice(-this.LIMIT_MESSAGES); } else { - result['canLoadMore'] = result['messages'].length == this.LIMIT_MESSAGES && (!hasReceived || !hasSent); + result.canLoadMore = result.messages.length == this.LIMIT_MESSAGES && (!hasReceived || !hasSent); } if (excludePending) { @@ -1140,7 +1166,7 @@ export class AddonMessagesProvider { message.text = message.smallmessage; }); - result['messages'] = result['messages'].concat(offlineMessages); + result.messages = result.messages.concat(offlineMessages); return result; }); @@ -1153,11 +1179,11 @@ export class AddonMessagesProvider { * If the site is 3.6 or higher, please use getConversations. * * @param siteId Site ID. If not defined, current site. - * @return Resolved with an object where the keys are the user ID of the other user. + * @return Promise resolved with an object where the keys are the user ID of the other user. */ - getDiscussions(siteId?: string): Promise { + getDiscussions(siteId?: string): Promise<{[userId: number]: AddonMessagesDiscussion}> { return this.sitesProvider.getSite(siteId).then((site) => { - const discussions = {}, + const discussions: {[userId: number]: AddonMessagesDiscussion} = {}, currentUserId = site.getUserId(), params = { useridto: currentUserId, @@ -1171,7 +1197,7 @@ export class AddonMessagesProvider { /** * Convenience function to treat a recent message, adding it to discussions list if needed. */ - const treatRecentMessage = (message: any, userId: number, userFullname: string): void => { + const treatRecentMessage = (message: AddonMessagesGetMessagesMessage, userId: number, userFullname: string): void => { if (typeof discussions[userId] === 'undefined') { discussions[userId] = { fullname: userFullname, @@ -1272,7 +1298,7 @@ export class AddonMessagesProvider { * @return Promise resolved with the member info. * @since 3.6 */ - getMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise { + getMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -1287,7 +1313,9 @@ export class AddonMessagesProvider { includeprivacyinfo: 1, }; - return site.read('core_message_get_member_info', params, preSets).then((members) => { + return site.read('core_message_get_member_info', params, preSets) + .then((members: AddonMessagesConversationMember[]): any => { + if (!members || members.length < 1) { // Should never happen. return Promise.reject(null); @@ -1313,7 +1341,7 @@ export class AddonMessagesProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the message preferences. */ - getMessagePreferences(siteId?: string): Promise { + getMessagePreferences(siteId?: string): Promise { this.logger.debug('Get message preferences'); return this.sitesProvider.getSite(siteId).then((site) => { @@ -1322,7 +1350,9 @@ export class AddonMessagesProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_message_get_user_message_preferences', {}, preSets).then((data) => { + return site.read('core_message_get_user_message_preferences', {}, preSets) + .then((data: AddonMessagesGetUserMessagePreferencesResult): any => { + if (data.preferences) { data.preferences.blocknoncontacts = data.blocknoncontacts; @@ -1341,15 +1371,18 @@ export class AddonMessagesProvider { * @param preSets Set of presets for the WS. * @param toDisplay True if messages will be displayed to the user, either in view or in a notification. * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with the data. */ - protected getMessages(params: any, preSets: any, toDisplay: boolean = true, siteId?: string): Promise { + protected getMessages(params: any, preSets: any, toDisplay: boolean = true, siteId?: string) + : Promise { + params['type'] = 'conversations'; params['newestfirst'] = 1; return this.sitesProvider.getSite(siteId).then((site) => { const userId = site.getUserId(); - return site.read('core_message_get_messages', params, preSets).then((response) => { + return site.read('core_message_get_messages', params, preSets).then((response: AddonMessagesGetMessagesResult) => { response.messages.forEach((message) => { message.read = params.read == 0 ? 0 : 1; // Convert times to milliseconds. @@ -1377,9 +1410,10 @@ export class AddonMessagesProvider { * @param limitFromRead Number of unread messages already fetched, so fetch will be done from this number. * @param toDisplay True if messages will be displayed to the user, either in view or in a notification. * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with the data. */ protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0, - toDisplay: boolean = true, siteId?: string): Promise { + toDisplay: boolean = true, siteId?: string): Promise { limitFromUnread = limitFromUnread || 0; limitFromRead = limitFromRead || 0; @@ -1427,7 +1461,7 @@ export class AddonMessagesProvider { * @since 3.7 */ getSelfConversation(messageOffset: number = 0, messageLimit: number = 1, newestFirst: boolean = true, siteId?: string, - userId?: number): Promise { + userId?: number): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -1442,7 +1476,8 @@ export class AddonMessagesProvider { newestmessagesfirst: newestFirst ? 1 : 0 }; - return site.read('core_message_get_self_conversation', params, preSets).then((conversation) => { + return site.read('core_message_get_self_conversation', params, preSets) + .then((conversation: AddonMessagesConversation) => { return this.formatConversation(conversation, userId); }); }); @@ -1466,7 +1501,8 @@ export class AddonMessagesProvider { cacheKey: this.getCacheKeyForUnreadConversationCounts() }; - promise = site.read('core_message_get_unread_conversation_counts', {}, preSets).then((result) => { + promise = site.read('core_message_get_unread_conversation_counts', {}, preSets) + .then((result: AddonMessagesGetUnreadConversationCountsResult) => { return { favourites: result.favourites, individual: result.types[AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL], @@ -1485,7 +1521,7 @@ export class AddonMessagesProvider { typeExpected: 'number' }; - promise = site.read('core_message_get_unread_conversations_count', params, preSets).then((count) => { + promise = site.read('core_message_get_unread_conversations_count', params, preSets).then((count: number) => { return { favourites: 0, individual: count, group: 0, self: 0 }; }); } else { @@ -1536,7 +1572,7 @@ export class AddonMessagesProvider { * @return Promise resolved with the message unread count. */ getUnreadReceivedMessages(toDisplay: boolean = true, forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { read: 0, @@ -2049,7 +2085,7 @@ export class AddonMessagesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with boolean marking success or not. */ - markMessageRead(messageId: number, siteId?: string): Promise { + markMessageRead(messageId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { messageid: messageId, @@ -2067,7 +2103,7 @@ export class AddonMessagesProvider { * @return Promise resolved if success. * @since 3.6 */ - markAllConversationMessagesRead(conversationId?: number): Promise { + markAllConversationMessagesRead(conversationId?: number): Promise { const params = { userid: this.sitesProvider.getCurrentSiteUserId(), conversationid: conversationId @@ -2085,7 +2121,7 @@ export class AddonMessagesProvider { * @param userIdFrom User Id for the sender. * @return Promise resolved with boolean marking success or not. */ - markAllMessagesRead(userIdFrom?: number): Promise { + markAllMessagesRead(userIdFrom?: number): Promise { const params = { useridto: this.sitesProvider.getCurrentSiteUserId(), useridfrom: userIdFrom @@ -2217,8 +2253,9 @@ export class AddonMessagesProvider { * @param query The query string. * @param limit The number of results to return, 0 for none. * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the contacts. */ - searchContacts(query: string, limit: number = 100, siteId?: string): Promise { + searchContacts(query: string, limit: number = 100, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const data = { searchtext: query, @@ -2228,7 +2265,9 @@ export class AddonMessagesProvider { getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. }; - return site.read('core_message_search_contacts', data, preSets).then((contacts) => { + return site.read('core_message_search_contacts', data, preSets) + .then((contacts: AddonMessagesSearchContactsContact[]) => { + if (limit && contacts.length > limit) { contacts = contacts.splice(0, limit); } @@ -2250,7 +2289,7 @@ export class AddonMessagesProvider { * @return Promise resolved with the results. */ searchMessages(query: string, userId?: number, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH, - siteId?: string): Promise<{messages: any[], canLoadMore: boolean}> { + siteId?: string): Promise<{messages: AddonMessagesMessageAreaContact[], canLoadMore: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -2263,13 +2302,15 @@ export class AddonMessagesProvider { getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. }; - return site.read('core_message_data_for_messagearea_search_messages', params, preSets).then((result) => { + return site.read('core_message_data_for_messagearea_search_messages', params, preSets) + .then((result: AddonMessagesDataForMessageAreaSearchMessagesResult) => { + if (!result.contacts || !result.contacts.length) { return { messages: [], canLoadMore: false }; } - result.contacts.forEach((result) => { - result.id = result.userid; + result.contacts.forEach((contact) => { + contact.id = contact.userid; }); this.userProvider.storeUsers(result.contacts, site.id); @@ -2297,7 +2338,8 @@ export class AddonMessagesProvider { * @since 3.6 */ searchUsers(query: string, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH, siteId?: string): - Promise<{contacts: any[], nonContacts: any[], canLoadMoreContacts: boolean, canLoadMoreNonContacts: boolean}> { + Promise<{contacts: AddonMessagesConversationMember[], nonContacts: AddonMessagesConversationMember[], + canLoadMoreContacts: boolean, canLoadMoreNonContacts: boolean}> { return this.sitesProvider.getSite(siteId).then((site) => { const data = { @@ -2310,7 +2352,7 @@ export class AddonMessagesProvider { getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. }; - return site.read('core_message_message_search_users', data, preSets).then((result) => { + return site.read('core_message_message_search_users', data, preSets).then((result: AddonMessagesSearchUsersResult) => { const contacts = result.contacts || []; const nonContacts = result.noncontacts || []; @@ -2341,7 +2383,9 @@ export class AddonMessagesProvider { * - sent (Boolean) True if message was sent to server, false if stored in device. * - message (Object) If sent=false, contains the stored message. */ - sendMessage(toUserId: number, message: string, siteId?: string): Promise { + sendMessage(toUserId: number, message: string, siteId?: string) + : Promise<{sent: boolean, message: AddonMessagesSendInstantMessagesMessage}> { + // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { return this.messagesOffline.saveMessage(toUserId, message, siteId).then((entry) => { @@ -2395,7 +2439,7 @@ export class AddonMessagesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success, rejected if failure. */ - sendMessageOnline(toUserId: number, message: string, siteId?: string): Promise { + sendMessageOnline(toUserId: number, message: string, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const messages = [ @@ -2430,7 +2474,7 @@ export class AddonMessagesProvider { * @return Promise resolved if success, rejected if failure. Promise resolved doesn't mean that messages * have been sent, the resolve param can contain errors for messages not sent. */ - sendMessagesOnline(messages: any, siteId?: string): Promise { + sendMessagesOnline(messages: any[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const data = { messages: messages @@ -2451,7 +2495,9 @@ export class AddonMessagesProvider { * - message (any) If sent=false, contains the stored message. * @since 3.6 */ - sendMessageToConversation(conversation: any, message: string, siteId?: string): Promise { + sendMessageToConversation(conversation: any, message: string, siteId?: string) + : Promise<{sent: boolean, message: AddonMessagesSendMessagesToConversationMessage}> { + // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { return this.messagesOffline.saveConversationMessage(conversation, message, siteId).then((entry) => { @@ -2506,7 +2552,8 @@ export class AddonMessagesProvider { * @return Promise resolved if success, rejected if failure. * @since 3.6 */ - sendMessageToConversationOnline(conversationId: number, message: string, siteId?: string): Promise { + sendMessageToConversationOnline(conversationId: number, message: string, siteId?: string) + : Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const messages = [ @@ -2534,7 +2581,9 @@ export class AddonMessagesProvider { * @return Promise resolved if success, rejected if failure. * @since 3.6 */ - sendMessagesToConversationOnline(conversationId: number, messages: any, siteId?: string): Promise { + sendMessagesToConversationOnline(conversationId: number, messages: any[], siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { conversationid: conversationId, @@ -2603,10 +2652,10 @@ export class AddonMessagesProvider { * @param conversations Array of conversations. * @return Conversations sorted with most recent last. */ - sortConversations(conversations: any[]): any[] { + sortConversations(conversations: AddonMessagesConversationFormatted[]): AddonMessagesConversationFormatted[] { return conversations.sort((a, b) => { - const timeA = parseInt(a.lastmessagedate, 10), - timeB = parseInt(b.lastmessagedate, 10); + const timeA = Number(a.lastmessagedate), + timeB = Number(b.lastmessagedate); if (timeA == timeB && a.id) { // Same time, sort by ID. @@ -2651,7 +2700,9 @@ export class AddonMessagesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - protected storeLastReceivedMessageIfNeeded(convIdOrUserIdFrom: number, message: any, siteId?: string): Promise { + protected storeLastReceivedMessageIfNeeded(convIdOrUserIdFrom: number, + message: AddonMessagesGetMessagesMessage | AddonMessagesConversationMessage, siteId?: string): Promise { + const component = AddonMessagesProvider.PUSH_SIMULATION_COMPONENT; // Get the last received message. @@ -2675,7 +2726,7 @@ export class AddonMessagesProvider { * * @param contactTypes List of contacts grouped in types. */ - protected storeUsersFromAllContacts(contactTypes: any): void { + protected storeUsersFromAllContacts(contactTypes: AddonMessagesGetContactsResult): void { for (const x in contactTypes) { this.userProvider.storeUsers(contactTypes[x]); } @@ -2735,3 +2786,377 @@ export class AddonMessagesProvider { }); } } + +/** + * Conversation. + */ +export type AddonMessagesConversation = { + id: number; // The conversation id. + name: string; // The conversation name, if set. + subname: string; // A subtitle for the conversation name, if set. + imageurl: string; // A link to the conversation picture, if set. + type: number; // The type of the conversation (1=individual,2=group,3=self). + membercount: number; // Total number of conversation members. + ismuted: boolean; // @since 3.7. If the user muted this conversation. + isfavourite: boolean; // If the user marked this conversation as a favourite. + isread: boolean; // If the user has read all messages in the conversation. + unreadcount: number; // The number of unread messages in this conversation. + members: AddonMessagesConversationMember[]; + messages: AddonMessagesConversationMessage[]; + candeletemessagesforallusers: boolean; // @since 3.7. If the user can delete messages in the conversation for all users. +}; + +/** + * Conversation with some calculated data. + */ +export type AddonMessagesConversationFormatted = AddonMessagesConversation & { + lastmessage?: string; // Calculated in the app. Last message. + lastmessagedate?: number; // Calculated in the app. Date the last message was sent. + sentfromcurrentuser?: boolean; // Calculated in the app. Whether last message was sent by the current user. + name?: string; // Calculated in the app. If private conversation, name of the other user. + userid?: number; // Calculated in the app. URL. If private conversation, ID of the other user. + showonlinestatus?: boolean; // Calculated in the app. If private conversation, whether to show online status of the other user. + isonline?: boolean; // Calculated in the app. If private conversation, whether the other user is online. + isblocked?: boolean; // Calculated in the app. If private conversation, whether the other user is blocked. + otherUser?: AddonMessagesConversationMember; // Calculated in the app. Other user in the conversation. +}; + +/** + * Conversation member. + */ +export type AddonMessagesConversationMember = { + id: number; // The user id. + fullname: string; // The user's name. + profileurl: string; // The link to the user's profile page. + profileimageurl: string; // User picture URL. + profileimageurlsmall: string; // Small user picture URL. + isonline: boolean; // The user's online status. + showonlinestatus: boolean; // Show the user's online status?. + isblocked: boolean; // If the user has been blocked. + iscontact: boolean; // Is the user a contact?. + isdeleted: boolean; // Is the user deleted?. + canmessageevenifblocked: boolean; // @since 3.8. If the user can still message even if they get blocked. + canmessage: boolean; // If the user can be messaged. + requirescontact: boolean; // If the user requires to be contacts. + contactrequests?: { // The contact requests. + id: number; // The id of the contact request. + userid: number; // The id of the user who created the contact request. + requesteduserid: number; // The id of the user confirming the request. + timecreated: number; // The timecreated timestamp for the contact request. + }[]; + conversations?: { // Conversations between users. + id: number; // Conversations id. + type: number; // Conversation type: private or public. + name: string; // Multilang compatible conversation name2. + timecreated: number; // The timecreated timestamp for the conversation. + }[]; +}; + +/** + * Conversation message. + */ +export type AddonMessagesConversationMessage = { + id: number; // The id of the message. + useridfrom: number; // The id of the user who sent the message. + text: string; // The text of the message. + timecreated: number; // The timecreated timestamp for the message. +}; + +/** + * Message preferences. + */ +export type AddonMessagesMessagePreferences = { + userid: number; // User id. + disableall: number; // Whether all the preferences are disabled. + processors: { // Config form values. + displayname: string; // Display name. + name: string; // Processor name. + hassettings: boolean; // Whether has settings. + contextid: number; // Context id. + userconfigured: number; // Whether is configured by the user. + }[]; + components: { // Available components. + displayname: string; // Display name. + notifications: AddonMessagesMessagePreferencesNotification[]; // List of notificaitons for the component. + }[]; +} & AddonMessagesMessagePreferencesCalculatedData; + +/** + * Notification processor in message preferences. + */ +export type AddonMessagesMessagePreferencesNotification = { + displayname: string; // Display name. + preferencekey: string; // Preference key. + processors: AddonMessagesMessagePreferencesNotificationProcessor[]; // Processors values for this notification. +}; + +/** + * Notification processor in message preferences. + */ +export type AddonMessagesMessagePreferencesNotificationProcessor = { + displayname: string; // Display name. + name: string; // Processor name. + locked: boolean; // Is locked by admin?. + lockedmessage?: string; // @since 3.6. Text to display if locked. + userconfigured: number; // Is configured?. + loggedin: { + name: string; // Name. + displayname: string; // Display name. + checked: boolean; // Is checked?. + }; + loggedoff: { + name: string; // Name. + displayname: string; // Display name. + checked: boolean; // Is checked?. + }; +}; + +/** + * Message discussion (before 3.6). + */ +export type AddonMessagesDiscussion = { + fullname: string; // Full name of the other user in the discussion. + profileimageurl: string; // Profile image of the other user in the discussion. + message?: { // Last message. + id: number; // Message ID. + user: number; // User ID that sent the message. + message: string; // Text of the message. + timecreated: number; // Time the message was sent. + pending?: boolean; // Whether the message is pending to be sent. + }; + unread?: boolean; // Whether the discussion has unread messages. +}; + +/** + * Contact for message area. + */ +export type AddonMessagesMessageAreaContact = { + userid: number; // The user's id. + fullname: string; // The user's name. + profileimageurl: string; // User picture URL. + profileimageurlsmall: string; // Small user picture URL. + ismessaging: boolean; // If we are messaging the user. + sentfromcurrentuser: boolean; // Was the last message sent from the current user?. + lastmessage: string; // The user's last message. + lastmessagedate: number; // @since 3.6. Timestamp for last message. + messageid: number; // The unique search message id. + showonlinestatus: boolean; // Show the user's online status?. + isonline: boolean; // The user's online status. + isread: boolean; // If the user has read the message. + isblocked: boolean; // If the user has been blocked. + unreadcount: number; // The number of unread messages in this conversation. + conversationid: number; // @since 3.6. The id of the conversation. +} & AddonMessagesMessageAreaContactCalculatedData; + +/** + * Result of WS core_message_get_blocked_users. + */ +export type AddonMessagesGetBlockedUsersResult = { + users: AddonMessagesBlockedUser[]; // List of blocked users. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * User data returned by core_message_get_blocked_users. + */ +export type AddonMessagesBlockedUser = { + id: number; // User ID. + fullname: string; // User full name. + profileimageurl?: string; // User picture URL. +}; + +/** + * Result of WS core_message_get_contacts. + */ +export type AddonMessagesGetContactsResult = { + online: AddonMessagesGetContactsContact[]; // List of online contacts. + offline: AddonMessagesGetContactsContact[]; // List of offline contacts. + strangers: AddonMessagesGetContactsContact[]; // List of users that are not in the user's contact list but have sent a message. +} & AddonMessagesGetContactsCalculatedData; + +/** + * User data returned by core_message_get_contacts. + */ +export type AddonMessagesGetContactsContact = { + id: number; // User ID. + fullname: string; // User full name. + profileimageurl?: string; // User picture URL. + profileimageurlsmall?: string; // Small user picture URL. + unread: number; // Unread message count. +}; + +/** + * User data returned by core_message_search_contacts. + */ +export type AddonMessagesSearchContactsContact = { + id: number; // User ID. + fullname: string; // User full name. + profileimageurl?: string; // User picture URL. + profileimageurlsmall?: string; // Small user picture URL. +}; + +/** + * Result of WS core_message_get_conversation_messages. + */ +export type AddonMessagesGetConversationMessagesResult = { + id: number; // The conversation id. + members: AddonMessagesConversationMember[]; + messages: AddonMessagesConversationMessage[]; +} & AddonMessagesGetConversationMessagesCalculatedData; + +/** + * Result of WS core_message_get_conversations. + */ +export type AddonMessagesGetConversationsResult = { + conversations: AddonMessagesConversation[]; +}; + +/** + * Result of WS core_message_get_conversation_counts. + */ +export type AddonMessagesGetConversationCountsResult = { + favourites: number; // Total number of favourite conversations. + types: { + 1: number; // Total number of individual conversations. + 2: number; // Total number of group conversations. + 3: number; // @since 3.7. Total number of self conversations. + }; +}; + +/** + * Result of WS core_message_get_unread_conversation_counts. + */ +export type AddonMessagesGetUnreadConversationCountsResult = { + favourites: number; // Total number of unread favourite conversations. + types: { + 1: number; // Total number of unread individual conversations. + 2: number; // Total number of unread group conversations. + 3: number; // @since 3.7. Total number of unread self conversations. + }; +}; + +/** + * Result of WS core_message_get_user_message_preferences. + */ +export type AddonMessagesGetUserMessagePreferencesResult = { + preferences: AddonMessagesMessagePreferences; + blocknoncontacts: number; // Privacy messaging setting to define who can message you. + entertosend: boolean; // @since 3.6. User preference for using enter to send messages. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_message_get_messages. + */ +export type AddonMessagesGetMessagesResult = { + messages: AddonMessagesGetMessagesMessage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Message data returned by core_message_get_messages. + */ +export type AddonMessagesGetMessagesMessage = { + id: number; // Message id. + useridfrom: number; // User from id. + useridto: number; // User to id. + subject: string; // The message subject. + text: string; // The message text formated. + fullmessage: string; // The message. + fullmessageformat: number; // Fullmessage format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + fullmessagehtml: string; // The message in html. + smallmessage: string; // The shorten message. + notification: number; // Is a notification?. + contexturl: string; // Context URL. + contexturlname: string; // Context URL link name. + timecreated: number; // Time created. + timeread: number; // Time read. + usertofullname: string; // User to full name. + userfromfullname: string; // User from full name. + component?: string; // @since 3.7. The component that generated the notification. + eventtype?: string; // @since 3.7. The type of notification. + customdata?: string; // @since 3.7. Custom data to be passed to the message processor. +} & AddonMessagesGetMessagesMessageCalculatedData; + +/** + * Result of WS core_message_data_for_messagearea_search_messages. + */ +export type AddonMessagesDataForMessageAreaSearchMessagesResult = { + contacts: AddonMessagesMessageAreaContact[]; +}; + +/** + * Result of WS core_message_message_search_users. + */ +export type AddonMessagesSearchUsersResult = { + contacts: AddonMessagesConversationMember[]; + noncontacts: AddonMessagesConversationMember[]; +}; + +/** + * Result of WS core_message_mark_message_read. + */ +export type AddonMessagesMarkMessageReadResult = { + messageid: number; // The id of the message in the messages table. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_message_send_instant_messages. + */ +export type AddonMessagesSendInstantMessagesMessage = { + msgid: number; // Test this to know if it succeeds: id of the created message if it succeeded, -1 when failed. + clientmsgid?: string; // Your own id for the message. + errormessage?: string; // Error message - if it failed. + text?: string; // @since 3.6. The text of the message. + timecreated?: number; // @since 3.6. The timecreated timestamp for the message. + conversationid?: number; // @since 3.6. The conversation id for this message. + useridfrom?: number; // @since 3.6. The user id who sent the message. + candeletemessagesforallusers: boolean; // @since 3.7. If the user can delete messages in the conversation for all users. +}; + +/** + * Result of WS core_message_send_messages_to_conversation. + */ +export type AddonMessagesSendMessagesToConversationMessage = { + id: number; // The id of the message. + useridfrom: number; // The id of the user who sent the message. + text: string; // The text of the message. + timecreated: number; // The timecreated timestamp for the message. +}; + +/** + * Calculated data for core_message_get_contacts. + */ +export type AddonMessagesGetContactsCalculatedData = { + blocked?: AddonMessagesBlockedUser[]; // Calculated in the app. List of blocked users. +}; + +/** + * Calculated data for core_message_get_conversation_messages. + */ +export type AddonMessagesGetConversationMessagesCalculatedData = { + canLoadMore?: boolean; // Calculated in the app. Whether more messages can be loaded. +}; + +/** + * Calculated data for message preferences. + */ +export type AddonMessagesMessagePreferencesCalculatedData = { + blocknoncontacts?: number; // Calculated in the app. Based on the result of core_message_get_user_message_preferences. +}; + +/** + * Calculated data for messages returned by core_message_get_messages. + */ +export type AddonMessagesGetMessagesMessageCalculatedData = { + pending?: boolean; // Calculated in the app. Whether the message is pending to be sent. + read?: number; // Calculated in the app. Whether the message has been read. +}; + +/** + * Calculated data for contact for message area. + */ +export type AddonMessagesMessageAreaContactCalculatedData = { + id?: number; // Calculated in the app. User ID. +}; diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 6c7d33886..20a3a27c1 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -18,7 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { AddonMessagesOfflineProvider } from './messages-offline'; -import { AddonMessagesProvider } from './messages'; +import { AddonMessagesProvider, AddonMessagesConversationFormatted } from './messages'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreEventsProvider } from '@providers/events'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -258,7 +258,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { // Get conversation name and add errors to warnings array. return this.messagesProvider.getConversation(conversationId, false, false).catch(() => { // Ignore errors. - return {}; + return {}; }).then((conversation) => { errors.forEach((error) => { warnings.push(this.translate.instant('addon.messages.warningconversationmessagenotsent', { diff --git a/src/addon/mod/assign/classes/base-feedback-handler.ts b/src/addon/mod/assign/classes/base-feedback-handler.ts index 33e08c2d4..a63a11a82 100644 --- a/src/addon/mod/assign/classes/base-feedback-handler.ts +++ b/src/addon/mod/assign/classes/base-feedback-handler.ts @@ -15,6 +15,7 @@ import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModAssignFeedbackHandler } from '../providers/feedback-delegate'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '../providers/assign'; /** * Base handler for feedback plugins. @@ -48,7 +49,7 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param plugin The plugin object. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin): any | Promise { // Nothing to do. } @@ -74,7 +75,8 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return []; } @@ -84,7 +86,7 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param plugin The plugin object. * @return The plugin name. */ - getPluginName(plugin: any): string { + getPluginName(plugin: AddonModAssignPlugin): string { // Check if there's a translated string for the plugin. const translationId = 'addon.mod_assign_feedback_' + plugin.type + '.pluginname', translation = this.translate.instant(translationId); @@ -109,7 +111,8 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param inputData Data entered by the user for the feedback. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + hasDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, userId: number): boolean | Promise { return false; } @@ -144,7 +147,8 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + prefetch(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise { return Promise.resolve(); } @@ -158,7 +162,8 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise { + prepareFeedbackData(assignId: number, userId: number, plugin: AddonModAssignPlugin, pluginData: any, + siteId?: string): void | Promise { // Nothing to do. } @@ -172,7 +177,8 @@ export class AddonModAssignBaseFeedbackHandler implements AddonModAssignFeedback * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - saveDraft(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise { + saveDraft(assignId: number, userId: number, plugin: AddonModAssignPlugin, data: any, siteId?: string) + : void | Promise { // Nothing to do. } } diff --git a/src/addon/mod/assign/classes/base-submission-handler.ts b/src/addon/mod/assign/classes/base-submission-handler.ts index e0e6bb34e..1c34ce157 100644 --- a/src/addon/mod/assign/classes/base-submission-handler.ts +++ b/src/addon/mod/assign/classes/base-submission-handler.ts @@ -15,6 +15,7 @@ import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModAssignSubmissionHandler } from '../providers/submission-delegate'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '../providers/assign'; /** * Base handler for submission plugins. @@ -38,7 +39,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ - canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + canEditOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): boolean | Promise { return false; } @@ -50,7 +52,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @param inputData Data entered by the user for the submission. */ - clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void { + clearTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): void { // Nothing to do. } @@ -65,7 +68,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + copySubmissionData(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: any, + userId?: number, siteId?: string): void | Promise { // Nothing to do. } @@ -79,7 +83,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - deleteOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise { + deleteOfflineData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, siteId?: string): void | Promise { // Nothing to do. } @@ -92,7 +97,7 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param edit Whether the user is editing. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): any | Promise { // Nothing to do. } @@ -106,7 +111,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return []; } @@ -116,7 +122,7 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @return The plugin name. */ - getPluginName(plugin: any): string { + getPluginName(plugin: AddonModAssignPlugin): string { // Check if there's a translated string for the plugin. const translationId = 'addon.mod_assign_submission_' + plugin.type + '.pluginname', translation = this.translate.instant(translationId); @@ -139,7 +145,7 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @return The size (or promise resolved with size). */ - getSizeForCopy(assign: any, plugin: any): number | Promise { + getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): number | Promise { return 0; } @@ -147,10 +153,12 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * Get the size of data (in bytes) this plugin will send to add or edit a submission. * * @param assign The assignment. + * @param submission The submission. * @param plugin The plugin object. * @return The size (or promise resolved with size). */ - getSizeForEdit(assign: any, plugin: any): number | Promise { + getSizeForEdit(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): number | Promise { return 0; } @@ -163,7 +171,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param inputData Data entered by the user for the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + hasDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): boolean | Promise { return false; } @@ -194,7 +203,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + prefetch?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise { return Promise.resolve(); } @@ -211,7 +221,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSubmissionData?(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + prepareSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, pluginData: any, offline?: boolean, userId?: number, siteId?: string): void | Promise { // Nothing to do. } @@ -228,8 +239,8 @@ export class AddonModAssignBaseSubmissionHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSyncData?(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) - : void | Promise { + prepareSyncData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, pluginData: any, siteId?: string): void | Promise { // Nothing to do. } } diff --git a/src/addon/mod/assign/classes/feedback-plugin-component.ts b/src/addon/mod/assign/classes/feedback-plugin-component.ts index d315d1b7f..0076ba466 100644 --- a/src/addon/mod/assign/classes/feedback-plugin-component.ts +++ b/src/addon/mod/assign/classes/feedback-plugin-component.ts @@ -14,14 +14,15 @@ import { Input } from '@angular/core'; import { ModalController } from 'ionic-angular'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '../providers/assign'; /** * Base class for component to render a feedback plugin. */ export class AddonModAssignFeedbackPluginComponentBase { - @Input() assign: any; // The assignment. - @Input() submission: any; // The submission. - @Input() plugin: any; // The plugin object. + @Input() assign: AddonModAssignAssign; // The assignment. + @Input() submission: AddonModAssignSubmission; // The submission. + @Input() plugin: AddonModAssignPlugin; // The plugin object. @Input() userId: number; // The user ID of the submission. @Input() configs: any; // The configs for the plugin. @Input() canEdit: boolean; // Whether the user can edit. diff --git a/src/addon/mod/assign/classes/submission-plugin-component.ts b/src/addon/mod/assign/classes/submission-plugin-component.ts index 39e8cdd2b..552657b9d 100644 --- a/src/addon/mod/assign/classes/submission-plugin-component.ts +++ b/src/addon/mod/assign/classes/submission-plugin-component.ts @@ -13,15 +13,16 @@ // limitations under the License. import { Input } from '@angular/core'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '../providers/assign'; /** * Base class for component to render a submission plugin. */ export class AddonModAssignSubmissionPluginComponent { - @Input() assign: any; // The assignment. - @Input() submission: any; // The submission. - @Input() plugin: any; // The plugin object. - @Input() configs: any; // The configs for the plugin. + @Input() assign: AddonModAssignAssign; // The assignment. + @Input() submission: AddonModAssignSubmission; // The submission. + @Input() plugin: AddonModAssignPlugin; // The plugin object. + @Input() configs: {[name: string]: string}; // The configs for the plugin. @Input() edit: boolean; // Whether the user is editing. @Input() allowOffline: boolean; // Whether to allow offline. diff --git a/src/addon/mod/assign/components/feedback-plugin/feedback-plugin.ts b/src/addon/mod/assign/components/feedback-plugin/feedback-plugin.ts index 94b847fda..d28973ae8 100644 --- a/src/addon/mod/assign/components/feedback-plugin/feedback-plugin.ts +++ b/src/addon/mod/assign/components/feedback-plugin/feedback-plugin.ts @@ -13,7 +13,9 @@ // limitations under the License. import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; @@ -28,9 +30,9 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp export class AddonModAssignFeedbackPluginComponent implements OnInit { @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; - @Input() assign: any; // The assignment. - @Input() submission: any; // The submission. - @Input() plugin: any; // The plugin object. + @Input() assign: AddonModAssignAssign; // The assignment. + @Input() submission: AddonModAssignSubmission; // The submission. + @Input() plugin: AddonModAssignPlugin; // The plugin object. @Input() userId: number; // The user ID of the submission. @Input() canEdit: boolean | string; // Whether the user can edit. @Input() edit: boolean | string; // Whether the user is editing. diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index 2697f6c36..0361570eb 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -17,7 +17,7 @@ import { Content, NavController } from 'ionic-angular'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmissionGradingSummary } from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; import { AddonModAssignSyncProvider } from '../../providers/assign-sync'; @@ -36,13 +36,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo component = AddonModAssignProvider.COMPONENT; moduleName = 'assign'; - assign: any; // The assign object. + assign: AddonModAssignAssign; // The assign object. canViewAllSubmissions: boolean; // Whether the user can view all submissions. canViewOwnSubmission: boolean; // Whether the user can view their own submission. timeRemaining: string; // Message about time remaining to submit. lateSubmissions: string; // Message about late submissions. showNumbers = true; // Whether to show number of submissions with each status. - summary: any; // The summary. + summary: AddonModAssignSubmissionGradingSummary; // The grading summary. needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading. groupInfo: CoreGroupInfo = { @@ -153,7 +153,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo this.assign = assignData; this.dataRetrieved.emit(this.assign); - this.description = this.assign.intro || this.description; + this.description = this.assign.intro; if (sync) { // Try to synchronize the assign. diff --git a/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts b/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts index f8ba6d005..de2d664aa 100644 --- a/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts +++ b/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts @@ -13,7 +13,9 @@ // limitations under the License. import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; @@ -28,9 +30,9 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp export class AddonModAssignSubmissionPluginComponent implements OnInit { @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; - @Input() assign: any; // The assignment. - @Input() submission: any; // The submission. - @Input() plugin: any; // The plugin object. + @Input() assign: AddonModAssignAssign; // The assignment. + @Input() submission: AddonModAssignSubmission; // The submission. + @Input() plugin: AddonModAssignPlugin; // The plugin object. @Input() edit: boolean | string; // Whether the user is editing. @Input() allowOffline: boolean | string; // Whether to allow offline. diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 762270fb6..b5fa53729 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -29,7 +29,10 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreUserProvider } from '@core/user/providers/user'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmissionFeedback, AddonModAssignSubmission, + AddonModAssignSubmissionAttempt, AddonModAssignSubmissionPreviousAttempt, AddonModAssignPlugin +} from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; import { CoreTabsComponent } from '@components/tabs/tabs'; @@ -55,11 +58,11 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { loaded: boolean; // Whether data has been loaded. selectedTab: number; // Tab selected on start. - assign: any; // The assignment the submission belongs to. - userSubmission: any; // The submission object. + assign: AddonModAssignAssign; // The assignment the submission belongs to. + userSubmission: AddonModAssignSubmission; // The submission object. isSubmittedForGrading: boolean; // Whether the submission has been submitted for grading. submitModel: any = {}; // Model where to store the data to submit (for grading). - feedback: any; // The feedback. + feedback: AddonModAssignSubmissionFeedbackFormatted; // The feedback. hasOffline: boolean; // Whether there is offline data. submittedOffline: boolean; // Whether it was submitted in offline. fromDate: string; // Readable date when the assign started accepting submissions. @@ -67,7 +70,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { maxAttemptsText: string; // The text for maximum attempts. blindMarking: boolean; // Whether blind marking is enabled. user: any; // The user. - lastAttempt: any; // The last attempt. + lastAttempt: AddonModAssignSubmissionAttemptFormatted; // The last attempt. membersToSubmit: any[]; // Team members that need to submit the assignment. canSubmit: boolean; // Whether the user can submit for grading. canEdit: boolean; // Whether the user can edit the submission. @@ -77,7 +80,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { gradingStatusTranslationId: string; // Key of the text to display for the grading status. gradingColor: string; // Color to apply to the grading status. workflowStatusTranslationId: string; // Key of the text to display for the workflow status. - submissionPlugins: string[]; // List of submission plugins names. + submissionPlugins: AddonModAssignPlugin[]; // List of submission plugins. timeRemaining: string; // Message about time remaining. timeRemainingClass: string; // Class to apply to time remaining message. statusTranslated: string; // Status. @@ -99,7 +102,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { protected siteId: string; // Current site ID. protected currentUserId: number; // Current user ID. - protected previousAttempt: any; // The previous attempt. + protected previousAttempt: AddonModAssignSubmissionPreviousAttempt; // The previous attempt. protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status. protected originalGrades: any = {}; // Object with the original grade data, to check for changes. protected isDestroyed: boolean; // Whether the component has been destroyed. @@ -209,7 +212,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return this.goToEdit(); } - const previousSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, this.previousAttempt); + const previousSubmission = this.previousAttempt.submission; let modal = this.domUtils.showModalLoading(); this.assignHelper.getSubmissionSizeForCopy(this.assign, previousSubmission).catch(() => { @@ -303,7 +306,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { } if (this.feedback && this.feedback.plugins) { - return this.assignHelper.hasFeedbackDataChanged(this.assign, this.submitId, this.feedback).catch(() => { + return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId) + .catch(() => { // Error ocurred, consider there are no changes. return false; }); @@ -438,7 +442,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { // Check if there's any unsupported plugin for editing. if (!this.userSubmission || !this.userSubmission.plugins) { // Submission not created yet, we have to use assign configs to detect the plugins used. - this.userSubmission = {}; + this.userSubmission = this.assignHelper.createEmptySubmission(); this.userSubmission.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignsubmission'); } @@ -461,7 +465,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { * @param feedback The feedback data from the submission status. * @return Promise resolved when done. */ - protected loadFeedback(feedback: any): Promise { + protected loadFeedback(feedback: AddonModAssignSubmissionFeedback): Promise { this.grade = { method: false, grade: false, @@ -571,7 +575,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { if (!this.feedback || !this.feedback.plugins) { // Feedback plugins not present, we have to use assign configs to detect the plugins used. - this.feedback = {}; + this.feedback = this.assignHelper.createEmptyFeedback(); this.feedback.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignfeedback'); } @@ -885,7 +889,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { // Show error if submission statement should be shown but it couldn't be retrieved. this.showErrorStatementEdit = submissionStatementMissing && !this.assign.submissiondrafts && this.submitId == this.currentUserId; - this.showErrorStatementSubmit = submissionStatementMissing && this.assign.submissiondrafts; + this.showErrorStatementSubmit = submissionStatementMissing && !!this.assign.submissiondrafts; this.userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); @@ -954,3 +958,17 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { } } } + +/** + * Submission attempt with some calculated data. + */ +type AddonModAssignSubmissionAttemptFormatted = AddonModAssignSubmissionAttempt & { + submissiongroupname?: string; // Calculated in the app. Group name the attempt belongs to. +}; + +/** + * Feedback of an assign submission with some calculated data. + */ +type AddonModAssignSubmissionFeedbackFormatted = AddonModAssignSubmissionFeedback & { + advancedgrade?: boolean; // Calculated in the app. Whether it uses advanced grading. +}; diff --git a/src/addon/mod/assign/feedback/comments/providers/handler.ts b/src/addon/mod/assign/feedback/comments/providers/handler.ts index ef4e1655b..1533fd67d 100644 --- a/src/addon/mod/assign/feedback/comments/providers/handler.ts +++ b/src/addon/mod/assign/feedback/comments/providers/handler.ts @@ -16,7 +16,9 @@ import { Injectable, Injector } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { AddonModAssignProvider } from '../../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate'; import { AddonModAssignFeedbackCommentsComponent } from '../component/comments'; @@ -50,14 +52,14 @@ export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeed } /** - * Return the Component to use to display the plugin data, either in read or in edit mode. + * Return the Component to use to display the plugin data. * It's recommended to return the class of the component, but you can also return an instance of the component. * * @param injector Injector. * @param plugin The plugin object. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin): any | Promise { return AddonModAssignFeedbackCommentsComponent; } @@ -101,7 +103,8 @@ export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeed * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return this.assignProvider.getSubmissionPluginAttachments(plugin); } @@ -135,7 +138,9 @@ export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeed * @param userId User ID of the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged(assign: any, submission: any, plugin: any, inputData: any, userId: number): boolean | Promise { + hasDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, userId: number): boolean | Promise { + // Get it from plugin or offline. return this.assignOfflineProvider.getSubmissionGrade(assign.id, userId).catch(() => { // No offline data found. @@ -191,7 +196,9 @@ export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeed * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise { + prepareFeedbackData(assignId: number, userId: number, plugin: AddonModAssignPlugin, pluginData: any, + siteId?: string): void | Promise { + const draft = this.getDraft(assignId, userId, siteId); if (draft) { @@ -212,7 +219,9 @@ export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeed * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - saveDraft(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise { + saveDraft(assignId: number, userId: number, plugin: AddonModAssignPlugin, data: any, siteId?: string) + : void | Promise { + if (data) { this.drafts[this.getDraftId(assignId, userId, siteId)] = data; } diff --git a/src/addon/mod/assign/feedback/editpdf/providers/handler.ts b/src/addon/mod/assign/feedback/editpdf/providers/handler.ts index 45276e932..5b2f8e0ef 100644 --- a/src/addon/mod/assign/feedback/editpdf/providers/handler.ts +++ b/src/addon/mod/assign/feedback/editpdf/providers/handler.ts @@ -14,7 +14,9 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { AddonModAssignProvider } from '../../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate'; import { AddonModAssignFeedbackEditPdfComponent } from '../component/editpdf'; @@ -29,14 +31,14 @@ export class AddonModAssignFeedbackEditPdfHandler implements AddonModAssignFeedb constructor(private assignProvider: AddonModAssignProvider) { } /** - * Return the Component to use to display the plugin data, either in read or in edit mode. + * Return the Component to use to display the plugin data. * It's recommended to return the class of the component, but you can also return an instance of the component. * * @param injector Injector. * @param plugin The plugin object. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin): any | Promise { return AddonModAssignFeedbackEditPdfComponent; } @@ -50,7 +52,8 @@ export class AddonModAssignFeedbackEditPdfHandler implements AddonModAssignFeedb * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return this.assignProvider.getSubmissionPluginAttachments(plugin); } diff --git a/src/addon/mod/assign/feedback/file/providers/handler.ts b/src/addon/mod/assign/feedback/file/providers/handler.ts index 2d55cc35d..adc6da09d 100644 --- a/src/addon/mod/assign/feedback/file/providers/handler.ts +++ b/src/addon/mod/assign/feedback/file/providers/handler.ts @@ -14,7 +14,9 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { AddonModAssignProvider } from '../../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate'; import { AddonModAssignFeedbackFileComponent } from '../component/file'; @@ -29,14 +31,14 @@ export class AddonModAssignFeedbackFileHandler implements AddonModAssignFeedback constructor(private assignProvider: AddonModAssignProvider) { } /** - * Return the Component to use to display the plugin data, either in read or in edit mode. + * Return the Component to use to display the plugin data. * It's recommended to return the class of the component, but you can also return an instance of the component. * * @param injector Injector. * @param plugin The plugin object. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin): any | Promise { return AddonModAssignFeedbackFileComponent; } @@ -50,7 +52,8 @@ export class AddonModAssignFeedbackFileHandler implements AddonModAssignFeedback * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return this.assignProvider.getSubmissionPluginAttachments(plugin); } diff --git a/src/addon/mod/assign/pages/edit-feedback-modal/edit-feedback-modal.ts b/src/addon/mod/assign/pages/edit-feedback-modal/edit-feedback-modal.ts index 3c60de76e..ace3043c0 100644 --- a/src/addon/mod/assign/pages/edit-feedback-modal/edit-feedback-modal.ts +++ b/src/addon/mod/assign/pages/edit-feedback-modal/edit-feedback-modal.ts @@ -17,6 +17,9 @@ import { IonicPage, ViewController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate'; +import { + AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../providers/assign'; /** * Modal that allows editing a feedback plugin. @@ -28,9 +31,9 @@ import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegat }) export class AddonModAssignEditFeedbackModalPage { - @Input() assign: any; // The assignment. - @Input() submission: any; // The submission. - @Input() plugin: any; // The plugin object. + @Input() assign: AddonModAssignAssign; // The assignment. + @Input() submission: AddonModAssignSubmission; // The submission. + @Input() plugin: AddonModAssignPlugin; // The plugin object. @Input() userId: number; // The user ID of the submission. protected forceLeave = false; // To allow leaving the page without checking for changes. @@ -99,8 +102,8 @@ export class AddonModAssignEditFeedbackModalPage { * @return Promise resolved with boolean: whether the data has changed. */ protected hasDataChanged(): Promise { - return this.feedbackDelegate.hasPluginDataChanged(this.assign, this.userId, this.plugin, this.getInputData(), this.userId) - .catch(() => { + return this.feedbackDelegate.hasPluginDataChanged(this.assign, this.submission, this.plugin, this.getInputData(), + this.userId).catch(() => { // Ignore errors. return true; }); diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index 51f72b0a6..f8569eb0f 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -20,7 +20,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from '../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; import { AddonModAssignSyncProvider } from '../../providers/assign-sync'; import { AddonModAssignHelperProvider } from '../../providers/helper'; @@ -35,9 +35,9 @@ import { AddonModAssignHelperProvider } from '../../providers/helper'; }) export class AddonModAssignEditPage implements OnInit, OnDestroy { title: string; // Title to display. - assign: any; // Assignment. + assign: AddonModAssignAssign; // Assignment. courseId: number; // Course ID the assignment belongs to. - userSubmission: any; // The user submission. + userSubmission: AddonModAssignSubmission; // The user submission. allowOffline: boolean; // Whether offline is allowed. submissionStatement: string; // The submission statement. submissionStatementAccepted: boolean; // Whether submission statement is accepted. @@ -129,7 +129,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); // Check if the user can edit it in offline. - return this.assignHelper.canEditSubmissionOffline(this.assign, userSubmission).then((canEditOffline) => { + return this.assignHelper.canEditSubmissionOffline(this.assign, userSubmission).then((canEditOffline): any => { if (canEditOffline) { return response; } @@ -301,7 +301,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { } else { // Try to send it to server. promise = this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline, - this.userSubmission.timemodified, this.assign.submissiondrafts, this.userId); + this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId); } return promise.then(() => { diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts index 9cc87c045..c3a364b4d 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -19,9 +19,11 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission +} from '../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; -import { AddonModAssignHelperProvider } from '../../providers/helper'; +import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** @@ -36,7 +38,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; title: string; // Title to display. - assign: any; // Assignment. + assign: AddonModAssignAssign; // Assignment. submissions: any[]; // List of submissions loaded: boolean; // Whether data has been loaded. haveAllParticipants: boolean; // Whether all participants have been loaded. @@ -53,7 +55,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { protected courseId: number; // Course ID the assignment belongs to. protected selectedStatus: string; // The status to see. protected gradedObserver; // Observer to refresh data when a grade changes. - protected submissionsData: any; + protected submissionsData: {canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}; constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, @@ -161,14 +163,14 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { !this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id) : Promise.resolve(null), ]; - return Promise.all(promises).then(([submissions, grades]) => { + return Promise.all(promises).then(([submissions, grades]: [AddonModAssignSubmissionFormatted[], AddonModAssignGrade[]]) => { // Filter the submissions to get only the ones with the right status and add some extra data. const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING, searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus, promises = [], showSubmissions = []; - submissions.forEach((submission) => { + submissions.forEach((submission: AddonModAssignSubmissionForList) => { if (!searchStatus || searchStatus == submission.status) { promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => { // Ignore errors. @@ -213,7 +215,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { submission.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + submission.status); } else { - submission.statusTranslated = false; + submission.statusTranslated = ''; } if (notSynced) { @@ -224,7 +226,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { submission.gradingStatusTranslationId = this.assignProvider.getSubmissionGradingStatusTranslationId(submission.gradingstatus); } else { - submission.gradingStatusTranslationId = false; + submission.gradingStatusTranslationId = ''; } showSubmissions.push(submission); @@ -299,3 +301,13 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { this.gradedObserver && this.gradedObserver.off(); } } + +/** + * Calculated data for an assign submission. + */ +type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & { + statusColor?: string; // Calculated in the app. Color of the submission status. + gradingColor?: string; // Calculated in the app. Color of the submission grading status. + statusTranslated?: string; // Calculated in the app. Translated text of the submission status. + gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status. +}; diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.ts b/src/addon/mod/assign/pages/submission-review/submission-review.ts index a3652667d..9c96c1c07 100644 --- a/src/addon/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addon/mod/assign/pages/submission-review/submission-review.ts @@ -17,7 +17,7 @@ import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignProvider, AddonModAssignAssign } from '../../providers/assign'; import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; /** @@ -40,7 +40,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { loaded: boolean; // Whether data has been loaded. canSaveGrades: boolean; // Whether the user can save grades. - protected assign: any; // The assignment the submission belongs to. + protected assign: AddonModAssignAssign; // The assignment the submission belongs to. protected blindMarking: boolean; // Whether it uses blind marking. protected forceLeave = false; // To allow leaving the page without checking for changes. diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index 60967dc07..c61e09cfa 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -26,7 +26,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreSyncBaseProvider } from '@classes/base-sync'; -import { AddonModAssignProvider } from './assign'; +import { AddonModAssignProvider, AddonModAssignAssign } from './assign'; import { AddonModAssignOfflineProvider } from './assign-offline'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; @@ -169,14 +169,14 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { syncAssign(assignId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const promises = [], + const promises: Promise[] = [], result: AddonModAssignSyncResult = { warnings: [], updated: false }; - let assign, - courseId, - syncPromise; + let assign: AddonModAssignAssign, + courseId: number, + syncPromise: Promise; if (this.isSyncing(assignId, siteId)) { // There's already a sync ongoing for this assign, return the promise. @@ -269,7 +269,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success, rejected otherwise. */ - protected syncSubmission(assign: any, offlineData: any, warnings: string[], siteId?: string): Promise { + protected syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string): Promise { const userId = offlineData.userid, pluginData = {}; let discardError, @@ -358,8 +358,8 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success, rejected otherwise. */ - protected syncSubmissionGrade(assign: any, offlineData: any, warnings: string[], courseId: number, siteId?: string) - : Promise { + protected syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number, + siteId?: string): Promise { const userId = offlineData.userid; let discardError; diff --git a/src/addon/mod/assign/providers/assign.ts b/src/addon/mod/assign/providers/assign.ts index e819751a6..07a656c1a 100644 --- a/src/addon/mod/assign/providers/assign.ts +++ b/src/addon/mod/assign/providers/assign.ts @@ -27,6 +27,7 @@ import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignOfflineProvider } from './assign-offline'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreInterceptor } from '@classes/interceptor'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some functions for assign. @@ -123,7 +124,7 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the assignment. */ - getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { + getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); } @@ -138,7 +139,7 @@ export class AddonModAssignProvider { * @return Promise resolved when the assignment is retrieved. */ protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) - : Promise { + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -161,7 +162,7 @@ export class AddonModAssignProvider { delete params.includenotenrolledcourses; return site.read('mod_assign_get_assignments', params, preSets); - }).then((response) => { + }).then((response: AddonModAssignGetAssignmentsResult): any => { // Search the assignment to return. if (response.courses && response.courses.length) { const assignments = response.courses[0].assignments; @@ -187,7 +188,7 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the assignment. */ - getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { + getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); } @@ -225,7 +226,9 @@ export class AddonModAssignProvider { preSets.emergencyCache = false; } - return site.read('mod_assign_get_user_mappings', params, preSets).then((response) => { + return site.read('mod_assign_get_user_mappings', params, preSets) + .then((response: AddonModAssignGetUserMappingsResult): any => { + // Search the user. if (response.assignments && response.assignments.length) { if (!userId || userId < 0) { @@ -271,7 +274,7 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Resolved with requested info when done. */ - getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { + getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentids: [assignId] @@ -285,7 +288,7 @@ export class AddonModAssignProvider { preSets.emergencyCache = false; } - return site.read('mod_assign_get_grades', params, preSets).then((response) => { + return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => { // Search the assignment. if (response.assignments && response.assignments.length) { const assignment = response.assignments[0]; @@ -294,7 +297,7 @@ export class AddonModAssignProvider { return assignment.grades; } } else if (response.warnings && response.warnings.length) { - if (response.warnings[0].warningcode == 3) { + if (response.warnings[0].warningcode == '3') { // No grades found. return []; } @@ -362,7 +365,9 @@ export class AddonModAssignProvider { * @param attempt Attempt. * @return Submission object or null. */ - getSubmissionObjectFromAttempt(assign: any, attempt: any): any { + getSubmissionObjectFromAttempt(assign: AddonModAssignAssign, attempt: AddonModAssignSubmissionAttempt) + : AddonModAssignSubmission { + if (!attempt) { return null; } @@ -432,7 +437,7 @@ export class AddonModAssignProvider { * @return Promise resolved when done. */ getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) - : Promise<{canviewsubmissions: boolean, submissions?: any[]}> { + : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -448,9 +453,11 @@ export class AddonModAssignProvider { preSets.emergencyCache = false; } - return site.read('mod_assign_get_submissions', params, preSets).then((response): any => { + return site.read('mod_assign_get_submissions', params, preSets) + .then((response: AddonModAssignGetSubmissionsResult): any => { + // Check if we can view submissions, with enough permissions. - if (response.warnings.length > 0 && response.warnings[0].warningcode == 1) { + if (response.warnings.length > 0 && response.warnings[0].warningcode == '1') { return {canviewsubmissions: false}; } @@ -489,7 +496,7 @@ export class AddonModAssignProvider { * @return Promise always resolved with the user submission status. */ getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + ignoreCache?: boolean, siteId?: string): Promise { userId = userId || 0; @@ -540,7 +547,7 @@ export class AddonModAssignProvider { * @return Promise always resolved with the user submission status. */ getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + ignoreCache?: boolean, siteId?: string): Promise { return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => { const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); @@ -630,7 +637,9 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the list of participants and summary of submissions. */ - listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { + listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string) + : Promise { + groupId = groupId || 0; return this.sitesProvider.getSite(siteId).then((site) => { @@ -1051,14 +1060,14 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when saved, rejected otherwise. */ - saveSubmissionOnline(assignId: number, pluginData: any, siteId?: string): Promise { + saveSubmissionOnline(assignId: number, pluginData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentid: assignId, plugindata: pluginData }; - return site.write('mod_assign_save_submission', params).then((warnings) => { + return site.write('mod_assign_save_submission', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); @@ -1120,14 +1129,14 @@ export class AddonModAssignProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when submitted, rejected otherwise. */ - submitForGradingOnline(assignId: number, acceptStatement: boolean, siteId?: string): Promise { + submitForGradingOnline(assignId: number, acceptStatement: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentid: assignId, acceptsubmissionstatement: acceptStatement ? 1 : 0 }; - return site.write('mod_assign_submit_for_grading', params).then((warnings) => { + return site.write('mod_assign_submit_for_grading', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); @@ -1169,7 +1178,10 @@ export class AddonModAssignProvider { return this.isGradingOfflineEnabled(siteId).then((enabled) => { if (!enabled) { return this.submitGradingFormOnline(assignId, userId, grade, attemptNumber, addAttempt, workflowState, - applyToAll, outcomes, pluginData, siteId); + applyToAll, outcomes, pluginData, siteId).then(() => { + + return true; + }); } if (!this.appProvider.isOnline()) { @@ -1212,7 +1224,7 @@ export class AddonModAssignProvider { * @return Promise resolved when submitted, rejected otherwise. */ submitGradingFormOnline(assignId: number, userId: number, grade: number, attemptNumber: number, addAttempt: boolean, - workflowState: string, applyToAll: boolean, outcomes: any, pluginData: any, siteId?: string): Promise { + workflowState: string, applyToAll: boolean, outcomes: any, pluginData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -1243,7 +1255,7 @@ export class AddonModAssignProvider { jsonformdata: JSON.stringify(serialized) }; - return site.write('mod_assign_submit_grading_form', params).then((warnings) => { + return site.write('mod_assign_submit_grading_form', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); @@ -1271,3 +1283,285 @@ export class AddonModAssignProvider { }); } } + +/** + * Assign data returned by mod_assign_get_assignments. + */ +export type AddonModAssignAssign = { + id: number; // Assignment id. + cmid: number; // Course module id. + course: number; // Course id. + name: string; // Assignment name. + nosubmissions: number; // No submissions. + submissiondrafts: number; // Submissions drafts. + sendnotifications: number; // Send notifications. + sendlatenotifications: number; // Send notifications. + sendstudentnotifications: number; // Send student notifications (default). + duedate: number; // Assignment due date. + allowsubmissionsfromdate: number; // Allow submissions from date. + grade: number; // Grade type. + timemodified: number; // Last time assignment was modified. + completionsubmit: number; // If enabled, set activity as complete following submission. + cutoffdate: number; // Date after which submission is not accepted without an extension. + gradingduedate?: number; // @since 3.3. The expected date for marking the submissions. + teamsubmission: number; // If enabled, students submit as a team. + requireallteammemberssubmit: number; // If enabled, all team members must submit. + teamsubmissiongroupingid: number; // The grouping id for the team submission groups. + blindmarking: number; // If enabled, hide identities until reveal identities actioned. + hidegrader?: number; // @since 3.7. If enabled, hide grader to student. + revealidentities: number; // Show identities for a blind marking assignment. + attemptreopenmethod: string; // Method used to control opening new attempts. + maxattempts: number; // Maximum number of attempts allowed. + markingworkflow: number; // Enable marking workflow. + markingallocation: number; // Enable marking allocation. + requiresubmissionstatement: number; // Student must accept submission statement. + preventsubmissionnotingroup?: number; // @since 3.2. Prevent submission not in group. + submissionstatement?: string; // @since 3.2. Submission statement formatted. + submissionstatementformat?: number; // @since 3.2. Submissionstatement format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + configs: AddonModAssignConfig[]; // Configuration settings. + intro?: string; // Assignment intro, not allways returned because it deppends on the activity configuration. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + introattachments?: CoreWSExternalFile[]; +}; + +/** + * Config setting in an assign. + */ +export type AddonModAssignConfig = { + id?: number; // Assign_plugin_config id. + assignment?: number; // Assignment id. + plugin: string; // Plugin. + subtype: string; // Subtype. + name: string; // Name. + value: string; // Value. +}; + +/** + * Grade of an assign, returned by mod_assign_get_grades. + */ +export type AddonModAssignGrade = { + id: number; // Grade id. + assignment?: number; // Assignment id. + userid: number; // Student id. + attemptnumber: number; // Attempt number. + timecreated: number; // Grade creation time. + timemodified: number; // Grade last modified time. + grader: number; // Grader, -1 if grader is hidden. + grade: string; // Grade. + gradefordisplay?: string; // Grade rendered into a format suitable for display. +}; + +/** + * Assign submission returned by mod_assign_get_submissions. + */ +export type AddonModAssignSubmission = { + id: number; // Submission id. + userid: number; // Student id. + attemptnumber: number; // Attempt number. + timecreated: number; // Submission creation time. + timemodified: number; // Submission last modified time. + status: string; // Submission status. + groupid: number; // Group id. + assignment?: number; // Assignment id. + latest?: number; // Latest attempt. + plugins?: AddonModAssignPlugin[]; // Plugins. + gradingstatus?: string; // @since 3.2. Grading status. +}; + +/** + * Assign plugin. + */ +export type AddonModAssignPlugin = { + type: string; // Submission plugin type. + name: string; // Submission plugin name. + fileareas?: { // Fileareas. + area: string; // File area. + files?: CoreWSExternalFile[]; + }[]; + editorfields?: { // Editorfields. + name: string; // Field name. + description: string; // Field description. + text: string; // Field value. + format: number; // Text format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + }[]; +}; + +/** + * Grading summary of an assign submission. + */ +export type AddonModAssignSubmissionGradingSummary = { + participantcount: number; // Number of users who can submit. + submissiondraftscount: number; // Number of submissions in draft status. + submissionsenabled: boolean; // Whether submissions are enabled or not. + submissionssubmittedcount: number; // Number of submissions in submitted status. + submissionsneedgradingcount: number; // Number of submissions that need grading. + warnofungroupedusers: string; // Whether we need to warn people about groups. +}; + +/** + * Attempt of an assign submission. + */ +export type AddonModAssignSubmissionAttempt = { + submission?: AddonModAssignSubmission; // Submission info. + teamsubmission?: AddonModAssignSubmission; // Submission info. + submissiongroup?: number; // The submission group id (for group submissions only). + submissiongroupmemberswhoneedtosubmit?: number[]; // List of users who still need to submit (for group submissions only). + submissionsenabled: boolean; // Whether submissions are enabled or not. + locked: boolean; // Whether new submissions are locked. + graded: boolean; // Whether the submission is graded. + canedit: boolean; // Whether the user can edit the current submission. + caneditowner?: boolean; // @since 3.2. Whether the owner of the submission can edit it. + cansubmit: boolean; // Whether the user can submit. + extensionduedate: number; // Extension due date. + blindmarking: boolean; // Whether blind marking is enabled. + gradingstatus: string; // Grading status. + usergroups: number[]; // User groups in the course. +}; + +/** + * Previous attempt of an assign submission. + */ +export type AddonModAssignSubmissionPreviousAttempt = { + attemptnumber: number; // Attempt number. + submission?: AddonModAssignSubmission; // Submission info. + grade?: AddonModAssignGrade; // Grade information. + feedbackplugins?: AddonModAssignPlugin[]; // Feedback info. +}; + +/** + * Feedback of an assign submission. + */ +export type AddonModAssignSubmissionFeedback = { + grade: AddonModAssignGrade; // Grade information. + gradefordisplay: string; // Grade rendered into a format suitable for display. + gradeddate: number; // The date the user was graded. + plugins?: AddonModAssignPlugin[]; // Plugins info. +}; + +/** + * Participant returned by mod_assign_list_participants. + */ +export type AddonModAssignParticipant = { + id: number; // ID of the user. + username?: string; // The username. + firstname?: string; // The first name(s) of the user. + lastname?: string; // The family name of the user. + fullname: string; // The fullname of the user. + email?: string; // Email address. + address?: string; // Postal address. + phone1?: string; // Phone 1. + phone2?: string; // Phone 2. + icq?: string; // Icq number. + skype?: string; // Skype id. + yahoo?: string; // Yahoo id. + aim?: string; // Aim id. + msn?: string; // Msn number. + department?: string; // Department. + institution?: string; // Institution. + idnumber?: string; // The idnumber of the user. + interests?: string; // User interests (separated by commas). + firstaccess?: number; // First access to the site (0 if never). + lastaccess?: number; // Last access to the site (0 if never). + suspended?: boolean; // @since 3.2. Suspend user account, either false to enable user login or true to disable it. + description?: string; // User profile description. + descriptionformat?: number; // Int format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + city?: string; // Home city of the user. + url?: string; // URL of the user. + country?: string; // Home country code of the user, such as AU or CZ. + profileimageurlsmall?: string; // User image profile URL - small version. + profileimageurl?: string; // User image profile URL - big version. + customfields?: { // User custom fields (also known as user profile fields). + type: string; // The type of the custom field - text field, checkbox... + value: string; // The value of the custom field. + name: string; // The name of the custom field. + shortname: string; // The shortname of the custom field - to be able to build the field class in the code. + }[]; + preferences?: { // Users preferences. + name: string; // The name of the preferences. + value: string; // The value of the preference. + }[]; + recordid?: number; // @since 3.7. Record id. + groups?: { // User groups. + id: number; // Group id. + name: string; // Group name. + description: string; // Group description. + }[]; + roles?: { // User roles. + roleid: number; // Role id. + name: string; // Role name. + shortname: string; // Role shortname. + sortorder: number; // Role sortorder. + }[]; + enrolledcourses?: { // Courses where the user is enrolled - limited by which courses the user is able to see. + id: number; // Id of the course. + fullname: string; // Fullname of the course. + shortname: string; // Shortname of the course. + }[]; + submitted: boolean; // Have they submitted their assignment. + requiregrading: boolean; // Is their submission waiting for grading. + grantedextension?: boolean; // @since 3.3. Have they been granted an extension. + groupid?: number; // For group assignments this is the group id. + groupname?: string; // For group assignments this is the group name. +}; + +/** + * Result of WS mod_assign_get_assignments. + */ +export type AddonModAssignGetAssignmentsResult = { + courses: { // List of courses. + id: number; // Course id. + fullname: string; // Course full name. + shortname: string; // Course short name. + timemodified: number; // Last time modified. + assignments: AddonModAssignAssign[]; // Assignment info. + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_assign_get_user_mappings. + */ +export type AddonModAssignGetUserMappingsResult = { + assignments: { // List of assign user mapping data. + assignmentid: number; // Assignment id. + mappings: { + id: number; // User mapping id. + userid: number; // Student id. + }[]; + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_assign_get_grades. + */ +export type AddonModAssignGetGradesResult = { + assignments: { // List of assignment grade information. + assignmentid: number; // Assignment id. + grades: AddonModAssignGrade[]; + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_assign_get_submissions. + */ +export type AddonModAssignGetSubmissionsResult = { + assignments: { // Assignment submissions. + assignmentid: number; // Assignment id. + submissions: AddonModAssignSubmission[]; + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_assign_get_submission_status. + */ +export type AddonModAssignGetSubmissionStatusResult = { + gradingsummary?: AddonModAssignSubmissionGradingSummary; // Grading information. + lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information. + feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt. + previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/assign/providers/feedback-delegate.ts b/src/addon/mod/assign/providers/feedback-delegate.ts index c72dda969..4565f64be 100644 --- a/src/addon/mod/assign/providers/feedback-delegate.ts +++ b/src/addon/mod/assign/providers/feedback-delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { AddonModAssignDefaultFeedbackHandler } from './default-feedback-handler'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign'; /** * Interface that all feedback handlers must implement. @@ -47,7 +48,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent?(injector: Injector, plugin: any): any | Promise; + getComponent?(injector: Injector, plugin: AddonModAssignPlugin): any | Promise; /** * Return the draft saved data of the feedback plugin. @@ -69,7 +70,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles?(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise; + getPluginFiles?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise; /** * Get a readable name to use for the plugin. @@ -77,7 +79,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @return The plugin name. */ - getPluginName?(plugin: any): string; + getPluginName?(plugin: AddonModAssignPlugin): string; /** * Check if the feedback data has changed for this plugin. @@ -89,7 +91,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param userId User ID of the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged?(assign: any, submission: any, plugin: any, inputData: any, userId: number): boolean | Promise; + hasDataChanged?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, userId: number): boolean | Promise; /** * Check whether the plugin has draft data stored. @@ -111,7 +114,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch?(assign: any, submission: any, plugin: any, siteId?: string): Promise; + prefetch?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise; /** * Prepare and add to pluginData the data to send to the server based on the draft data saved. @@ -123,7 +127,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareFeedbackData?(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise; + prepareFeedbackData?(assignId: number, userId: number, plugin: AddonModAssignPlugin, pluginData: any, + siteId?: string): void | Promise; /** * Save draft data of the feedback plugin. @@ -135,7 +140,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - saveDraft?(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise; + saveDraft?(assignId: number, userId: number, plugin: AddonModAssignPlugin, data: any, siteId?: string) + : void | Promise; } /** @@ -160,7 +166,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - discardPluginFeedbackData(assignId: number, userId: number, plugin: any, siteId?: string): Promise { + discardPluginFeedbackData(assignId: number, userId: number, plugin: AddonModAssignPlugin, siteId?: string) + : Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'discardDraft', [assignId, userId, siteId])); } @@ -171,7 +178,7 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param plugin The plugin object. * @return Promise resolved with the component to use, undefined if not found. */ - getComponentForPlugin(injector: Injector, plugin: any): Promise { + getComponentForPlugin(injector: Injector, plugin: AddonModAssignPlugin): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getComponent', [injector, plugin])); } @@ -184,7 +191,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the draft data. */ - getPluginDraftData(assignId: number, userId: number, plugin: any, siteId?: string): Promise { + getPluginDraftData(assignId: number, userId: number, plugin: AddonModAssignPlugin, siteId?: string) + : Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getDraft', [assignId, userId, siteId])); } @@ -198,7 +206,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the files. */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId])); } @@ -208,7 +217,7 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param plugin Plugin to get the name for. * @return Human readable name. */ - getPluginName(plugin: any): string { + getPluginName(plugin: AddonModAssignPlugin): string { return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]); } @@ -222,7 +231,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param userId User ID of the submission. * @return Promise resolved with true if data has changed, resolved with false otherwise. */ - hasPluginDataChanged(assign: any, submission: any, plugin: any, inputData: any, userId: number): Promise { + hasPluginDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, userId: number): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDataChanged', [assign, submission, plugin, inputData, userId])); } @@ -236,7 +246,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if it has draft data. */ - hasPluginDraftData(assignId: number, userId: number, plugin: any, siteId?: string): Promise { + hasPluginDraftData(assignId: number, userId: number, plugin: AddonModAssignPlugin, siteId?: string) + : Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDraftData', [assignId, userId, siteId])); } @@ -259,7 +270,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + prefetch(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, + siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId])); } @@ -273,7 +285,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been gathered. */ - preparePluginFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): Promise { + preparePluginFeedbackData(assignId: number, userId: number, plugin: AddonModAssignPlugin, pluginData: any, + siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prepareFeedbackData', [assignId, userId, plugin, pluginData, siteId])); @@ -289,7 +302,8 @@ export class AddonModAssignFeedbackDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been saved. */ - saveFeedbackDraft(assignId: number, userId: number, plugin: any, inputData: any, siteId?: string): Promise { + saveFeedbackDraft(assignId: number, userId: number, plugin: AddonModAssignPlugin, inputData: any, + siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'saveDraft', [assignId, userId, plugin, inputData, siteId])); } diff --git a/src/addon/mod/assign/providers/helper.ts b/src/addon/mod/assign/providers/helper.ts index a634e520c..d89d14c97 100644 --- a/src/addon/mod/assign/providers/helper.ts +++ b/src/addon/mod/assign/providers/helper.ts @@ -21,7 +21,10 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; -import { AddonModAssignProvider } from './assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignParticipant, + AddonModAssignSubmissionFeedback +} from './assign'; import { AddonModAssignOfflineProvider } from './assign-offline'; /** @@ -46,7 +49,7 @@ export class AddonModAssignHelperProvider { * @param submission Submission. * @return Whether it can be edited offline. */ - canEditSubmissionOffline(assign: any, submission: any): Promise { + canEditSubmissionOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission): Promise { if (!submission) { return Promise.resolve(false); } @@ -81,7 +84,7 @@ export class AddonModAssignHelperProvider { * @param submission Submission to clear the data for. * @param inputData Data entered in the submission form. */ - clearSubmissionPluginTmpData(assign: any, submission: any, inputData: any): void { + clearSubmissionPluginTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): void { submission.plugins.forEach((plugin) => { this.submissionDelegate.clearTmpData(assign, submission, plugin, inputData); }); @@ -95,7 +98,7 @@ export class AddonModAssignHelperProvider { * @param previousSubmission Submission to copy. * @return Promise resolved when done. */ - copyPreviousAttempt(assign: any, previousSubmission: any): Promise { + copyPreviousAttempt(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise { const pluginData = {}, promises = []; @@ -112,6 +115,36 @@ export class AddonModAssignHelperProvider { }); } + /** + * Create an empty feedback object. + * + * @return Feedback. + */ + createEmptyFeedback(): AddonModAssignSubmissionFeedback { + return { + grade: undefined, + gradefordisplay: undefined, + gradeddate: undefined + }; + } + + /** + * Create an empty submission object. + * + * @return Submission. + */ + createEmptySubmission(): AddonModAssignSubmissionFormatted { + return { + id: undefined, + userid: undefined, + attemptnumber: undefined, + timecreated: undefined, + timemodified: undefined, + status: undefined, + groupid: undefined + }; + } + /** * Delete stored submission files for a plugin. See storeSubmissionFiles. * @@ -136,7 +169,9 @@ export class AddonModAssignHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - discardFeedbackPluginData(assignId: number, userId: number, feedback: any, siteId?: string): Promise { + discardFeedbackPluginData(assignId: number, userId: number, feedback: AddonModAssignSubmissionFeedback, + siteId?: string): Promise { + const promises = []; feedback.plugins.forEach((plugin) => { @@ -155,7 +190,9 @@ export class AddonModAssignHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the list of participants and summary of submissions. */ - getParticipants(assign: any, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { + getParticipants(assign: AddonModAssignAssign, groupId?: number, ignoreCache?: boolean, siteId?: string) + : Promise { + groupId = groupId || 0; siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -167,7 +204,7 @@ export class AddonModAssignHelperProvider { // If no participants returned and all groups specified, get participants by groups. return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((info) => { const promises = [], - participants = {}; + participants: {[id: number]: AddonModAssignParticipant} = {}; info.groups.forEach((userGroup) => { promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId) @@ -194,8 +231,8 @@ export class AddonModAssignHelperProvider { * @param type Name of the subplugin. * @return Object containing all configurations of the subplugin selected. */ - getPluginConfig(assign: any, subtype: string, type: string): any { - const configs = {}; + getPluginConfig(assign: AddonModAssignAssign, subtype: string, type: string): {[name: string]: string} { + const configs: {[name: string]: string} = {}; assign.configs.forEach((config) => { if (config.subtype == subtype && config.plugin == type) { @@ -213,7 +250,7 @@ export class AddonModAssignHelperProvider { * @param subtype Subtype name (assignsubmission or assignfeedback) * @return List of enabled plugins for the assign. */ - getPluginsEnabled(assign: any, subtype: string): any[] { + getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): any[] { const enabled = []; assign.configs.forEach((config) => { @@ -250,7 +287,7 @@ export class AddonModAssignHelperProvider { * @param previousSubmission Submission to copy. * @return Promise resolved with the size. */ - getSubmissionSizeForCopy(assign: any, previousSubmission: any): Promise { + getSubmissionSizeForCopy(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise { const promises = []; let totalSize = 0; @@ -273,7 +310,8 @@ export class AddonModAssignHelperProvider { * @param inputData Data entered in the submission form. * @return Promise resolved with the size. */ - getSubmissionSizeForEdit(assign: any, submission: any, inputData: any): Promise { + getSubmissionSizeForEdit(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): Promise { + const promises = []; let totalSize = 0; @@ -298,14 +336,14 @@ export class AddonModAssignHelperProvider { * @param siteId Site id (empty for current site). * @return Promise always resolved. Resolve param is the formatted submissions. */ - getSubmissionsUserData(assign: any, submissions: any[], groupId?: number, ignoreCache?: boolean, siteId?: string): - Promise { - return this.getParticipants(assign, groupId).then((participants) => { + getSubmissionsUserData(assign: AddonModAssignAssign, submissions: AddonModAssignSubmissionFormatted[], groupId?: number, + ignoreCache?: boolean, siteId?: string): Promise { + + return this.getParticipants(assign, groupId).then((parts) => { const blind = assign.blindmarking && !assign.revealidentities; const promises = []; - const result = []; - - participants = this.utils.arrayToObject(participants, 'id'); + const result: AddonModAssignSubmissionFormatted[] = []; + const participants: {[id: number]: AddonModAssignParticipant} = this.utils.arrayToObject(parts, 'id'); submissions.forEach((submission) => { submission.submitid = submission.userid > 0 ? submission.userid : submission.blindid; @@ -356,10 +394,10 @@ export class AddonModAssignHelperProvider { return Promise.all(promises).then(() => { // Create a submission for each participant left in the list (the participants already treated were removed). - this.utils.objectToArray(participants).forEach((participant) => { - const submission: any = { - submitid: participant.id - }; + this.utils.objectToArray(participants).forEach((participant: AddonModAssignParticipant) => { + const submission = this.createEmptySubmission(); + + submission.submitid = participant.id; if (!blind) { submission.userid = participant.id; @@ -390,17 +428,20 @@ export class AddonModAssignHelperProvider { * Check if the feedback data has changed for a certain submission and assign. * * @param assign Assignment. - * @param userId User Id. + * @param submission The submission. * @param feedback Feedback data. + * @param userId The user ID. * @return Promise resolved with true if data has changed, resolved with false otherwise. */ - hasFeedbackDataChanged(assign: any, userId: number, feedback: any): Promise { + hasFeedbackDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + feedback: AddonModAssignSubmissionFeedback, userId: number): Promise { + const promises = []; let hasChanged = false; feedback.plugins.forEach((plugin) => { promises.push(this.prepareFeedbackPluginData(assign.id, userId, feedback).then((inputData) => { - return this.feedbackDelegate.hasPluginDataChanged(assign, userId, plugin, inputData, userId).then((changed) => { + return this.feedbackDelegate.hasPluginDataChanged(assign, submission, plugin, inputData, userId).then((changed) => { if (changed) { hasChanged = true; } @@ -423,7 +464,9 @@ export class AddonModAssignHelperProvider { * @param inputData Data entered in the submission form. * @return Promise resolved with true if data has changed, resolved with false otherwise. */ - hasSubmissionDataChanged(assign: any, submission: any, inputData: any): Promise { + hasSubmissionDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any) + : Promise { + const promises = []; let hasChanged = false; @@ -451,7 +494,9 @@ export class AddonModAssignHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with plugin data to send to server. */ - prepareFeedbackPluginData(assignId: number, userId: number, feedback: any, siteId?: string): Promise { + prepareFeedbackPluginData(assignId: number, userId: number, feedback: AddonModAssignSubmissionFeedback, siteId?: string) + : Promise { + const pluginData = {}, promises = []; @@ -473,7 +518,9 @@ export class AddonModAssignHelperProvider { * @param offline True to prepare the data for an offline submission, false otherwise. * @return Promise resolved with plugin data to send to server. */ - prepareSubmissionPluginData(assign: any, submission: any, inputData: any, offline?: boolean): Promise { + prepareSubmissionPluginData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any, + offline?: boolean): Promise { + const pluginData = {}, promises = []; @@ -553,3 +600,16 @@ export class AddonModAssignHelperProvider { } } } + +/** + * Assign submission with some calculated data. + */ +export type AddonModAssignSubmissionFormatted = AddonModAssignSubmission & { + blindid?: number; // Calculated in the app. Blindid of the user that did the submission. + submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission. + userfullname?: string; // Calculated in the app. Full name of the user that did the submission. + userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission. + manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group. + noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group. + groupname?: string; // Calculated in the app. Name of the group the submission belongs to. +}; diff --git a/src/addon/mod/assign/providers/prefetch-handler.ts b/src/addon/mod/assign/providers/prefetch-handler.ts index 284d558ab..228e8e5ad 100644 --- a/src/addon/mod/assign/providers/prefetch-handler.ts +++ b/src/addon/mod/assign/providers/prefetch-handler.ts @@ -26,8 +26,8 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreUserProvider } from '@core/user/providers/user'; -import { AddonModAssignProvider } from './assign'; -import { AddonModAssignHelperProvider } from './helper'; +import { AddonModAssignProvider, AddonModAssignGetSubmissionStatusResult, AddonModAssignSubmission } from './assign'; +import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from './helper'; import { AddonModAssignSyncProvider } from './assign-sync'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; @@ -106,7 +106,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan if (data.canviewsubmissions) { // Teacher, get all submissions. return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, false, siteId) - .then((submissions) => { + .then((submissions: AddonModAssignSubmissionFormatted[]) => { const promises = []; @@ -161,9 +161,10 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId) .then((response) => { const promises = []; + let userSubmission: AddonModAssignSubmission; if (response.lastattempt) { - const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, response.lastattempt); + userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, response.lastattempt); if (userSubmission && userSubmission.plugins) { // Add submission plugin files. userSubmission.plugins.forEach((plugin) => { @@ -175,7 +176,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan if (response.feedback && response.feedback.plugins) { // Add feedback plugin files. response.feedback.plugins.forEach((plugin) => { - promises.push(this.feedbackDelegate.getPluginFiles(assign, response, plugin, siteId)); + promises.push(this.feedbackDelegate.getPluginFiles(assign, userSubmission, plugin, siteId)); }); } @@ -303,7 +304,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan groupInfo.groups.forEach((group) => { groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, true, siteId) - .then((submissions) => { + .then((submissions: AddonModAssignSubmissionFormatted[]) => { const subPromises = []; @@ -327,7 +328,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan } // Prefetch the submission of the current user even if it does not exist, this will be create it. - if (!data.submissions || !data.submissions.find((subm) => subm.submitid == userId)) { + if (!data.submissions || + !data.submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id, false, true, true, siteId).then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); @@ -385,15 +387,16 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan * @param siteId Site ID. If not defined, current site. * @return Promise resolved when prefetched, rejected otherwise. */ - protected prefetchSubmission(assign: any, courseId: number, moduleId: number, submission: any, userId?: number, - siteId?: string): Promise { + protected prefetchSubmission(assign: any, courseId: number, moduleId: number, + submission: AddonModAssignGetSubmissionStatusResult, userId?: number, siteId?: string): Promise { const promises = [], blindMarking = assign.blindmarking && !assign.revealidentities; - let userIds = []; + let userIds = [], + userSubmission: AddonModAssignSubmission; if (submission.lastattempt) { - const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, submission.lastattempt); + userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, submission.lastattempt); // Get IDs of the members who need to submit. if (!blindMarking && submission.lastattempt.submissiongroupmemberswhoneedtosubmit) { @@ -440,10 +443,10 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan if (submission.feedback.plugins) { submission.feedback.plugins.forEach((plugin) => { // Prefetch the plugin WS data. - promises.push(this.feedbackDelegate.prefetch(assign, submission, plugin, siteId)); + promises.push(this.feedbackDelegate.prefetch(assign, userSubmission, plugin, siteId)); // Prefetch the plugin files. - promises.push(this.feedbackDelegate.getPluginFiles(assign, submission, plugin, siteId).then((files) => { + promises.push(this.feedbackDelegate.getPluginFiles(assign, userSubmission, plugin, siteId).then((files) => { return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); }).catch(() => { // Ignore errors. diff --git a/src/addon/mod/assign/providers/submission-delegate.ts b/src/addon/mod/assign/providers/submission-delegate.ts index 70c8e9752..7b65d55ce 100644 --- a/src/addon/mod/assign/providers/submission-delegate.ts +++ b/src/addon/mod/assign/providers/submission-delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { AddonModAssignDefaultSubmissionHandler } from './default-submission-handler'; +import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign'; /** * Interface that all submission handlers must implement. @@ -39,7 +40,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ - canEditOffline?(assign: any, submission: any, plugin: any): boolean | Promise; + canEditOffline?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): boolean | Promise; /** * Should clear temporary data for a cancelled submission. @@ -49,7 +51,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @param inputData Data entered by the user for the submission. */ - clearTmpData?(assign: any, submission: any, plugin: any, inputData: any): void; + clearTmpData?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): void; /** * This function will be called when the user wants to create a new submission based on the previous one. @@ -62,7 +65,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - copySubmissionData?(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise; + copySubmissionData?(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: any, + userId?: number, siteId?: string): void | Promise; /** * Delete any stored data for the plugin and submission. @@ -74,7 +78,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - deleteOfflineData?(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise; + deleteOfflineData?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, siteId?: string): void | Promise; /** * Return the Component to use to display the plugin data, either in read or in edit mode. @@ -85,7 +90,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param edit Whether the user is editing. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent?(injector: Injector, plugin: any, edit?: boolean): any | Promise; + getComponent?(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): any | Promise; /** * Get files used by this plugin. @@ -97,7 +102,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles?(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise; + getPluginFiles?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise; /** * Get a readable name to use for the plugin. @@ -105,7 +111,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @return The plugin name. */ - getPluginName?(plugin: any): string; + getPluginName?(plugin: AddonModAssignPlugin): string; /** * Get the size of data (in bytes) this plugin will send to copy a previous submission. @@ -114,7 +120,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param plugin The plugin object. * @return The size (or promise resolved with size). */ - getSizeForCopy?(assign: any, plugin: any): number | Promise; + getSizeForCopy?(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): number | Promise; /** * Get the size of data (in bytes) this plugin will send to add or edit a submission. @@ -125,7 +131,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param inputData Data entered by the user for the submission. * @return The size (or promise resolved with size). */ - getSizeForEdit?(assign: any, submission: any, plugin: any, inputData: any): number | Promise; + getSizeForEdit?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): number | Promise; /** * Check if the submission data has changed for this plugin. @@ -136,7 +143,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param inputData Data entered by the user for the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged?(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise; + hasDataChanged?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): boolean | Promise; /** * Whether or not the handler is enabled for edit on a site level. @@ -155,7 +163,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch?(assign: any, submission: any, plugin: any, siteId?: string): Promise; + prefetch?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise; /** * Prepare and add to pluginData the data to send to the server based on the input data. @@ -170,8 +179,9 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSubmissionData?(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, - userId?: number, siteId?: string): void | Promise; + prepareSubmissionData?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, pluginData: any, offline?: boolean, + userId?: number, siteId?: string): void | Promise; /** * Prepare and add to pluginData the data to send to the server based on the offline data stored. @@ -185,8 +195,8 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSyncData?(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) - : void | Promise; + prepareSyncData?(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, pluginData: any, siteId?: string): void | Promise; } /** @@ -210,7 +220,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param plugin The plugin object. * @return Promise resolved with boolean: whether it can be edited in offline. */ - canPluginEditOffline(assign: any, submission: any, plugin: any): Promise { + canPluginEditOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'canEditOffline', [assign, submission, plugin])); } @@ -222,7 +233,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param plugin The plugin object. * @param inputData Data entered by the user for the submission. */ - clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void { + clearTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): void { return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]); } @@ -236,7 +248,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data has been copied. */ - copyPluginSubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + copyPluginSubmissionData(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: any, + userId?: number, siteId?: string): void | Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'copySubmissionData', [assign, plugin, pluginData, userId, siteId])); } @@ -251,7 +264,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - deletePluginOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): Promise { + deletePluginOfflineData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'deleteOfflineData', [assign, submission, plugin, offlineData, siteId])); } @@ -264,7 +278,7 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param edit Whether the user is editing. * @return Promise resolved with the component to use, undefined if not found. */ - getComponentForPlugin(injector: Injector, plugin: any, edit?: boolean): Promise { + getComponentForPlugin(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getComponent', [injector, plugin, edit])); } @@ -278,7 +292,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the files. */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId])); } @@ -288,7 +303,7 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param plugin Plugin to get the name for. * @return Human readable name. */ - getPluginName(plugin: any): string { + getPluginName(plugin: AddonModAssignPlugin): string { return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]); } @@ -299,7 +314,7 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param plugin The plugin object. * @return Promise resolved with size. */ - getPluginSizeForCopy(assign: any, plugin: any): Promise { + getPluginSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getSizeForCopy', [assign, plugin])); } @@ -312,7 +327,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param inputData Data entered by the user for the submission. * @return Promise resolved with size. */ - getPluginSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): Promise { + getPluginSizeForEdit(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getSizeForEdit', [assign, submission, plugin, inputData])); } @@ -326,7 +342,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param inputData Data entered by the user for the submission. * @return Promise resolved with true if data has changed, resolved with false otherwise. */ - hasPluginDataChanged(assign: any, submission: any, plugin: any, inputData: any): Promise { + hasPluginDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDataChanged', [assign, submission, plugin, inputData])); } @@ -360,7 +377,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + prefetch(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, + siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId])); } @@ -377,8 +395,9 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been gathered. */ - preparePluginSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, - userId?: number, siteId?: string): Promise { + preparePluginSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, pluginData: any, offline?: boolean, userId?: number, + siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prepareSubmissionData', [assign, submission, plugin, inputData, pluginData, offline, userId, siteId])); @@ -395,8 +414,8 @@ export class AddonModAssignSubmissionDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been gathered. */ - preparePluginSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) - : Promise { + preparePluginSyncData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, pluginData: any, siteId?: string): Promise { return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prepareSyncData', [assign, submission, plugin, offlineData, pluginData, siteId])); diff --git a/src/addon/mod/assign/submission/comments/providers/handler.ts b/src/addon/mod/assign/submission/comments/providers/handler.ts index 54f450df5..a608d2132 100644 --- a/src/addon/mod/assign/submission/comments/providers/handler.ts +++ b/src/addon/mod/assign/submission/comments/providers/handler.ts @@ -17,6 +17,9 @@ import { Injectable, Injector } from '@angular/core'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; import { AddonModAssignSubmissionCommentsComponent } from '../component/comments'; +import { + AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; /** * Handler for comments submission plugin. @@ -38,7 +41,8 @@ export class AddonModAssignSubmissionCommentsHandler implements AddonModAssignSu * @param plugin The plugin object. * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ - canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + canEditOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): boolean | Promise { // This plugin is read only, but return true to prevent blocking the edition. return true; } @@ -52,7 +56,7 @@ export class AddonModAssignSubmissionCommentsHandler implements AddonModAssignSu * @param edit Whether the user is editing. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): any | Promise { return edit ? undefined : AddonModAssignSubmissionCommentsComponent; } @@ -84,7 +88,9 @@ export class AddonModAssignSubmissionCommentsHandler implements AddonModAssignSu * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + prefetch(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): Promise { + return this.commentsProvider.getComments('module', assign.cmid, 'assignsubmission_comments', submission.id, 'submission_comments', 0, siteId).catch(() => { // Fail silently (Moodle < 3.1.1, 3.2) diff --git a/src/addon/mod/assign/submission/file/providers/handler.ts b/src/addon/mod/assign/submission/file/providers/handler.ts index 57a4a70bf..7fe7388dc 100644 --- a/src/addon/mod/assign/submission/file/providers/handler.ts +++ b/src/addon/mod/assign/submission/file/providers/handler.ts @@ -21,7 +21,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreWSProvider } from '@providers/ws'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; -import { AddonModAssignProvider } from '../../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; import { AddonModAssignHelperProvider } from '../../../providers/helper'; import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; @@ -53,7 +55,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ - canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + canEditOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): boolean | Promise { // This plugin doesn't use Moodle filters, it can be edited in offline. return true; } @@ -66,7 +69,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @param inputData Data entered by the user for the submission. */ - clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void { + clearTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): void { const files = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id); // Clear the files in session for this assign. @@ -87,7 +91,9 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + copySubmissionData(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: any, + userId?: number, siteId?: string): void | Promise { + // We need to re-upload all the existing files. const files = this.assignProvider.getSubmissionPluginAttachments(plugin); @@ -105,7 +111,7 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param edit Whether the user is editing. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): any | Promise { return AddonModAssignSubmissionFileComponent; } @@ -119,7 +125,9 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - deleteOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise { + deleteOfflineData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, siteId?: string): void | Promise { + return this.assignHelper.deleteStoredSubmissionFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME, submission.userid, siteId).catch(() => { // Ignore errors, maybe the folder doesn't exist. @@ -136,7 +144,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return this.assignProvider.getSubmissionPluginAttachments(plugin); } @@ -147,7 +156,7 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param plugin The plugin object. * @return The size (or promise resolved with size). */ - getSizeForCopy(assign: any, plugin: any): number | Promise { + getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): number | Promise { const files = this.assignProvider.getSubmissionPluginAttachments(plugin), promises = []; let totalSize = 0; @@ -177,7 +186,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param inputData Data entered by the user for the submission. * @return The size (or promise resolved with size). */ - getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise { + getSizeForEdit(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): number | Promise { const siteId = this.sitesProvider.getCurrentSiteId(); // Check if there's any change. @@ -232,7 +242,9 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param inputData Data entered by the user for the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + hasDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): boolean | Promise { + // Check if there's any offline data. return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => { // No offline data found. @@ -299,7 +311,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + prepareSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, pluginData: any, offline?: boolean, userId?: number, siteId?: string): void | Promise { if (this.hasDataChanged(assign, submission, plugin, inputData)) { @@ -330,8 +343,8 @@ export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmis * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) - : void | Promise { + prepareSyncData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, pluginData: any, siteId?: string): void | Promise { const filesData = offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager; if (filesData) { diff --git a/src/addon/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html b/src/addon/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html index a231c0cfa..5d2565e06 100644 --- a/src/addon/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html +++ b/src/addon/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html @@ -10,7 +10,7 @@
{{ plugin.name }} - +

{{ 'addon.mod_assign.wordlimit' | translate }}

{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}

diff --git a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts index 25b0cd5e6..b5c6d4045 100644 --- a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts +++ b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -34,6 +34,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS component = AddonModAssignProvider.COMPONENT; text: string; loaded: boolean; + wordLimitEnabled: boolean; protected wordCountTimeout: any; protected element: HTMLElement; @@ -61,9 +62,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS // No offline data found, return online text. return this.assignProvider.getSubmissionPluginText(this.plugin); }).then((text) => { - // We receive them as strings, convert to int. - this.configs.wordlimit = parseInt(this.configs.wordlimit, 10); - this.configs.wordlimitenabled = parseInt(this.configs.wordlimitenabled, 10); + this.wordLimitEnabled = !!parseInt(this.configs.wordlimitenabled, 10); // Set the text. this.text = text; @@ -85,7 +84,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS } // Calculate initial words. - if (this.configs.wordlimitenabled) { + if (this.wordLimitEnabled) { this.words = this.textUtils.countWords(text); } }).finally(() => { @@ -100,7 +99,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS */ onChange(text: string): void { // Count words if needed. - if (this.configs.wordlimitenabled) { + if (this.wordLimitEnabled) { // Cancel previous wait. clearTimeout(this.wordCountTimeout); diff --git a/src/addon/mod/assign/submission/onlinetext/providers/handler.ts b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts index dd4b847a4..69a84b85e 100644 --- a/src/addon/mod/assign/submission/onlinetext/providers/handler.ts +++ b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts @@ -18,7 +18,9 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreWSProvider } from '@providers/ws'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { AddonModAssignProvider } from '../../../providers/assign'; +import { + AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin +} from '../../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; import { AddonModAssignHelperProvider } from '../../../providers/helper'; import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; @@ -46,7 +48,8 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param plugin The plugin object. * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ - canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + canEditOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin): boolean | Promise { // This plugin uses Moodle filters, it cannot be edited in offline. return false; } @@ -62,7 +65,9 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + copySubmissionData(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: any, + userId?: number, siteId?: string): void | Promise { + const text = this.assignProvider.getSubmissionPluginText(plugin, true), files = this.assignProvider.getSubmissionPluginAttachments(plugin); let promise; @@ -93,7 +98,7 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param edit Whether the user is editing. * @return The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + getComponent(injector: Injector, plugin: AddonModAssignPlugin, edit?: boolean): any | Promise { return AddonModAssignSubmissionOnlineTextComponent; } @@ -107,7 +112,8 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ - getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + getPluginFiles(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, siteId?: string): any[] | Promise { return this.assignProvider.getSubmissionPluginAttachments(plugin); } @@ -118,7 +124,7 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param plugin The plugin object. * @return The size (or promise resolved with size). */ - getSizeForCopy(assign: any, plugin: any): number | Promise { + getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): number | Promise { const text = this.assignProvider.getSubmissionPluginText(plugin, true), files = this.assignProvider.getSubmissionPluginAttachments(plugin), promises = []; @@ -153,7 +159,8 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param inputData Data entered by the user for the submission. * @return The size (or promise resolved with size). */ - getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise { + getSizeForEdit(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): number | Promise { const text = this.assignProvider.getSubmissionPluginText(plugin, true); return text.length; @@ -182,7 +189,9 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param inputData Data entered by the user for the submission. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ - hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + hasDataChanged(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any): boolean | Promise { + // Get the original text from plugin or offline. return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => { // No offline data found. @@ -234,7 +243,8 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + prepareSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, inputData: any, pluginData: any, offline?: boolean, userId?: number, siteId?: string): void | Promise { let text = this.getTextToSubmit(plugin, inputData); @@ -274,8 +284,8 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ - prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) - : void | Promise { + prepareSyncData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, offlineData: any, pluginData: any, siteId?: string): void | Promise { const textData = offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor; if (textData) { diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 794d4970c..267c0343a 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -121,7 +121,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp // Try to get the book data. promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => { this.dataRetrieved.emit(book); - this.description = book.intro || this.description; + this.description = book.intro; }).catch(() => { // Ignore errors since this WS isn't available in some Moodle versions. })); diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 1e97a6749..d1061ef65 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -25,38 +25,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; import { CoreTagItem } from '@core/tag/providers/tag'; - -/** - * A book chapter inside the toc list. - */ -export interface AddonModBookTocChapter { - /** - * ID to identify the chapter. - */ - id: string; - - /** - * Chapter's title. - */ - title: string; - - /** - * The chapter's level. - */ - level: number; -} - -/** - * Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path - * is identified by the relative path in the book, and the value is the URL of the file. - */ -export type AddonModBookContentsMap = { - [chapter: string]: { - indexUrl?: string, - paths: {[path: string]: string}, - tags?: CoreTagItem[] - } -}; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for books. @@ -83,7 +52,7 @@ export class AddonModBookProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - getBook(courseId: number, cmId: number, siteId?: string): Promise { + getBook(courseId: number, cmId: number, siteId?: string): Promise { return this.getBookByField(courseId, 'coursemodule', cmId, siteId); } @@ -96,7 +65,7 @@ export class AddonModBookProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -106,7 +75,9 @@ export class AddonModBookProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_book_get_books_by_courses', params, preSets).then((response) => { + return site.read('mod_book_get_books_by_courses', params, preSets) + .then((response: AddonModBookGetBooksByCoursesResult): any => { + // Search the book. if (response && response.books) { for (const i in response.books) { @@ -401,3 +372,66 @@ export class AddonModBookProvider { {chapterid: chapterId}, siteId); } } + +/** + * A book chapter inside the toc list. + */ +export type AddonModBookTocChapter = { + /** + * ID to identify the chapter. + */ + id: string; + + /** + * Chapter's title. + */ + title: string; + + /** + * The chapter's level. + */ + level: number; +}; + +/** + * Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path + * is identified by the relative path in the book, and the value is the URL of the file. + */ +export type AddonModBookContentsMap = { + [chapter: string]: { + indexUrl?: string, + paths: {[path: string]: string}, + tags?: CoreTagItem[] + } +}; + +/** + * Book returned by mod_book_get_books_by_courses. + */ +export type AddonModBookBook = { + id: number; // Book id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Book name. + intro: string; // The Book intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + numbering: number; // Book numbering configuration. + navstyle: number; // Book navigation style configuration. + customtitles: number; // Book custom titles type. + revision?: number; // Book revision. + timecreated?: number; // Time of creation. + timemodified?: number; // Time of last modification. + section?: number; // Course section id. + visible?: boolean; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Result of WS mod_book_get_books_by_courses. + */ +export type AddonModBookGetBooksByCoursesResult = { + books: AddonModBookBook[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/chat/components/index/index.ts b/src/addon/mod/chat/components/index/index.ts index 71e646d53..4945a5ebe 100644 --- a/src/addon/mod/chat/components/index/index.ts +++ b/src/addon/mod/chat/components/index/index.ts @@ -16,7 +16,7 @@ import { Component, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; -import { AddonModChatProvider } from '../../providers/chat'; +import { AddonModChatProvider, AddonModChatChat } from '../../providers/chat'; /** * Component that displays a chat. @@ -29,7 +29,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp component = AddonModChatProvider.COMPONENT; moduleName = 'chat'; - chat: any; + chat: AddonModChatChat; chatInfo: any; protected title: string; @@ -66,7 +66,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { return this.chatProvider.getChat(this.courseId, this.module.id).then((chat) => { this.chat = chat; - this.description = chat.intro || chat.description; + this.description = chat.intro; const now = this.timeUtils.timestamp(); const span = chat.chattime - now; diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index e4951d6e3..47be73f24 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -20,7 +20,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { AddonModChatProvider } from '../../providers/chat'; +import { AddonModChatProvider, AddonModChatMessageWithUserData } from '../../providers/chat'; import { Network } from '@ionic-native/network'; import * as moment from 'moment'; @@ -37,7 +37,7 @@ export class AddonModChatChatPage { loaded = false; title: string; - messages = []; + messages: AddonModChatMessageWithUserData[] = []; newMessage: string; polling: any; isOnline: boolean; @@ -46,7 +46,7 @@ export class AddonModChatChatPage { protected logger; protected courseId: number; protected chatId: number; - protected sessionId: number; + protected sessionId: string; protected lastTime = 0; protected oldContentHeight = 0; protected onlineObserver: any; @@ -131,9 +131,9 @@ export class AddonModChatChatPage { /** * Convenience function to login the user. * - * @return Resolved when done. + * @return Promise resolved when done. */ - protected loginUser(): Promise { + protected loginUser(): Promise { return this.chatProvider.loginUser(this.chatId).then((sessionId) => { this.sessionId = sessionId; }); @@ -144,12 +144,12 @@ export class AddonModChatChatPage { * * @return Promise resolved when done. */ - protected fetchMessages(): Promise { + protected fetchMessages(): Promise { return this.chatProvider.getLatestMessages(this.sessionId, this.lastTime).then((messagesInfo) => { this.lastTime = messagesInfo.chatnewlasttime || 0; return this.chatProvider.getMessagesUserData(messagesInfo.messages, this.courseId).then((messages) => { - this.messages = this.messages.concat(messages); + this.messages = this.messages.concat( messages); if (messages.length) { // New messages or beeps, scroll to bottom. setTimeout(() => this.scrollToBottom()); @@ -190,7 +190,7 @@ export class AddonModChatChatPage { * * @return Promised resolved when done. */ - protected fetchMessagesInterval(): Promise { + protected fetchMessagesInterval(): Promise { this.logger.debug('Polling for messages'); if (!this.isOnline || this.pollingRunning) { // Obviously we cannot check for new messages when the app is offline. @@ -225,7 +225,7 @@ export class AddonModChatChatPage { * @param prevMessage Previous message object. * @return True if messages are from diferent days, false othetwise. */ - showDate(message: any, prevMessage: any): boolean { + showDate(message: AddonModChatMessageWithUserData, prevMessage: AddonModChatMessageWithUserData): boolean { if (!prevMessage) { return true; } @@ -267,7 +267,7 @@ export class AddonModChatChatPage { }); } - reconnect(): Promise { + reconnect(): Promise { const modal = this.domUtils.showModalLoading(); // Call startPolling would take a while for the first execution, so we'll execute it manually to check if it works now. diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.ts b/src/addon/mod/chat/pages/session-messages/session-messages.ts index b96ed597a..1a29a8365 100644 --- a/src/addon/mod/chat/pages/session-messages/session-messages.ts +++ b/src/addon/mod/chat/pages/session-messages/session-messages.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { AddonModChatProvider } from '../../providers/chat'; +import { AddonModChatProvider, AddonModChatSessionMessageWithUserData } from '../../providers/chat'; import * as moment from 'moment'; /** @@ -34,7 +34,7 @@ export class AddonModChatSessionMessagesPage { protected sessionEnd: number; protected groupId: number; protected loaded = false; - protected messages = []; + protected messages: AddonModChatSessionMessageWithUserData[] = []; constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) { this.courseId = navParams.get('courseId'); @@ -55,7 +55,7 @@ export class AddonModChatSessionMessagesPage { return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) .then((messages) => { return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { - this.messages = messages; + this.messages = messages; }); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); @@ -84,7 +84,7 @@ export class AddonModChatSessionMessagesPage { * @param prevMessage Previous message object. * @return True if messages are from diferent days, false othetwise. */ - showDate(message: any, prevMessage: any): boolean { + showDate(message: AddonModChatSessionMessageWithUserData, prevMessage: AddonModChatSessionMessageWithUserData): boolean { if (!prevMessage) { return true; } diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts index 6043f3f3b..1f6ddcf0d 100644 --- a/src/addon/mod/chat/pages/sessions/sessions.ts +++ b/src/addon/mod/chat/pages/sessions/sessions.ts @@ -20,7 +20,7 @@ import { CoreUserProvider } from '@core/user/providers/user'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { AddonModChatProvider } from '../../providers/chat'; +import { AddonModChatProvider, AddonModChatSession, AddonModChatSessionUser } from '../../providers/chat'; /** * Page that displays list of chat sessions. @@ -73,13 +73,13 @@ export class AddonModChatSessionsPage { this.groupId = this.groupsProvider.validateGroupId(this.groupId, groupInfo); return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll); - }).then((sessions) => { + }).then((sessions: AddonModChatSessionFormatted[]) => { // Fetch user profiles. const promises = []; sessions.forEach((session) => { session.duration = session.sessionend - session.sessionstart; - session.sessionusers.forEach((sessionUser) => { + session.sessionusers.forEach((sessionUser: AddonModChatUserSessionFormatted) => { if (!sessionUser.userfullname) { // The WS does not return the user name, fetch user profile. promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => { @@ -156,3 +156,18 @@ export class AddonModChatSessionsPage { $event.stopPropagation(); } } + +/** + * Fields added to chat session in this view. + */ +type AddonModChatSessionFormatted = AddonModChatSession & { + duration?: number; // Session duration. + allsessionusers?: AddonModChatUserSessionFormatted[]; // All session users. +}; + +/** + * Fields added to user session in this view. + */ +type AddonModChatUserSessionFormatted = AddonModChatSessionUser & { + userfullname?: string; // User full name. +}; diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts index 4cd59049d..f54560ba3 100644 --- a/src/addon/mod/chat/pages/users/users.ts +++ b/src/addon/mod/chat/pages/users/users.ts @@ -17,7 +17,7 @@ import { IonicPage, NavParams, ViewController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { AddonModChatProvider } from '../../providers/chat'; +import { AddonModChatProvider, AddonModChatUser } from '../../providers/chat'; import { Network } from '@ionic-native/network'; /** @@ -30,12 +30,12 @@ import { Network } from '@ionic-native/network'; }) export class AddonModChatUsersPage { - users = []; + users: AddonModChatUser[] = []; usersLoaded = false; currentUserId: number; isOnline: boolean; - protected sessionId: number; + protected sessionId: string; protected onlineObserver: any; constructor(navParams: NavParams, network: Network, zone: NgZone, private appProvider: CoreAppProvider, @@ -77,7 +77,7 @@ export class AddonModChatUsersPage { * * @param user User object. */ - talkTo(user: any): void { + talkTo(user: AddonModChatUser): void { this.viewCtrl.dismiss({talkTo: user.fullname}); } @@ -86,7 +86,7 @@ export class AddonModChatUsersPage { * * @param user User object. */ - beepTo(user: any): void { + beepTo(user: AddonModChatUser): void { this.viewCtrl.dismiss({beepTo: user.id}); } diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts index 9b9a02e39..f656375a7 100644 --- a/src/addon/mod/chat/providers/chat.ts +++ b/src/addon/mod/chat/providers/chat.ts @@ -19,6 +19,7 @@ import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for chats. @@ -41,7 +42,7 @@ export class AddonModChatProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the chat is retrieved. */ - getChat(courseId: number, cmId: number, siteId?: string): Promise { + getChat(courseId: number, cmId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -51,7 +52,9 @@ export class AddonModChatProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { + return site.read('mod_chat_get_chats_by_courses', params, preSets) + .then((response: AddonModChatGetChatsByCoursesResult): any => { + if (response.chats) { const chat = response.chats.find((chat) => chat.coursemodule == cmId); if (chat) { @@ -70,12 +73,14 @@ export class AddonModChatProvider { * @param chatId Chat instance ID. * @return Promise resolved when the WS is executed. */ - loginUser(chatId: number): Promise { + loginUser(chatId: number): Promise { const params = { chatid: chatId }; - return this.sitesProvider.getCurrentSite().write('mod_chat_login_user', params).then((response) => { + return this.sitesProvider.getCurrentSite().write('mod_chat_login_user', params) + .then((response: AddonModChatLoginUserResult): any => { + if (response.chatsid) { return response.chatsid; } @@ -108,14 +113,16 @@ export class AddonModChatProvider { * @param beepUserId Beep user ID. * @return Promise resolved when the WS is executed. */ - sendMessage(sessionId: number, message: string, beepUserId: number): Promise { + sendMessage(sessionId: string, message: string, beepUserId: number): Promise { const params = { chatsid: sessionId, messagetext: message, beepid: beepUserId }; - return this.sitesProvider.getCurrentSite().write('mod_chat_send_chat_message', params).then((response) => { + return this.sitesProvider.getCurrentSite().write('mod_chat_send_chat_message', params) + .then((response: AddonModChatSendChatMessageResult): any => { + if (response.messageid) { return response.messageid; } @@ -131,7 +138,7 @@ export class AddonModChatProvider { * @param lastTime Last time when messages were retrieved. * @return Promise resolved when the WS is executed. */ - getLatestMessages(sessionId: number, lastTime: number): Promise { + getLatestMessages(sessionId: string, lastTime: number): Promise { const params = { chatsid: sessionId, chatlasttime: lastTime @@ -149,8 +156,10 @@ export class AddonModChatProvider { * @param courseId ID of the course the messages belong to. * @return Promise always resolved with the formatted messages. */ - getMessagesUserData(messages: any[], courseId: number): Promise { - const promises = messages.map((message) => { + getMessagesUserData(messages: (AddonModChatMessage | AddonModChatSessionMessage)[], courseId: number) + : Promise<(AddonModChatMessageWithUserData | AddonModChatSessionMessageWithUserData)[]> { + + const promises = messages.map((message: AddonModChatMessageWithUserData | AddonModChatSessionMessageWithUserData) => { return this.userProvider.getProfile(message.userid, courseId, true).then((user) => { message.userfullname = user.fullname; message.userprofileimageurl = user.profileimageurl; @@ -171,7 +180,7 @@ export class AddonModChatProvider { * @param sessionId Chat sessiond ID. * @return Promise resolved when the WS is executed. */ - getChatUsers(sessionId: number): Promise { + getChatUsers(sessionId: string): Promise { const params = { chatsid: sessionId }; @@ -206,7 +215,8 @@ export class AddonModChatProvider { * @since 3.5 */ getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { chatid: chatId, @@ -222,7 +232,7 @@ export class AddonModChatProvider { preSets.emergencyCache = false; } - return site.read('mod_chat_get_sessions', params, preSets).then((response) => { + return site.read('mod_chat_get_sessions', params, preSets).then((response: AddonModChatGetSessionsResult): any => { if (!response || !response.sessions) { return Promise.reject(null); } @@ -245,7 +255,8 @@ export class AddonModChatProvider { * @since 3.5 */ getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, - siteId?: string): Promise { + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { chatid: chatId, @@ -262,7 +273,9 @@ export class AddonModChatProvider { preSets.emergencyCache = false; } - return site.read('mod_chat_get_session_messages', params, preSets).then((response) => { + return site.read('mod_chat_get_session_messages', params, preSets) + .then((response: AddonModChatGetSessionMessagesResult): any => { + if (!response || !response.messages) { return Promise.reject(null); } @@ -390,3 +403,152 @@ export class AddonModChatProvider { return this.ROOT_CACHE_KEY + 'sessionsMessages:' + chatId + ':'; } } + +/** + * Chat returned by mod_chat_get_chats_by_courses. + */ +export type AddonModChatChat = { + id: number; // Chat id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Chat name. + intro: string; // The Chat intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + chatmethod?: string; // Chat method (sockets, ajax, header_js). + keepdays?: number; // Keep days. + studentlogs?: number; // Student logs visible to everyone. + chattime?: number; // Chat time. + schedule?: number; // Schedule type. + timemodified?: number; // Time of last modification. + section?: number; // Course section id. + visible?: boolean; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Chat user returned by mod_chat_get_chat_users. + */ +export type AddonModChatUser = { + id: number; // User id. + fullname: string; // User full name. + profileimageurl: string; // User picture URL. +}; + +/** + * Meessage returned by mod_chat_get_chat_latest_messages. + */ +export type AddonModChatMessage = { + id: number; // Message id. + userid: number; // User id. + system: boolean; // True if is a system message (like user joined). + message: string; // Message text. + timestamp: number; // Timestamp for the message. +}; + +/** + * Message with user data + */ +export type AddonModChatMessageWithUserData = AddonModChatMessage & AddonModChatMessageUserData; + +/** + * Chat session. + */ +export type AddonModChatSession = { + sessionstart: number; // Session start time. + sessionend: number; // Session end time. + sessionusers: AddonModChatSessionUser[]; // Session users. + iscomplete: boolean; // Whether the session is completed or not. +}; + +/** + * Chat user returned by mod_chat_get_sessions. + */ +export type AddonModChatSessionUser = { + userid: number; // User id. + messagecount: number; // Number of messages in the session. +}; + +/** + * Message returned by mod_chat_get_session_messages. + */ +export type AddonModChatSessionMessage = { + id: number; // The message record id. + chatid: number; // The chat id. + userid: number; // The user who wrote the message. + groupid: number; // The group this message belongs to. + issystem: boolean; // Whether is a system message or not. + message: string; // The message text. + timestamp: number; // The message timestamp (indicates when the message was sent). +}; + +/** + * Message with user data + */ +export type AddonModChatSessionMessageWithUserData = AddonModChatSessionMessage & AddonModChatMessageUserData; + +/** + * Result of WS mod_chat_get_chats_by_courses. + */ +export type AddonModChatGetChatsByCoursesResult = { + chats: AddonModChatChat[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_get_chat_users. + */ +export type AddonModChatGetChatUsersResult = { + users: AddonModChatUser[]; // List of users. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_get_sessions. + */ +export type AddonModChatGetSessionsResult = { + sessions: AddonModChatSession[]; // List of sessions. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_get_session_messages. + */ +export type AddonModChatGetSessionMessagesResult = { + messages: AddonModChatSessionMessage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_send_chat_message. + */ +export type AddonModChatSendChatMessageResult = { + messageid: number; // Message sent id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_get_chat_latest_messages. + */ +export type AddonModChatGetChatLatestMessagesResult = { + messages: AddonModChatMessage[]; // List of messages. + chatnewlasttime: number; // New last time. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_chat_login_user. + */ +export type AddonModChatLoginUserResult = { + chatsid: string; // Unique chat session id. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * User data added to messages. + */ +type AddonModChatMessageUserData = { + userfullname?: string; // Calculated in the app. Full name of the user who wrote the message. + userprofileimageurl?: string; // Calculated in the app. Full name of the user who wrote the message. +}; diff --git a/src/addon/mod/chat/providers/prefetch-handler.ts b/src/addon/mod/chat/providers/prefetch-handler.ts index ca146b06d..11b3d9183 100644 --- a/src/addon/mod/chat/providers/prefetch-handler.ts +++ b/src/addon/mod/chat/providers/prefetch-handler.ts @@ -23,7 +23,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreUserProvider } from '@core/user/providers/user'; -import { AddonModChatProvider } from './chat'; +import { AddonModChatProvider, AddonModChatChat } from './chat'; /** * Handler to prefetch chats. @@ -116,12 +116,12 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl */ protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise { // Prefetch chat and group info. - const promises = [ + const promises: Promise[] = [ this.chatProvider.getChat(courseId, module.id, siteId), this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId) ]; - return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => { + return Promise.all(promises).then(([chat, groupInfo]: [AddonModChatChat, CoreGroupInfo]) => { const promises = []; let groupIds = [0]; diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index a9e81b5f6..cfeaddc5f 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -19,13 +19,13 @@ -

{{ 'addon.mod_choice.previewonly' | translate:{$a: choice.openTimeReadable} }}

-

{{ 'addon.mod_choice.notopenyet' | translate:{$a: choice.openTimeReadable} }}

+

{{ 'addon.mod_choice.previewonly' | translate:{$a: openTimeReadable} }}

+

{{ 'addon.mod_choice.notopenyet' | translate:{$a: openTimeReadable} }}

{{ 'addon.mod_choice.yourselection' | translate }}

-

{{ 'addon.mod_choice.expired' | translate:{$a: choice.closeTimeReadable} }}

+

{{ 'addon.mod_choice.expired' | translate:{$a: closeTimeReadable} }}

@@ -80,7 +80,7 @@

-

{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamount} }})

+

{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})

diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index c5bcdc852..6e3b5799e 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -16,7 +16,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; -import { AddonModChoiceProvider } from '../../providers/choice'; +import { AddonModChoiceProvider, AddonModChoiceChoice, AddonModChoiceOption, AddonModChoiceResult } from '../../providers/choice'; import { AddonModChoiceOfflineProvider } from '../../providers/offline'; import { AddonModChoiceSyncProvider } from '../../providers/sync'; @@ -31,9 +31,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo component = AddonModChoiceProvider.COMPONENT; moduleName = 'choice'; - choice: any; - options = []; - selectedOption: any; + choice: AddonModChoiceChoice; + options: AddonModChoiceOption[] = []; + selectedOption: {id: number}; choiceNotOpenYet = false; choiceClosed = false; canEdit = false; @@ -43,6 +43,8 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo labels = []; results = []; publishInfo: string; // Message explaining the user what will happen with his choices. + openTimeReadable: string; + closeTimeReadable: string; protected userId: number; protected syncEventName = AddonModChoiceSyncProvider.AUTO_SYNCED; @@ -122,12 +124,12 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return this.choiceProvider.getChoice(this.courseId, this.module.id).then((choice) => { this.choice = choice; - this.choice.timeopen = parseInt(choice.timeopen) * 1000; - this.choice.openTimeReadable = this.timeUtils.userDate(choice.timeopen); - this.choice.timeclose = parseInt(choice.timeclose) * 1000; - this.choice.closeTimeReadable = this.timeUtils.userDate(choice.timeclose); + this.choice.timeopen = choice.timeopen * 1000; + this.choice.timeclose = choice.timeclose * 1000; + this.openTimeReadable = this.timeUtils.userDate(choice.timeopen); + this.closeTimeReadable = this.timeUtils.userDate(choice.timeclose); - this.description = choice.intro || choice.description; + this.description = choice.intro; this.choiceNotOpenYet = choice.timeopen && choice.timeopen > this.now; this.choiceClosed = choice.timeclose && choice.timeclose <= this.now; @@ -175,7 +177,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo if (hasOffline) { promise = this.choiceOffline.getResponse(this.choice.id).then((response) => { - const optionsKeys = {}; + const optionsKeys: {[id: number]: AddonModChoiceOption} = {}; options.forEach((option) => { optionsKeys[option.id] = option; }); @@ -223,7 +225,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo promise = Promise.resolve(options); } - promise.then((options) => { + promise.then((options: AddonModChoiceOption[]) => { const isOpen = this.isChoiceOpen(); let hasAnswered = false; @@ -291,11 +293,11 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo let hasVotes = false; this.data = []; this.labels = []; - results.forEach((result) => { + results.forEach((result: AddonModChoiceResultFormatted) => { if (result.numberofuser > 0) { hasVotes = true; } - result.percentageamount = parseFloat(result.percentageamount).toFixed(1); + result.percentageamountfixed = result.percentageamount.toFixed(1); this.data.push(result.numberofuser); this.labels.push(result.text); }); @@ -429,3 +431,10 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return result.updated; } } + +/** + * Choice result with some calculated data. + */ +export type AddonModChoiceResultFormatted = AddonModChoiceResult & { + percentageamountfixed: string; // Percentage of users answers with fixed decimals. +}; diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index b16119516..246903592 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -20,6 +20,7 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for choices. @@ -118,7 +119,9 @@ export class AddonModChoiceProvider { responses: responses }; - return site.write('mod_choice_delete_choice_responses', params).then((response) => { + return site.write('mod_choice_delete_choice_responses', params) + .then((response: AddonModChoiceDeleteChoiceResponsesResult) => { + // Other errors ocurring. if (!response || response.status === false) { return Promise.reject(this.utils.createFakeWSError('')); @@ -179,7 +182,7 @@ export class AddonModChoiceProvider { * @return Promise resolved when the choice is retrieved. */ protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, - ignoreCache?: boolean): Promise { + ignoreCache?: boolean): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -198,7 +201,9 @@ export class AddonModChoiceProvider { preSets.emergencyCache = false; } - return site.read('mod_choice_get_choices_by_courses', params, preSets).then((response) => { + return site.read('mod_choice_get_choices_by_courses', params, preSets) + .then((response: AddonModChoiceGetChoicesByCoursesResult): any => { + if (response && response.choices) { const currentChoice = response.choices.find((choice) => choice[key] == value); if (currentChoice) { @@ -221,7 +226,8 @@ export class AddonModChoiceProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) + : Promise { return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); } @@ -235,7 +241,8 @@ export class AddonModChoiceProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) + : Promise { return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); } @@ -247,7 +254,7 @@ export class AddonModChoiceProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with choice options. */ - getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { + getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId @@ -262,7 +269,9 @@ export class AddonModChoiceProvider { preSets.emergencyCache = false; } - return site.read('mod_choice_get_choice_options', params, preSets).then((response) => { + return site.read('mod_choice_get_choice_options', params, preSets) + .then((response: AddonModChoiceGetChoiceOptionsResult): any => { + if (response.options) { return response.options; } @@ -280,7 +289,7 @@ export class AddonModChoiceProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with choice results. */ - getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { + getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId @@ -294,7 +303,9 @@ export class AddonModChoiceProvider { preSets.emergencyCache = false; } - return site.read('mod_choice_get_choice_results', params, preSets).then((response) => { + return site.read('mod_choice_get_choice_results', params, preSets) + .then((response: AddonModChoiceGetChoiceResults): any => { + if (response.options) { return response.options; } @@ -456,3 +467,96 @@ export class AddonModChoiceProvider { }); } } + +/** + * Choice returned by mod_choice_get_choices_by_courses. + */ +export type AddonModChoiceChoice = { + id: number; // Choice instance id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Choice name. + intro: string; // The choice intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + publish?: boolean; // If choice is published. + showresults?: number; // 0 never, 1 after answer, 2 after close, 3 always. + display?: number; // Display mode (vertical, horizontal). + allowupdate?: boolean; // Allow update. + allowmultiple?: boolean; // Allow multiple choices. + showunanswered?: boolean; // Show users who not answered yet. + includeinactive?: boolean; // Include inactive users. + limitanswers?: boolean; // Limit unswers. + timeopen?: number; // Date of opening validity. + timeclose?: number; // Date of closing validity. + showpreview?: boolean; // Show preview before timeopen. + timemodified?: number; // Time of last modification. + completionsubmit?: boolean; // Completion on user submission. + section?: number; // Course section id. + visible?: boolean; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Option returned by mod_choice_get_choice_options. + */ +export type AddonModChoiceOption = { + id: number; // Option id. + text: string; // Text of the choice. + maxanswers: number; // Maximum number of answers. + displaylayout: boolean; // True for orizontal, otherwise vertical. + countanswers: number; // Number of answers. + checked: boolean; // We already answered. + disabled: boolean; // Option disabled. +}; + +/** + * Result returned by mod_choice_get_choice_results. + */ +export type AddonModChoiceResult = { + id: number; // Choice instance id. + text: string; // Text of the choice. + maxanswer: number; // Maximum number of answers. + userresponses: { + userid: number; // User id. + fullname: string; // User full name. + profileimageurl: string; // Profile user image url. + answerid?: number; // Answer id. + timemodified?: number; // Time of modification. + }[]; + numberofuser: number; // Number of users answers. + percentageamount: number; // Percentage of users answers. +}; + +/** + * Result of WS mod_choice_get_choices_by_courses. + */ +export type AddonModChoiceGetChoicesByCoursesResult = { + choices: AddonModChoiceChoice[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_choice_get_choice_options. + */ +export type AddonModChoiceGetChoiceOptionsResult = { + options: AddonModChoiceOption[]; // Options. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_choice_get_choice_results. + */ +export type AddonModChoiceGetChoiceResults = { + options: AddonModChoiceResult[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_choice_delete_choice_responses. + */ +export type AddonModChoiceDeleteChoiceResponsesResult = { + status: boolean; // Status, true if everything went right. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/folder/components/index/index.ts b/src/addon/mod/folder/components/index/index.ts index d23b70e38..dc9b246dd 100644 --- a/src/addon/mod/folder/components/index/index.ts +++ b/src/addon/mod/folder/components/index/index.ts @@ -34,6 +34,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo component = AddonModFolderProvider.COMPONENT; canGetFolder: boolean; contents: any; + moduleContents: any; constructor(injector: Injector, private folderProvider: AddonModFolderProvider, private courseProvider: CoreCourseProvider, private appProvider: CoreAppProvider, private folderHelper: AddonModFolderHelperProvider) { @@ -87,9 +88,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo if (this.path) { // Subfolder. - this.contents = module.contents; + this.contents = this.moduleContents; } else { - this.contents = this.folderHelper.formatContents(module.contents); + this.contents = this.folderHelper.formatContents(this.moduleContents); } } @@ -105,7 +106,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo if (this.canGetFolder) { promise = this.folderProvider.getFolder(this.courseId, this.module.id).then((folder) => { return this.courseProvider.loadModuleContents(this.module, this.courseId).then(() => { - folder.contents = this.module.contents; + this.moduleContents = this.module.contents; return folder; }); @@ -117,17 +118,13 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo folder.contents = this.module.contents; } this.module = folder; + this.moduleContents = folder.contents; return folder; }); } return promise.then((folder) => { - if (folder) { - this.description = folder.intro || folder.description; - this.dataRetrieved.emit(folder); - } - this.showModuleData(folder); // All data obtained, now fill the context menu. diff --git a/src/addon/mod/folder/providers/folder.ts b/src/addon/mod/folder/providers/folder.ts index 989a0c69a..c472ce0f6 100644 --- a/src/addon/mod/folder/providers/folder.ts +++ b/src/addon/mod/folder/providers/folder.ts @@ -19,6 +19,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for folder. @@ -43,7 +44,7 @@ export class AddonModFolderProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - getFolder(courseId: number, cmId: number, siteId?: string): Promise { + getFolder(courseId: number, cmId: number, siteId?: string): Promise { return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId); } @@ -56,7 +57,7 @@ export class AddonModFolderProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -66,7 +67,9 @@ export class AddonModFolderProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_folder_get_folders_by_courses', params, preSets).then((response) => { + return site.read('mod_folder_get_folders_by_courses', params, preSets) + .then((response: AddonModFolderGetFoldersByCoursesResult): any => { + if (response && response.folders) { const currentFolder = response.folders.find((folder) => { return folder[key] == value; @@ -147,3 +150,33 @@ export class AddonModFolderProvider { {}, siteId); } } + +/** + * Folder returned by mod_folder_get_folders_by_courses. + */ +export type AddonModFolderFolder = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Page name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + revision: number; // Incremented when after each file changes, to avoid cache. + timemodified: number; // Last time the folder was modified. + display: number; // Display type of folder contents on a separate page or inline. + showexpanded: number; // 1 = expanded, 0 = collapsed for sub-folders. + showdownloadfolder: number; // Whether to show the download folder button. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_folder_get_folders_by_courses. + */ +export type AddonModFolderGetFoldersByCoursesResult = { + folders: AddonModFolderFolder[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts index 37edb9b87..6940db0d8 100644 --- a/src/addon/mod/imscp/components/index/index.ts +++ b/src/addon/mod/imscp/components/index/index.ts @@ -79,7 +79,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom const promises = []; promises.push(this.imscpProvider.getImscp(this.courseId, this.module.id).then((imscp) => { - this.description = imscp.intro || imscp.description; + this.description = imscp.intro; this.dataRetrieved.emit(imscp); })); diff --git a/src/addon/mod/imscp/providers/imscp.ts b/src/addon/mod/imscp/providers/imscp.ts index 366cb7bf6..45adb57f1 100644 --- a/src/addon/mod/imscp/providers/imscp.ts +++ b/src/addon/mod/imscp/providers/imscp.ts @@ -21,6 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for IMSCP. @@ -157,7 +158,7 @@ export class AddonModImscpProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the imscp is retrieved. */ - protected getImscpByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getImscpByKey(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -167,7 +168,9 @@ export class AddonModImscpProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_imscp_get_imscps_by_courses', params, preSets).then((response) => { + return site.read('mod_imscp_get_imscps_by_courses', params, preSets) + .then((response: AddonModImscpGetImscpsByCoursesResult): any => { + if (response && response.imscps) { const currentImscp = response.imscps.find((imscp) => imscp[key] == value); if (currentImscp) { @@ -188,7 +191,7 @@ export class AddonModImscpProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the imscp is retrieved. */ - getImscp(courseId: number, cmId: number, siteId?: string): Promise { + getImscp(courseId: number, cmId: number, siteId?: string): Promise { return this.getImscpByKey(courseId, 'coursemodule', cmId, siteId); } @@ -324,3 +327,32 @@ export class AddonModImscpProvider { siteId); } } + +/** + * IMSCP returned by mod_imscp_get_imscps_by_courses. + */ +export type AddonModImscpImscp = { + id: number; // IMSCP id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Activity name. + intro?: string; // The IMSCP intro. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + revision?: number; // Revision. + keepold?: number; // Number of old IMSCP to keep. + structure?: string; // IMSCP structure. + timemodified?: string; // Time of last modification. + section?: number; // Course section id. + visible?: boolean; // If visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Result of WS mod_imscp_get_imscps_by_courses. + */ +export type AddonModImscpGetImscpsByCoursesResult = { + imscps: AddonModImscpImscp[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/label/providers/label.ts b/src/addon/mod/label/providers/label.ts index ec75856b6..64e3d5c31 100644 --- a/src/addon/mod/label/providers/label.ts +++ b/src/addon/mod/label/providers/label.ts @@ -17,6 +17,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for labels. @@ -52,7 +53,7 @@ export class AddonModLabelProvider { * @return Promise resolved when the label is retrieved. */ protected getLabelByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -70,7 +71,9 @@ export class AddonModLabelProvider { preSets.emergencyCache = false; } - return site.read('mod_label_get_labels_by_courses', params, preSets).then((response) => { + return site.read('mod_label_get_labels_by_courses', params, preSets) + .then((response: AddonModLabelGetLabelsByCoursesResult): any => { + if (response && response.labels) { const currentLabel = response.labels.find((label) => label[key] == value); if (currentLabel) { @@ -93,7 +96,8 @@ export class AddonModLabelProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the label is retrieved. */ - getLabel(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getLabel(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { return this.getLabelByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); } @@ -107,7 +111,8 @@ export class AddonModLabelProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the label is retrieved. */ - getLabelById(courseId: number, labelId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getLabelById(courseId: number, labelId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { return this.getLabelByField(courseId, 'id', labelId, forceCache, ignoreCache, siteId); } @@ -170,3 +175,29 @@ export class AddonModLabelProvider { return site.wsAvailable('mod_label_get_labels_by_courses'); } } + +/** + * Label returned by mod_label_get_labels_by_courses. + */ +export type AddonModLabelLabel = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Label name. + intro: string; // Label contents. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + timemodified: number; // Last time the label was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_label_get_labels_by_courses. + */ +export type AddonModLabelGetLabelsByCoursesResult = { + labels: AddonModLabelLabel[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/lti/components/index/index.ts b/src/addon/mod/lti/components/index/index.ts index 53819e792..f454dec08 100644 --- a/src/addon/mod/lti/components/index/index.ts +++ b/src/addon/mod/lti/components/index/index.ts @@ -15,7 +15,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; -import { AddonModLtiProvider } from '../../providers/lti'; +import { AddonModLtiProvider, AddonModLtiLti } from '../../providers/lti'; /** * Component that displays an LTI entry page. @@ -28,7 +28,7 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo component = AddonModLtiProvider.COMPONENT; moduleName = 'lti'; - lti: any; // The LTI object. + lti: AddonModLtiLti; // The LTI object. protected fetchContentDefaultError = 'addon.mod_lti.errorgetlti'; @@ -65,7 +65,7 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { return this.ltiProvider.getLti(this.courseId, this.module.id).then((ltiData) => { this.lti = ltiData; - this.description = this.lti.intro || this.description; + this.description = this.lti.intro; this.dataRetrieved.emit(this.lti); }).then(() => { // All data obtained, now fill the context menu. diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts index 73eef5121..8ea9b09e9 100644 --- a/src/addon/mod/lti/providers/lti.ts +++ b/src/addon/mod/lti/providers/lti.ts @@ -22,11 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; - -export interface AddonModLtiParam { - name: string; - value: string; -} +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for LTI. @@ -104,7 +100,7 @@ export class AddonModLtiProvider { * @param cmId Course module ID. * @return Promise resolved when the LTI is retrieved. */ - getLti(courseId: number, cmId: number): Promise { + getLti(courseId: number, cmId: number): Promise { const params: any = { courseids: [courseId] }; @@ -113,7 +109,9 @@ export class AddonModLtiProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return this.sitesProvider.getCurrentSite().read('mod_lti_get_ltis_by_courses', params, preSets).then((response) => { + return this.sitesProvider.getCurrentSite().read('mod_lti_get_ltis_by_courses', params, preSets) + .then((response: AddonModLtiGetLtisByCoursesResult): any => { + if (response.ltis) { const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); if (currentLti) { @@ -141,8 +139,8 @@ export class AddonModLtiProvider { * @param id LTI id. * @return Promise resolved when the launch data is retrieved. */ - getLtiLaunchData(id: number): Promise { - const params: any = { + getLtiLaunchData(id: number): Promise { + const params = { toolid: id }; @@ -154,7 +152,9 @@ export class AddonModLtiProvider { cacheKey: this.getLtiLaunchDataCacheKey(id) }; - return this.sitesProvider.getCurrentSite().read('mod_lti_get_tool_launch_data', params, preSets).then((response) => { + return this.sitesProvider.getCurrentSite().read('mod_lti_get_tool_launch_data', params, preSets) + .then((response: AddonModLtiGetToolLaunchDataResult): any => { + if (response.endpoint) { return response; } @@ -227,3 +227,66 @@ export class AddonModLtiProvider { return this.logHelper.logSingle('mod_lti_view_lti', params, AddonModLtiProvider.COMPONENT, id, name, 'lti', {}, siteId); } } + +/** + * LTI returned by mod_lti_get_ltis_by_courses. + */ +export type AddonModLtiLti = { + id: number; // External tool id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // LTI name. + intro?: string; // The LTI intro. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + timecreated?: number; // Time of creation. + timemodified?: number; // Time of last modification. + typeid?: number; // Type id. + toolurl?: string; // Tool url. + securetoolurl?: string; // Secure tool url. + instructorchoicesendname?: string; // Instructor choice send name. + instructorchoicesendemailaddr?: number; // Instructor choice send mail address. + instructorchoiceallowroster?: number; // Instructor choice allow roster. + instructorchoiceallowsetting?: number; // Instructor choice allow setting. + instructorcustomparameters?: string; // Instructor custom parameters. + instructorchoiceacceptgrades?: number; // Instructor choice accept grades. + grade?: number; // Enable grades. + launchcontainer?: number; // Launch container mode. + resourcekey?: string; // Resource key. + password?: string; // Shared secret. + debuglaunch?: number; // Debug launch. + showtitlelaunch?: number; // Show title launch. + showdescriptionlaunch?: number; // Show description launch. + servicesalt?: string; // Service salt. + icon?: string; // Alternative icon URL. + secureicon?: string; // Secure icon URL. + section?: number; // Course section id. + visible?: number; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Param to send to the LTI. + */ +export type AddonModLtiParam = { + name: string; // Parameter name. + value: string; // Parameter value. +}; + +/** + * Result of WS mod_lti_get_ltis_by_courses. + */ +export type AddonModLtiGetLtisByCoursesResult = { + ltis: AddonModLtiLti[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_lti_get_tool_launch_data. + */ +export type AddonModLtiGetToolLaunchDataResult = { + endpoint: string; // Endpoint URL. + parameters: AddonModLtiParam[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/page/components/index/index.ts b/src/addon/mod/page/components/index/index.ts index bc6b76671..a4297216c 100644 --- a/src/addon/mod/page/components/index/index.ts +++ b/src/addon/mod/page/components/index/index.ts @@ -17,7 +17,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; -import { AddonModPageProvider } from '../../providers/page'; +import { AddonModPageProvider, AddonModPagePage } from '../../providers/page'; import { AddonModPageHelperProvider } from '../../providers/helper'; import { AddonModPagePrefetchHandler } from '../../providers/prefetch-handler'; @@ -34,7 +34,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp contents: any; displayDescription = true; displayTimemodified = true; - page: any; + page: AddonModPagePage; protected fetchContentDefaultError = 'addon.mod_page.errorwhileloadingthepage'; @@ -109,8 +109,8 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp this.page = page; // Check if description and timemodified should be displayed. - if (page.displayoptions) { - const options = this.textUtils.unserialize(page.displayoptions) || {}; + if (this.page.displayoptions) { + const options = this.textUtils.unserialize(this.page.displayoptions) || {}; this.displayDescription = typeof options.printintro == 'undefined' || this.utils.isTrueOrOne(options.printintro); this.displayTimemodified = typeof options.printlastmodified == 'undefined' || diff --git a/src/addon/mod/page/providers/page.ts b/src/addon/mod/page/providers/page.ts index ba42a2412..f41739e9a 100644 --- a/src/addon/mod/page/providers/page.ts +++ b/src/addon/mod/page/providers/page.ts @@ -20,6 +20,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for page. @@ -43,9 +44,9 @@ export class AddonModPageProvider { * @param courseId Course ID. * @param cmId Course module ID. * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when the book is retrieved. + * @return Promise resolved when the page is retrieved. */ - getPageData(courseId: number, cmId: number, siteId?: string): Promise { + getPageData(courseId: number, cmId: number, siteId?: string): Promise { return this.getPageByKey(courseId, 'coursemodule', cmId, siteId); } @@ -56,9 +57,9 @@ export class AddonModPageProvider { * @param key Name of the property to check. * @param value Value to search. * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when the book is retrieved. + * @return Promise resolved when the page is retrieved. */ - protected getPageByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getPageByKey(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -68,7 +69,9 @@ export class AddonModPageProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_page_get_pages_by_courses', params, preSets).then((response) => { + return site.read('mod_page_get_pages_by_courses', params, preSets) + .then((response: AddonModPageGetPagesByCoursesResult): any => { + if (response && response.pages) { const currentPage = response.pages.find((page) => { return page[key] == value; @@ -161,3 +164,37 @@ export class AddonModPageProvider { return this.logHelper.logSingle('mod_page_view_page', params, AddonModPageProvider.COMPONENT, id, name, 'page', {}, siteId); } } + +/** + * Page returned by mod_page_get_pages_by_courses. + */ +export type AddonModPagePage = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Page name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + content: string; // Page content. + contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + contentfiles: CoreWSExternalFile[]; + legacyfiles: number; // Legacy files flag. + legacyfileslast: number; // Legacy files last control flag. + display: number; // How to display the page. + displayoptions: string; // Display options (width, height). + revision: number; // Incremented when after each file changes, to avoid cache. + timemodified: number; // Last time the page was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_page_get_pages_by_courses. + */ +export type AddonModPageGetPagesByCoursesResult = { + pages: AddonModPagePage[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/resource/providers/module-handler.ts b/src/addon/mod/resource/providers/module-handler.ts index f4a9888bf..b4c6b1b32 100644 --- a/src/addon/mod/resource/providers/module-handler.ts +++ b/src/addon/mod/resource/providers/module-handler.ts @@ -25,6 +25,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreConstants } from '@core/constants'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support resource modules. @@ -146,7 +147,7 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { */ protected getResourceData(module: any, courseId: number, handlerData: CoreCourseModuleHandlerData): Promise { const promises = []; - let infoFiles = [], + let infoFiles: CoreWSExternalFile[] = [], options: any = {}; // Check if the button needs to be shown or not. diff --git a/src/addon/mod/resource/providers/resource.ts b/src/addon/mod/resource/providers/resource.ts index 05c8e4620..19305d71d 100644 --- a/src/addon/mod/resource/providers/resource.ts +++ b/src/addon/mod/resource/providers/resource.ts @@ -20,6 +20,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for resources. @@ -56,7 +57,7 @@ export class AddonModResourceProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the resource is retrieved. */ - protected getResourceDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getResourceDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -66,7 +67,9 @@ export class AddonModResourceProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_resource_get_resources_by_courses', params, preSets).then((response) => { + return site.read('mod_resource_get_resources_by_courses', params, preSets) + .then((response: AddonModResourceGetResourcesByCoursesResult): any => { + if (response && response.resources) { const currentResource = response.resources.find((resource) => { return resource[key] == value; @@ -89,7 +92,7 @@ export class AddonModResourceProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the resource is retrieved. */ - getResourceData(courseId: number, cmId: number, siteId?: string): Promise { + getResourceData(courseId: number, cmId: number, siteId?: string): Promise { return this.getResourceDataByKey(courseId, 'coursemodule', cmId, siteId); } @@ -165,3 +168,37 @@ export class AddonModResourceProvider { 'resource', {}, siteId); } } + +/** + * Resource returned by mod_resource_get_resources_by_courses. + */ +export type AddonModResourceResource = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Page name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + contentfiles: CoreWSExternalFile[]; + tobemigrated: number; // Whether this resource was migrated. + legacyfiles: number; // Legacy files flag. + legacyfileslast: number; // Legacy files last control flag. + display: number; // How to display the resource. + displayoptions: string; // Display options (width, height). + filterfiles: number; // If filters should be applied to the resource content. + revision: number; // Incremented when after each file changes, to avoid cache. + timemodified: number; // Last time the resource was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_resource_get_resources_by_courses. + */ +export type AddonModResourceGetResourcesByCoursesResult = { + resources: AddonModResourceResource[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index da07713d4..3f5eec9f1 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -36,14 +36,14 @@ -
+

{{ question.text }}

{{ 'addon.mod_survey.responses' | translate }}
- +
{{ option }}
@@ -61,20 +61,20 @@ - + {{ 'core.choose' | translate }} - {{option}} + {{option}}
- + @@ -82,7 +82,7 @@ - {{option}} + {{option}} diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 23f479553..e1715f02a 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -15,8 +15,8 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; -import { AddonModSurveyProvider } from '../../providers/survey'; -import { AddonModSurveyHelperProvider } from '../../providers/helper'; +import { AddonModSurveyProvider, AddonModSurveySurvey } from '../../providers/survey'; +import { AddonModSurveyHelperProvider, AddonModSurveyQuestionFormatted } from '../../providers/helper'; import { AddonModSurveyOfflineProvider } from '../../providers/offline'; import { AddonModSurveySyncProvider } from '../../providers/sync'; @@ -31,8 +31,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo component = AddonModSurveyProvider.COMPONENT; moduleName = 'survey'; - survey: any; - questions: any; + survey: AddonModSurveySurvey; + questions: AddonModSurveyQuestionFormatted[]; answers = {}; protected userId: number; @@ -103,7 +103,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo return this.surveyProvider.getSurvey(this.courseId, this.module.id).then((survey) => { this.survey = survey; - this.description = survey.intro || survey.description; + this.description = survey.intro; this.dataRetrieved.emit(survey); if (sync) { @@ -144,13 +144,13 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo // Init answers object. this.questions.forEach((q) => { if (q.name) { - const isTextArea = q.multi && q.multi.length === 0 && q.type === 0; + const isTextArea = q.multiArray && q.multiArray.length === 0 && q.type === 0; this.answers[q.name] = q.required ? -1 : (isTextArea ? '' : '0'); } - if (q.multi && !q.multi.length && q.parent === 0 && q.type > 0) { + if (q.multiArray && !q.multiArray.length && q.parent === 0 && q.type > 0) { // Options shown in a select. Remove all HTML. - q.options = q.options.map((option) => { + q.optionsArray = q.optionsArray.map((option) => { return this.textUtils.cleanTags(option); }); } diff --git a/src/addon/mod/survey/providers/helper.ts b/src/addon/mod/survey/providers/helper.ts index 23d7dc9b4..1553bd9d4 100644 --- a/src/addon/mod/survey/providers/helper.ts +++ b/src/addon/mod/survey/providers/helper.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { AddonModSurveyQuestion } from './survey'; /** * Service that provides helper functions for surveys. @@ -29,7 +30,7 @@ export class AddonModSurveyHelperProvider { * @param value Value to convert. * @return Array. */ - protected commaStringToArray(value: any): string[] { + protected commaStringToArray(value: string | string[]): string[] { if (typeof value == 'string') { if (value.length > 0) { return value.split(','); @@ -47,7 +48,7 @@ export class AddonModSurveyHelperProvider { * @param questions Questions. * @return Object with parent questions. */ - protected getParentQuestions(questions: any[]): any { + protected getParentQuestions(questions: AddonModSurveyQuestion[]): {[id: number]: AddonModSurveyQuestion} { const parents = {}; questions.forEach((question) => { @@ -66,24 +67,24 @@ export class AddonModSurveyHelperProvider { * @param questions Questions. * @return Promise resolved with the formatted questions. */ - formatQuestions(questions: any[]): any[] { + formatQuestions(questions: AddonModSurveyQuestion[]): AddonModSurveyQuestionFormatted[] { const strIPreferThat = this.translate.instant('addon.mod_survey.ipreferthat'), strIFoundThat = this.translate.instant('addon.mod_survey.ifoundthat'), strChoose = this.translate.instant('core.choose'), - formatted = [], + formatted: AddonModSurveyQuestionFormatted[] = [], parents = this.getParentQuestions(questions); let num = 1; questions.forEach((question) => { // Copy the object to prevent modifying the original. - const q1 = Object.assign({}, question), + const q1: AddonModSurveyQuestionFormatted = Object.assign({}, question), parent = parents[q1.parent]; // Turn multi and options into arrays. - q1.multi = this.commaStringToArray(q1.multi); - q1.options = this.commaStringToArray(q1.options); + q1.multiArray = this.commaStringToArray(q1.multi); + q1.optionsArray = this.commaStringToArray(q1.options); if (parent) { // It's a sub-question. @@ -114,7 +115,7 @@ export class AddonModSurveyHelperProvider { q1.name = 'q' + q1.id; q1.num = num++; if (q1.type > 0) { // Add "choose" option since this question is not required. - q1.options.unshift(strChoose); + q1.optionsArray.unshift(strChoose); } } @@ -123,5 +124,15 @@ export class AddonModSurveyHelperProvider { return formatted; } - } + +/** + * Survey question with some calculated data. + */ +export type AddonModSurveyQuestionFormatted = AddonModSurveyQuestion & { + required?: boolean; // Calculated in the app. Whether the question is required. + name?: string; // Calculated in the app. The name of the question. + num?: number; // Calculated in the app. Number of the question. + multiArray?: string[]; // Subquestions ids, converted to an array. + optionsArray?: string[]; // Question options, converted to an array. +}; diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index 078fae02c..428a3db39 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -21,6 +21,7 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModSurveyOfflineProvider } from './offline'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for surveys. @@ -46,7 +47,7 @@ export class AddonModSurveyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the questions are retrieved. */ - getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { + getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { surveyid: surveyId @@ -61,7 +62,9 @@ export class AddonModSurveyProvider { preSets.emergencyCache = false; } - return site.read('mod_survey_get_questions', params, preSets).then((response) => { + return site.read('mod_survey_get_questions', params, preSets) + .then((response: AddonModSurveyGetQuestionsResult): any => { + if (response.questions) { return response.questions; } @@ -101,7 +104,9 @@ export class AddonModSurveyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the survey is retrieved. */ - protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string): Promise { + protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -116,7 +121,9 @@ export class AddonModSurveyProvider { preSets.emergencyCache = false; } - return site.read('mod_survey_get_surveys_by_courses', params, preSets).then((response) => { + return site.read('mod_survey_get_surveys_by_courses', params, preSets) + .then((response: AddonModSurveyGetSurveysByCoursesResult): any => { + if (response && response.surveys) { const currentSurvey = response.surveys.find((survey) => { return survey[key] == value; @@ -140,7 +147,7 @@ export class AddonModSurveyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the survey is retrieved. */ - getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { + getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, ignoreCache, siteId); } @@ -153,7 +160,7 @@ export class AddonModSurveyProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the survey is retrieved. */ - getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { + getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getSurveyDataByKey(courseId, 'id', id, ignoreCache, siteId); } @@ -277,17 +284,16 @@ export class AddonModSurveyProvider { * @param surveyId Survey ID. * @param answers Answers. * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when answers are successfully submitted. Rejected with object containing - * the error message (if any) and a boolean indicating if the error was returned by WS. + * @return Promise resolved when answers are successfully submitted. */ - submitAnswersOnline(surveyId: number, answers: any[], siteId?: string): Promise { + submitAnswersOnline(surveyId: number, answers: any[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { surveyid: surveyId, answers: answers }; - return site.write('mod_survey_submit_answers', params).then((response) => { + return site.write('mod_survey_submit_answers', params).then((response: AddonModSurveySubmitAnswersResult) => { if (!response.status) { return Promise.reject(this.utils.createFakeWSError('')); } @@ -295,3 +301,64 @@ export class AddonModSurveyProvider { }); } } + +/** + * Survey returned by WS mod_survey_get_surveys_by_courses. + */ +export type AddonModSurveySurvey = { + id: number; // Survey id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Survey name. + intro?: string; // The Survey intro. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + template?: number; // Survey type. + days?: number; // Days. + questions?: string; // Question ids. + surveydone?: number; // Did I finish the survey?. + timecreated?: number; // Time of creation. + timemodified?: number; // Time of last modification. + section?: number; // Course section id. + visible?: number; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Survey question. + */ +export type AddonModSurveyQuestion = { + id: number; // Question id. + text: string; // Question text. + shorttext: string; // Question short text. + multi: string; // Subquestions ids. + intro: string; // The question intro. + type: number; // Question type. + options: string; // Question options. + parent: number; // Parent question (for subquestions). +}; + +/** + * Result of WS mod_survey_get_questions. + */ +export type AddonModSurveyGetQuestionsResult = { + questions: AddonModSurveyQuestion[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_survey_get_surveys_by_courses. + */ +export type AddonModSurveyGetSurveysByCoursesResult = { + surveys: AddonModSurveySurvey[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_survey_submit_answers. + */ +export type AddonModSurveySubmitAnswersResult = { + status: boolean; // Status: true if success. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts index 05f475d59..d1ef86acd 100644 --- a/src/addon/mod/url/providers/module-handler.ts +++ b/src/addon/mod/url/providers/module-handler.ts @@ -19,7 +19,7 @@ import { AddonModUrlIndexComponent } from '../components/index/index'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { AddonModUrlProvider } from './url'; +import { AddonModUrlProvider, AddonModUrlUrl } from './url'; import { AddonModUrlHelperProvider } from './helper'; import { CoreConstants } from '@core/constants'; @@ -90,7 +90,8 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { if (handler.urlProvider.isGetUrlWSAvailable()) { return handler.urlProvider.getUrl(courseId, module.id).catch(() => { // Ignore errors. - }).then((url) => { + return undefined; + }).then((url: AddonModUrlUrl) => { const displayType = handler.urlProvider.getFinalDisplayType(url); return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN || diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts index 28ffe8da8..cd8f9e1fe 100644 --- a/src/addon/mod/url/providers/url.ts +++ b/src/addon/mod/url/providers/url.ts @@ -21,6 +21,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreConstants } from '@core/constants'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some features for urls. @@ -44,7 +45,7 @@ export class AddonModUrlProvider { * @param url URL data. * @return Final display type. */ - getFinalDisplayType(url: any): number { + getFinalDisplayType(url: AddonModUrlUrl): number { if (!url) { return -1; } @@ -109,7 +110,7 @@ export class AddonModUrlProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the url is retrieved. */ - protected getUrlDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getUrlDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -119,7 +120,9 @@ export class AddonModUrlProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; - return site.read('mod_url_get_urls_by_courses', params, preSets).then((response) => { + return site.read('mod_url_get_urls_by_courses', params, preSets) + .then((response: AddonModUrlGetUrlsByCoursesResult): any => { + if (response && response.urls) { const currentUrl = response.urls.find((url) => { return url[key] == value; @@ -142,7 +145,7 @@ export class AddonModUrlProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the url is retrieved. */ - getUrl(courseId: number, cmId: number, siteId?: string): Promise { + getUrl(courseId: number, cmId: number, siteId?: string): Promise { return this.getUrlDataByKey(courseId, 'coursemodule', cmId, siteId); } @@ -231,3 +234,33 @@ export class AddonModUrlProvider { return this.logHelper.logSingle('mod_url_view_url', params, AddonModUrlProvider.COMPONENT, id, name, 'url', {}, siteId); } } + +/** + * URL returnd by mod_url_get_urls_by_courses. + */ +export type AddonModUrlUrl = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // URL name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + externalurl: string; // External URL. + display: number; // How to display the url. + displayoptions: string; // Display options (width, height). + parameters: string; // Parameters to append to the URL. + timemodified: number; // Last time the url was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_url_get_urls_by_courses. + */ +export type AddonModUrlGetUrlsByCoursesResult = { + urls: AddonModUrlUrl[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index 2011957ed..c05cbbc05 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -21,7 +21,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; import { coreSlideInOut } from '@classes/animations'; -import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesProvider, AddonNotesNoteFormatted } from '../../providers/notes'; import { AddonNotesOfflineProvider } from '../../providers/notes-offline'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; @@ -44,7 +44,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { type = 'course'; refreshIcon = 'spinner'; syncIcon = 'spinner'; - notes: any[]; + notes: AddonNotesNoteFormatted[]; hasOffline = false; notesLoaded = false; user: any; @@ -101,21 +101,21 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { // Ignore errors. }).then(() => { return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => { - notes = notes[this.type + 'notes'] || []; + const notesList: AddonNotesNoteFormatted[] = notes[this.type + 'notes'] || []; - return this.notesProvider.setOfflineDeletedNotes(notes, this.courseId).then((notes) => { + return this.notesProvider.setOfflineDeletedNotes(notesList, this.courseId).then((notesList) => { - this.hasOffline = notes.some((note) => note.offline || note.deleted); + this.hasOffline = notesList.some((note) => note.offline || note.deleted); if (this.userId) { - this.notes = notes; + this.notes = notesList; // Get the user profile to retrieve the user image. return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { this.user = user; }); } else { - return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + return this.notesProvider.getNotesUserData(notesList, this.courseId).then((notes) => { this.notes = notes; }); } @@ -126,7 +126,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { }).finally(() => { let canDelete = this.notes && this.notes.length > 0; if (canDelete && this.type == 'personal') { - canDelete = this.notes.find((note) => { + canDelete = !!this.notes.find((note) => { return note.usermodified == this.currentUserId; }); } @@ -178,6 +178,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { addNote(e: Event): void { e.preventDefault(); e.stopPropagation(); + const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: this.userId, courseId: this.courseId, type: this.type }); modal.onDidDismiss((data) => { if (data && data.sent && data.type) { @@ -192,6 +193,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.typeChanged(); } }); + modal.present(); } @@ -201,7 +203,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { * @param e Click event. * @param note Note to delete. */ - deleteNote(e: Event, note: any): void { + deleteNote(e: Event, note: AddonNotesNoteFormatted): void { e.preventDefault(); e.stopPropagation(); @@ -226,7 +228,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { * @param e Click event. * @param note Note to delete. */ - undoDeleteNote(e: Event, note: any): void { + undoDeleteNote(e: Event, note: AddonNotesNoteFormatted): void { e.preventDefault(); e.stopPropagation(); diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 3753f9404..12e476b8c 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonNotesOfflineProvider } from './notes-offline'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle notes. @@ -119,9 +120,9 @@ export class AddonNotesProvider { * @return Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that notes * have been added, the resolve param can contain errors for notes not sent. */ - addNotesOnline(notes: any[], siteId?: string): Promise { + addNotesOnline(notes: any[], siteId?: string): Promise { if (!notes || !notes.length) { - return Promise.resolve(); + return Promise.resolve([]); } return this.sitesProvider.getSite(siteId).then((site) => { @@ -142,7 +143,7 @@ export class AddonNotesProvider { * @return Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes * have been deleted, the resolve param can contain errors for notes not deleted. */ - deleteNote(note: any, courseId: number, siteId?: string): Promise { + deleteNote(note: AddonNotesNoteFormatted, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (note.offline) { @@ -190,7 +191,7 @@ export class AddonNotesProvider { notes: noteIds }; - return site.write('core_notes_delete_notes', data).then((response) => { + return site.write('core_notes_delete_notes', data).then((response: CoreWSExternalWarning[]) => { // A note was deleted, invalidate the course notes. return this.invalidateNotes(courseId, undefined, siteId).catch(() => { // Ignore errors. @@ -288,7 +289,9 @@ export class AddonNotesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise to be resolved when the notes are retrieved. */ - getNotes(courseId: number, userId?: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise { + getNotes(courseId: number, userId?: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string) + : Promise { + this.logger.debug('Get notes for course ' + courseId); return this.sitesProvider.getSite(siteId).then((site) => { @@ -310,7 +313,7 @@ export class AddonNotesProvider { preSets.emergencyCache = false; } - return site.read('core_notes_get_course_notes', data, preSets).then((notes) => { + return site.read('core_notes_get_course_notes', data, preSets).then((notes: AddonNotesGetCourseNotesResult) => { if (onlyOnline) { return notes; } @@ -339,9 +342,11 @@ export class AddonNotesProvider { * @param notes Array of notes. * @param courseId ID of the course the notes belong to. * @param siteId Site ID. If not defined, current site. - * @return [description] + * @return Promise resolved when done. */ - setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise { + setOfflineDeletedNotes(notes: AddonNotesNoteFormatted[], courseId: number, siteId?: string) + : Promise { + return this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((deletedNotes) => { notes.forEach((note) => { note.deleted = deletedNotes.some((n) => n.noteid == note.id); @@ -358,7 +363,7 @@ export class AddonNotesProvider { * @param courseId ID of the course the notes belong to. * @return Promise always resolved. Resolve param is the formatted notes. */ - getNotesUserData(notes: any[], courseId: number): Promise { + getNotesUserData(notes: AddonNotesNoteFormatted[], courseId: number): Promise { const promises = notes.map((note) => { // Get the user profile to retrieve the user image. return this.userProvider.getProfile(note.userid, note.courseid, true).then((user) => { @@ -400,7 +405,7 @@ export class AddonNotesProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ - logView(courseId: number, userId?: number, siteId?: string): Promise { + logView(courseId: number, userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseid: courseId, @@ -413,3 +418,57 @@ export class AddonNotesProvider { }); } } + +/** + * Note data returned by core_notes_get_course_notes. + */ +export type AddonNotesNote = { + id: number; // Id of this note. + courseid: number; // Id of the course. + userid: number; // User id. + content: string; // The content text formated. + format: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + created: number; // Time created (timestamp). + lastmodified: number; // Time of last modification (timestamp). + usermodified: number; // User id of the creator of this note. + publishstate: string; // State of the note (i.e. draft, public, site). +}; + +/** + * Result of WS core_notes_get_course_notes. + */ +export type AddonNotesGetCourseNotesResult = { + sitenotes?: AddonNotesNote[]; // Site notes. + coursenotes?: AddonNotesNote[]; // Couse notes. + personalnotes?: AddonNotesNote[]; // Personal notes. + canmanagesystemnotes?: boolean; // @since 3.7. Whether the user can manage notes at system level. + canmanagecoursenotes?: boolean; // @since 3.7. Whether the user can manage notes at the given course. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Note returned by WS core_notes_create_notes. + */ +export type AddonNotesCreateNotesNote = { + clientnoteid?: string; // Your own id for the note. + noteid: number; // ID of the created note when successful, -1 when failed. + errormessage?: string; // Error message - if failed. +}; + +/** + * Result of WS core_notes_view_notes. + */ +export type AddonNotesViewNotesResult = { + status: boolean; // Status: true if success. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Notes with some calculated data. + */ +export type AddonNotesNoteFormatted = AddonNotesNote & { + offline?: boolean; // Calculated in the app. Whether it's an offline note. + deleted?: boolean; // Calculated in the app. Whether the note was deleted in offline. + userfullname?: string; // Calculated in the app. Full name of the user the note refers to. + userprofileimageurl?: string; // Calculated in the app. Avatar url of the user the note refers to. +}; diff --git a/src/addon/notifications/pages/list/list.ts b/src/addon/notifications/pages/list/list.ts index db9153bd8..6a38c3a11 100644 --- a/src/addon/notifications/pages/list/list.ts +++ b/src/addon/notifications/pages/list/list.ts @@ -20,7 +20,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { AddonNotificationsProvider } from '../../providers/notifications'; +import { AddonNotificationsProvider, AddonNotificationsAnyNotification } from '../../providers/notifications'; import { AddonNotificationsHelperProvider } from '../../providers/helper'; import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate'; @@ -34,7 +34,7 @@ import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers }) export class AddonNotificationsListPage { - notifications = []; + notifications: AddonNotificationsAnyNotification[] = []; notificationsLoaded = false; canLoadMore = false; loadMoreError = false; @@ -130,11 +130,12 @@ export class AddonNotificationsListPage { * * @param notifications Array of notification objects. */ - protected markNotificationsAsRead(notifications: any[]): void { + protected markNotificationsAsRead(notifications: AddonNotificationsAnyNotification[]): void { + let promise; if (notifications.length > 0) { - const promises = notifications.map((notification) => { + const promises: Promise[] = notifications.map((notification) => { if (notification.read) { // Already read, don't mark it. return Promise.resolve(); @@ -202,7 +203,7 @@ export class AddonNotificationsListPage { * * @param notification The notification object. */ - protected formatText(notification: any): void { + protected formatText(notification: AddonNotificationsAnyNotification): void { const text = notification.mobiletext.replace(/-{4,}/ig, ''); notification.mobiletext = this.textUtils.replaceNewLines(text, '
'); } diff --git a/src/addon/notifications/pages/settings/settings.ts b/src/addon/notifications/pages/settings/settings.ts index 5754f18a2..e4ca15b6f 100644 --- a/src/addon/notifications/pages/settings/settings.ts +++ b/src/addon/notifications/pages/settings/settings.ts @@ -14,7 +14,11 @@ import { Component, OnDestroy, Optional } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; -import { AddonNotificationsProvider } from '../../providers/notifications'; +import { + AddonNotificationsProvider, AddonNotificationsNotificationPreferences, AddonNotificationsNotificationPreferencesProcessor, + AddonNotificationsNotificationPreferencesComponent, AddonNotificationsNotificationPreferencesNotification, + AddonNotificationsNotificationPreferencesNotificationProcessorState +} from '../../providers/notifications'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSettingsHelper } from '@core/settings/providers/helper'; @@ -38,10 +42,10 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; export class AddonNotificationsSettingsPage implements OnDestroy { protected updateTimeout: any; - components: any[]; - preferences: any; + components: AddonNotificationsNotificationPreferencesComponent[]; + preferences: AddonNotificationsNotificationPreferences; preferencesLoaded: boolean; - currentProcessor: any; + currentProcessor: AddonNotificationsNotificationPreferencesProcessorFormatted; notifPrefsEnabled: boolean; canChangeSound: boolean; notificationSound: boolean; @@ -99,7 +103,7 @@ export class AddonNotificationsSettingsPage implements OnDestroy { // Get display data of message output handlers (thery are displayed in the context menu), this.processorHandlers = []; if (preferences.processors) { - preferences.processors.forEach((processor) => { + preferences.processors.forEach((processor: AddonNotificationsNotificationPreferencesProcessorFormatted) => { processor.supported = this.messageOutputDelegate.hasHandler(processor.name, true); if (processor.hassettings && processor.supported) { this.processorHandlers.push(this.messageOutputDelegate.getDisplayData(processor)); @@ -118,7 +122,7 @@ export class AddonNotificationsSettingsPage implements OnDestroy { * * @param processor Processor object. */ - protected loadProcessor(processor: any): void { + protected loadProcessor(processor: AddonNotificationsNotificationPreferencesProcessorFormatted): void { if (!processor) { return; } @@ -191,8 +195,9 @@ export class AddonNotificationsSettingsPage implements OnDestroy { * @param notification Notification object. * @param state State name, ['loggedin', 'loggedoff']. */ - changePreference(notification: any, state: string): void { - const processorState = notification.currentProcessor[state]; + changePreference(notification: AddonNotificationsNotificationPreferencesNotificationFormatted, state: string): void { + const processorState: AddonNotificationsNotificationPreferencesNotificationProcessorStateFormatted = + notification.currentProcessor[state]; const preferenceName = notification.preferencekey + '_' + processorState.name; let value; @@ -211,6 +216,7 @@ export class AddonNotificationsSettingsPage implements OnDestroy { } processorState.updating = true; + this.userProvider.updateUserPreference(preferenceName, value).then(() => { // Update the preferences since they were modified. this.updatePreferencesAfterDelay(); @@ -264,3 +270,25 @@ export class AddonNotificationsSettingsPage implements OnDestroy { } } } + +/** + * Notification preferences notification with some calculated data. + */ +type AddonNotificationsNotificationPreferencesNotificationFormatted = AddonNotificationsNotificationPreferencesNotification & { + currentProcessor?: AddonNotificationsNotificationPreferencesProcessorFormatted; // Calculated in the app. Current processor. +}; + +/** + * Notification preferences processor with some calculated data. + */ +type AddonNotificationsNotificationPreferencesProcessorFormatted = AddonNotificationsNotificationPreferencesProcessor & { + supported?: boolean; // Calculated in the app. Whether the processor is supported in the app. +}; + +/** + * State in notification processor in notification preferences component with some calculated data. + */ +type AddonNotificationsNotificationPreferencesNotificationProcessorStateFormatted = + AddonNotificationsNotificationPreferencesNotificationProcessorState & { + updating?: boolean; // Calculated in the app. Whether the state is being updated. +}; diff --git a/src/addon/notifications/providers/helper.ts b/src/addon/notifications/providers/helper.ts index 442a0b1e1..fce881a1e 100644 --- a/src/addon/notifications/providers/helper.ts +++ b/src/addon/notifications/providers/helper.ts @@ -14,7 +14,9 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; -import { AddonNotificationsProvider } from './notifications'; +import { + AddonNotificationsProvider, AddonNotificationsAnyNotification, AddonNotificationsGetMessagesMessage +} from './notifications'; /** * Service that provides some helper functions for notifications. @@ -37,7 +39,7 @@ export class AddonNotificationsHelperProvider { * @return Promise resolved with notifications and if can load more. */ getNotifications(notifications: any[], limit?: number, toDisplay: boolean = true, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise<{notifications: any[], canLoadMore: boolean}> { + siteId?: string): Promise<{notifications: AddonNotificationsAnyNotification[], canLoadMore: boolean}> { notifications = notifications || []; limit = limit || AddonNotificationsProvider.LIST_LIMIT; @@ -80,7 +82,7 @@ export class AddonNotificationsHelperProvider { promise = Promise.resolve(unread); } - return promise.then((notifications) => { + return promise.then((notifications: AddonNotificationsGetMessagesMessage[]) => { return { notifications: notifications, canLoadMore: notifications.length >= limit diff --git a/src/addon/notifications/providers/notifications.ts b/src/addon/notifications/providers/notifications.ts index 6e0af0129..cd7f37bfc 100644 --- a/src/addon/notifications/providers/notifications.ts +++ b/src/addon/notifications/providers/notifications.ts @@ -20,8 +20,9 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; -import { AddonMessagesProvider } from '@addon/messages/providers/messages'; +import { AddonMessagesProvider, AddonMessagesMarkMessageReadResult } from '@addon/messages/providers/messages'; import { CoreSite } from '@classes/site'; +import { CoreWSExternalWarning } from '@providers/ws'; /** * Service to handle notifications. @@ -51,14 +52,13 @@ export class AddonNotificationsProvider { * @param read Whether the notifications are read or unread. * @return Promise resolved with notifications. */ - protected formatNotificationsData(notifications: any[], read?: boolean): Promise { + protected formatNotificationsData(notifications: AddonNotificationsAnyNotification[], read?: boolean): Promise { + const promises = notifications.map((notification) => { // Set message to show. if (notification.component && notification.component == 'mod_forum') { notification.mobiletext = notification.smallmessage; - } else if (notification.component && notification.component == 'moodle' && notification.name == 'insights') { - notification.mobiletext = notification.fullmessagehtml; } else { notification.mobiletext = notification.fullmessage; } @@ -117,7 +117,7 @@ export class AddonNotificationsProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the notification preferences. */ - getNotificationPreferences(siteId?: string): Promise { + getNotificationPreferences(siteId?: string): Promise { this.logger.debug('Get notification preferences'); return this.sitesProvider.getSite(siteId).then((site) => { @@ -126,7 +126,9 @@ export class AddonNotificationsProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_message_get_user_notification_preferences', {}, preSets).then((data) => { + return site.read('core_message_get_user_notification_preferences', {}, preSets) + .then((data: AddonNotificationsGetUserNotificationPreferencesResult) => { + return data.preferences; }); }); @@ -154,7 +156,7 @@ export class AddonNotificationsProvider { * @return Promise resolved with notifications. */ getNotifications(read: boolean, limitFrom: number, limitNumber: number = 0, toDisplay: boolean = true, - forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { limitNumber = limitNumber || AddonNotificationsProvider.LIST_LIMIT; this.logger.debug('Get ' + (read ? 'read' : 'unread') + ' notifications from ' + limitFrom + '. Limit: ' + limitNumber); @@ -176,7 +178,7 @@ export class AddonNotificationsProvider { }; // Get unread notifications. - return site.read('core_message_get_messages', data, preSets).then((response) => { + return site.read('core_message_get_messages', data, preSets).then((response: AddonNotificationsGetMessagesResult) => { if (response.messages) { const notifications = response.messages; @@ -209,7 +211,7 @@ export class AddonNotificationsProvider { * @since 3.2 */ getPopupNotifications(offset: number, limit?: number, toDisplay: boolean = true, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise<{notifications: any[], canLoadMore: boolean}> { + siteId?: string): Promise<{notifications: AddonNotificationsPopupNotificationFormatted[], canLoadMore: boolean}> { limit = limit || AddonNotificationsProvider.LIST_LIMIT; @@ -230,17 +232,17 @@ export class AddonNotificationsProvider { }; // Get notifications. - return site.read('message_popup_get_popup_notifications', data, preSets).then((response) => { + return site.read('message_popup_get_popup_notifications', data, preSets) + .then((response: AddonNotificationsGetPopupNotificationsResult) => { + if (response.notifications) { - const result: any = { - canLoadMore: response.notifications.length > limit - }, - notifications = response.notifications.slice(0, limit); + const result = { + canLoadMore: response.notifications.length > limit, + notifications: response.notifications.slice(0, limit) + }; - result.notifications = notifications; - - return this.formatNotificationsData(notifications).then(() => { - const first = notifications[0]; + return this.formatNotificationsData(result.notifications).then(() => { + const first = result.notifications[0]; if (this.appProvider.isDesktop() && toDisplay && offset === 0 && first && !first.read) { // Store the last received notification. Don't block the user for this. @@ -269,7 +271,7 @@ export class AddonNotificationsProvider { * @return Promise resolved with notifications. */ getReadNotifications(limitFrom: number, limitNumber: number, toDisplay: boolean = true, - forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { return this.getNotifications(true, limitFrom, limitNumber, toDisplay, forceCache, ignoreCache, siteId); } @@ -285,7 +287,7 @@ export class AddonNotificationsProvider { * @return Promise resolved with notifications. */ getUnreadNotifications(limitFrom: number, limitNumber: number, toDisplay: boolean = true, - forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { return this.getNotifications(false, limitFrom, limitNumber, toDisplay, forceCache, ignoreCache, siteId); } @@ -349,7 +351,7 @@ export class AddonNotificationsProvider { * @return Resolved when done. * @since 3.2 */ - markAllNotificationsAsRead(): Promise { + markAllNotificationsAsRead(): Promise { const params = { useridto: this.sitesProvider.getCurrentSiteUserId() }; @@ -362,10 +364,12 @@ export class AddonNotificationsProvider { * * @param notificationId ID of notification to mark as read * @param siteId Site ID. If not defined, current site. - * @return Resolved when done. + * @return Promise resolved when done. * @since 3.5 */ - markNotificationRead(notificationId: number, siteId?: string): Promise { + markNotificationRead(notificationId: number, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { if (site.wsAvailable('core_message_mark_notification_read')) { @@ -436,3 +440,179 @@ export class AddonNotificationsProvider { return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_user_notification_preferences'); } } + +/** + * Preferences returned by core_message_get_user_notification_preferences. + */ +export type AddonNotificationsNotificationPreferences = { + userid: number; // User id. + disableall: number | boolean; // Whether all the preferences are disabled. + processors: AddonNotificationsNotificationPreferencesProcessor[]; // Config form values. + components: AddonNotificationsNotificationPreferencesComponent[]; // Available components. +}; + +/** + * Processor in notification preferences. + */ +export type AddonNotificationsNotificationPreferencesProcessor = { + displayname: string; // Display name. + name: string; // Processor name. + hassettings: boolean; // Whether has settings. + contextid: number; // Context id. + userconfigured: number; // Whether is configured by the user. +}; + +/** + * Component in notification preferences. + */ +export type AddonNotificationsNotificationPreferencesComponent = { + displayname: string; // Display name. + notifications: AddonNotificationsNotificationPreferencesNotification[]; // List of notificaitons for the component. +}; + +/** + * Notification processor in notification preferences component. + */ +export type AddonNotificationsNotificationPreferencesNotification = { + displayname: string; // Display name. + preferencekey: string; // Preference key. + processors: AddonNotificationsNotificationPreferencesNotificationProcessor[]; // Processors values for this notification. +}; + +/** + * Notification processor in notification preferences component. + */ +export type AddonNotificationsNotificationPreferencesNotificationProcessor = { + displayname: string; // Display name. + name: string; // Processor name. + locked: boolean; // Is locked by admin?. + lockedmessage?: string; // @since 3.6. Text to display if locked. + userconfigured: number; // Is configured?. + loggedin: AddonNotificationsNotificationPreferencesNotificationProcessorState; + loggedoff: AddonNotificationsNotificationPreferencesNotificationProcessorState; +}; + +/** + * State in notification processor in notification preferences component. + */ +export type AddonNotificationsNotificationPreferencesNotificationProcessorState = { + name: string; // Name. + displayname: string; // Display name. + checked: boolean; // Is checked?. +}; + +/** + * Result of WS core_message_get_messages. + */ +export type AddonNotificationsGetMessagesResult = { + messages: AddonNotificationsGetMessagesMessage[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Message data returned by core_message_get_messages. + */ +export type AddonNotificationsGetMessagesMessage = { + id: number; // Message id. + useridfrom: number; // User from id. + useridto: number; // User to id. + subject: string; // The message subject. + text: string; // The message text formated. + fullmessage: string; // The message. + fullmessageformat: number; // Fullmessage format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + fullmessagehtml: string; // The message in html. + smallmessage: string; // The shorten message. + notification: number; // Is a notification?. + contexturl: string; // Context URL. + contexturlname: string; // Context URL link name. + timecreated: number; // Time created. + timeread: number; // Time read. + usertofullname: string; // User to full name. + userfromfullname: string; // User from full name. + component?: string; // @since 3.7. The component that generated the notification. + eventtype?: string; // @since 3.7. The type of notification. + customdata?: any; // @since 3.7. Custom data to be passed to the message processor. +}; + +/** + * Message data returned by core_message_get_messages with some calculated data. + */ +export type AddonNotificationsGetMessagesMessageFormatted = + AddonNotificationsGetMessagesMessage & AddonNotificationsNotificationCalculatedData; + +/** + * Result of WS message_popup_get_popup_notifications. + */ +export type AddonNotificationsGetPopupNotificationsResult = { + notifications: AddonNotificationsPopupNotification[]; + unreadcount: number; // The number of unread message for the given user. +}; + +/** + * Notification returned by message_popup_get_popup_notifications. + */ +export type AddonNotificationsPopupNotification = { + id: number; // Notification id (this is not guaranteed to be unique within this result set). + useridfrom: number; // User from id. + useridto: number; // User to id. + subject: string; // The notification subject. + shortenedsubject: string; // The notification subject shortened with ellipsis. + text: string; // The message text formated. + fullmessage: string; // The message. + fullmessageformat: number; // Fullmessage format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + fullmessagehtml: string; // The message in html. + smallmessage: string; // The shorten message. + contexturl: string; // Context URL. + contexturlname: string; // Context URL link name. + timecreated: number; // Time created. + timecreatedpretty: string; // Time created in a pretty format. + timeread: number; // Time read. + read: boolean; // Notification read status. + deleted: boolean; // Notification deletion status. + iconurl: string; // URL for notification icon. + component?: string; // The component that generated the notification. + eventtype?: string; // The type of notification. + customdata?: any; // @since 3.7. Custom data to be passed to the message processor. +}; + +/** + * Notification returned by message_popup_get_popup_notifications. + */ +export type AddonNotificationsPopupNotificationFormatted = + AddonNotificationsPopupNotification & AddonNotificationsNotificationCalculatedData; + +/** + * Any kind of notification that can be retrieved. + */ +export type AddonNotificationsAnyNotification = + AddonNotificationsPopupNotificationFormatted | AddonNotificationsGetMessagesMessageFormatted; + +/** + * Result of WS core_message_get_user_notification_preferences. + */ +export type AddonNotificationsGetUserNotificationPreferencesResult = { + preferences: AddonNotificationsNotificationPreferences; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_message_mark_notification_read. + */ +export type AddonNotificationsMarkNotificationReadResult = { + notificationid: number; // Id of the notification. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Calculated data for messages returned by core_message_get_messages. + */ +export type AddonNotificationsNotificationCalculatedData = { + mobiletext?: string; // Calculated in the app. Text to display for the notification. + moodlecomponent?: string; // Calculated in the app. Moodle's component. + notif?: number; // Calculated in the app. Whether it's a notification. + notification?: number; // Calculated in the app in some cases. Whether it's a notification. + read?: boolean; // Calculated in the app. Whether the notifications is read. + courseid?: number; // Calculated in the app. Course the notification belongs to. + profileimageurlfrom?: string; // Calculated in the app. Avatar of user that sent the notification. + userfromfullname?: string; // Calculated in the app in some cases. User from full name. +}; diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index 24475b69a..b7a132cfe 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -407,3 +407,27 @@ export class CoreCommentsProvider { }); } } + +/** + * Data returned by comment_area_exporter. + */ +export type CoreCommentsArea = { + component: string; // Component. + commentarea: string; // Commentarea. + itemid: number; // Itemid. + courseid: number; // Courseid. + contextid: number; // Contextid. + cid: string; // Cid. + autostart: boolean; // Autostart. + canpost: boolean; // Canpost. + canview: boolean; // Canview. + count: number; // Count. + collapsediconkey: string; // @since 3.3. Collapsediconkey. + displaytotalcount: boolean; // Displaytotalcount. + displaycancel: boolean; // Displaycancel. + fullwidth: boolean; // Fullwidth. + linktext: string; // Linktext. + notoggle: boolean; // Notoggle. + template: string; // Template. + canpostorhascomments: boolean; // Canpostorhascomments. +}; diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 1b1af7c17..ed2d9fc7e 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -1135,3 +1135,38 @@ export class CoreCourseProvider { }, siteId); } } + +/** + * Data returned by course_summary_exporter. + */ +export type CoreCourseSummary = { + id: number; // Id. + fullname: string; // Fullname. + shortname: string; // Shortname. + idnumber: string; // Idnumber. + summary: string; // @since 3.3. Summary. + summaryformat: number; // @since 3.3. Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + startdate: number; // @since 3.3. Startdate. + enddate: number; // @since 3.3. Enddate. + visible: boolean; // @since 3.8. Visible. + fullnamedisplay: string; // @since 3.3. Fullnamedisplay. + viewurl: string; // Viewurl. + courseimage: string; // @since 3.6. Courseimage. + progress?: number; // @since 3.6. Progress. + hasprogress: boolean; // @since 3.6. Hasprogress. + isfavourite: boolean; // @since 3.6. Isfavourite. + hidden: boolean; // @since 3.6. Hidden. + timeaccess?: number; // @since 3.6. Timeaccess. + showshortname: boolean; // @since 3.6. Showshortname. + coursecategory: string; // @since 3.7. Coursecategory. +}; + +/** + * Data returned by course_module_summary_exporter. + */ +export type CoreCourseModuleSummary = { + id: number; // Id. + name: string; // Name. + url?: string; // Url. + iconurl: string; // Iconurl. +}; diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts index 4b012d32f..0f993914a 100644 --- a/src/core/tag/providers/tag.ts +++ b/src/core/tag/providers/tag.ts @@ -17,74 +17,6 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -/** - * Structure of a tag cloud returned by WS. - */ -export interface CoreTagCloud { - tags: CoreTagCloudTag[]; - tagscount: number; - totalcount: number; -} - -/** - * Structure of a tag cloud tag returned by WS. - */ -export interface CoreTagCloudTag { - name: string; - viewurl: string; - flag: boolean; - isstandard: boolean; - count: number; - size: number; -} - -/** - * Structure of a tag collection returned by WS. - */ -export interface CoreTagCollection { - id: number; - name: string; - isdefault: boolean; - component: string; - sortoder: number; - searchable: boolean; - customurl: string; -} - -/** - * Structure of a tag index returned by WS. - */ -export interface CoreTagIndex { - tagid: number; - ta: number; - component: string; - itemtype: string; - nextpageurl: string; - prevpageurl: string; - exclusiveurl: string; - exclusivetext: string; - title: string; - content: string; - hascontent: number; - anchor: string; -} - -/** - * Structure of a tag item returned by WS. - */ -export interface CoreTagItem { - id: number; - name: string; - rawname: string; - isstandard: boolean; - tagcollid: number; - taginstanceid: number; - taginstancecontextid: number; - itemid: number; - ordering: number; - flag: number; -} - /** * Service to handle tags. */ @@ -343,3 +275,71 @@ export class CoreTagProvider { + contextId + ':' + (recursive ? 1 : 0); } } + +/** + * Structure of a tag cloud returned by WS. + */ +export type CoreTagCloud = { + tags: CoreTagCloudTag[]; + tagscount: number; + totalcount: number; +}; + +/** + * Structure of a tag cloud tag returned by WS. + */ +export type CoreTagCloudTag = { + name: string; + viewurl: string; + flag: boolean; + isstandard: boolean; + count: number; + size: number; +}; + +/** + * Structure of a tag collection returned by WS. + */ +export type CoreTagCollection = { + id: number; + name: string; + isdefault: boolean; + component: string; + sortoder: number; + searchable: boolean; + customurl: string; +}; + +/** + * Structure of a tag index returned by WS. + */ +export type CoreTagIndex = { + tagid: number; + ta: number; + component: string; + itemtype: string; + nextpageurl: string; + prevpageurl: string; + exclusiveurl: string; + exclusivetext: string; + title: string; + content: string; + hascontent: number; + anchor: string; +}; + +/** + * Structure of a tag item returned by WS. + */ +export type CoreTagItem = { + id: number; + name: string; + rawname: string; + isstandard: boolean; + tagcollid: number; + taginstanceid: number; + taginstancecontextid: number; + itemid: number; + ordering: number; + flag: number; +}; diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 7529b0b90..1541cecf5 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -634,3 +634,21 @@ export class CoreUserProvider { }); } } + +/** + * Data returned by user_summary_exporter. + */ +export type CoreUserSummary = { + id: number; // Id. + email: string; // Email. + idnumber: string; // Idnumber. + phone1: string; // Phone1. + phone2: string; // Phone2. + department: string; // Department. + institution: string; // Institution. + fullname: string; // Fullname. + identity: string; // Identity. + profileurl: string; // Profileurl. + profileimageurl: string; // Profileimageurl. + profileimageurlsmall: string; // Profileimageurlsmall. +}; diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 1d891cfa3..4bd4ac412 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -81,41 +81,6 @@ export interface CoreWSAjaxPreSets { useGet?: boolean; } -/** - * Error returned by a WS call. - */ -export interface CoreWSError { - /** - * The error message. - */ - message: string; - - /** - * Name of the exception. Undefined for local errors (fake WS errors). - */ - exception?: string; - - /** - * The error code. Undefined for local errors (fake WS errors). - */ - errorcode?: string; -} - -/** - * File upload options. - */ -export interface CoreWSFileUploadOptions extends FileUploadOptions { - /** - * The file area where to put the file. By default, 'draft'. - */ - fileArea?: string; - - /** - * Item ID of the area where to put the file. By default, 0. - */ - itemId?: number; -} - /** * This service allows performing WS calls and download/upload files. */ @@ -863,3 +828,127 @@ export class CoreWSProvider { }); } } + +/** + * Error returned by a WS call. + */ +export interface CoreWSError { + /** + * The error message. + */ + message: string; + + /** + * Name of the exception. Undefined for local errors (fake WS errors). + */ + exception?: string; + + /** + * The error code. Undefined for local errors (fake WS errors). + */ + errorcode?: string; +} + +/** + * File upload options. + */ +export interface CoreWSFileUploadOptions extends FileUploadOptions { + /** + * The file area where to put the file. By default, 'draft'. + */ + fileArea?: string; + + /** + * Item ID of the area where to put the file. By default, 0. + */ + itemId?: number; +} + +/** + * Structure of warnings returned by WS. + */ +export type CoreWSExternalWarning = { + /** + * Item. + */ + item?: string; + + /** + * Item id. + */ + itemid?: number; + + /** + * The warning code can be used by the client app to implement specific behaviour. + */ + warningcode: string; + + /** + * Untranslated english message to explain the warning. + */ + message: string; + +}; + +/** + * Structure of files returned by WS. + */ +export type CoreWSExternalFile = { + /** + * File name. + */ + filename?: string; + + /** + * File path. + */ + filepath?: string; + + /** + * File size. + */ + filesize?: number; + + /** + * Downloadable file url. + */ + fileurl?: string; + + /** + * Time modified. + */ + timemodified?: number; + + /** + * File mime type. + */ + mimetype?: string; + + /** + * Whether is an external file. + */ + isexternalfile?: number; + + /** + * The repository type for external files. + */ + repositorytype?: string; + +}; + +/** + * Data returned by date_exporter. + */ +export type CoreWSDate = { + seconds: number; // Seconds. + minutes: number; // Minutes. + hours: number; // Hours. + mday: number; // Mday. + wday: number; // Wday. + mon: number; // Mon. + year: number; // Year. + yday: number; // Yday. + weekday: string; // Weekday. + month: string; // Month. + timestamp: number; // Timestamp. +};