MOBILE-3657 workshop: Index page

main
Pau Ferrer Ocaña 2021-04-23 13:24:48 +02:00
parent 8e7b148205
commit 8db22cc54a
16 changed files with 1444 additions and 26 deletions

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { AddonModWorkshopIndexComponent } from './index/index';
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModWorkshopPhaseInfoComponent } from './phase/phase';
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
@NgModule({
declarations: [
AddonModWorkshopIndexComponent,
AddonModWorkshopSubmissionComponent,
AddonModWorkshopPhaseInfoComponent,
AddonModWorkshopAssessmentComponent,
AddonModWorkshopAssessmentStrategyComponent,
],
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
CoreEditorComponentsModule,
],
exports: [
AddonModWorkshopIndexComponent,
AddonModWorkshopSubmissionComponent,
AddonModWorkshopPhaseInfoComponent,
AddonModWorkshopAssessmentComponent,
AddonModWorkshopAssessmentStrategyComponent,
],
})
export class AddonModWorkshopComponentsModule {}

View File

@ -0,0 +1,245 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600"
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item> </core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-card class="with-borders" *ngIf="phases">
<ion-item (click)="viewPhaseInfo()" detail="true">
<ion-label>
<h2 class="ion-text-wrap">{{ phases[workshop!.phase].title }}</h2>
</ion-label>
</ion-item>
<ng-container *ngIf="phases && phases[workshop!.phase] && phases[workshop!.phase].tasks &&
phases[workshop!.phase].tasks.length">
<ion-item class="ion-text-wrap" *ngFor="let task of phases[workshop!.phase].tasks"
[class.item-dimmed]="task.code == 'submit' && !showSubmit" (click)="runTask(task)" detail="false">
<ion-icon slot="start" name="far-circle" *ngIf="task.completed == null"></ion-icon>
<ion-icon slot="start" name="fas-times-circle" color="danger" *ngIf="task.completed == ''"></ion-icon>
<ion-icon slot="start" name="fas-info-circle" color="info" *ngIf="task.completed == 'info'"></ion-icon>
<ion-icon slot="start" name="fas-check-circle" color="success" *ngIf="task.completed == '1'"></ion-icon>
<ion-label>
<h2>{{task.title}}</h2>
<p *ngIf="task.details" [innerHTML]="task.details"></p>
</ion-label>
<ion-icon slot="end" *ngIf="task.link && task.code != 'submit'" name="fas-external-link-alt"></ion-icon>
</ion-item>
</ng-container>
</ion-card>
<!-- Has something offline. -->
<ion-card class="core-warning-card" *ngIf="hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<!-- Description (setup phase only) -->
<ion-card *ngIf="description && workshop && workshop!.phase == PHASE_SETUP">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.description' | translate }}</h2>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<div *ngIf="access && workshop && workshop!.phase >= PHASE_SUBMISSION">
<!-- CLOSED PHASE -->
<ng-container *ngIf="workshop!.phase >= PHASE_CLOSED">
<ion-card *ngIf="workshop!.conclusion">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.conclusion' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
[text]="workshop!.conclusion" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<ion-card class="with-borders" *ngIf="userGrades">
<ion-item-divider class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.yourgrades' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="userGrades.submissionlongstrgrade">
<ion-label>
<h2>{{ 'addon.mod_workshop.submissiongrade' | translate }}</h2>
<p>{{ userGrades.submissionlongstrgrade }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="userGrades.assessmentlongstrgrade">
<ion-label>
<h2>{{ 'addon.mod_workshop.gradinggrade' | translate }}</h2>
<p>{{ userGrades.assessmentlongstrgrade }}</p>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<!-- SUBMISSION PHASE -->
<ion-card *ngIf="workshop!.phase == PHASE_SUBMISSION && workshop!.instructauthors">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
[text]="workshop!.instructauthors" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="canSubmit">
<ion-item class="ion-text-wrap" *ngIf="!submission">
<ion-label>
<h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
<p>{{ 'addon.mod_workshop.noyoursubmission' | translate }}</p>
</ion-label>
</ion-item>
<ng-container *ngIf="submission">
<ion-item-divider class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2></ion-label>
</ion-item-divider>
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
[workshop]="workshop" [access]="access">
</addon-mod-workshop-submission>
</ng-container>
</ion-card>
<!-- Show only on current phase -->
<ng-container *ngIf="workshop!.phase == PHASE_SUBMISSION">
<ion-item class="ion-text-wrap" *ngIf="showSubmit">
<ion-label>
<ion-button expand="block" *ngIf="access.creatingsubmissionallowed && !submission" (click)="gotoSubmit()">
<ion-icon slot="start" name="fas-plus"></ion-icon>
{{ 'addon.mod_workshop.createsubmission' | translate }}
</ion-button>
<ion-button expand="block" *ngIf="access.modifyingsubmissionallowed && submission" (click)="gotoSubmit()">
<ion-icon slot="start" name="fas-edit"></ion-icon>
{{ 'addon.mod_workshop.editsubmission' | translate }}
</ion-button>
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="workshop!.phase >= PHASE_CLOSED">
<ion-card class="with-borders" *ngIf="publishedSubmissions && publishedSubmissions.length">
<ion-item-divider class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.publishedsubmissions' | translate }}</h2></ion-label>
</ion-item-divider>
<ng-container *ngFor="let submission of publishedSubmissions">
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
[workshop]="workshop" [access]="access" summary="true" class="core-as-item">
</addon-mod-workshop-submission>
</ng-container>
</ion-card>
</ng-container>
<!-- ASSESSMENT PHASE -->
<ng-container *ngIf="workshop!.phase >= PHASE_ASSESSMENT">
<ion-card *ngIf="workshop!.phase == PHASE_ASSESSMENT && workshop!.instructreviewers">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
[text]="workshop!.instructreviewers" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<ion-card class="with-borders" *ngIf="canAssess">
<ion-item-divider class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.assignedassessments' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="!assessments || !assessments.length">
<ion-label><p>{{ 'addon.mod_workshop.assignedassessmentsnone' | translate }}</p></ion-label>
</ion-item>
<ng-container *ngFor="let assessment of (assessments || [])">
<addon-mod-workshop-submission [submission]="assessment.submission" [assessment]="assessment"
[courseId]="workshop!.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"
class="core-as-item">
</addon-mod-workshop-submission>
</ng-container>
</ion-card >
</ng-container>
<!-- MULTIPLE PHASES SUBMISSION OR GREATER only teachers -->
<ion-card class="with-borders" *ngIf="access.canviewallsubmissions && workshop!.phase >= PHASE_SUBMISSION &&
((grades && grades.length) || (groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)))">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h2 *ngIf="workshop!.phase == PHASE_SUBMISSION">{{ 'addon.mod_workshop.submissionsreport' | translate }}</h2>
<h2 *ngIf="workshop!.phase > PHASE_SUBMISSION">{{ 'addon.mod_workshop.gradesreport' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-workshop-groupslabel" *ngIf="groupInfo.separateGroups">
{{ 'core.groupsseparate' | translate }}
</ion-label>
<ion-label id="addon-workshop-groupslabel" *ngIf="groupInfo.visibleGroups">
{{ 'core.groupsvisible' | translate }}
</ion-label>
<ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-workshop-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>
<ng-container *ngFor="let submission of grades">
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
[workshop]="workshop" [access]="access" summary="true" class="core-as-item">
</addon-mod-workshop-submission>
</ng-container>
<ion-grid *ngIf="page > 0 || hasNextPage">
<ion-row class="ion-align-items-center">
<ion-col *ngIf="page > 0">
<ion-button expand="block" fill="outline" (click)="gotoSubmissionsPage(page! -1)">
<ion-icon name="fas-chevron-left" slot="start"></ion-icon>
{{ 'core.previous' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasNextPage">
<ion-button expand="block" (click)="gotoSubmissionsPage(page! + 1)">
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-card>
</div>
</core-loading>

View File

@ -0,0 +1,555 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Platform } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Subscription } from 'rxjs';
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
import {
AddonModWorkshopProvider,
AddonModWorkshopPhase,
AddonModWorkshop,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
AddonModWorkshopPhaseData,
AddonModWorkshopGetGradesWSResponse,
AddonModWorkshopAssessmentSavedChangedEventData,
AddonModWorkshopSubmissionChangedEventData,
AddonModWorkshopGradesData,
AddonModWorkshopPhaseTaskData,
AddonModWorkshopReviewer,
} from '../../services/workshop';
import {
AddonModWorkshopHelper,
AddonModWorkshopSubmissionAssessmentWithFormData,
AddonModWorkshopSubmissionDataWithOfflineData,
} from '../../services/workshop-helper';
import { AddonModWorkshopOffline, AddonModWorkshopOfflineSubmission } from '../../services/workshop-offline';
import {
AddonModWorkshopSyncProvider,
AddonModWorkshopSync,
AddonModWorkshopAutoSyncData,
AddonModWorkshopSyncResult,
} from '../../services/workshop-sync';
import { AddonModWorkshopPhaseInfoComponent } from '../phase/phase';
/**
* Component that displays a workshop index page.
*/
@Component({
selector: 'addon-mod-workshop-index',
templateUrl: 'addon-mod-workshop-index.html',
})
export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
@Input() group = 0;
component = AddonModWorkshopProvider.COMPONENT;
moduleName = 'workshop';
workshop?: AddonModWorkshopData;
page = 0;
access?: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
phases?: Record<string, AddonModWorkshopPhaseData>;
grades: AddonModWorkshopSubmissionDataWithOfflineData[] = [];
assessments: AddonModWorkshopSubmissionAssessmentWithFormData[] = [];
userGrades?: AddonModWorkshopGetGradesWSResponse;
publishedSubmissions: AddonModWorkshopSubmissionDataWithOfflineData[] = [];
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false,
defaultGroupId: 0,
};
canSubmit = false;
showSubmit = false;
canAssess = false;
hasNextPage = false;
readonly PHASE_SETUP = AddonModWorkshopPhase.PHASE_SETUP;
readonly PHASE_SUBMISSION = AddonModWorkshopPhase.PHASE_SUBMISSION;
readonly PHASE_ASSESSMENT = AddonModWorkshopPhase.PHASE_ASSESSMENT;
readonly PHASE_EVALUATION = AddonModWorkshopPhase.PHASE_EVALUATION;
readonly PHASE_CLOSED = AddonModWorkshopPhase.PHASE_CLOSED;
protected offlineSubmissions: AddonModWorkshopOfflineSubmission[] = [];
protected obsSubmissionChanged: CoreEventObserver;
protected obsAssessmentSaved: CoreEventObserver;
protected appResumeSubscription: Subscription;
protected syncObserver?: CoreEventObserver;
protected syncEventName = AddonModWorkshopSyncProvider.AUTO_SYNCED;
constructor (
@Optional() content: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModWorkshopIndexComponent', content, courseContentsPage);
// Listen to submission and assessment changes.
this.obsSubmissionChanged = CoreEvents.on(AddonModWorkshopProvider.SUBMISSION_CHANGED, (data) => {
this.eventReceived(data);
}, this.siteId);
// Listen to submission and assessment changes.
this.obsAssessmentSaved = CoreEvents.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => {
this.eventReceived(data);
}, this.siteId);
// Since most actions will take the user out of the app, we should refresh the view when the app is resumed.
this.appResumeSubscription = Platform.resume.subscribe(() => {
this.showLoadingAndRefresh(true);
});
// Refresh workshop on sync.
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
// Update just when all database is synced.
this.eventReceived(data);
}, this.siteId);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
await this.loadContent(false, true);
if (!this.workshop) {
return;
}
try {
await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch (error) {
// Ignore errors.
}
}
/**
* Function called when we receive an event of submission changes.
*
* @param data Data received by the event.
*/
protected eventReceived(
data: AddonModWorkshopAutoSyncData |
AddonModWorkshopSubmissionChangedEventData |
AddonModWorkshopAssessmentSavedChangedEventData,
): void {
if (this.workshop?.id === data.workshopId) {
this.showLoadingAndRefresh(true);
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModWorkshop.invalidateWorkshopData(this.courseId));
if (this.workshop) {
promises.push(AddonModWorkshop.invalidateWorkshopAccessInformationData(this.workshop.id));
promises.push(AddonModWorkshop.invalidateUserPlanPhasesData(this.workshop.id));
if (this.canSubmit) {
promises.push(AddonModWorkshop.invalidateSubmissionsData(this.workshop.id));
}
if (this.access?.canviewallsubmissions) {
promises.push(AddonModWorkshop.invalidateGradeReportData(this.workshop.id));
promises.push(CoreGroups.invalidateActivityAllowedGroups(this.workshop.coursemodule));
promises.push(CoreGroups.invalidateActivityGroupMode(this.workshop.coursemodule));
}
if (this.canAssess) {
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshop.id));
}
promises.push(AddonModWorkshop.invalidateGradesData(this.workshop.id));
promises.push(AddonModWorkshop.invalidateWorkshopWSData(this.workshop.id));
}
await Promise.all(promises);
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param syncEventData Data receiven on sync observer.
* @return True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: AddonModWorkshopAutoSyncData): boolean {
if (this.workshop && syncEventData.workshopId == this.workshop.id) {
// Refresh the data.
this.content?.scrollToTop();
return true;
}
return false;
}
/**
* Download feedback contents.
*
* @param refresh If it's refreshing content.
* @param sync If it should try to sync.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
try {
this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id);
this.description = this.workshop.intro;
this.dataRetrieved.emit(this.workshop);
if (sync) {
// Try to synchronize the feedback.
await this.syncActivity(showErrors);
}
// Check if there are answers stored in offline.
this.access = await AddonModWorkshop.getWorkshopAccessInformation(this.workshop.id, { cmId: this.module.id });
if (this.access.canviewallsubmissions) {
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.workshop.coursemodule);
this.group = CoreGroups.validateGroupId(this.group, this.groupInfo);
}
this.phases = await AddonModWorkshop.getUserPlanPhases(this.workshop.id, { cmId: this.module.id });
this.phases[this.workshop.phase].tasks.forEach((task) => {
if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) {
// Add links to manage examples.
task.link = this.externalUrl!;
}
});
// Check if there are info stored in offline.
this.hasOffline = await AddonModWorkshopOffline.hasWorkshopOfflineData(this.workshop.id);
if (this.hasOffline) {
this.offlineSubmissions = await AddonModWorkshopOffline.getSubmissions(this.workshop.id);
} else {
this.offlineSubmissions = [];
}
await this.setPhaseInfo();
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Retrieves and shows submissions grade page.
*
* @param page Page number to be retrieved.
* @return Resolved when done.
*/
async gotoSubmissionsPage(page: number): Promise<void> {
const report = await AddonModWorkshop.getGradesReport(this.workshop!.id, {
groupId: this.group,
page,
cmId: this.module.id,
});
const numEntries = (report && report.grades && report.grades.length) || 0;
this.page = page;
this.hasNextPage = numEntries >= AddonModWorkshopProvider.PER_PAGE && ((this.page + 1) *
AddonModWorkshopProvider.PER_PAGE) < report.totalcount;
const grades: AddonModWorkshopGradesData[] = report.grades || [];
this.grades = [];
await Promise.all(grades.map(async (grade) => {
const submission: AddonModWorkshopSubmissionDataWithOfflineData = {
id: grade.submissionid,
workshopid: this.workshop!.id,
example: false,
authorid: grade.userid,
timecreated: grade.submissionmodified,
timemodified: grade.submissionmodified,
title: grade.submissiontitle,
content: '',
contenttrust: 0,
attachment: 0,
grade: grade.submissiongrade,
gradeover: grade.submissiongradeover,
gradeoverby: grade.submissiongradeoverby,
published: !!grade.submissionpublished,
gradinggrade: grade.gradinggrade,
late: 0,
reviewedby: this.parseReviewer(grade.reviewedby),
reviewerof: this.parseReviewer(grade.reviewerof),
};
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
submission.reviewedbydone = grade.reviewedby?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
submission.reviewerofdone = grade.reviewerof?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
submission.reviewedbycount = grade.reviewedby?.length || 0;
submission.reviewerofcount = grade.reviewerof?.length || 0;
}
const offlineData = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
if (typeof offlineData != 'undefined') {
this.grades!.push(offlineData);
}
}));
}
protected parseReviewer(reviewers: AddonModWorkshopReviewer[] = []): AddonModWorkshopSubmissionAssessmentWithFormData[] {
return reviewers.map((reviewer: AddonModWorkshopReviewer) => {
const parsed: AddonModWorkshopSubmissionAssessmentWithFormData = {
grade: reviewer.grade,
gradinggrade: reviewer.gradinggrade,
gradinggradeover: reviewer.gradinggradeover,
id: reviewer.assessmentid,
reviewerid: reviewer.userid,
submissionid: reviewer.submissionid,
weight: reviewer.weight,
timecreated: 0,
timemodified: 0,
feedbackauthor: '',
gradinggradeoverby: 0,
feedbackattachmentfiles: [],
feedbackcontentfiles: [],
feedbackauthorattachment: 0,
};
return parsed;
});
}
/**
* Open task.
*
* @param task Task to be done.
*/
runTask(task: AddonModWorkshopPhaseTaskData): void {
if (task.code == 'submit') {
this.gotoSubmit();
} else if (task.link) {
CoreUtils.openInBrowser(task.link);
}
}
/**
* Go to submit page.
*/
gotoSubmit(): void {
if (this.canSubmit && ((this.access!.creatingsubmissionallowed && !this.submission) ||
(this.access!.modifyingsubmissionallowed && this.submission))) {
const params: Params = {
module: this.module,
access: this.access,
};
const submissionId = this.submission?.id || 0;
CoreNavigator.navigateToSitePath(
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${submissionId}/edit`,
{ params },
);
}
}
/**
* View Phase info.
*/
async viewPhaseInfo(): Promise<void> {
if (this.phases) {
const modal = await ModalController.create({
component: AddonModWorkshopPhaseInfoComponent,
componentProps: {
phases: CoreUtils.objectToArray(this.phases),
workshopPhase: this.workshop!.phase,
externalUrl: this.externalUrl,
showSubmit: this.showSubmit,
},
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data === true) {
this.gotoSubmit();
}
}
}
/**
* Set group to see the workshop.
*
* @param groupId Group Id.
* @return Promise resolved when done.
*/
async setGroup(groupId: number): Promise<void> {
this.group = groupId;
await this.gotoSubmissionsPage(0);
}
/**
* Convenience function to set current phase information.
*
* @return Promise resolved when done.
*/
protected async setPhaseInfo(): Promise<void> {
this.submission = undefined;
this.canAssess = false;
this.assessments = [];
this.userGrades = undefined;
this.publishedSubmissions = [];
this.canSubmit = AddonModWorkshopHelper.canSubmit(
this.workshop!,
this.access!,
this.phases![AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
);
this.showSubmit = this.workshop!.phase == AddonModWorkshopPhase.PHASE_SUBMISSION && this.canSubmit &&
((this.access!.creatingsubmissionallowed && !this.submission) ||
(this.access!.modifyingsubmissionallowed && !!this.submission));
const promises: Promise<void>[] = [];
if (this.canSubmit) {
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop!.id, { cmId: this.module.id })
.then(async (submission) => {
this.submission = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
return;
}));
}
if (this.access!.canviewallsubmissions && this.workshop!.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
promises.push(this.gotoSubmissionsPage(this.page));
}
let assessPromise = Promise.resolve();
if (this.workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop!, this.access!);
if (this.canAssess) {
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop!.id, {
cmId: this.module.id,
}).then(async (assessments) => {
await Promise.all(assessments.map(async (assessment) => {
assessment.strategy = this.workshop!.strategy;
if (!this.hasOffline) {
return;
}
try {
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop!.id, assessment.id);
assessment.offline = true;
assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
} catch {
// Ignore errors.
}
}));
this.assessments = assessments;
return;
});
}
}
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
promises.push(AddonModWorkshop.getGrades(this.workshop!.id, { cmId: this.module.id }).then((grades) => {
this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : undefined;
return;
}));
if (this.access!.canviewpublishedsubmissions) {
promises.push(assessPromise.then(async () => {
const submissions: AddonModWorkshopSubmissionDataWithOfflineData[] =
await AddonModWorkshop.getSubmissions(this.workshop!.id, { cmId: this.module.id });
this.publishedSubmissions = submissions.filter((submission) => {
if (submission.published) {
submission.reviewedby = [];
this.assessments.forEach((assessment) => {
if (assessment.submissionid == submission.id) {
submission.reviewedby!.push(AddonModWorkshopHelper.realGradeValue(this.workshop!, assessment));
}
});
return true;
}
return false;
});
return;
}));
}
}
await Promise.all(promises);
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected sync(): Promise<AddonModWorkshopSyncResult> {
return AddonModWorkshopSync.syncWorkshop(this.workshop!.id);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @return If suceed or not.
*/
protected hasSyncSucceed(result: AddonModWorkshopSyncResult): boolean {
return result.updated;
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.obsSubmissionChanged?.off();
this.obsAssessmentSaved?.off();
this.appResumeSubscription?.unsubscribe();
}
}

View File

@ -0,0 +1,48 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.mod_workshop.userplan' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ng-container *ngFor="let phase of phases">
<ion-item-divider [class.core-selected-item]="workshopPhase == phase.code">
<ion-label>
<h2>{{ phase.title }}</h2>
<p class="ion-text-wrap" *ngIf="workshopPhase == phase.code">
{{ 'addon.mod_workshop.userplancurrentphase' | translate }}
</p>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="phase.switchUrl" [href]="phase.switchUrl" detail="false">
<ion-icon slot="start" name="fas-exchange-alt"></ion-icon>
<ion-label>
<p>{{ 'addon.mod_workshop.switchphase' + phase.code | translate }}</p>
</ion-label>
<ion-icon slot="end" name="fas-external-link-alt"></ion-icon>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let task of phase.tasks"
[class.item-dimmed]="phase.code != workshopPhase || (task.code == 'submit' && !showSubmit)"
(click)="runTask(task)" detail="false">
<ion-icon slot="start" name="far-circle" *ngIf="task.completed == null"></ion-icon>
<ion-icon slot="start" name="fas-times-circle" color="danger" *ngIf="task.completed == ''"></ion-icon>
<ion-icon slot="start" name="fas-info-circle" color="info" *ngIf="task.completed == 'info'"></ion-icon>
<ion-icon slot="start" name="fas-check-circle" color="success" *ngIf="task.completed == '1'"></ion-icon>
<ion-label>
<h2 class="ion-text-wrap">{{task.title}}</h2>
<p *ngIf="task.details" [innerHTML]="task.details"></p>
</ion-label>
<ion-icon slot="end" *ngIf="task.link && task.code != 'submit'" name="fas-external-link-alt"></ion-icon>
</ion-item>
</ng-container>
</ion-list>
</ion-content>

View File

@ -0,0 +1,73 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { AddonModWorkshopPhaseData, AddonModWorkshopPhase, AddonModWorkshopPhaseTaskData } from '../../services/workshop';
/**
* Page that displays the phase info modal.
*/
@Component({
templateUrl: 'phase.html',
})
export class AddonModWorkshopPhaseInfoComponent implements OnInit {
@Input() phases!: AddonModWorkshopPhaseDataWithSwitch[];
@Input() workshopPhase!: AddonModWorkshopPhase;
@Input() showSubmit = false;
@Input() protected externalUrl!: string;
ngOnInit(): void {
// Treat phases.
for (const x in this.phases) {
this.phases[x].tasks.forEach((task) => {
if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) {
// Add links to manage examples.
task.link = this.externalUrl;
}
});
const action = this.phases[x].actions.find((action) => action.url && action.type == 'switchphase');
this.phases[x].switchUrl = action ? action.url : '';
}
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss();
}
/**
* Open task.
*
* @param task Task to be done.
*/
runTask(task: AddonModWorkshopPhaseTaskData): void {
if (task.code == 'submit') {
// This will close the modal and go to the submit.
ModalController.dismiss(true);
} else if (task.link) {
CoreUtils.openInBrowser(task.link);
}
}
}
type AddonModWorkshopPhaseDataWithSwitch = AddonModWorkshopPhaseData & {
switchUrl?: string;
};

View File

@ -0,0 +1,108 @@
<core-loading [hideUntil]="loaded">
<div *ngIf="!summary">
<ion-item class="ion-text-wrap addon-workshop-submission-title">
<core-user-avatar [user]="profile" [courseId]="courseId" [userId]="profile?.id" slot="start">
</core-user-avatar>
<ion-label>
<h2>
<core-format-text [text]="submission.title" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</h2>
<p *ngIf="profile && profile?.fullname">{{profile.fullname}}</p>
<p *ngIf="showGrade(submission.grade)"
[class.addon-has-overriden-grade]="showGrade(submission.gradeover)">
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.grade}}
</p>
<p *ngIf="showGrade(submission.gradeover)" class="addon-overriden-grade">
{{ 'addon.mod_workshop.gradeover' | translate }}: {{submission.gradeover}}
</p>
<p *ngIf="access.canviewallsubmissions && showGrade(submission.gradinggrade)">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}}
</p>
</ion-label>
<ion-note slot="end" *ngIf="!submission.timemodified">
<ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}
</ion-note>
<ion-note slot="end" *ngIf="submission.timemodified">
{{submission.timemodified | coreDateDayOrTime}}
<ng-container *ngIf="submission.offline">
<ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}
</ng-container>
<ng-container *ngIf="submission.deleted">
<ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}
</ng-container>
</ion-note>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="submission.content">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="submission.content"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<core-files [files]="submission.attachmentfiles" [component]="component" [componentId]="componentId"></core-files>
<ion-item class="ion-text-wrap" *ngIf="viewDetails && submission.feedbackauthor">
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" [courseId]="courseId"
[userId]="evaluateByProfile.id"></core-user-avatar>
<ion-label>
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
</h2>
<core-format-text [text]="submission.feedbackauthor" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="viewDetails">
<ion-label>
<ion-button expand="block" (click)="gotoSubmission()">
{{ 'core.showmore' | translate }}
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
</ion-button>
</ion-label>
</ion-item>
</div>
<ion-item class="ion-text-wrap" *ngIf="summary" [detail]="submission.timemodified" (click)="gotoSubmission()">
<core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id">
</core-user-avatar>
<ion-label>
<h2>
<core-format-text [text]="submission.title" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</h2>
<p *ngIf="profile && profile.fullname">{{profile.fullname}}</p>
<p *ngIf="submission.reviewedbydone">
{{ 'addon.mod_workshop.receivedgrades' | translate }}: {{submission.reviewedbydone}} / {{submission.reviewedbycount}}
</p>
<p *ngIf="submission.reviewerofdone">
{{ 'addon.mod_workshop.givengrades' | translate }}: {{submission.reviewerofdone}} / {{submission.reviewerofcount}}
</p>
<p *ngIf="!showGrade(submission.gradeover) && showGrade(submission.grade)">
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.grade}}
</p>
<p *ngIf="showGrade(submission.gradeover)" class="addon-overriden-grade">
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.gradeover}}
</p>
<p *ngIf="access.canviewallsubmissions && showGrade(submission.gradinggrade)">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}}
</p>
<ion-badge *ngIf="assessment && (showGrade(assessment.grade) || assessment.offline)" color="success">
{{ 'addon.mod_workshop.assessedsubmission' | translate }}
</ion-badge>
<ion-badge *ngIf="assessment && !showGrade(assessment.grade) && !assessment.offline" color="danger">
{{ 'addon.mod_workshop.notassessed' | translate }}
</ion-badge>
</ion-label>
<ion-note slot="end" *ngIf="submission.timemodified">
{{submission.timemodified | coreDateDayOrTime}}
<div *ngIf="offline"><ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}</div>
<div *ngIf="submission.deleted"><ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}</div>
</ion-note>
</ion-item>
</core-loading>

View File

@ -0,0 +1,10 @@
:host {
p.addon-overriden-grade {
color: var(--ion-color-success);
}
p.addon-has-overriden-grade {
color: var(--ion-color-danger);
text-decoration: line-through;
}
}

View File

@ -0,0 +1,138 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { AddonModWorkshopSubmissionPage } from '../../pages/submission/submission';
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
import {
AddonModWorkshopProvider,
AddonModWorkshopPhase,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
} from '../../services/workshop';
import {
AddonModWorkshopHelper,
AddonModWorkshopSubmissionAssessmentWithFormData,
AddonModWorkshopSubmissionDataWithOfflineData,
} from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
/**
* Component that displays workshop submission.
*/
@Component({
selector: 'addon-mod-workshop-submission',
templateUrl: 'addon-mod-workshop-submission.html',
styleUrls: ['submission.scss'],
})
export class AddonModWorkshopSubmissionComponent implements OnInit {
@Input() submission!: AddonModWorkshopSubmissionDataWithOfflineData;
@Input() module!: CoreCourseModule;
@Input() workshop!: AddonModWorkshopData;
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
@Input() courseId!: number;
@Input() assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
@Input() summary = false;
component = AddonModWorkshopProvider.COMPONENT;
componentId?: number;
userId: number;
loaded = false;
offline = false;
viewDetails = false;
profile?: CoreUserProfile;
showGrade: (grade?: number|string) => boolean;
evaluateByProfile?: CoreUserProfile;
constructor() {
this.userId = CoreSites.getCurrentSiteUserId();
this.showGrade = AddonModWorkshopHelper.showGrade;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.componentId = this.module.instance;
this.userId = this.submission.authorid || this.userId;
const promises: Promise<void>[] = [];
this.offline = !!this.submission?.offline || !!this.assessment?.offline;
if (this.submission.id) {
promises.push(AddonModWorkshopOffline.getEvaluateSubmission(this.workshop.id, this.submission.id)
.then((offlineSubmission) => {
this.submission.gradeover = parseInt(offlineSubmission.gradeover, 10);
this.offline = true;
return;
}).catch(() => {
// Ignore errors.
}));
}
if (this.userId) {
promises.push(CoreUser.getProfile(this.userId, this.courseId, true).then((profile) => {
this.profile = profile;
return;
}));
}
this.viewDetails = !this.summary && this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED &&
CoreNavigator.getCurrentRoute().component != AddonModWorkshopSubmissionPage;
if (this.viewDetails && this.submission.gradeoverby) {
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
this.evaluateByProfile = profile;
return;
}));
}
Promise.all(promises).finally(() => {
this.loaded = true;
});
}
/**
* Navigate to the submission.
*/
gotoSubmission(): void {
if (this.submission.timemodified) {
const params: Params = {
module: this.module,
workshop: this.workshop,
access: this.access,
profile: this.profile,
submission: this.submission,
assessment: this.assessment,
};
CoreNavigator.navigateToSitePath(
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${this.submission.id}`,
{ params },
);
}
}
}

View File

@ -0,0 +1,63 @@
{
"alreadygraded": "Already graded",
"areainstructauthors": "Instructions for submission",
"areainstructreviewers": "Instructions for assessment",
"assess": "Assess",
"assessedsubmission": "Assessed submission",
"assessmentform": "Assessment form",
"assessmentsettings": "Assessment settings",
"assessmentstrategynotsupported": "Assessment strategy {{$a}} not supported",
"assessmentweight": "Assessment weight",
"assignedassessments": "Assigned submissions to assess",
"assignedassessmentsnone": "You have no assigned submission to assess",
"conclusion": "Conclusion",
"createsubmission": "Add submission",
"deletesubmission": "Delete submission",
"editsubmission": "Edit submission",
"feedbackauthor": "Feedback for the author",
"feedbackby": "Feedback by {{$a}}",
"feedbackreviewer": "Feedback for the reviewer",
"givengrades": "Grades given",
"gradecalculated": "Calculated grade for submission",
"gradeinfo": "Grade: {{$a.received}} of {{$a.max}}",
"gradeover": "Override grade for submission",
"gradesreport": "Workshop grades report",
"gradinggrade": "Grade for assessment",
"gradinggradecalculated": "Calculated grade for assessment",
"gradinggradeof": "Grade for assessment (of {{$a}})",
"gradinggradeover": "Override grade for assessment",
"modulenameplural": "Workshops",
"nogradeyet": "No grade yet",
"notassessed": "Not assessed yet",
"notoverridden": "Not overridden",
"noyoursubmission": "You have not submitted your work yet",
"overallfeedback": "Overall feedback",
"publishedsubmissions": "Published submissions",
"publishsubmission": "Publish submission",
"publishsubmission_help": "Published submissions are available to the others when the workshop is closed.",
"reassess": "Re-assess",
"receivedgrades": "Grades received",
"submissionattachment": "Attachment",
"submissioncontent": "Submission content",
"submissiondeleteconfirm": "Are you sure you want to delete the following submission?",
"submissiongrade": "Grade for submission",
"submissiongradeof": "Grade for submission (of {{$a}})",
"submissionrequiredcontent": "You need to enter some text or add a file.",
"submissionrequiredtitle": "You need to enter a title.",
"submissionsreport": "Workshop submissions report",
"submissiontitle": "Title",
"switchphase10": "Switch to the setup phase",
"switchphase20": "Switch to the submission phase",
"switchphase30": "Switch to the assessment phase",
"switchphase40": "Switch to the evaluation phase",
"switchphase50": "Close workshop",
"userplan": "Workshop planner",
"userplancurrentphase": "Current phase",
"warningassessmentmodified": "The submission was modified on the site.",
"warningsubmissionmodified": "The assessment was modified on the site.",
"weightinfo": "Weight: {{$a}}",
"yourassessment": "Your assessment",
"yourassessmentfor": "Your assessment for {{$a}}",
"yourgrades": "Your grades",
"yoursubmission": "Your submission"
}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-workshop-index [module]="module" [courseId]="courseId" [group]="selectedGroup" (dataRetrieved)="updateData($event)">
</addon-mod-workshop-index>
</ion-content>

View File

@ -0,0 +1,41 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { CoreNavigator } from '@services/navigator';
import { AddonModWorkshopIndexComponent } from '../../components/index/index';
/**
* Page that displays a workshop.
*/
@Component({
selector: 'page-addon-mod-workshop-index',
templateUrl: 'index.html',
})
export class AddonModWorkshopIndexPage extends CoreCourseModuleMainActivityPage<AddonModWorkshopIndexComponent> implements OnInit {
@ViewChild(AddonModWorkshopIndexComponent) activityComponent?: AddonModWorkshopIndexComponent;
selectedGroup = 0;
/**
* @inheritdoc
*/
ngOnInit(): void {
super.ngOnInit();
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
}
}

View File

@ -0,0 +1,63 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanLeaveGuard } from '@guards/can-leave';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModWorkshopIndexPage } from './pages/index/index';
import { AddonModWorkshopComponentsModule } from './components/components.module';
import { AddonModWorkshopSubmissionPage } from './pages/submission/submission';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { AddonModWorkshopAssessmentPage } from './pages/assessment/assessment';
import { AddonModWorkshopEditSubmissionPage } from './pages/edit-submission/edit-submission';
const routes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModWorkshopIndexPage,
},
{
path: ':courseId/:cmId/:submissionId',
component: AddonModWorkshopSubmissionPage,
canDeactivate: [CanLeaveGuard],
},
{
path: ':courseId/:cmId/:submissionId/edit', // @todo
component: AddonModWorkshopEditSubmissionPage,
canDeactivate: [CanLeaveGuard],
},
{
path: ':courseId/:cmId/:submissionId/:assessmentId',
component: AddonModWorkshopAssessmentPage,
canDeactivate: [CanLeaveGuard],
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModWorkshopComponentsModule,
CoreEditorComponentsModule,
],
declarations: [
AddonModWorkshopIndexPage,
AddonModWorkshopSubmissionPage,
AddonModWorkshopAssessmentPage,
AddonModWorkshopEditSubmissionPage,
],
})
export class AddonModWorkshopLazyModule {}

View File

@ -67,12 +67,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
prefetchStatus?: string; // Used when calling fillContextMenu.
prefetchText?: string; // Used when calling fillContextMenu.
size?: string; // Used when calling fillContextMenu.
isDestroyed?: boolean; // Whether the component is destroyed, used when calling fillContextMenu.
isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
protected isCurrentView?: boolean; // Whether the component is in the current view.
protected isCurrentView = false; // Whether the component is in the current view.
protected siteId?: string; // Current Site ID.
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.

View File

@ -306,20 +306,16 @@ export class CoreGradesHelperProvider {
* @param selectedGrade Selected grade value.
* @return Selected grade label.
*/
getGradeLabelFromValue(grades: CoreGradesMenuItem[], selectedGrade: number): string {
getGradeLabelFromValue(grades: CoreGradesMenuItem[], selectedGrade?: number): string {
selectedGrade = Number(selectedGrade);
if (!grades || !selectedGrade || selectedGrade <= 0) {
return '';
}
for (const x in grades) {
if (grades[x].value == selectedGrade) {
return grades[x].label;
}
}
const grade = grades.find((grade) => grade.value == selectedGrade);
return '';
return grade ? grade.label : '';
}
/**
@ -633,31 +629,35 @@ export class CoreGradesHelperProvider {
* @param scale Scale csv list String. If not provided, it will take it from the module grade info.
* @return Array with objects with value and label to create a propper HTML select.
*/
makeGradesMenu(
gradingType: number,
async makeGradesMenu(
gradingType?: number,
moduleId?: number,
defaultLabel: string = '',
defaultValue: string | number = '',
scale?: string,
): Promise<CoreGradesMenuItem[]> {
if (typeof gradingType == 'undefined') {
return [];
}
if (gradingType < 0) {
if (scale) {
return Promise.resolve(CoreUtils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue));
} else if (moduleId) {
return CoreCourse.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => {
if (gradeInfo && gradeInfo.scale) {
return CoreUtils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue);
}
return [];
});
} else {
return Promise.resolve([]);
return CoreUtils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue);
}
if (moduleId) {
const gradeInfo = await CoreCourse.getModuleBasicGradeInfo(moduleId);
if (gradeInfo && gradeInfo.scale) {
return CoreUtils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue);
}
}
return [];
}
if (gradingType > 0) {
const grades: CoreGradesMenuItem[] = [];
if (defaultLabel) {
// Key as string to avoid resorting of the object.
grades.push({
@ -665,6 +665,7 @@ export class CoreGradesHelperProvider {
value: defaultValue,
});
}
for (let i = gradingType; i >= 0; i--) {
grades.push({
label: i + ' / ' + gradingType,
@ -672,10 +673,10 @@ export class CoreGradesHelperProvider {
});
}
return Promise.resolve(grades);
return grades;
}
return Promise.resolve([]);
return [];
}
/**

View File

@ -63,7 +63,7 @@ export class CoreForms {
* @param form Form element.
* @param siteId The site affected. If not provided, no site affected.
*/
static triggerFormCancelledEvent(formRef: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
if (!formRef) {
return;
}
@ -81,7 +81,7 @@ export class CoreForms {
* @param online Whether the action was done in offline or not.
* @param siteId The site affected. If not provided, no site affected.
*/
static triggerFormSubmittedEvent(formRef: ElementRef | HTMLFormElement | undefined, online?: boolean, siteId?: string): void {
static triggerFormSubmittedEvent(formRef?: ElementRef | HTMLFormElement | undefined, online?: boolean, siteId?: string): void {
if (!formRef) {
return;
}

View File

@ -275,6 +275,11 @@ ion-toolbar {
color: $base;
}
}
ion-icon.ion-color-#{$color-name} {
color: $base;
--ion-color-base: #{$base};
}
}
// Avatar