Cloud-CV/EvalAI

View on GitHub
frontend_v2/src/app/components/challenge/challengesubmit/challengesubmit.component.ts

Summary

Maintainability
F
4 days
Test Coverage
import { Component, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { NGXLogger } from 'ngx-logger';

// import service
import { AuthService } from '../../../services/auth.service';
import { ApiService } from '../../../services/api.service';
import { GlobalService } from '../../../services/global.service';
import { ChallengeService } from '../../../services/challenge.service';
import { EndpointsService } from '../../../services/endpoints.service';

/**
 * Component Class
 */
@Component({
  selector: 'app-challengesubmit',
  templateUrl: './challengesubmit.component.html',
  styleUrls: ['./challengesubmit.component.scss'],
})
export class ChallengesubmitComponent implements OnInit {
  /**
   * Input error Message
   */
  inputErrorMessage = '';

  /**
   * Is input valid
   */
  validFileUrl = false;

  /**
   * Is file url input
   */
  isSubmissionUsingUrl: any;

  /**
   * Is submission through CLI
   */
  isSubmissionUsingCli: any;

  /**
   * If phase has been selected
   */
  isPhaseSelected = false;

  /**
   * Is user logged in
   */
  isLoggedIn = false;

  /**
   * Is submission submitted
   */
  isSubmitted = false;

  /**
   * Is submission submitted
   */
  isPublicSubmission = true;

  /**
   * Is submission allowed by host
   */
  isLeaderboardPublic = false;

  /**
   * Challenge object
   */
  challenge: any;

  /**
   * Is challenge host
   */
  isChallengeHost: any;

  /**
   * Router public instance
   */
  routerPublic: any;

  /**
   * Is user a participant
   */
  isParticipated: any;

  /**
   * Is challenge currently active
   */
  isActive: any;

  /**
   * Submission error
   */
  submissionError = '';

  /**
   * Guidelines text
   */
  submissionGuidelines = '';

  /**
   * Stores the attributes format and phase ID for all the phases of a challenge.
   */
  submissionMetaAttributes = [];

  /**
   * Stores the attributes while making a submission for a selected phase.
   */
  metaAttributesforCurrentSubmission = null;

  /**
   * Stores the default meta attributes for all the phases of a challenge.
   */
   defaultMetaAttributes = [];

  /**
   * Stores the default meta attributes for a selected phase.
   */
  defaultMetaAttributesforCurrentPhase = null;
  /**
   * Form fields name
   */
  submitForm = 'formsubmit';

  /**
   * Challenge phases list
   */
  phases = [];

  /**
   * Filtered challenge phases
   */
  filteredPhases = [];

  /**
   * Selected phase object
   */
  selectedPhase = null;

  /**
   * Cli version
   */
  cliVersion = '';

  /**
   * Auth token
   */
  authToken = '';

  /**
   * Phase selection type (radio button or select box)
   */
  phaseSelectionType = 'radioButton';

  /**
   * Select box list type
   */
  phaseSelectionListType = 'phase';

  /**
   * Api call inside the modal to edit the submission guidelines
   */
  apiCall: any;

  /**
   * Selected phase submission conditions
   * @param showSubmissionDetails show the selected phase submission details
   * @param showClock when max submissions per day exceeded
   * @param maxExceeded max submissions exceeded
   * @param remainingSubmissions remaining submissions details
   * @param maxExceededMessage message for max submissions exceeded
   * @param clockMessage message for max submissions per day exceeded
   */
  selectedPhaseSubmissions = {
    showSubmissionDetails: false,
    showClock: false,
    maxExceeded: false,
    remainingSubmissions: {},
    maxExceededMessage: '',
    clockMessage: '',
  };

  /**
   * Phase remaining submissions for docker based challenge
   */
  phaseRemainingSubmissions: any = {};

  /**
   * Flog for phase if submissions max exceeded, details, clock
   */
  phaseRemainingSubmissionsFlags = {};

  /**
   * Phase remaining submissions countdown (days, hours, minutes, seconds)
   */
  phaseRemainingSubmissionsCountdown = {};

  /**
   * Clock variables
   * @param days number of days remaining
   * @param hours number of hours remaining
   * @param minutes number of minutes remaining
   * @param seconds number of seconds remaining
   * @param remainingTime remaining time (in seconds) for submission of a challenge phase
   */
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  remainingTime: number;

  /**
   * Is clock initialised
   */
  isClockStarted: boolean;

  /**
   * Set interval timer
   */
  timer: any;

  /**
   * Component Class
   */
  @ViewChildren('formsubmit')
  components: QueryList<ChallengesubmitComponent>;

  /**
   * Constructor.
   * @param authService  AuthService Injection.
   * @param router  Router Injection.
   * @param route  ActivatedRoute Injection.
   * @param challengeService  ChallengeService Injection.
   * @param globalService  GlobalService Injection.
   * @param apiService  Router Injection.
   * @param endpointsService  EndpointsService Injection.
   */
  constructor(
    private authService: AuthService,
    private router: Router,
    private route: ActivatedRoute,
    private challengeService: ChallengeService,
    private globalService: GlobalService,
    private apiService: ApiService,
    private endpointsService: EndpointsService,
    private logger: NGXLogger
  ) {}

  /**
   * Component on intialization.
   */
  ngOnInit() {
    if (this.authService.isLoggedIn()) {
      this.isLoggedIn = true;
    }
    this.routerPublic = this.router;
    this.challengeService.currentChallenge.subscribe((challenge) => {
      this.challenge = challenge;
      this.isActive = this.challenge['is_active'];
      this.submissionGuidelines = this.challenge['submission_guidelines'];
      if (this.challenge.cli_version !== null) {
        this.cliVersion = this.challenge.cli_version;
      }
    });
    this.challengeService.currentParticipationStatus.subscribe((status) => {
      this.isParticipated = status;
      if (!status) {
        this.router.navigate(['../participate'], { relativeTo: this.route });
      }
    });
    this.challengeService.currentPhases.subscribe((phases) => {
      this.phases = phases;
      this.filteredPhases = this.phases.filter((phase) => phase['is_active'] === true);
      for (let j = 0; j < this.phases.length; j++) {
        if (phases[j].is_public === false) {
          this.phases[j].showPrivate = true;
        } else {
          this.phases[j].showPrivate = false;
        }
      }
    });

    this.challengeService.isChallengeHost.subscribe((status) => {
      this.isChallengeHost = status;
    });

    this.challengeService.isChallengeHost.subscribe((status) => {
      this.isChallengeHost = status;
    });

    if (this.challenge.is_docker_based) {
      this.displayDockerSubmissionInstructions(this.challenge.id, this.isParticipated);
    }
    this.authToken = this.globalService.getData('refreshJWT');
  }

  /**
   * @param SELF current context
   * @param eachPhase particular phase of a challenge
   */
  countDownTimer(SELF, eachPhase) {
    if (!SELF.isClockStarted) {
      SELF.remainingTime = parseInt(eachPhase.limits.remaining_time, 10);
    }
    SELF.days = Math.floor(SELF.remainingTime / 24 / 60 / 60);
    const hoursLeft = Math.floor(SELF.remainingTime - SELF.days * 86400);
    SELF.hours = Math.floor(hoursLeft / 3600);
    const minutesLeft = Math.floor(hoursLeft - SELF.hours * 3600);
    SELF.minutes = Math.floor(minutesLeft / 60);
    SELF.seconds = Math.floor(SELF.remainingTime % 60);

    if (SELF.days < 10) {
      SELF.days = '0' + SELF.days;
    }
    if (SELF.hours < 10) {
      SELF.hours = '0' + SELF.hours;
    }
    if (SELF.minutes < 10) {
      SELF.minutes = '0' + SELF.minutes;
    }
    if (SELF.seconds < 10) {
      SELF.seconds = '0' + SELF.seconds;
    }

    // Used when the challenge is docker based
    SELF.phaseRemainingSubmissionsCountdown[eachPhase.id] = {
      days: SELF.days,
      hours: SELF.hours,
      minutes: SELF.minutes,
      seconds: SELF.seconds,
    };
    if (SELF.remainingTime === 0) {
      SELF.selectedPhaseSubmissions.showSubmissionDetails = true;
      SELF.phaseRemainingSubmissionsFlags[eachPhase.id] = 'showSubmissionDetails';
    } else {
      SELF.remainingTime--;
    }
    SELF.isClockStarted = true;
  }

  /**
   * @param challenge challenge id
   * @param isParticipated Is user a participant
   */
  displayDockerSubmissionInstructions(challenge, isParticipated) {
    if (isParticipated) {
      const API_PATH = this.endpointsService.challengeSubmissionsRemainingURL(challenge);
      const SELF = this;
      this.apiService.getUrl(API_PATH).subscribe(
        (data) => {
          SELF.phaseRemainingSubmissions = data;
          const details = SELF.phaseRemainingSubmissions.phases;
          for (let i = 0; i < details.length; i++) {
            if (details[i].limits.submission_limit_exceeded === true) {
              SELF.phaseRemainingSubmissionsFlags[details[i].id] = 'maxExceeded';
            } else if (details[i].limits.remaining_submissions_today_count > 0) {
              SELF.phaseRemainingSubmissionsFlags[details[i].id] = 'showSubmissionDetails';
            } else {
              const eachPhase = details[i];
              SELF.phaseRemainingSubmissionsFlags[details[i].id] = 'showClock';
              setInterval(function () {
                SELF.countDownTimer(SELF, eachPhase);
              }, 1000);
              SELF.countDownTimer(SELF, eachPhase);
            }
          }
        },
        (err) => {
          SELF.globalService.handleApiError(err);
        },
        () => this.logger.info('Remaining submissions fetched for docker based challenge')
      );
    }
  }

  /**
   * Fetch remaining submissions for a challenge phase.
   * @param challenge  challenge id
   * @param phase  phase id
   */
  fetchRemainingSubmissions(challenge, phase) {
    const API_PATH = this.endpointsService.challengeSubmissionsRemainingURL(challenge);
    const SELF = this;
    clearInterval(SELF.timer);
    SELF.isClockStarted = false;
    SELF.selectedPhaseSubmissions.showClock = false;
    SELF.selectedPhaseSubmissions.showSubmissionDetails = false;
    SELF.selectedPhaseSubmissions.maxExceeded = false;
    this.apiService.getUrl(API_PATH).subscribe(
      (data) => {
        let phaseDetails, eachPhase;
        for (let i = 0; i < data.phases.length; i++) {
          if (data.phases[i].id === phase) {
            eachPhase = data.phases[i];
            phaseDetails = data.phases[i].limits;
            break;
          }
        }
        if (phaseDetails == undefined) {
          return;
        }
        if (phaseDetails.submission_limit_exceeded) {
          this.selectedPhaseSubmissions.maxExceeded = true;
          this.selectedPhaseSubmissions.maxExceededMessage = phaseDetails.message;
        } else if (phaseDetails.remaining_submissions_today_count > 0) {
          this.selectedPhaseSubmissions.remainingSubmissions = phaseDetails;
          this.selectedPhaseSubmissions.showSubmissionDetails = true;
        } else {
          this.selectedPhaseSubmissions.showClock = true;
          this.selectedPhaseSubmissions.clockMessage = phaseDetails;
          SELF.timer = setInterval(function () {
            SELF.countDownTimer(SELF, eachPhase);
          }, 1000);
          SELF.countDownTimer(SELF, eachPhase);
        }
      },
      (err) => {
        SELF.globalService.handleApiError(err);
      },
      () => {
        this.logger.info('Remaining submissions fetched for challenge-phase', challenge, phase);
      }
    );
  }

  /**
   * Store Meta Attributes for a particular challenge phase.
   */
  storeMetadata(data) {
    for (let i = 0; i < data.count; i++) {
      if (data.results[i].submission_meta_attributes) {
        const attributes = data.results[i].submission_meta_attributes;
        attributes.forEach(function (attribute) {
          if (attribute['type'] === 'checkbox') {
            attribute['values'] = [];
          } else {
            attribute['value'] = null;
          }
        });
        const detail = { phaseId: data.results[i].id, attributes: attributes };
        this.submissionMetaAttributes.push(detail);
      } else {
        const detail = { phaseId: data.results[i].id, attributes: null };
        this.submissionMetaAttributes.push(detail);
      }
      if (data.results[i].default_submission_meta_attributes) {
        const attributes = data.results[i].default_submission_meta_attributes;
        var attributeDict = {};
        attributes.forEach(function (attribute) {
          attributeDict[attribute["name"]] = attribute;
        });
        const detail = { phaseId: data.results[i].id, attributes: attributeDict };
        this.defaultMetaAttributes.push(detail);
      } else {
        const detail = { phaseId: data.results[i].id, attributes: {} };
        this.defaultMetaAttributes.push(detail);
      }
    }
  }

  /**
   * Fetch Meta Attributes for a particular challenge phase.
   * @param challenge  challenge id
   * @param phase  phase id
   */
  getMetaDataDetails(challenge, phaseId) {
    const API_PATH = this.endpointsService.challengePhaseURL(challenge);
    const SELF = this;
    this.apiService.getUrl(API_PATH).subscribe(
      (data) => {
        SELF.storeMetadata(data);
        // Loads attributes of a phase into this.submissionMetaAttributes
        this.metaAttributesforCurrentSubmission = this.submissionMetaAttributes.find(function (element) {
          return element['phaseId'] === phaseId;
        }).attributes;
        this.defaultMetaAttributesforCurrentPhase = this.defaultMetaAttributes.find(function (element) {
          return element['phaseId'] === phaseId;
        }).attributes;
      },
      (err) => {
        SELF.globalService.handleApiError(err);
      },
      () => {}
    );
  }

  /**
   * Get current phase default meta attributes dict
   */
  isAttributeVisible(attributeName) {
    if (this.defaultMetaAttributesforCurrentPhase != null && this.defaultMetaAttributes != undefined) {
      if (this.defaultMetaAttributesforCurrentPhase[attributeName] != null && this.defaultMetaAttributesforCurrentPhase[attributeName] != undefined) {
        return this.defaultMetaAttributesforCurrentPhase[attributeName]['is_visible'];
      }
    }
    // All attributes are visible by default
    return true;
  }


  /**
   * Clear the data of metaAttributesforCurrentSubmission
   */
  clearMetaAttributeValues() {
    if (this.metaAttributesforCurrentSubmission != null) {
      this.metaAttributesforCurrentSubmission.forEach(function (attribute) {
        if (attribute.type === 'checkbox') {
          attribute.values = [];
        } else {
          attribute.value = null;
        }
      });
    }
  }

  /**
   * Called when a phase is selected (from child components)
   */
  phaseSelected() {
    const SELF = this;
    return (phase) => {
      SELF.selectedPhase = phase;
      SELF.isPhaseSelected = true;
      SELF.isLeaderboardPublic = phase['leaderboard_public'];
      if (SELF.challenge['id'] && phase['id']) {
        SELF.getMetaDataDetails(SELF.challenge['id'], phase['id']);
        SELF.fetchRemainingSubmissions(SELF.challenge['id'], phase['id']);
        SELF.clearMetaAttributeValues();
        SELF.submissionError = '';
        if (SELF.components) {
          SELF.components['_results'].forEach((element) => {
            element.value = '';
            element.message = '';
          });
        }
      }
    };
  }

  /**
   * Form validate function
   */
  formValidate() {
    if (this.selectedPhaseSubmissions.remainingSubmissions['remaining_submissions_today_count']) {
      this.globalService.formValidate(this.components, this.formSubmit, this);
      if (this.isSubmitted) {
        this.router.navigate(['../my-submissions'], { relativeTo: this.route });
      }
    } else {
      this.globalService.showToast('info', "You have exhausted today's submission limit");
    }
  }

  /**
   * Form submit function
   * @param self  context value of this
   */
  formSubmit(self) {
    self.submissionError = '';
    let metaValue = true;
    const submissionFile = self.globalService.formItemForLabel(self.components, 'input_file').fileValue;
    const submissionProjectUrl = self.globalService.formValueForLabel(self.components, 'project_url');
    const submissionPublicationUrl = self.globalService.formValueForLabel(self.components, 'publication_url');
    const submissionFileUrl = self.globalService.formItemForLabel(self.components, 'file_url');
    const regex = new RegExp(/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/);
    if (!self.isSubmissionUsingUrl && (submissionFile === null || submissionFile === '')) {
      self.submissionError = 'Please upload file!';
      return;
    } else if (self.isSubmissionUsingUrl && submissionFileUrl !== '' && !self.validFileUrl) {
      self.submissionError = 'Please enter a valid Submission URL!';
      return;
    } else if (self.selectedPhase['id'] === undefined) {
      self.submissionError = 'Please select phase!';
      return;
    } else if (submissionProjectUrl !== '' && !regex.test(submissionProjectUrl)) {
      self.submissionError = 'Please provide a valid project URL!';
      return;
    } else if (submissionPublicationUrl !== '' && !regex.test(submissionPublicationUrl)) {
      self.submissionError = 'Please provide a valid publication URL!';
      return;
    }
    if (self.metaAttributesforCurrentSubmission != null) {
      self.metaAttributesforCurrentSubmission.forEach((attribute) => {
        if (attribute.required === true) {
          if (attribute.type === 'checkbox') {
            if (attribute.values.length === 0) {
              metaValue = false;
            }
          } else if (attribute.type == 'text') {
            // Fetch value of text attributes manually as we are using modular components
            let value = self.globalService.formValueForLabel(self.components, attribute.name);
            if (value === null || value === undefined || value.length === 0) {
              metaValue = false;
            }
            attribute.value = value;
          } else {
            if (attribute.value === null || attribute.value === undefined) {
              metaValue = false;
            }
          }
        }
      });
    }
    if (metaValue !== true) {
      self.submissionError = 'Please provide input for meta attributes!';
      return;
    }

    const FORM_DATA: FormData = new FormData();
    FORM_DATA.append('status', 'submitting');
    if (!self.isSubmissionUsingUrl) {
      FORM_DATA.append('input_file', self.globalService.formItemForLabel(self.components, 'input_file').fileSelected);
    } else if (self.validFileUrl && self.isSubmissionUsingUrl) {
      FORM_DATA.append('file_url', self.globalService.formValueForLabel(self.components, 'file_url'));
    }
    FORM_DATA.append('is_public', self.isPublicSubmission);
    FORM_DATA.append('method_name', self.globalService.formValueForLabel(self.components, 'method_name'));
    FORM_DATA.append('method_description', self.globalService.formValueForLabel(self.components, 'method_description'));
    FORM_DATA.append('project_url', self.globalService.formValueForLabel(self.components, 'project_url'));
    FORM_DATA.append('publication_url', self.globalService.formValueForLabel(self.components, 'publication_url'));
    FORM_DATA.append('submission_metadata', JSON.stringify(self.metaAttributesforCurrentSubmission));
    self.challengeService.challengeSubmission(self.challenge['id'], self.selectedPhase['id'], FORM_DATA, () => {
      if (!self.isSubmissionUsingUrl) {
        self.globalService.setFormValueForLabel(self.components, 'input_file', null);
      } else if (self.validFileUrl && self.isSubmissionUsingUrl) {
        self.globalService.setFormValueForLabel(self.components, 'file_url', '');
      }
      self.globalService.setFormValueForLabel(self.components, 'method_name', '');
      self.globalService.setFormValueForLabel(self.components, 'method_description', '');
      self.globalService.setFormValueForLabel(self.components, 'project_url', '');
      self.globalService.setFormValueForLabel(self.components, 'publication_url', '');
    });
    self.isSubmitted = true;
  }

  copyTextToClipboard(ref: HTMLElement) {
    const textBox = document.createElement('textarea');
    textBox.style.position = 'fixed';
    textBox.style.left = '0';
    textBox.style.top = '0';
    textBox.style.opacity = '0';
    textBox.value = ref.innerText.split('$ ')[1];
    document.body.appendChild(textBox);
    textBox.focus();
    textBox.select();
    document.execCommand('copy');
    document.body.removeChild(textBox);

    this.globalService.showToast('success', 'Command copied to clipboard');
  }

  /**
   * Edit submission guidelines
   */
  editSubmissionGuideline() {
    const SELF = this;
    SELF.apiCall = (params) => {
      const BODY = JSON.stringify(params);
      SELF.apiService
        .patchUrl(SELF.endpointsService.editChallengeDetailsURL(SELF.challenge.creator.id, SELF.challenge.id), BODY)
        .subscribe(
          (data) => {
            SELF.submissionGuidelines = data.submission_guidelines;
            SELF.globalService.showToast('success', 'The submission guidelines is successfully updated!', 5);
          },
          (err) => {
            SELF.globalService.handleApiError(err, true);
            SELF.globalService.showToast('error', err);
          },
          () => {}
        );
    };

    const PARAMS = {
      title: 'Edit Submission Guidelines',
      label: 'submission_guidelines',
      isEditorRequired: true,
      editorContent: this.challenge.submission_guidelines,
      confirm: 'Submit',
      deny: 'Cancel',
      confirmCallback: SELF.apiCall,
    };
    SELF.globalService.showModal(PARAMS);
  }

  /**
   * Edit challenge overview with file function
   */
   editSubmissionGuidelineUpload() {
    const SELF = this;
    SELF.apiCall = (params) => {
      const FORM_DATA: FormData = new FormData();
      FORM_DATA.append('submission_guidelines_file', params['submission_guidelines_file']);
      SELF.apiService
        .patchFileUrl(
          SELF.endpointsService.editChallengeDetailsURL(SELF.challenge.creator.id, SELF.challenge.id),
          FORM_DATA
        )
        .subscribe(
          (data) => {
            SELF.challenge.submission_guidelines = data.submission_guidelines;   
            SELF.globalService.showToast('success', 'The submission guidelines are successfully updated!', 5);
          },
          (err) => {
            SELF.globalService.handleApiError(err, true);
            SELF.globalService.showToast('error', err);
          },
          () => this.logger.info('EDIT-CHALLENGE-DESCRIPTION-FINISHED')
        );
    };

    /**
     * Parameters of the modal
     */
    const PARAMS = {
      title: 'Edit Submission Guidelines',
      content: '',
      confirm: 'Submit',
      deny: 'Cancel',
      form: [
        {
          name: 'Submission Guidelines',
          isRequired: true,
          label: 'submission_guidelines_file',
          placeholder: '',
          type: 'file',
          value: '',
        },
      ],
      confirmCallback: SELF.apiCall,
    };
    SELF.globalService.showModal(PARAMS);
  }

  validateInput(inputValue) {
    const regex = new RegExp(/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/);
    const validExtensions = ['json', 'zip', 'txt', 'tsv', 'gz', 'csv', 'h5', 'npy'];
    if (this.isSubmissionUsingUrl) {
      if (regex.test(inputValue)) {
        this.inputErrorMessage = '';
        this.validFileUrl = true;
      } else {
        this.inputErrorMessage = 'Please enter a valid URL!';
        this.validFileUrl = false;
      }
    } else {
      const extension = inputValue.split('.').pop();
      if (!validExtensions.includes(extension)) {
        this.inputErrorMessage = 'Please enter a valid File!';
        this.validFileUrl = false;
      } else if (validExtensions.includes(extension)) {
        this.inputErrorMessage = '';
        this.validFileUrl = true;
      }
    }
  }

  // unchecking checked options
  toggleSelection(attribute, value) {
    const idx = attribute.values.indexOf(value);
    if (idx > -1) {
      attribute.values.splice(idx, 1);
    } else {
      attribute.values.push(value);
    }
  }
}