diff --git a/docker-compose.yml b/docker-compose.yml index 46f9843d..a9c2888b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,8 @@ services: - "8003:8003" networks: - mynetwork + environment: + QUESTIONS_SERVICE_URL: http://questionservice:8004 questionservice: container_name: questionservice-${teamname:-defaultASW} diff --git a/question_generator/geography/cities/citiesQuestions.js b/question_generator/geography/cities/citiesQuestions.js index ae3e971d..88f124a4 100644 --- a/question_generator/geography/cities/citiesQuestions.js +++ b/question_generator/geography/cities/citiesQuestions.js @@ -1,4 +1,5 @@ -const queryExecutor=require("../../queryExecutor") +const queryExecutor=require("../../queryExecutor"); +const QuestionsUtils = require("../../questions-utils"); class CitiesQuestions{ #citiesQuestions=null; static getInstance(){ @@ -10,54 +11,128 @@ class CitiesQuestions{ constructor(){ this.cities={}; } - async loadData(){ - let newResults={}; - const query=` - SELECT ?city ?cityLabel ?population ?countryLabel ?elevation_above_sea_level - WITH{ - SELECT ?city ?cityLabel - WHERE{ - ?city wdt:P31 wd:Q515 - } - LIMIT 1000 - } AS %i - WHERE { - INCLUDE %i - OPTIONAL{ - ?city wdt:P1082 ?population. - ?city wdt:P17 ?country. - ?city wdt:P2044 ?elevation_above_sea_level + async loadCities(){ + let result={}; + const citiesQueries=[ + //Instancia de CITY + `SELECT ?city ?cityLabel ?population ?countryLabel + WITH{ + SELECT ?city ?cityLabel + WHERE{ + ?city wdt:P31 wd:Q515 } - FILTER EXISTS{ + LIMIT 1000 + } AS %i + WHERE { + INCLUDE %i + OPTIONAL{ ?city wdt:P1082 ?population. ?city wdt:P17 ?country. - ?city wdt:P2044 ?elevation_above_sea_level + } + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } } - SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } - } - ORDER BY DESC(?population) - LIMIT 100 - ` - let cities = await queryExecutor.execute(query); - cities.forEach(city => { - const cityId = city.city.value; - const cityName = city.cityLabel.value; - const population = city.population.value; - const country = city.countryLabel.value; - const elevationAboveSeaLevel = city.elevation_above_sea_level.value; + ORDER BY DESC(?population) + LIMIT 100`, + //Ciudades de España + `SELECT ?city ?cityLabel ?population ?countryLabel + WITH{ + SELECT ?city ?cityLabel + WHERE{ + ?city wdt:P31 wd:Q2074737 + } + LIMIT 1000 + } AS %i + WHERE { + INCLUDE %i + OPTIONAL{ + ?city wdt:P1082 ?population. + ?city wdt:P17 ?country. + } + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } + } + ORDER BY DESC(?population) + LIMIT 20`, + //Metropolis + `SELECT ?city ?cityLabel ?population ?countryLabel + WITH{ + SELECT ?city ?cityLabel + WHERE{ + ?city wdt:P31 wd:Q200250 + } + LIMIT 1000 + } AS %i + WHERE { + INCLUDE %i + OPTIONAL{ + ?city wdt:P1082 ?population. + ?city wdt:P17 ?country. + } + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } + } + ORDER BY DESC(?population) + LIMIT 5` - if (!newResults[cityId]) { - newResults[cityId] = { - cityId: cityId, - cityName: cityName, - population: population, - country: country, - elevation_above_sea_level: [] - }; + ]; + for(let i = 0; i { + const cityId = city.city.value.match(/Q\d+/)[0]; + const cityName = city.cityLabel.value; + const population = city.population.value; + const country = city.countryLabel.value; + if (!result[cityId]) { + result[cityId] = { + cityId: cityId, + name: cityName, + population: population, + country: country + } + } + }); + } + return result; + + } + async loadData(){ + let newResults = await this.loadCities(); + const propertiesToLoad=[ + { + name:'elevation_above_sea_level', + id: 'P2044' + }, + { + name:'highest_point', + id: 'P610' + }, + { + name: 'area', + id: 'P2046' + }, + { + name: 'continent', + id: 'P30' + }, + { + name: 'head_of_government', + id: 'P6' + }, + { + name: 'located_in_time_zone', + id: 'P421' } - - newResults[cityId].elevation_above_sea_level.push(parseFloat(elevationAboveSeaLevel)); - }); + ] + for(let i = 0; i 0){ + for(let j=0;j { return { - item: result.cityName, + item: result.name, value:parseFloat(result.population), }; }).sort((a, b) => b.value - a.value); @@ -92,26 +167,12 @@ class CitiesQuestions{ } return finalResults } - async getCityForCountry(){ - let numberOfCities=4; - let result =(await this.getRandomCities(1))[0]; - let country=result.country; - let correct = result.cityName; - let incorrects = [] - let i=1; - while(i { - const results= await citiesQuery.getCityForCountry(); + const results= await citiesQuery.doQuestion('country',4); return{ "question":"Which city is in?", - "question_param":results.country, + "question_param":results.question_param, "correct":results.correct, "incorrects":results.incorrects } }, async ()=> { - const results= await citiesQuery.getHigherCity(); + const results= await citiesQuery.doQuestion('elevation_above_sea_level',4); return{ "question":"Which city is higher above sea level?", "correct":results.correct, "incorrects":results.incorrects } + }, + async () => { + const results = await citiesQuery.doQuestion('area', 4); + return { + "question": "Which city has an area of?", + "question_param": results.question_param, + "correct": results.correct, + "incorrects": results.incorrects + } + }, + async () => { + const results = await citiesQuery.doQuestion('continent', 4); + return { + "question": "Which city is in?", + "question_param": results.question_param, + "correct": results.correct, + "incorrects": results.incorrects + } + }, + async () => { + const results = await citiesQuery.doQuestion('head_of_government', 4); + return { + "question": "Which city is governed by?", + "question_param": results.question_param, + "correct": results.correct, + "incorrects": results.incorrects + } + }, + async () => { + const results = await citiesQuery.doQuestion('located_in_time_zone', 4); + return { + "question": "Which city is in the time zone?", + "question_param": results.question_param, + "correct": results.correct, + "incorrects": results.incorrects + } } - ] module.exports.getRandomQuestion = () => templates[Math.floor(Math.random()*templates.length)](); module.exports.loadData = () =>loadData(); \ No newline at end of file diff --git a/question_generator/locales/en.json b/question_generator/locales/en.json index 9703c5e2..c7b75061 100644 --- a/question_generator/locales/en.json +++ b/question_generator/locales/en.json @@ -2,8 +2,15 @@ "Which city is higher above sea level?": "Which city is higher above sea level?", "Which city has more population?": "Which city has more population?", "Which city is in?": "Which city is in %s?", + "Which city has an area of?": "Which city has an area od %s km2?", + "Which city is governed by?": "Which city is governed by %s?", + "Which city is in the time zone?": "Which city is in the time zone %s?", + "Which planet is bigger?": "Which planet is bigger?", + "Who has more Grand Slams?": "Who has more Grand Slams?", "Who has more followers?": "Who has more followers?", - "Who has more looses?": "Who has more looses?" + "Who has more looses?": "Who has more looses?", + "Who has more wins?": "Who has more wins?", + "Which tennis player is from?" : "Which tennis player is from %s?" } \ No newline at end of file diff --git a/question_generator/locales/es.json b/question_generator/locales/es.json index 37c8d2da..8f2b67f4 100644 --- a/question_generator/locales/es.json +++ b/question_generator/locales/es.json @@ -1,15 +1,17 @@ { - - "Which city is higher above sea level?": "¿Qué ciudad tiene más altitud sobre el nivel del mar?", + "Which city is higher above sea level?": "¿Qué ciudad está a mayor altitud?", "Which city has more population?": "¿Qué ciudad tiene más población?", - "Which city is in?": "¿Qué ciudad está en %s?", - - - - "Which planet is bigger?": "¿Qué planeta tiene un tamaño mayor?", - - + "Which city is in?": "¿Qué ciudad se encuentra %s?", + "Which city has an area of?": "¿Qué ciudad tiene un área de %s km2?", + "Which city is governed by?": "¿Qué ciudad es gobernada por %s?", + "Which city is in the time zone?": "¿Qué ciudad está en la zona horaria %s?", + "Which planet is bigger?": "¿Qué planeta es más grande?", - "Who has more Grand Slams?": "¿Quién tiene más Grand Slams?" + "Who has more Grand Slams?": "¿Quién tiene más Grand Slams?", + "Who has more followers?": "¿Quién tiene más seguidores?", + "Who has more looses?": "¿Quién tiene más derrotas?", + "Who has more wins?": "¿Quién tiene más victorias?", + "Which tennis player is from?" : "¿Qué jugador es de %s?" + } \ No newline at end of file diff --git a/question_generator/queryExecutor.js b/question_generator/queryExecutor.js index 5af84116..46fedfd9 100644 --- a/question_generator/queryExecutor.js +++ b/question_generator/queryExecutor.js @@ -19,6 +19,10 @@ class QueryExecutor{ }, ...config, }); + if (!response || !response.data) { + console.error('La consulta a Wikidata no devolvió ningún resultado'); + return; + } return response.data.results.bindings; @@ -28,5 +32,22 @@ class QueryExecutor{ console.error('Error al realizar la consulta a Wikidata:', error.message); } } + static async executeQueryForEntityAndProperty(entity, properties){ + if(!Array.isArray(properties) || properties.length==0){ + return []; + } + const query= + `SELECT ${properties.map(property=>`?${property.name}Label`).join(' ')} WHERE {${properties.map(property=>`OPTIONAL {wd:${entity} wdt:${property.id} ?${property.name}.}`).join(' ')} SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }}LIMIT 1` + let results=await this.execute(query); + const editedResults = results.map(result => { + const editedResult = {}; + for (const key in result) { + const newKey = key.replace(/Label$/, ''); + editedResult[newKey] = result[key]; + } + return editedResult; + }); + return editedResults; + } } module.exports=QueryExecutor \ No newline at end of file diff --git a/question_generator/questionGenerationService.js b/question_generator/questionGenerationService.js index 840798ba..485120f1 100644 --- a/question_generator/questionGenerationService.js +++ b/question_generator/questionGenerationService.js @@ -6,6 +6,8 @@ const geographyTemplate=require('./geography/geographyTemplate'); const planetTemplate=require('./planets/planetsTemplates'); const sportTemplate=require('./sports/sportTemplate'); const generalTemplate=require('./questionTemplate'); +const axios = require('axios'); +const questionServiceUrl = process.env.QUESTIONS_SERVICE_URL || 'http://localhost:8004'; const app = express(); const port = 8003; @@ -33,7 +35,9 @@ app.use( app.get('/api/questions/create', async (req, res) => { try { - const category = req.query.category; + let category = req.query.category; + //User is null because we are not using authentication yet + let user=null; let randomQuestion; switch (category) { @@ -48,8 +52,25 @@ app.get('/api/questions/create', async (req, res) => { break; default: randomQuestion = await generalTemplate.getRandomQuestion(); + category = 'general'; } randomQuestion.question = i18n.__(randomQuestion.question, randomQuestion.question_param); + const saveQuestion = async (question) => { + const url = questionServiceUrl+'/addquestion'; + try { + const response = await axios.post(url, question); + } catch (error) { + console.error(error); + } + }; + saveQuestion({ + question: randomQuestion.question, + correct: randomQuestion.correct, + incorrects: randomQuestion.incorrects, + user: user, + category: category + }); + res.status(200).json( { @@ -59,7 +80,7 @@ app.get('/api/questions/create', async (req, res) => { } ); } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); + res.status(500).json({ error: 'Internal Server Error'}); } }); @@ -67,7 +88,7 @@ app.get('/api/questions/create', async (req, res) => { function loadData() { generalTemplate.loadData(); } - +loadData(); // Ejecuta loadData cada hora (60 minutos * 60 segundos * 1000 milisegundos) setInterval(loadData, 60 * 60 * 1000); diff --git a/question_generator/questionGenerationService.test.js b/question_generator/questionGenerationService.test.js index 17cc1733..a19620f8 100644 --- a/question_generator/questionGenerationService.test.js +++ b/question_generator/questionGenerationService.test.js @@ -1,4 +1,5 @@ const request = require('supertest'); +const axios = require('axios'); let app; beforeAll(async () => { @@ -12,6 +13,16 @@ afterAll(async () => { }); describe('Question generation service', () => { + jest.spyOn(axios, 'post').mockResolvedValue({ + data: { + question: 'Mocked Question', + correct: 'Mocked Correct Answer', + incorrects: ['Mocked Option 1', 'Mocked Option 2'], + user: 'Mocked User', + category: 'Mocked Category' + } + }); + it('should forward create question request to question generation service', async () => { const response = await request(app) .get('/api/questions/create'); @@ -20,7 +31,7 @@ describe('Question generation service', () => { expect(response.body).toHaveProperty('question'); expect(response.body).toHaveProperty('correct'); expect(response.body).toHaveProperty('incorrects'); - },100000); + }, 100000); it('should forward create question request to question generation service', async () => { const response = await request(app) diff --git a/question_generator/questions-utils.js b/question_generator/questions-utils.js new file mode 100644 index 00000000..bf2d1f32 --- /dev/null +++ b/question_generator/questions-utils.js @@ -0,0 +1,35 @@ +class QuestionsUtils{ + static getValuesFromDataAndProperty(data, property, nValues){ + const result = { + correct: null, + incorrects: [], + propertyResult:null + }; + const dataArray = Object.values(data); + for (let i=0; i Math.random() - 0.5); + for (let i = 0; i < random.length; i++) { + const value = random[i]; + if(result.correct==null && value[property]!=undefined){ + result.propertyResult=value[property]; + result.correct=value.name; + break; + } + else if ((!(result.incorrects.includes(value.name)||result.propertyResult==value[property]))&& value[property]!=undefined) { + result.incorrects.push(value.name); + break; + } + } + } +} + +module.exports=QuestionsUtils; \ No newline at end of file diff --git a/question_generator/sports/tennis/tennisTemplates.js b/question_generator/sports/tennis/tennisTemplates.js index 04201b96..89556306 100644 --- a/question_generator/sports/tennis/tennisTemplates.js +++ b/question_generator/sports/tennis/tennisTemplates.js @@ -17,7 +17,9 @@ const templates=[ { const results = await tennisQuery.getPlayerForCountry(); return{ - "question":"Which tennis player is from " + results.country + "?", + + "question":"Which tennis player is from?", + "question_param":results.country, "correct":results.correct, "incorrects":results.incorrects } diff --git a/questionservice/question-model.js b/questionservice/question-model.js index 5e3e72aa..004d62a5 100644 --- a/questionservice/question-model.js +++ b/questionservice/question-model.js @@ -19,6 +19,17 @@ const questionSchema = new mongoose.Schema({ message: 'Options must be between 2 and 4 elements.', }, }, + generationDate: { + type: Date, + default: Date.now, + required: true, + }, + user: { + type: String + }, + category: { + type: String + }, }); const Question = mongoose.model('Question', questionSchema); diff --git a/questionservice/question-service.js b/questionservice/question-service.js index afe67831..76a526e4 100644 --- a/questionservice/question-service.js +++ b/questionservice/question-service.js @@ -20,18 +20,20 @@ const validateRequiredFields = (req, fields) => { } }; -// Ruta para agregar una nueva pregunta -app.post('/addquestion', async (req, res) => { + +const addQuestion = async (req, res) => { try { - validateRequiredFields(req, ['question', 'correct', 'incorrects']); + validateRequiredFields(req, ['question', 'correct', 'incorrects', 'user', 'category']); - const { question, correct, incorrects } = req.body; + const { question, correct, incorrects, user, category } = req.body; // Crea una nueva instancia del modelo de preguntas const newQuestion = new Question({ question, correct, incorrects, + user, + category }); // Guarda la nueva pregunta en la base de datos @@ -41,7 +43,9 @@ app.post('/addquestion', async (req, res) => { } catch (error) { res.status(500).json({ error: error.message }); } -}); +}; +// Ruta para agregar una nueva pregunta +app.post('/addquestion', addQuestion); // logica para preguntas?? @@ -55,3 +59,4 @@ server.on('close', () => { }); module.exports = server; +module.exports.addQuestion = addQuestion; diff --git a/questionservice/question-service.test.js b/questionservice/question-service.test.js index 91d2c29e..183c971e 100644 --- a/questionservice/question-service.test.js +++ b/questionservice/question-service.test.js @@ -21,7 +21,9 @@ describe('Question Service', () => { const newQuestion = { question: 'Mocked Question', correct: 'Mocked Correct Answer', - incorrects: ['Mocked Option 1', 'Mocked Option 2'] + incorrects: ['Mocked Option 1', 'Mocked Option 2'], + user: 'Mocked User', + category: 'Mocked Category' }; const response = await request(app).post('/addquestion').send(newQuestion);