MOBILE-4543 course: Do not show open in browser for students

main
Pau Ferrer Ocaña 2024-03-22 12:04:23 +01:00
parent 4f56e08f9b
commit 9038e883e7
18 changed files with 96 additions and 78 deletions

View File

@ -79,14 +79,11 @@ Feature: Test basic usage of BBB activity in app
And I should not be able to press "Join session" in the app And I should not be able to press "Join session" in the app
# Join the session as moderator in a browser. # Join the session as moderator in a browser.
When I press "Information" in the app When I open a browser tab with url "$WWWROOT"
And I press "Open in browser" in the app And I am on the "bbb1" Activity page logged in as teacher1
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I click on "Join session" "link" And I click on "Join session" "link"
And I wait for the BigBlueButton room to start And I wait for the BigBlueButton room to start
And I switch back to the app And I switch back to the app
And I press "Close" in the app
And I pull to refresh until I find "The session is in progress" in the app And I pull to refresh until I find "The session is in progress" in the app
Then I should find "1" near "Moderator" in the app Then I should find "1" near "Moderator" in the app
And I should find "0" near "Viewer" in the app And I should find "0" near "Viewer" in the app

View File

@ -31,10 +31,8 @@ Feature: Test basic usage of choice activity in app
Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app
Then I should find "Test choice description" in the app Then I should find "Test choice description" in the app
When I press "Information" in the app When I open a browser tab with url "$WWWROOT"
And I press "Open in browser" in the app And I am on the "choice1" Activity page logged in as teacher1
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I press "Actions menu" And I press "Actions menu"
And I follow "View 1 responses" And I follow "View 1 responses"
And I press "Download in text format" And I press "Download in text format"

View File

@ -176,10 +176,8 @@ Feature: Test basic usage of choice activity in app
Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app
Then I should find "Test choice description" in the app Then I should find "Test choice description" in the app
When I press "Information" in the app When I open a browser tab with url "$WWWROOT"
And I press "Open in browser" in the app And I am on the "choice1" Activity page logged in as teacher1
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I follow "Responses" And I follow "Responses"
And I press "Download in text format" And I press "Download in text format"
# TODO Then I should find "..." in the downloads folder # TODO Then I should find "..." in the downloads folder

View File

@ -207,12 +207,9 @@ Feature: Attempt a quiz in app
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
Then the UI should match the snapshot Then the UI should match the snapshot
Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app Given I open a browser tab with url "$WWWROOT"
When I press "Information" in the app And I am on the "quiz1" Activity page logged in as teacher1
And I press "Open in browser" in the app When I follow "Attempts: 1"
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I follow "Attempts: 1"
And I follow "Review attempt" And I follow "Review attempt"
Then I should see "Finished" Then I should see "Finished"
And I should see "1.00/2.00" And I should see "1.00/2.00"

View File

@ -206,11 +206,8 @@ Feature: Attempt a quiz in app
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]" When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]"
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app Given I open a browser tab with url "$WWWROOT"
When I press "Information" in the app When I am on the "quiz1" Activity page logged in as teacher1
And I press "Open in browser" in the app
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I follow "Attempts: 1" And I follow "Attempts: 1"
And I follow "Review attempt" And I follow "Review attempt"
Then I should see "Finished" Then I should see "Finished"

View File

@ -211,12 +211,9 @@ Feature: Attempt a quiz in app
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
Then the UI should match the snapshot Then the UI should match the snapshot
Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app Given I open a browser tab with url "$WWWROOT"
When I press "Information" in the app And I am on the "quiz1" Activity page logged in as teacher1
And I press "Open in browser" in the app When I follow "Attempts: 1"
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I follow "Attempts: 1"
And I follow "Review attempt" And I follow "Review attempt"
Then I should see "Finished" Then I should see "Finished"
And I should see "1.00/2.00" And I should see "1.00/2.00"

View File

@ -233,8 +233,7 @@ Feature: Test basic usage of SCORM activity in app
Then I should find "2 / 11" in the app Then I should find "2 / 11" in the app
When I open a browser tab with url "$WWWROOT" When I open a browser tab with url "$WWWROOT"
And I log in as "admin" And I am on the "System logs report" page logged in as "admin"
And I am on the "System logs report" page
And I set the field "id" to "Course 1" And I set the field "id" to "Course 1"
And I set the field "user" to "Student student" And I set the field "user" to "Student student"
And I press "Get these logs" And I press "Get these logs"

View File

@ -24,7 +24,8 @@
</h1> </h1>
</ion-label> </ion-label>
<ion-button fill="clear" *ngIf="displayOptions.displayOpenInBrowser && externalUrl" [href]="externalUrl" core-link <ion-button fill="clear" *ngIf="displayOptions.displayOpenInBrowser && externalUrl" [href]="externalUrl" core-link
[showBrowserWarning]="false" [attr.aria-label]="'core.openinbrowser' | translate" slot="end"> [showBrowserWarning]="false" [attr.aria-label]="'core.openinbrowser' | translate" slot="end" [class.hidden]="!isTeacher"
class="core-module-oib-button">
<ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" /> <ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" />
</ion-button> </ion-button>
</ion-item> </ion-item>

View File

@ -39,3 +39,7 @@ ion-item.card-header {
margin: 0px; margin: 0px;
} }
} }
.core-module-oib-button.hidden {
display: none;
}

View File

@ -70,6 +70,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
course?: CoreEnrolledCourseData; course?: CoreEnrolledCourseData;
modicon = ''; modicon = '';
moduleNameTranslated = ''; moduleNameTranslated = '';
isTeacher = false;
protected onlineSubscription: Subscription; // It will observe the status of the network connection. protected onlineSubscription: Subscription; // It will observe the status of the network connection.
protected packageStatusObserver?: CoreEventObserver; // Observer of package status. protected packageStatusObserver?: CoreEventObserver; // Observer of package status.
@ -269,13 +270,14 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
* Fetch course. * Fetch course.
*/ */
protected async fetchCourse(): Promise<void> { protected async fetchCourse(): Promise<void> {
// Fix that.
try { try {
this.course = await CoreCourses.getUserCourse(this.courseId, true); this.course = await CoreCourses.getUserCourse(this.courseId, true);
} catch { } catch {
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course. // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
this.course = await CoreCourses.getCourse(this.courseId); this.course = await CoreCourses.getCourse(this.courseId);
} }
this.isTeacher = await CoreUtils.ignoreErrors(CoreCourseHelper.guessIsTeacher(this.courseId, this.course), false);
} }
/** /**

View File

@ -41,7 +41,8 @@
</ion-chip> </ion-chip>
</ion-label> </ion-label>
<ion-button *ngIf="displayOpenInBrowser" fill="clear" [href]="courseUrl" core-link [showBrowserWarning]="false" <ion-button *ngIf="displayOpenInBrowser" fill="clear" [href]="courseUrl" core-link [showBrowserWarning]="false"
[attr.aria-label]="'core.openinbrowser' | translate" slot="end"> [attr.aria-label]="'core.openinbrowser' | translate" slot="end" [class.hidden]="!isTeacher"
class="core-course-oib-button">
<ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" /> <ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" />
</ion-button> </ion-button>
</ion-item> </ion-item>

View File

@ -73,6 +73,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
progress?: number; progress?: number;
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
displayOpenInBrowser = false; displayOpenInBrowser = false;
isTeacher = false;
protected actionSheet?: HTMLIonActionSheetElement; protected actionSheet?: HTMLIonActionSheetElement;
protected waitStart = 0; protected waitStart = 0;
@ -172,6 +173,9 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
await this.loadMenuHandlers(refresh); await this.loadMenuHandlers(refresh);
// After loading menu handlers, admOptions should be available.
this.isTeacher = await CoreUtils.ignoreErrors(CoreCourseHelper.guessIsTeacher(this.courseId, this.course), false);
this.dataLoaded = true; this.dataLoaded = true;
} }

View File

@ -104,4 +104,8 @@
display: inline; display: inline;
} }
} }
.core-course-oib-button.hidden {
display: none;
}
} }

View File

@ -2058,6 +2058,30 @@ export class CoreCourseHelperProvider {
} }
} }
/**
* Guess if the user is a teacher in a course.
*
* @param courseId Course Id.
* @param course Course object.
* @returns Promise resolved with boolean: whether the user is a teacher.
*/
async guessIsTeacher(
courseId: number,
course?: CoreEnrolledCourseData | CoreCourseSearchedData,
): Promise<boolean> {
if (course && 'admOptions' in course && course.admOptions) {
return !!course.admOptions['reports'];
}
// Not loaded yet, try to load it.
const adminOptions = await CoreCourses.getUserAdministrationOptions(
[courseId],
{ readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE },
);
return !!adminOptions[courseId]?.['reports'];
}
} }
export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);

View File

@ -101,18 +101,9 @@ Feature: Test basic usage of one course in app
And I should not find "Test glossary" in the app And I should not find "Test glossary" in the app
Scenario: Guest access Scenario: Guest access
Given I entered the course "Course 1" as "teacher1" in the app Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1"
And I press "Course summary" in the app
And I press "Open in browser" in the app
And I switch to the browser tab opened by the app
And I log in as "teacher1"
And I press "Actions menu"
And I follow "More..."
And I follow "Users"
And I follow "Enrolment methods"
And I click on "Enable" "icon" in the "Guest access" "table_row" And I click on "Enable" "icon" in the "Guest access" "table_row"
And I close the browser tab opened by the app And I entered the app as "student2"
Given I entered the app as "student2"
When I press "Site home" in the app When I press "Site home" in the app
And I press "Available courses" in the app And I press "Available courses" in the app
And I press "Course 1" in the app And I press "Course 1" in the app

View File

@ -23,8 +23,7 @@ Feature: Test basic usage of guest access course in app
@lms_from4.0 @lms_from4.0
Scenario: Guest access without password (student) Scenario: Guest access without password (student)
Given I log in as "teacher1" Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1"
And I am on the "Course 1" "enrolment methods" page
And I click on "Edit" "link" in the "Guest access" "table_row" And I click on "Edit" "link" in the "Guest access" "table_row"
And I set the following fields to these values: And I set the following fields to these values:
| Allow guest access | Yes | | Allow guest access | Yes |
@ -47,8 +46,7 @@ Feature: Test basic usage of guest access course in app
@lms_from4.3 @lms_from4.3
Scenario: Guest access with password (student) Scenario: Guest access with password (student)
Given I log in as "teacher1" Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1"
And I am on the "Course 1" "enrolment methods" page
And I click on "Edit" "link" in the "Guest access" "table_row" And I click on "Edit" "link" in the "Guest access" "table_row"
And I set the following fields to these values: And I set the following fields to these values:
| Allow guest access | Yes | | Allow guest access | Yes |

View File

@ -27,8 +27,6 @@ import { AddonEnrolSelf } from '@addons/enrol/self/services/self';
import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol';
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site'; import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
const ROOT_CACHE_KEY = 'mmCourses:';
declare module '@singletons/events' { declare module '@singletons/events' {
/** /**
@ -50,6 +48,8 @@ declare module '@singletons/events' {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CoreCoursesProvider { export class CoreCoursesProvider {
protected static readonly ROOT_CACHE_KEY = 'mmCourses:';
static readonly SEARCH_PER_PAGE = 20; static readonly SEARCH_PER_PAGE = 20;
static readonly RECENT_PER_PAGE = 10; static readonly RECENT_PER_PAGE = 10;
static readonly ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey'; static readonly ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey';
@ -114,7 +114,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getCategoriesCacheKey(categoryId: number, addSubcategories?: boolean): string { protected getCategoriesCacheKey(categoryId: number, addSubcategories?: boolean): string {
return ROOT_CACHE_KEY + 'categories:' + categoryId + ':' + !!addSubcategories; return `${CoreCoursesProvider.ROOT_CACHE_KEY}categories:${categoryId}:${!!addSubcategories}`;
} }
/** /**
@ -131,16 +131,16 @@ export class CoreCoursesProvider {
if (courseIds.length == 1) { if (courseIds.length == 1) {
// Only 1 course, check if it belongs to the user courses. If so, use all user courses. // Only 1 course, check if it belongs to the user courses. If so, use all user courses.
return this.getCourseIdsIfEnrolled(courseIds[0], siteId); return this.getCourseIdsIfEnrolled(courseIds[0], siteId);
} else {
if (courseIds.length > 1 && courseIds.indexOf(siteHomeId) == -1) {
courseIds.push(siteHomeId);
}
// Sort the course IDs.
courseIds.sort((a, b) => b - a);
return courseIds;
} }
if (courseIds.length > 1 && courseIds.indexOf(siteHomeId) == -1) {
courseIds.push(siteHomeId);
}
// Sort the course IDs.
courseIds.sort((a, b) => b - a);
return courseIds;
} }
/** /**
@ -363,7 +363,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getCoursesCacheKey(ids: number[]): string { protected getCoursesCacheKey(ids: number[]): string {
return ROOT_CACHE_KEY + 'course:' + JSON.stringify(ids); return `${CoreCoursesProvider.ROOT_CACHE_KEY}course:${JSON.stringify(ids)}`;
} }
/** /**
@ -536,7 +536,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getCoursesByFieldCacheKey(field: string = '', value: string | number = ''): string { protected getCoursesByFieldCacheKey(field: string = '', value: string | number = ''): string {
return ROOT_CACHE_KEY + 'coursesbyfield:' + field + ':' + value; return `${CoreCoursesProvider.ROOT_CACHE_KEY}coursesbyfield:${field}:${value}`;
} }
/** /**
@ -651,7 +651,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getRecentCoursesCacheKey(userId: number): string { protected getRecentCoursesCacheKey(userId: number): string {
return `${ROOT_CACHE_KEY}:recentcourses:${userId}`; return `${CoreCoursesProvider.ROOT_CACHE_KEY}:recentcourses:${userId}`;
} }
/** /**
@ -684,7 +684,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getUserAdministrationOptionsCommonCacheKey(): string { protected getUserAdministrationOptionsCommonCacheKey(): string {
return ROOT_CACHE_KEY + 'administrationOptions:'; return `${CoreCoursesProvider.ROOT_CACHE_KEY}administrationOptions:`;
} }
/** /**
@ -701,11 +701,14 @@ export class CoreCoursesProvider {
* Get user administration options for a set of courses. * Get user administration options for a set of courses.
* *
* @param courseIds IDs of courses to get. * @param courseIds IDs of courses to get.
* @param siteId Site ID. If not defined, current site. * @param options Options.
* @returns Promise resolved with administration options for each course. * @returns Promise resolved with administration options for each course.
*/ */
getUserAdministrationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { getUserAdministrationOptions(
return firstValueFrom(this.getUserAdministrationOptionsObservable(courseIds, { siteId })); courseIds: number[],
options?: CoreSitesCommonWSOptions,
): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
return firstValueFrom(this.getUserAdministrationOptionsObservable(courseIds, options));
} }
/** /**
@ -752,7 +755,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getUserNavigationOptionsCommonCacheKey(): string { protected getUserNavigationOptionsCommonCacheKey(): string {
return ROOT_CACHE_KEY + 'navigationOptions:'; return `${CoreCoursesProvider.ROOT_CACHE_KEY}navigationOptions:`;
} }
/** /**
@ -768,11 +771,14 @@ export class CoreCoursesProvider {
* Get user navigation options for a set of courses. * Get user navigation options for a set of courses.
* *
* @param courseIds IDs of courses to get. * @param courseIds IDs of courses to get.
* @param siteId Site ID. If not defined, current site. * @param options Options.
* @returns Promise resolved with navigation options for each course. * @returns Promise resolved with navigation options for each course.
*/ */
async getUserNavigationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { getUserNavigationOptions(
return firstValueFrom(this.getUserNavigationOptionsObservable(courseIds, { siteId })); courseIds: number[],
options?: CoreSitesCommonWSOptions,
): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
return firstValueFrom(this.getUserNavigationOptionsObservable(courseIds, options));
} }
/** /**
@ -981,7 +987,7 @@ export class CoreCoursesProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getUserCoursesCacheKey(): string { protected getUserCoursesCacheKey(): string {
return ROOT_CACHE_KEY + 'usercourses'; return `${CoreCoursesProvider.ROOT_CACHE_KEY}usercourses`;
} }
/** /**

View File

@ -4,22 +4,22 @@ Feature: It opens external links properly.
Background: Background:
Given the following "users" exist: Given the following "users" exist:
| username | | username |
| student1 | | teacher1 |
And the following "courses" exist: And the following "courses" exist:
| fullname | shortname | | fullname | shortname |
| Course 1 | C1 | | Course 1 | C1 |
And the following "course enrolments" exist: And the following "course enrolments" exist:
| user | course | role | | user | course | role |
| student1 | C1 | student | | teacher1 | C1 | teacher |
And the following "activities" exist: And the following "activities" exist:
| activity | name | intro | course | idnumber | | activity | name | intro | course | idnumber |
| forum | Test forum | Test forum | C1 | forum | | forum | Test forum | Test forum | C1 | forum |
And the following forum discussions exist in course "Course 1": And the following forum discussions exist in course "Course 1":
| forum | user | name | message | | forum | user | name | message |
| Test forum | student1 | Forum topic | See <a href="https://moodle.org/">moodle.org external link</a> | | Test forum | teacher1 | Forum topic | See <a href="https://moodle.org/">moodle.org external link</a> |
Scenario: Click an external link Scenario: Click an external link
Given I entered the forum activity "Test forum" on course "Course 1" as "student1" in the app Given I entered the forum activity "Test forum" on course "Course 1" as "teacher1" in the app
When I press "Forum topic" in the app When I press "Forum topic" in the app
And I press "moodle.org external link" in the app And I press "moodle.org external link" in the app
Then I should find "You are about to leave the app" in the app Then I should find "You are about to leave the app" in the app