diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4356a1da4..d8af58683 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -21,6 +21,9 @@ import { ChallengeparticipateComponent } from './components/challenge/challengep import { ChallengeleaderboardComponent } from './components/challenge/challengeleaderboard/challengeleaderboard.component'; import { ChallengesubmitComponent } from './components/challenge/challengesubmit/challengesubmit.component'; import { ChallengesubmissionsComponent } from './components/challenge/challengesubmissions/challengesubmissions.component'; +import { + ChallengeviewallsubmissionsComponent +} from './components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component'; import { ChallengeCreateComponent } from './components/challenge-create/challenge-create.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { ProfileComponent } from './components/profile/profile.component'; @@ -67,6 +70,7 @@ const routes: Routes = [ {path: 'my-submissions', component: ChallengesubmissionsComponent}, {path: 'my-submissions/:phase', component: ChallengesubmissionsComponent}, {path: 'mysubmissions/:phase/:submission', component: ChallengesubmissionsComponent}, + {path: 'view-all-submissions', component: ChallengeviewallsubmissionsComponent}, {path: 'leaderboard', component: ChallengeleaderboardComponent}, {path: 'leaderboard/:split', component: ChallengeleaderboardComponent}, {path: 'leaderboard/:split/:entry', component: ChallengeleaderboardComponent} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 700f2c948..2a3c6942b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -68,6 +68,9 @@ import { RulesComponent } from './components/home/rules/rules.component'; import { TestimonialsComponent } from './components/home/testimonials/testimonials.component'; import { FeaturedChallengesComponent } from './components/home/featured-challenges/featured-challenges.component'; import { EditphasemodalComponent } from './components/challenge/challengephases/editphasemodal/editphasemodal.component'; +import { + ChallengeviewallsubmissionsComponent +} from './components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component'; @NgModule({ declarations: [ @@ -116,7 +119,8 @@ import { EditphasemodalComponent } from './components/challenge/challengephases/ RulesComponent, TestimonialsComponent, FeaturedChallengesComponent, - EditphasemodalComponent + EditphasemodalComponent, + ChallengeviewallsubmissionsComponent ], imports: [ BrowserModule, diff --git a/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.html b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.html new file mode 100644 index 000000000..832d4cf9f --- /dev/null +++ b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.html @@ -0,0 +1,135 @@ +
+
+
+
+
+ + Please select a phase to get list of submissions! + +
+
+
+
+ +
+
+
+
+ + File + + + {{key.name}} + + + +
+
+ Download +
+
+
+ +
+
+
+
No phase selected.
+
No results found.
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TeamCreated ByStatusExecution Time (sec)Submission No.Submitted atSubmitted FileStdout FileStderr FileResult FileMetadata FileShow on Leaderboard
{{key.participant_team}}{{key.created_by}}{{key.status}}{{key.execution_time}}{{key.submission_number}}{{key.submitted_at | date:'medium'}} + Link + + LinkNone + + LinkNone + LinkNone + LinkNone + + + N/A +
+
+
+
+
+ + +
+
diff --git a/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.scss b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.scss new file mode 100644 index 000000000..240bc99f3 --- /dev/null +++ b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.scss @@ -0,0 +1,47 @@ +@import './variables.scss'; +@import './mixins.scss'; + +.ev-md-container { + .phase-title { + margin-bottom: 20px; + } +} + +.show-submission-count { + background: #252833; + text-align: center; + color: white; + padding: 2px 5px 2px 5px; + border-radius: 3px 3px 3px 3px; + font-weight: 300; + transition: all 0.2s ease-in-out; +} + +@include screen-small-medium { + .download-submissions { + margin-left: 20px; + margin-right: 20px; + } +} + +.add-line-height { + line-height: 51px; +} + +.result-wrn { + margin-top: 15px; + margin-bottom: 20px; +} + +table { + width: 160% !important; +} + +.cbx-label { + margin-left: 55px; + margin-right: 55px; +} + +.cbx:checked ~ .cbx-label { + background: #4CAF50; +} diff --git a/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.spec.ts b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.spec.ts new file mode 100644 index 000000000..19363652d --- /dev/null +++ b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChallengeviewallsubmissionsComponent } from './challengeviewallsubmissions.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChallengeService } from '../../../services/challenge.service'; +import { GlobalService } from '../../../services/global.service'; +import { AuthService } from '../../../services/auth.service'; +import { ApiService } from '../../../services/api.service'; +import { EndpointsService } from '../../../services/endpoints.service'; +import { WindowService } from '../../../services/window.service'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('ChallengeviewallsubmissionsComponent', () => { + let component: ChallengeviewallsubmissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChallengeviewallsubmissionsComponent ], + providers: [ ChallengeService, GlobalService, AuthService, ApiService, + EndpointsService, WindowService ], + imports: [ HttpClientModule, RouterTestingModule ], + schemas: [ NO_ERRORS_SCHEMA ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChallengeviewallsubmissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.ts b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.ts new file mode 100644 index 000000000..f99632b57 --- /dev/null +++ b/src/app/components/challenge/challengeviewallsubmissions/challengeviewallsubmissions.component.ts @@ -0,0 +1,382 @@ +import { Component, OnInit, QueryList, ViewChildren, ViewChild, AfterViewInit } from '@angular/core'; +import { AuthService } from '../../../services/auth.service'; +import { ApiService } from '../../../services/api.service'; +import { WindowService } from '../../../services/window.service'; +import { GlobalService } from '../../../services/global.service'; +import { ChallengeService } from '../../../services/challenge.service'; +import { EndpointsService } from '../../../services/endpoints.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { SelectphaseComponent } from '../../utility/selectphase/selectphase.component'; +import { environment } from '../../../../environments/environment.staging'; + +/** + * Component Class + */ +@Component({ + selector: 'app-challengeviewallsubmissions', + templateUrl: './challengeviewallsubmissions.component.html', + styleUrls: ['./challengeviewallsubmissions.component.scss'] +}) +export class ChallengeviewallsubmissionsComponent implements OnInit, AfterViewInit { + + /** + * Phase select card components + */ + @ViewChildren('phaseselect') + components: QueryList; + + /** + * Is user logged in + */ + isLoggedIn = false; + + /** + * Has view been initialized + */ + viewInit = false; + + /** + * Challenge object + */ + challenge: any; + + /** + * Router's public instance + */ + routerPublic: any; + + /** + * User participated + */ + isParticipated: any; + + /** + * Is user a challenge host + */ + isChallengeHost = false; + + /** + * Submissions list + */ + submissions = []; + + /** + * Total submissions + */ + submissionCount = 0; + + /** + * Challenge phase list + */ + phases = []; + + /** + * Challenge phases filtered + */ + filteredPhases = []; + + /** + * Currently selected phase's id + */ + selectedPhaseId: any; + + /** + * Currently selected phase + */ + selectedPhase: any = null; + + /** + * Is phase selected + */ + isPhaseSelected = false; + + /** + * Download file types + */ + fileTypes = [{ 'name': 'csv' }]; + + /** + * Selected file type + */ + fileSelected = ''; + + /** + * Phase selection type (radio button or select box) + */ + phaseSelectionType = 'selectBox'; + + /** + * Fields to be exported + */ + fieldsToGetExport: any = []; + + /** + * @param showPagination Is pagination + * @param paginationMessage Pagination message + * @param isPrev Previous page state + * @param isNext Next page state + * @param currentPage Current Page number + */ + paginationDetails: any = {}; + + /** + * Constructor. + * @param route ActivatedRoute Injection. + * @param router GlobalService Injection. + * @param authService AuthService Injection. + * @param globalService GlobalService Injection. + * @param apiService Router Injection. + * @param endpointsService EndpointsService Injection. + * @param challengeService ChallengeService Injection. + */ + constructor(private authService: AuthService, private router: Router, private route: ActivatedRoute, + private challengeService: ChallengeService, private globalService: GlobalService, + private apiService: ApiService, private windowService: WindowService, private endpointsService: EndpointsService) { } + + /** + * Component after view initialized. + */ + ngAfterViewInit() { + this.viewInit = true; + } + + /** + * Component on initialized. + */ + ngOnInit() { + if (this.authService.isLoggedIn()) { + this.isLoggedIn = true; + } + this.routerPublic = this.router; + this.challengeService.currentChallenge.subscribe(challenge => { + this.challenge = challenge; + }); + this.challengeService.currentParticipationStatus.subscribe(status => { + this.isParticipated = status; + if (!status) { + this.globalService.storeData(this.globalService.redirectStorageKey, {path: this.routerPublic.url}); + let redirectToPath = ''; + if (this.router.url.split('/').length === 4) { + redirectToPath = '../participate'; + } else if (this.router.url.split('/').length === 5) { + redirectToPath = '../../participate'; + } else if (this.router.url.split('/').length === 6) { + redirectToPath = '../../../participate'; + } + this.router.navigate([redirectToPath], {relativeTo: this.route}); + } + }); + this.challengeService.currentPhases.subscribe( + phases => { + this.phases = phases; + for (let i = 0; i < this.phases.length; i++) { + if (this.phases[i].is_public === false) { + this.phases[i].showPrivate = true; + } + } + this.filteredPhases = this.phases; + }); + + this.challengeService.isChallengeHost.subscribe(status => { + this.isChallengeHost = status; + }); + } + + /** + * Called when a phase is selected (from child component) + */ + phaseSelected() { + const SELF = this; + return (phase) => { + SELF.selectedPhase = phase; + SELF.isPhaseSelected = true; + SELF.submissionCount = 0; + if (SELF.challenge['id'] && phase['id']) { + SELF.fetchSubmissions(SELF.challenge['id'], phase['id']); + SELF.fetchSubmissionCounts(this.challenge['id'], phase['id']); + } + }; + } + + /** + * Fetch submissions from API. + * @param challenge challenge id + * @param phase phase id + */ + fetchSubmissions(challenge, phase) { + const API_PATH = this.endpointsService.allChallengeSubmissionURL(challenge, phase); + const SELF = this; + this.apiService.getUrl(API_PATH).subscribe( + data => { + SELF.submissions = data['results']; + SELF.paginationDetails.next = data.next; + SELF.paginationDetails.previous = data.previous; + SELF.paginationDetails.totalPage = Math.ceil(data.count / 100); + + if (data.count === 0) { + SELF.paginationDetails.showPagination = false; + SELF.paginationDetails.paginationMessage = 'No results found'; + } else { + SELF.paginationDetails.showPagination = true; + SELF.paginationDetails.paginationMessage = ''; + } + + // condition for pagination + if (data.next === null) { + SELF.paginationDetails.isNext = 'disabled'; + SELF.paginationDetails.currentPage = 1; + } else { + SELF.paginationDetails.isNext = ''; + SELF.paginationDetails.currentPage = Math.ceil(data.next.split('page=')[1] - 1); + } + if (data.previous === null) { + SELF.paginationDetails.isPrev = 'disabled'; + } else { + SELF.paginationDetails.isPrev = ''; + } + }, + err => { + SELF.globalService.handleApiError(err); + }, + () => { + console.log('Fetched submissions', challenge, phase); + } + ); + } + + /** + * Download Submission csv. + */ + downloadSubmission() { + if (this.challenge['id'] && this.selectedPhase && this.fileSelected) { + const API_PATH = this.endpointsService.challengeSubmissionDownloadURL( + this.challenge['id'], this.selectedPhase['id'], this.fileSelected + ); + const SELF = this; + if (SELF.fieldsToGetExport.length === 0 || SELF.fieldsToGetExport === undefined) { + SELF.apiService.getUrl(API_PATH, false).subscribe( + data => { + SELF.windowService.downloadFile(data, 'all_submissions.csv'); + }, + err => { + SELF.globalService.handleApiError(err); + }, + () => { + console.log('Download complete.', SELF.challenge['id'], SELF.selectedPhase['id']); + } + ); + } + } else { + if (this.selectedPhase === null) { + this.globalService.showToast('error', 'Please select a challenge phase!'); + } else if (this.fileSelected === '') { + this.globalService.showToast('error', 'The file type requested is not valid!'); + } + } + } + + /** + * load data with pagination + */ + loadPaginationData(url) { + if (url !== null) { + const SELF = this; + const API_PATH = url.split(environment.api_endpoint)[1]; + + SELF.apiService.getUrl(API_PATH, true).subscribe( + data => { + SELF.submissions = data['results']; + SELF.paginationDetails.next = data.next; + SELF.paginationDetails.previous = data.previous; + + // condition for pagination + if (data.next === null) { + SELF.paginationDetails.isNext = 'disabled'; + SELF.paginationDetails.currentPage = Math.ceil(data.count / 100); + } else { + SELF.paginationDetails.isNext = ''; + SELF.paginationDetails.currentPage = Math.ceil(data.next.split('page=')[1] - 1); + } + + if (data.previous === null) { + SELF.paginationDetails.isPrev = 'disabled'; + } else { + SELF.paginationDetails.isPrev = ''; + } + }, + err => { + SELF.globalService.handleApiError(err); + }, + () => { + console.log('Fetched pagination submissions'); + } + ); + } + } + + /** + * Update submission's leaderboard visibility. + * @param id Submission id + */ + updateSubmissionVisibility(id) { + for (let i = 0; i < this.submissions.length; i++) { + if (this.submissions[i]['id'] === id) { + this.submissions[i]['is_public'] = !this.submissions[i]['is_public']; + break; + } + } + } + + /** + * Change Submission's leaderboard visibility API. + * @param id Submission id + * @param is_public visibility boolean flag + */ + changeSubmissionVisibility(id, is_public) { + is_public = !is_public; + this.updateSubmissionVisibility(id); + if (this.challenge['id'] && this.selectedPhase && this.selectedPhase['id'] && id) { + const API_PATH = this.endpointsService.challengeSubmissionUpdateURL(this.challenge['id'], this.selectedPhase['id'], id); + const SELF = this; + const BODY = JSON.stringify({is_public: is_public}); + this.apiService.patchUrl(API_PATH, BODY).subscribe( + () => { + if (is_public) { + SELF.globalService.showToast('success', 'The submission is made public'); + } else { + SELF.globalService.showToast('success', 'The submission is made private'); + } + }, + err => { + SELF.globalService.handleApiError(err); + }, + () => { + console.log('Updated submission visibility', id); + } + ); + } + } + + /** + * Fetch number of submissions for a challenge phase. + * @param challenge challenge id + * @param phase phase id + */ + fetchSubmissionCounts(challenge, phase) { + const API_PATH = this.endpointsService.challengeSubmissionCountURL(challenge, phase); + const SELF = this; + this.apiService.getUrl(API_PATH).subscribe( + data => { + if (data['participant_team_submission_count']) { + SELF.submissionCount = data['participant_team_submission_count']; + } + }, + err => { + SELF.globalService.handleApiError(err); + }, + () => { + console.log('Fetched submission counts', challenge, phase); + } + ); + } +} diff --git a/src/app/services/endpoints.service.ts b/src/app/services/endpoints.service.ts index 54b8fc7c1..3dd184ac5 100644 --- a/src/app/services/endpoints.service.ts +++ b/src/app/services/endpoints.service.ts @@ -193,6 +193,15 @@ export class EndpointsService { return `${this.jobs}${this.challenge}${challenge}/challenge_phase/${phase}/submission/`; } + /** + * Get all Challenge Submission + * @param challenge challenge id + * @param phase phase id + */ + allChallengeSubmissionURL(challenge, phase) { + return `${this.challenges}${challenge}/challenge_phase/${phase}/submissions`; + } + /** * Challenge Submission Download * @param challenge challenge id diff --git a/src/styles/base.scss b/src/styles/base.scss index d54bd45ce..c3f4c3cbc 100644 --- a/src/styles/base.scss +++ b/src/styles/base.scss @@ -202,6 +202,13 @@ mat-label { } } +.mat-select-trigger { + line-height: 24px; + letter-spacing: 0.1px; + font-size: 15px; + color: rgba(0, 0, 0, 0.87); +} + @include screen-medium { .phase-select-box { .mat-form-field-infix { @@ -232,6 +239,55 @@ mat-label { } } +.all-submissions-file { + .mat-form-field-infix { + width: 144px !important; + } +} + +.custom-field-export { + .mat-form-field-infix { + width: 311px !important; + } +} + +@include screen-medium { + .download-submissions { + margin-left: 50px; + } + + .custom-field-export { + .mat-form-field-infix { + width: 270px !important; + } + } + + .all-submissions-file { + .mat-form-field-infix { + width: 110px !important; + } + } +} + +@include screen-small-medium { + .download-submissions { + margin-left: 40px; + } + + .custom-field-export { + .mat-form-field-infix { + width: 195px !important; + } + } + + .all-submissions-file { + .mat-form-field-infix { + width: 75px !important; + } + } +} + + /**Rich text editor styles*/ .reusable-editor.fr-box.fr-basic .fr-element { height: 320px;