From a0936fe8aff172833740ed2bf2212d03c6b4ff66 Mon Sep 17 00:00:00 2001 From: angelalvaigle Date: Mon, 4 Nov 2024 00:31:33 +0100 Subject: [PATCH] basic question-service added --- docker-compose.yml | 16 ++++ gatewayservice/gateway-service.js | 32 +++++++ questionservice/.dockerignore | 2 + questionservice/Dockerfile | 20 ++++ questionservice/package.json | 31 ++++++ questionservice/question-model.js | 32 +++++++ questionservice/question-service.js | 54 +++++++++++ .../assets/wrappers/AddQuestionContainer.js | 20 ++++ .../src/components/AddQuestionContainer.jsx | 95 +++++++++++++++++++ webapp/src/components/index.js | 1 + webapp/src/pages/Admin.jsx | 5 +- webapp/src/utils/SPARQLQueryDispatcher.js | 14 +++ webapp/src/utils/artworksQuery.js | 30 ++++++ 13 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 questionservice/.dockerignore create mode 100644 questionservice/Dockerfile create mode 100644 questionservice/package.json create mode 100644 questionservice/question-model.js create mode 100644 questionservice/question-service.js create mode 100644 webapp/src/assets/wrappers/AddQuestionContainer.js create mode 100644 webapp/src/components/AddQuestionContainer.jsx create mode 100644 webapp/src/utils/SPARQLQueryDispatcher.js create mode 100644 webapp/src/utils/artworksQuery.js diff --git a/docker-compose.yml b/docker-compose.yml index c0fdf57..b637276 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,20 @@ services: environment: MONGODB_URI: mongodb://mongodb:27017/userdb + questionservice: + container_name: questionservice-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_7/questionservice:latest + profiles: ['dev', 'prod'] + build: ./questionservice + depends_on: + - mongodb + ports: + - '8003:8003' + networks: + - mynetwork + environment: + MONGODB_URI: mongodb://mongodb:27017/questiondb + gatewayservice: container_name: gatewayservice-${teamname:-defaultASW} image: ghcr.io/arquisoft/wiq_7/gatewayservice:latest @@ -48,6 +62,7 @@ services: - mongodb - userservice - authservice + - questionservice ports: - '8000:8000' networks: @@ -55,6 +70,7 @@ services: environment: AUTH_SERVICE_URL: http://authservice:8002 USER_SERVICE_URL: http://userservice:8001 + QUESTION_SERVICE_URL: http://questionservice:8003 webapp: container_name: webapp-${teamname:-defaultASW} diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 4d7bce3..9c41d96 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -12,6 +12,8 @@ const port = 8000; const authServiceUrl = process.env.AUTH_SERVICE_URL || 'http://localhost:8002'; const userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:8001'; +const questionServiceUrl = + process.env.QUESTION_SERVICE_URL || 'http://localhost:8003'; app.use(cors()); app.use(express.json()); @@ -64,6 +66,36 @@ app.get('/users', async (req, res) => { } }); +app.post('/addquestion', async (req, res) => { + try { + // Forward the add question request to the question generation service + const addQuestionResponse = await axios.post( + questionServiceUrl + '/addquestion', + req.body + ); + res.json(addQuestionResponse.data); + } catch (error) { + res + .status(error.response.status) + .json({ error: error.response.data.error }); + } +}); + +app.get('/questions', async (req, res) => { + try { + // Forward the get question request to the question asking service + const getQuestionResponse = await axios.get( + questionServiceUrl + '/questions', + req.body + ); + res.json(getQuestionResponse.data); + } catch (error) { + res + .status(error.response.status) + .json({ error: error.response.data.error }); + } +}); + // Read the OpenAPI YAML file synchronously openapiPath = './openapi.yaml'; if (fs.existsSync(openapiPath)) { diff --git a/questionservice/.dockerignore b/questionservice/.dockerignore new file mode 100644 index 0000000..3091757 --- /dev/null +++ b/questionservice/.dockerignore @@ -0,0 +1,2 @@ +node_modules +coverage \ No newline at end of file diff --git a/questionservice/Dockerfile b/questionservice/Dockerfile new file mode 100644 index 0000000..b3b83f3 --- /dev/null +++ b/questionservice/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Node.js runtime as a parent image +FROM node:20 + +# Set the working directory in the container +WORKDIR /usr/src/questionservice + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Install app dependencies +RUN npm install + +# Copy the app source code to the working directory +COPY . . + +# Expose the port the app runs on +EXPOSE 8003 + +# Define the command to run your app +CMD ["node", "question-service.js"] \ No newline at end of file diff --git a/questionservice/package.json b/questionservice/package.json new file mode 100644 index 0000000..f0f8c97 --- /dev/null +++ b/questionservice/package.json @@ -0,0 +1,31 @@ +{ + "name": "questionservice", + "version": "1.0.0", + "description": "Question service, in charge of generating questions in the application", + "main": "service.js", + "scripts": { + "start": "node question-service.js", + "test": "jest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Arquisoft/wiq_7.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Arquisoft/wiq_7/issues" + }, + "homepage": "https://github.com/Arquisoft/wiq_7#readme", + "dependencies": { + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "express": "^4.18.2", + "mongoose": "^8.0.4" + }, + "devDependencies": { + "jest": "^29.7.0", + "mongodb-memory-server": "^9.1.5", + "supertest": "^6.3.4" + } +} diff --git a/questionservice/question-model.js b/questionservice/question-model.js new file mode 100644 index 0000000..623e0a3 --- /dev/null +++ b/questionservice/question-model.js @@ -0,0 +1,32 @@ +const mongoose = require('mongoose'); + +const questionSchema = new mongoose.Schema({ + type: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + right: { + type: String, + required: true, + }, + wrong1: { + type: String, + // required: true, + }, + wrong2: { + type: String, + // required: true, + }, + wrong3: { + type: String, + // required: true, + }, +}); + +const Question = mongoose.model('Question', questionSchema); + +module.exports = Question; diff --git a/questionservice/question-service.js b/questionservice/question-service.js new file mode 100644 index 0000000..df0b1d4 --- /dev/null +++ b/questionservice/question-service.js @@ -0,0 +1,54 @@ +// question-service.js +const express = require('express'); +const mongoose = require('mongoose'); +const bodyParser = require('body-parser'); +const Question = require('./question-model'); + +const app = express(); +const port = 8003; + +// Middleware to parse JSON in request body +app.use(bodyParser.json()); + +// Connect to MongoDB +const mongoUri = + process.env.MONGODB_URI || 'mongodb://localhost:27017/questiondb'; +mongoose.connect(mongoUri); + +app.post('/addquestion', async (req, res) => { + try { + const newQuestion = new Question({ + type: req.body.type, + path: req.body.path, + right: req.body.right, + // wrong1: req.body.wrong1, + // wrong2: req.body.wrong2, + // wrong3: req.body.wrong3, + }); + await newQuestion.save(); + res.json(newQuestion); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.get('/questions', async (req, res) => { + try { + const questions = await Question.find(); // Fetch all questions + res.json(questions); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +const server = app.listen(port, () => { + console.log(`Question Service listening at http://localhost:${port}`); +}); + +// Listen for the 'close' event on the Express.js server +server.on('close', () => { + // Close the Mongoose connection + mongoose.connection.close(); +}); + +module.exports = server; diff --git a/webapp/src/assets/wrappers/AddQuestionContainer.js b/webapp/src/assets/wrappers/AddQuestionContainer.js new file mode 100644 index 0000000..9219500 --- /dev/null +++ b/webapp/src/assets/wrappers/AddQuestionContainer.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const Wrapper = styled.section` + display: grid; + row-gap: 2rem; + @media (min-width: 768px) { + grid-template-columns: 1fr 1fr; + column-gap: 1rem; + } + @media (min-width: 1120px) { + grid-template-columns: 1fr 1fr 1fr; + } + + .generateDb { + display: grid; + grid-template-columns: 1fr; + row-gap: 2rem; + } +`; +export default Wrapper; diff --git a/webapp/src/components/AddQuestionContainer.jsx b/webapp/src/components/AddQuestionContainer.jsx new file mode 100644 index 0000000..c12e292 --- /dev/null +++ b/webapp/src/components/AddQuestionContainer.jsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import Wrapper from '../assets/wrappers/AddQuestionContainer.js'; +import { Snackbar } from '@mui/material'; +import SPARQLQueryDispatcher from '../utils/SPARQLQueryDispatcher'; +import artworksQuery from '../utils/artworksQuery'; + +const endpointUrl = 'https://query.wikidata.org/sparql'; +const queryDispatcher = new SPARQLQueryDispatcher(endpointUrl); +const apiEndpoint = + process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const AddQuestionContainer = () => { + const [error, setError] = useState(''); + const [openSnackbar, setOpenSnackbar] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const addQuestion = async ({ type, path, right }) => { + try { + await axios.post(`${apiEndpoint}/addquestion`, { type, path, right }); + setOpenSnackbar(true); + } catch (error) { + console.log(error); + setError(error.response?.data?.error); + } + }; + + const generateArtworks = async () => { + setIsSubmitting(true); + try { + const query = await queryDispatcher.query(artworksQuery); + // handle results + const bindings = query.results.bindings; + for (const result of bindings) { + // Accedemos a cada propiedad + const workLabel = result.workLabel.value; // Título de la obra + const creator = result.creatorLabel.value; // Nombre del creador + const imageUrl = result.image.value; // URL de la imagen + const sitelinks = result.sitelinks.value; // Número de sitelinks + + // Muestra los resultados en la consola + if (!workLabel.startsWith('http') && !creator.startsWith('http')) { + await addQuestion({ + type: 'artwork', + path: workLabel, + right: creator, + }); + } + } + setOpenSnackbar(true); + } catch (error) { + console.log('error'); + setError(error.response?.data?.error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCloseSnackbar = () => { + setOpenSnackbar(false); + }; + + return ( + +
+

Update "Por su obra..."

+ +
+ + + {error && ( + setError('')} + message={`Error: ${error}`} + /> + )} +
+ ); +}; + +export default AddQuestionContainer; diff --git a/webapp/src/components/index.js b/webapp/src/components/index.js index b9c5889..672c5ac 100644 --- a/webapp/src/components/index.js +++ b/webapp/src/components/index.js @@ -3,3 +3,4 @@ export { default as FormRow } from './FormRow'; export { default as BigSidebar } from './BigSidebar'; export { default as SmallSidebar } from './SmallSidebar'; export { default as Navbar } from './Navbar'; +export { default as AddQuestionContainer } from './AddQuestionContainer'; diff --git a/webapp/src/pages/Admin.jsx b/webapp/src/pages/Admin.jsx index 0dbb6de..50894cc 100644 --- a/webapp/src/pages/Admin.jsx +++ b/webapp/src/pages/Admin.jsx @@ -1,4 +1,7 @@ +import { AddQuestionContainer } from '../components'; + const Admin = () => { - return

Admin Page

; + return ; }; + export default Admin; diff --git a/webapp/src/utils/SPARQLQueryDispatcher.js b/webapp/src/utils/SPARQLQueryDispatcher.js new file mode 100644 index 0000000..0d75219 --- /dev/null +++ b/webapp/src/utils/SPARQLQueryDispatcher.js @@ -0,0 +1,14 @@ +class SPARQLQueryDispatcher { + constructor(endpoint) { + this.endpoint = endpoint; + } + + query(sparqlQuery) { + const fullUrl = this.endpoint + '?query=' + encodeURIComponent(sparqlQuery); + const headers = { Accept: 'application/sparql-results+json' }; + + return fetch(fullUrl, { headers }).then((body) => body.json()); + } +} + +export default SPARQLQueryDispatcher; diff --git a/webapp/src/utils/artworksQuery.js b/webapp/src/utils/artworksQuery.js new file mode 100644 index 0000000..95f698c --- /dev/null +++ b/webapp/src/utils/artworksQuery.js @@ -0,0 +1,30 @@ +const artworksQuery = `SELECT ?work ?workLabel ?creatorLabel ?image ?sitelinks WHERE { + # Filtra solo obras de pintura + ?work wdt:P31 wd:Q3305213. # Pintura + + # La obra debe tener un creador + ?work wdt:P170 ?creator. + + # La obra debe tener una imagen + ?work wdt:P18 ?image. + + # Contar el número de enlaces en Wikipedia + ?work wikibase:sitelinks ?sitelinks. + + # Excluir obras que tengan más de un creador + MINUS { + ?work wdt:P170 ?otherCreator. + FILTER(?otherCreator != ?creator) + } + + # Filtro para incluir solo obras con un número mínimo de sitelinks (popularidad) + FILTER(?sitelinks >= 20) # Cambia el número según el umbral que desees + + # Etiquetas para mostrar los nombres de la obra y el autor + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } +} +# Limitar el número de resultados para acelerar la consulta +LIMIT 100 +`; + +export default artworksQuery;