Commit fc09a656 authored by Konstantin Schulz's avatar Konstantin Schulz
Browse files

added exports (Moodle XML & PDF) for exercises, introduced LocalStorage

parent a9d9c87a
......@@ -17,7 +17,6 @@
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="FadeSplashScreenDuration" value="300" />
<preference name="SplashShowOnlyFirstTime" value="false" />
<preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="3000" />
<platform name="android">
<allow-intent href="market:*" />
......@@ -78,7 +77,6 @@
</platform>
<plugin name="cordova-plugin-whitelist" spec="1.3.3" />
<plugin name="cordova-plugin-device" spec="2.0.2" />
<plugin name="cordova-plugin-splashscreen" spec="5.0.2" />
<plugin name="cordova-plugin-ionic-webview" spec="1.1.19" />
<plugin name="cordova-plugin-ionic-keyboard" spec="2.0.5" />
</widget>
This diff is collapsed.
......@@ -22,12 +22,15 @@
"@angular/platform-browser": "5.2.11",
"@angular/platform-browser-dynamic": "5.2.11",
"@ionic-native/core": "4.7.0",
"@ionic-native/splash-screen": "4.7.0",
"@ionic-native/status-bar": "4.7.0",
"@ionic/storage": "2.1.3",
"@ngx-translate/core": "^9.1.1",
"@ngx-translate/http-loader": "^2.0.1",
"@types/xml2js": "^0.4.3",
"cordova-plugin-device": "^2.0.2",
"cordova-plugin-ionic-keyboard": "^2.0.5",
"cordova-plugin-ionic-webview": "^1.1.19",
"cordova-plugin-whitelist": "^1.3.3",
"ionic": "^4.0.6",
"ionic-angular": "3.9.2",
"ionicons": "3.0.0",
......@@ -42,6 +45,12 @@
},
"description": "An Ionic project",
"cordova": {
"plugins": {}
"plugins": {
"cordova-plugin-whitelist": {},
"cordova-plugin-device": {},
"cordova-plugin-ionic-webview": {},
"cordova-plugin-ionic-keyboard": {}
},
"platforms": []
}
}
import {Component} from '@angular/core';
import {Config, Platform} from 'ionic-angular';
import {StatusBar} from '@ionic-native/status-bar';
import {SplashScreen} from '@ionic-native/splash-screen';
import {HomePage} from '../pages/home/home';
import {TranslateService} from "@ngx-translate/core";
......@@ -12,12 +11,11 @@ import {TranslateService} from "@ngx-translate/core";
export class MCclient {
rootPage: any = HomePage;
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, private translate: TranslateService, private config: Config) {
constructor(platform: Platform, statusBar: StatusBar, private translate: TranslateService, private config: Config) {
platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
statusBar.styleDefault();
splashScreen.hide();
});
this.initTranslate();
}
......
import {BrowserModule} from '@angular/platform-browser';
import {ErrorHandler, NgModule} from '@angular/core';
import {IonicApp, IonicErrorHandler, IonicModule} from 'ionic-angular';
import {SplashScreen} from '@ionic-native/splash-screen';
import {StatusBar} from '@ionic-native/status-bar';
import {MCclient} from './app.component';
......@@ -23,9 +22,11 @@ export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
class CustomErrorHandler implements ErrorHandler {
class CustomErrorHandler extends IonicErrorHandler {
handleError(err: any): void {
// do something with the error
// TODO: send error messages to the backend so we can log them in a file
super.handleError(err);
}
}
......@@ -57,8 +58,7 @@ class CustomErrorHandler implements ErrorHandler {
],
providers: [
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler}, // CustomErrorHandler
{provide: ErrorHandler, useClass: CustomErrorHandler},
CorpusProvider,
ExerciseProvider
]
......
......@@ -5,32 +5,41 @@
"CORPUS_DETAIL_TITLE": "Korpus-Details",
"CORPUS_SELECT": "Korpus auswählen",
"EXERCISE_DOWNLOAD_NEXT_STEPS": "Nächste Schritte:",
"EXERCISE_FEEDBACK": "Feedback",
"EXERCISE_FEEDBACK_CORRECT": "korrekt",
"EXERCISE_FEEDBACK_CORRECT_DEFAULT": "Das ist korrekt. Gut gemacht!",
"EXERCISE_FEEDBACK_GENERAL": "allgemein",
"EXERCISE_FEEDBACK_GENERAL_DEFAULT": "Die Aufgabe ist jetzt abgeschlossen.",
"EXERCISE_FEEDBACK_INCORRECT": "inkorrekt",
"EXERCISE_FEEDBACK_INCORRECT_DEFAULT": "Das ist leider inkorrekt.",
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT": "teilweise korrekt",
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT_DEFAULT": "Das ist teilweise korrekt.",
"EXERCISE_FILL_THE_GAP": "Lückentext",
"EXERCISE_GENERATE": "Aufgabe erstellen",
"EXERCISE_TYPE": "Aufgabentyp",
"EXPORT": "Exportieren",
"INSTRUCTION_CHOOSE_FORMAT_AND_IMPORT": "Wählen Sie als Dateiformat das 'Moodle-XML-Format'. Laden Sie anschließend die soeben erhaltene XML-Datei hoch und klicken Sie auf 'Import'.",
"INSTRUCTION_COGWHEEL_MORE": "Klicken Sie auf das Zahnrad-Symbol oben rechts, dann auf 'Mehr'.",
"INSTRUCTION_GO_TO_QUESTION_BANK": "Klicken Sie auf 'Fragensammlung', dann auf 'Import'.",
"INSTRUCTION_LOGIN_MOODLE": "Loggen Sie sich in Moodle ein und betreten Sie den Kurs, der die Übung enthalten soll.",
"INSTRUCTION_CHOOSE_FORMAT_AND_IMPORT": "Wähle als Dateiformat das 'Moodle-XML-Format'. Lade anschließend die soeben heruntergeladene XML-Datei hoch und klicke auf 'Import'.",
"INSTRUCTION_COGWHEEL_MORE": "Klicke auf das Zahnrad-Symbol oben rechts, dann auf 'Mehr'.",
"INSTRUCTION_GO_TO_QUESTION_BANK": "Klicke auf 'Fragensammlung', dann auf 'Import'.",
"INSTRUCTION_LOGIN_MOODLE": "Lade die Übung mit einem Klick auf 'XML' herunter. Nun logge dich in Moodle ein und betrete den Kurs, der die Übung enthalten soll.",
"INSTRUCTIONS": "Anweisungen",
"INSTRUCTIONS_FILL_THE_GAP": "Ordne die Wörter aus dem Pool den richtigen Lücken zu!",
"LOCAL_STORAGE_KEY_CORPORA": "mc/corpora",
"MC_API_CORPORA_URL": "http://141.20.186.246:5000/mc/api/v1.0/corpora",
"MC_API_EXERCISE_URL": "http://141.20.186.246:5000/mc/api/v1.0/exercise",
"MC_API_FILE_URL": "http://141.20.186.246:5000/mc/api/v1.0/file",
"MC_API_RAW_TEXT_URL": "http://141.20.186.246:5000/mc/api/v1.0/rawtext",
"PART_OF_SPEECH_CONJUNCTION": "Konjunktion",
"PDF": "PDF",
"PERSEIDS_CTS_API_BASE_URL": "https://cts.perseids.org/api/cts?request=",
"PERSEIDS_CTS_API_GET_CAPABILITIES": "GetCapabilities",
"PERSEIDS_CTS_API_GET_TEXT_PASSAGE": "GetPassage",
"PERSEIDS_CTS_API_GET_VALID_REFF": "GetValidReff",
"PERSEIDS_CTS_API_URN_SNIPPET": "&urn=",
"PHENOMENON_DEPENDENCY": "Dependenz",
"PHENOMENON_PART_OF_SPEECH": "Wortart",
"PREVIEW": "Vorschau",
"PRINT": "Drucken",
"QUERY_PHENOMENON": "Phänomen",
"QUERY_VALUE": "Suche",
"TEST": "Test",
"WELCOME": "Willkommen!"
"WELCOME": "Willkommen!",
"XML": "XML"
}
\ No newline at end of file
......@@ -14,3 +14,8 @@ export enum Phenomenon {
export enum PartOfSpeech {
conjunction = <any>"conjunction"
}
export enum FileType {
xml = <any>"xml",
pdf = <any>"pdf"
}
import {ExerciseType} from "./enum";
import {QueryMC} from "./queryMC";
import {Feedback} from "./feedback";
export class Exercise {
public type: ExerciseType;
public query: QueryMC;
public feedback: Feedback;
constructor(init?:Partial<Exercise>) {
Object.assign(this, init);
}
......
export class Feedback {
public correct: string;
public incorrect: string;
public partiallyCorrect: string;
public general: string;
constructor(init?:Partial<Feedback>) {
Object.assign(this, init);
}
}
......@@ -4,7 +4,6 @@ import {CorpusProvider} from "../../providers/corpus/corpus";
import {CorpusMC} from "../../models/corpusMC";
import {TranslateService} from "@ngx-translate/core";
import {HttpClient} from "@angular/common/http";
import {GetValidReff} from "../../models/getValidReff";
import {CorpusDetailPage} from "../corpus-detail/corpus-detail";
/**
......
import {Component} from '@angular/core';
import {IonicPage, NavController, NavParams} from 'ionic-angular';
import {IonicPage, NavController} from 'ionic-angular';
import {HttpClient} from "@angular/common/http";
import {TranslateService} from "@ngx-translate/core";
import {HomePage} from "../home/home";
import {Author} from "../../models/author";
import {CorpusProvider} from "../../providers/corpus/corpus";
import {AuthorDetailPage} from "../author-detail/author-detail";
......@@ -20,21 +19,21 @@ import {AuthorDetailPage} from "../author-detail/author-detail";
templateUrl: 'author.html',
})
export class AuthorPage {
private corpusUrl: string;
// private corpusUrl: string;
public authorsDisplayed: Author[];
constructor(public navCtrl: NavController,
public translate: TranslateService,
public corpusProvider: CorpusProvider,
public http: HttpClient) {
if (!this.corpusProvider.availableCorpora) {
this.corpusProvider.availableCorpora = [];
this.corpusProvider.availableAuthors = [];
this.translate.get("MC_API_CORPORA_URL").subscribe((value) => {
this.corpusUrl = value;
this.corpusProvider.getCorpora(this.corpusUrl);
});
}
// if (!this.corpusProvider.availableCorpora) {
// this.corpusProvider.availableCorpora = [];
// this.corpusProvider.availableAuthors = [];
// this.translate.get("MC_API_CORPORA_URL").subscribe((value) => {
// this.corpusUrl = value;
// this.corpusProvider.getCorpora(this.corpusUrl);
// });
// }
this.authorsDisplayed = this.corpusProvider.availableAuthors;
}
......
......@@ -4,7 +4,7 @@ import {AuthorPage} from "../author/author";
import {HttpClient} from "@angular/common/http";
import {ExerciseProvider} from "../../providers/exercise/exercise";
import {CorpusProvider} from "../../providers/corpus/corpus";
import {AnnisResponse} from "../../models/annisResponse";
import {TranslateService} from "@ngx-translate/core";
@Component({
selector: 'page-home',
......@@ -16,7 +16,8 @@ export class HomePage {
constructor(public navCtrl: NavController, public navParams: NavParams,
public http: HttpClient,
public exerciseProvider: ExerciseProvider,
public corpusProvider: CorpusProvider) {
public corpusProvider: CorpusProvider,
public translate: TranslateService) {
}
goToAuthorPage() {
......@@ -24,26 +25,14 @@ export class HomePage {
}
test() {
let formData = new FormData();
// TODO: change the corpus title to something meaningful
this.corpusProvider.currentText = "Gallia est omnis divisa in partes tres et Galli fortes sunt.";
formData.append("title", "TestTitel");
formData.append("text", this.corpusProvider.currentText);
formData.append("type", "Lückentext");
formData.append("instructions", "Fülle die Lücke!");
this.http.post(this.exerciseProvider.MCapiExerciseUrlString, formData).subscribe((data: any) => {
let ar: AnnisResponse = data as AnnisResponse;
// let annisResponse = new AnnisResponse({
// graph: data,
// solution: {}
// });
let a = 0;
});
// let guid = "/8efea04d-aa11-4f75-b540-338dd0d1e435"; //"/45f1323f-0f82-4655-9236-276d8e76d76b";
// window.open("http://localhost:5000/mc/api/v1.0/file" + guid, "_blank");
// this.http.get("http://localhost:5000/mc/api/v1.0/file" + guid);
// .subscribe((retVal: any) => { // this.exerciseProvider.MCapiFileUrlString
// let a = 0;
// window.localStorage.removeItem("__mc_storage__");
// this.translate.get("MC_API_CORPORA_URL").subscribe((corpusUrl: string) => {
// this.http.get(corpusUrl).subscribe((data: object) => {
// let jsonString: string = JSON.stringify(data["corpora"]);
// window.localStorage.setItem("mc/corpora", jsonString);
// });
// });
let a = JSON.parse(window.localStorage.getItem("mc/corpora"));
let b = 0;
}
}
......@@ -24,8 +24,8 @@
<div *ngFor="let node of exerciseProvider.annisResponse?.graph.nodes">{{node.annis_tok}}&#32;</div>
<br>
<br>
<!--<button ion-button block (click)="printExercise()">{{ 'PRINT' | translate }}</button>-->
<button ion-button block (click)="downloadExercise()">{{ 'EXPORT' | translate }}</button>
<button ion-button block (click)="downloadExercise(FileType[FileType.pdf])">{{ 'PDF' | translate }}</button>
<button ion-button block (click)="downloadExercise(FileType[FileType.xml])">{{ 'XML' | translate }}</button>
<br>
<br>
<h4>{{ 'EXERCISE_DOWNLOAD_NEXT_STEPS' | translate }}</h4>
......
......@@ -2,12 +2,10 @@ import {Component} from '@angular/core';
import {IonicPage, NavController, NavParams} from 'ionic-angular';
import {HttpClient} from "@angular/common/http";
import {AnnisResponse} from "../../models/annisResponse";
import {Graph} from "../../models/graph";
import {LinkMC} from "../../models/linkMC";
import {NodeMC} from "../../models/nodeMC";
import {ExerciseProvider} from "../../providers/exercise/exercise";
import {TranslateService} from "@ngx-translate/core";
import {CorpusProvider} from "../../providers/corpus/corpus";
import {FileType} from "../../models/enum";
/**
* Generated class for the PreviewPage page.
......@@ -24,6 +22,7 @@ import {CorpusProvider} from "../../providers/corpus/corpus";
export class PreviewPage {
private exerciseFillTheGapString: string;
private instructionsFillTheGapString: string;
FileType = FileType;
constructor(public navCtrl: NavController, public navParams: NavParams,
public http: HttpClient,
......@@ -43,11 +42,15 @@ export class PreviewPage {
private getExerciseData() {
let formData = new FormData();
// TODO: change the corpus title to something meaningful
formData.append("title", "placeholder");
// TODO: change the corpus title to something meaningful, e.g. concatenate user ID and wanted exercise title
formData.append("title", this.exerciseProvider.createGuid());
formData.append("text", this.corpusProvider.currentText);
formData.append("type", this.exerciseFillTheGapString);
formData.append("instructions", this.instructionsFillTheGapString);
formData.append("correct_feedback", this.exerciseProvider.exercise.feedback.correct);
formData.append("partially_correct_feedback", this.exerciseProvider.exercise.feedback.partiallyCorrect);
formData.append("incorrect_feedback", this.exerciseProvider.exercise.feedback.incorrect);
formData.append("general_feedback", this.exerciseProvider.exercise.feedback.general);
this.http.post(this.exerciseProvider.MCapiExerciseUrlString, formData).subscribe((ar: AnnisResponse) => {
let maxGapLength = Math.max.apply(Math, Object.keys(ar.solution).map(key => ar.solution[key].length));
ar.graph.nodes.forEach((node) => {
......@@ -59,12 +62,10 @@ export class PreviewPage {
});
}
printExercise() {
}
downloadExercise() {
downloadExercise(type: string) {
let uriParts: string[] = this.exerciseProvider.annisResponse.uri.split("/");
window.open(this.exerciseProvider.MCapiFileUrlString + "/" + uriParts[uriParts.length - 1], "_blank");
let fileId: string = uriParts[uriParts.length - 1];
let fileTypeString: string = "?type=" + type;
window.open(this.exerciseProvider.MCapiFileUrlString + "/" + fileId + fileTypeString, "_blank");
}
}
......@@ -46,6 +46,30 @@
</ion-option>
</ion-select>
</ion-item>
<br>
<ion-list>
<ion-title>{{ 'EXERCISE_FEEDBACK' | translate }}</ion-title>
<ion-item>
<ion-label>{{ 'EXERCISE_FEEDBACK_GENERAL' | translate }}</ion-label>
<ion-textarea [(ngModel)]="exerciseProvider.exercise.feedback.general" name="feedbackGeneral"></ion-textarea>
<!--placeholder="{{ 'EXERCISE_FEEDBACK_GENERAL_DEFAULT' | translate }}"-->
</ion-item>
<ion-item>
<ion-label>{{ 'EXERCISE_FEEDBACK_CORRECT' | translate }}</ion-label>
<ion-textarea [(ngModel)]="exerciseProvider.exercise.feedback.correct" name="feedbackCorrect"
placeholder="{{ 'EXERCISE_FEEDBACK_CORRECT_DEFAULT' | translate }}"></ion-textarea>
</ion-item>
<ion-item>
<ion-label>{{ 'EXERCISE_FEEDBACK_PARTIALLY_CORRECT' | translate }}</ion-label>
<ion-textarea [(ngModel)]="exerciseProvider.exercise.feedback.partiallyCorrect" name="feedbackPartiallyCorrect"
placeholder="{{ 'EXERCISE_FEEDBACK_PARTIALLY_CORRECT_DEFAULT' | translate }}"></ion-textarea>
</ion-item>
<ion-item>
<ion-label>{{ 'EXERCISE_FEEDBACK_INCORRECT' | translate }}</ion-label>
<ion-textarea [(ngModel)]="exerciseProvider.exercise.feedback.incorrect" name="feedbackIncorrect"
placeholder="{{ 'EXERCISE_FEEDBACK_INCORRECT_DEFAULT' | translate }}"></ion-textarea>
</ion-item>
</ion-list>
</ion-list>
<button ion-button (click)="generateExercise()">{{ 'PREVIEW' | translate }}</button>
</div>
......
......@@ -21,7 +21,6 @@ export class CorpusProvider {
public CTSurnString: string;
public CTSbaseURL: string;
public CTSgetCapString: string;
private CTSgetTextPassageString: string;
public availableCorpora: CorpusMC[];
public availableAuthors: Author[];
public currentAuthor: Author;
......@@ -45,13 +44,13 @@ export class CorpusProvider {
this.translate.get("PERSEIDS_CTS_API_GET_VALID_REFF").subscribe((value: string) => {
this.CTSgetValReffString = value;
});
this.translate.get("PERSEIDS_CTS_API_GET_TEXT_PASSAGE").subscribe((value: string) => {
this.CTSgetTextPassageString = value;
});
this.translate.get("MC_API_RAW_TEXT_URL").subscribe((value: string) => {
this.MCapiRawTextUrlString = value;
});
this.xml2jsParser = new Parser();
if (!this.availableCorpora) {
this.getCorpora();
}
}
ctsJson2mc(json: object) {
......@@ -87,7 +86,7 @@ export class CorpusProvider {
}
}
getCTStextPassage(urn: string){
getCTStextPassage(urn: string) {
return this.http.get(this.MCapiRawTextUrlString, {params: {"urn": urn}});
}
......@@ -96,44 +95,63 @@ export class CorpusProvider {
return this.http.get(fullUrl, {responseType: 'text'});
}
getCorpora(corpusUrl: string) {
this.http.get(corpusUrl).subscribe((data: object) => {
let corpusList: CorpusMC[] = data["corpora"] as CorpusMC[];
corpusList.forEach((corpus: CorpusMC) => {
corpus.subCorpora = {};
corpus.validReff = null;
this.availableCorpora.push(corpus);
let authorIndex = this.availableAuthors.map(author => author.name).indexOf(corpus.author);
if (authorIndex > -1) {
this.availableAuthors[authorIndex].corpora.push(corpus);
}
else {
this.availableAuthors.push(new Author({
name: corpus.author,
corpora: [corpus]
}));
}
});
this.availableAuthors.sort((author1, author2) => {
if (author1.name < author2.name) {
getCorpora() {
this.availableCorpora = [];
this.availableAuthors = [];
// check local storage for corpora
this.translate.get("LOCAL_STORAGE_KEY_CORPORA").subscribe((key: string) => {
let storedCorporaJSONstring: string = window.localStorage.getItem(key);
if (!storedCorporaJSONstring) {
// get corpora from REST API
this.translate.get("MC_API_CORPORA_URL").subscribe((url: string) => {
this.http.get(url).subscribe((data: object) => {
let corpusList: CorpusMC[] = data["corpora"] as CorpusMC[];
this.processCorpora(corpusList);
});
});
}
else {
let corpusList: CorpusMC[] = JSON.parse(storedCorporaJSONstring) as CorpusMC[];
this.processCorpora(corpusList);
}
});
}
private processCorpora(corpusList: CorpusMC[]) {
corpusList.forEach((corpus: CorpusMC) => {
corpus.subCorpora = {};
corpus.validReff = null;
this.availableCorpora.push(corpus);
let authorIndex = this.availableAuthors.map(author => author.name).indexOf(corpus.author);
if (authorIndex > -1) {
this.availableAuthors[authorIndex].corpora.push(corpus);
}
else {
this.availableAuthors.push(new Author({
name: corpus.author,
corpora: [corpus]
}));
}
});
this.availableAuthors.sort((author1, author2) => {
if (author1.name < author2.name) {
return -1;
}
else if (author1.name > author2.name) {
return 1;
}
return 0;
});
this.availableAuthors.forEach((author) => {
author.corpora.sort((corpus1, corpus2) => {
if (corpus1.title < corpus2.title) {
return -1;
}
else if (author1.name > author2.name) {
else if (corpus1.title > corpus2.title) {
return 1;
}
return 0;
});
this.availableAuthors.forEach((author) => {
author.corpora.sort((corpus1, corpus2) => {
if (corpus1.title < corpus2.title) {
return -1;
}
else if (corpus1.title > corpus2.title) {
return 1;
}
return 0;
});
});
});
}
}
......@@ -5,6 +5,8 @@ import {Exercise} from "../../models/exercise";
import {QueryMC} from "../../models/queryMC";
import {AnnisResponse} from "../../models/annisResponse";
import {TranslateService} from "@ngx-translate/core";
import {Feedback} from "../../models/feedback";
import {NodeMC} from "../../models/nodeMC";
/*
Generated class for the ExerciseProvider provider.
......@@ -19,11 +21,12 @@ export class ExerciseProvider {
query: new QueryMC({
phenomenon: Phenomenon.pos,
value: PartOfSpeech.conjunction
})
}),
feedback: new Feedback()
});
public annisResponse: AnnisResponse;
public MCapiExerciseUrlString: string;
public MCapiFileUrlString: string
public MCapiFileUrlString: string;
constructor(public http: HttpClient,
public translateService: TranslateService) {
......@@ -33,6 +36,29 @@ export class ExerciseProvider {
this.translateService.get("MC_API_FILE_URL").subscribe((value) => {
this.MCapiFileUrlString = value;
});
this.translateService.get("EXERCISE_FEEDBACK_CORRECT_DEFAULT").subscribe((value: string) => {
this.exercise.feedback.correct = value;
});
this.translateService.get("EXERCISE_FEEDBACK_INCORRECT_DEFAULT").subscribe((value: string) => {
this.exercise.feedback.incorrect = value;
});
this.translateService.get("EXERCISE_FEEDBACK_PARTIALLY_CORRECT_DEFAULT").subscribe((value: string) => {
this.exercise.feedback.partiallyCorrect = value;
});
this.translateService.get("EXERCISE_FEEDBACK_GENERAL_DEFAULT").subscribe((value) => {
this.exercise.feedback.general = value;
});
}
createGuid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment