Code uitleg voor de design rationale - EmileKost/Discover GitHub Wiki

Mongodb en Mongoose

Om data op te slaan hebben wij voor het eerst met een database gewerkt. Hiervoor hebben wij de cloud based database Atlas van Mongodb gebruikt. Dit hebben wij gecombineerd met Mongoose, zodat er makkelijk gecommuniceerd kan worden tussen de server en de database. In dit kopje gaan we bespreken hoe we hier tot gekomen zijn.

Account en database maken op Mongodb

Na het maken van een account kan je gelijk een gratis database aanmaken. Deze hoef je alleen een titel te geven en voila. In deze database kan je verschillende collecties aanmaken. Deze collecties kan je in een Javascript bestand meegeven hoe je ze gestructureerd wilt hebben en welke data er moet worden opgeslagen. Het is een good practice als de naam van de collectie een meervoud is van de Schema's die er later in komen te staan. Schermafbeelding 2022-06-23 om 19 39 42

Connectie krijgen met de database

Om een connectie met de database op te zetten kan je op de website een url aanvragen. In deze url moet je alleen nog je developer gebruikersnaam en wachtwoord toevoegen.

mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then((result) => app.listen(3500), console.log('Mongodb connected'))
  .catch((error) => console.log(error + 'has occured'))

Schema opstellen

Om de data te structureren hebben wij in een Javascript bestand een Schema opgezet. In dit schema's maken we titels aan en willen wij weten om wat voor data het gaat. Dit kan bijvoorbeeld een String, Array of Boolean zijn.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

//This is how the structure of the data is going to look like
const blogSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    email: {
        type: String,
        required: true
    },
    password: {
        type: String,
        required: true
    },
    favorites: {
        type: [String],
        required: false
    },
    dominant: {
        type: Number,
        required: false
    },
    interactive: {
        type: Number,
        required: false
    },
    stable: {
        type: Number,
        required: false
    },
    conscientious: {
        type: Number,
        required: false
    }
}, { timestamps: true });

//Blog is user

const Blog = mongoose.model('Blog', blogSchema);
module.exports = Blog;

Dit datamodel wordt later weer geexporteerd naar het app.js bestand zodat de data daadwerkelijk erin kan worden gezet. Omdat de titel het enkelvoud is van de meervoudige collectie naam, weet Mongoose precies om welke collectie het gaat.

Opslaan van data

Het opslaan van data wordt hieronder in de code uitleg besproken.

Uitleg van de code

Hieronder gaan wij de gehele code uitleggen. Wij hebben Node.js gebruikt gecombineerd met EJS. Hieronder vertellen wij de technische keuzes die wij hebben gemaakt en hoe wij hierdoor tot het uiteindelijke eindproduct zijn gekomen.

Registreren en inloggen met een account

Bij ons concept moet er veel persoonlijke data worden verwerkt. Discover is namelijk een vacaturebank die interactief met gebruikers omgaat en daarom informatie moet kunnen opslaan dat gekoppeld kan worden aan een account. Voor het data opslaan hebben wij gebruik gemaakt van Mongodb database, hierover later meer. Voor het registreer en inlog systeem hebben wij gebruikt gemaakt van Passport en Bcrypt. Passport wordt gebruikt voor het relatief gemakkelijk opstellen van een login systeem en Bcrypt wordt gebruikt voor het encrypten van het wachtwoord. De gebruiker kan pas daadwerkelijk toegang krijgen tot ons product als hij een account heeft geregistreerd en succesvol heeft ingelogd.

Registreren van het account

app.post('/register', checkNotAuthenticated, async (req, res) => {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 10)

    //This is the required data that has to be stored in mongodb
    const blog = new Blog({
      name: req.body.name,
      email: req.body.email,
      password: hashedPassword,
    })
    
    //Saves user account data in mongodb, password is still secure and encrypted
    blog.save()
      .then((result) => {
        console.log(result)
      })
      .catch((err) => {
        console.log(error)
      })

    res.redirect('/login')
  } catch {
    res.redirect('/register')
  }
})

Op de registreer pagina moet de gebruiker zijn email, naam en een zelfbedacht wachtwoord invullen. Deze data wordt door middel van Body-parser uit de formulieren opgehaald. Dit is mogelijk door req.body omdat wij Body-parser hebben gebruikt. Na het ophalen van de waardes. Wij hebben gebruikt gemaakt van een try statement zodat tijdens het uitvoeren van de code tegelijkertijd naar foutmeldingen wordt gezocht. Bij de variabele user wordt er een nieuw dataobject voor Mongodb aangemaakt. Door middel van .save wordt uiteindelijk de data in de database opgeslagen. Deze informatie zal gekoppeld worden aan een uniek objectId en kan later opnieuw worden opgehaald.

Inloggen

Na het aanmaken van een account wordt de gebruiker doorverwezen naar de login pagina. Hier moet de gebruiker inloggen met zijn email en wachtwoord. Als dit succesvol wordt gedaan, dan wordt in het app.js bestand aangegeven dat de gebruiker moet worden doorverwezen naar de index pagina door middel van succesRedirect: '/'. Als dit niet zo is, dan blijft de gebruiker op de login pagina en kan hij het opnieuw proberen.

app.post('/login', checkNotAuthenticated, passport.authenticate('local', {
  successRedirect: '/',
  failureRedirect: '/login',
  failureFlash: true
}))

In de app.post van de login pagina wordt hier aangegeven wat moet gebeuren bij het succesvol of onsuccesvol invullen van het login form.

Passport-config.js

Het passport-config Javascript bestand is de functionaliteit die controleert of de waarden van het inlog form hetzelfde zijn als eerder aangemaakte data vanuit de registreer pagina. De functie controleert tegelijkertijd ook of het juiste email adres bij het juiste wachtwoord is ingevuld. Waar nodig zal deze functionaliteit de juiste feedback en foutmeldingen geven. Deze functie staat in een apart Javascript bestand om het overzicht wat meer te behouden. De functie wordt later weer geexporteerd naar het app.js bestand.


const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const Blog = require('./models/blog.js');

module.exports = function(passport) {
    passport.use(
        new LocalStrategy({usernameField : 'email'},(email,password,done)=> {
                //match user
                Blog.findOne({email : email})
                .then((user)=>{
                 if(!user) {
                     return done(null,false,{message : 'that email is not registered'});
                 }
                 //match pass
                 bcrypt.compare(password,user.password,(err,isMatch)=>{
                     if(err) throw err;

                     if(isMatch) {
                         return done(null,user);
                     } else {
                         return done(null,false,{message : 'pass incorrect'});
                     }
                 })
                })
                .catch((err)=> {console.log(err)})
        })
        
    )
    passport.serializeUser(function(user, done) {
        done(null, user.id);
      });
      
      passport.deserializeUser(function(id, done) {
        Blog.findById(id, function(err, user) {
          done(err, user);
        });
      }); 
}; 

Boven aan het bestand worden eerst de benodigde variabelen gedeclareerd. LocalStrategy zorgt ervoor dat de het email adres en het wachtwoord worden geauthenticeerd. Daarnaast is ook hier Bcrypt nodig om het wachtwoord te vergelijken. Allerlaatst wordt ook nog het Blog model gedeclareerd. Dit is het datamodel voor het account van de gebruikers. Uiteindelijk wordt de data hieruit vergeleken met de waarden van het inlog formulier. In de functie worden allereerst de waarden uit het inlog formulier opgehaald. Als dit voltooid is wordt deze data een op een met elkaar vergeleken door de .findOne methode die gaat zoeken naar een email die gelijk staan aan een opgeslagen email in de database. Als dit niet zo is verschijnt de melding dat er geen email geregistreerd is. Bestaat het email wel al? Dan gaat de functie verder en wordt ook het wachtwoord met elkaar vergeleken. Als dit een isMatch is (een match) dan heeft de gebruiker succesvol ingelogd.

Home/index pagina

Na het succesvol inloggen komt de gebruiker op de indexpagina terecht. Vanuit hier kan die gelijk de DISC test starten of kan die al browsen en zoeken naar de beschikbare vacatures. Ook kan hij hier door navigeren naar andere pagina's. Voordat de gebruiker kan beginnen met het browsen en zoeken naar vacatures moeten deze eerst worden opgehaald vanuit de database.

Ophalen van vacatures

const Offers = require('./models/joboffers.js');

app.get('/', checkAuthenticated, async (req, res, next) => {

    Offers.find((err, docs) => {
      if(!err) {
        res.render('index', {
          data: docs,
          name: req.user.name
        })
      } else {
        console.log('Failed to retrieve data')
      }
    })
})

Voordat er informatie vanuit de juiste collectie kan worden opgehaald moet eerst het passende datamodel worden gedeclareerd. In dit geval is dit het model jobOffers, omdat we opzoek zijn naar de vacatures. Omdat we alle vacatures willen ophalen kunnen we de methode .find() gebruiken. Hierin geven wij aan dat we errors en alle documenten zoeken. Als er geen foutmelding naar boven komt, dan wordt de indexpagina gerenderd en worden de vacatures in een object meegegeven. En als er wel een error is dan zal dit in de console.log komen te staan, maar zolang er een connectie is met de database zal dit niet zo snel gebeuren. De functie checkAuthenticated wordt vaak meegegeven. Deze functie zorgt er namelijk voor dat wij gebruikersdata zoals de naam gemakkelijk kunnen opvragen.

Renderen van de vacatures

Voor het gemakkelijk renderen van data vanuit Node.js hebben wij gebruikt gemaakt van ejs. Zodat wij gemakkelijk client side Javascript in de HTML kunnen schrijven. Omdat er voor meerdere vacatures moet en worden gerenderd maken wij hier gebruik van een for loop. Dit had tevens ook met een forEach gekund. Met deze for loop gaan we door de gehele array heen en wordt een voor een elke vacature gerenderd tot dat ze allemaal zijn gerenderd.

  <% if(data.length) { %>
         <div class="containerVacatures">
             <ul id="vacatures">
                 <% for(let i = 0; i < data.length; i++) { %>
                 <section>
                     <li class="vacatures-li">
                         <h2> <%= data[i].title %> </h2>
                         <p> <%= data[i].introduction %> </p>
                           <ul>
                               <li> <%= data[i].location %></li>
                               <li> <%= data[i].businessSectors %></li>
                           </ul>
                           <p> <%= data[i].study %> </p>
                           <h3>Kernwoorden</h3>
                           <ul>
                               <li><%= data[i].keyword1 %></li>
                               <li><%= data[i].keyword2 %></li>
                               <li><%= data[i].keyword3 %></li>
                           </ul>
                         <button><a href="/<%= data[i].id %>"> Meer lezen...</a></button>
                     </li>
                 </section>
                 <% } %>
             </ul>
             <% } else { %>
                <p id="geen-resultaat">
                    Wij hebben helaas geen vacature gevonden met deze zoekopdracht.
                    Probeer het alsjeblieft opnieuw!
                </p>
                <% } %>

Na het opstellen van de for loop kunnen we met [i] in de HTML voor elke vacature de structuur en content bepalen. De link in de pagina staat gelijk aan het objectId. Vanuit hier kan de gebruiker gemakkelijk door naar de detailpagina.

Zoeken van vacatures op de indexpagina

Op de index pagina heeft de gebruiker naast het browsen van vacatures en het starten van de DISC test ook de optie om te kunnen zoeken in de database naar vacatures. Wij hebben hier gebruik gemaakt van body parser om zo gemakkelijk waarden uit de HTML op te halen. Ook hebben wij Regex gebruikt, wat het mogelijk maakt om snel en gemakkelijk naar data te zoeken in de database.

app.post('/', checkAuthenticated, async (req, res) => {
  const input = req.body.search;
  console.log(input);
  let search = await Offers.find({title: {$regex: new RegExp('^' + input + '.*', 'i')}}).exec()
   .then(response => {
     console.log(response)
     res.render('index', {data: response, name: req.user.name})
   })
})

In de app.post van de indexpagina wordt allereerst een variabele aangemaakt die gelijk staat aan de ingevulde waarde in de zoekbalk, dit kan gemakkelijk worden opgehaald door body parser. Als dit opgehaald is dan wordt door middel van Offers.find en Regex letter voor letter gezocht naar matchende waarden. Dit wordt door Regex heel gemakkelijk gemaakt. Dit is een async functie omdat er data uit een database moet worden opgehaald. Het zoeken van deze data is een promis, waardoor er na het voltooien nog een .then gebruikt wordt om de response door te geven aan het opnieuw renderen van de indexpagina. Hier worden de vacatures vervangen door de resultaten van de zoekopdracht omdat het meegegeven object "data" wordt verandert naar deze resultaten. Om goede feedback aan de gebruiker te geven hebben wij in de EJS gebruik gemaakt van een if else statement. Deze checkt of de data uberhaupt bestaat. Is dit niet zo, dan krijgt de gebruiker de tekst te zien met dat er geen zoekresultaten zijn gevonden.

      <% } else { %>
                <p id="geen-resultaat">
                    Wij hebben helaas geen vacature gevonden met deze zoekopdracht.
                    Probeer het alsjeblieft opnieuw!
                </p>
                <% } %>

Dit is het else statement die na het if statement komt als er geen data is gevonden vanuit de database naar aanleiding van de zoekopdracht. Hierdoor krijgt de gebruiker duidelijke feedback over zijn zoekopdracht.

Detail pagina

Op de detailpagina wordt voor elke vacature meer gedetailleerde informatie gegeven. Op deze pagina krijgen gebruikers tevens de optie om een vacature bij hun favoriete toe te voegen. De detail pagina kan per vacature gerenderd worden omdat de het objectId van de vacature in de link naar de detailpagina zit.

app.get('/:id', (req, res) => {
  Offers.findById(req.params.id)
  .then(results => {
    res.render('detailed', {data: results})
  })
  .catch((err) => {
    console.log(err);
    })
  })  

Wij hebben body parser gebruikt, wat het hier mogelijk maakt om gemakkelijk het objectId uit de url op te halen. Dit objectId kunnen we weer plaatsen in de .findById() methode om zo de juiste data voor de detailpagina te laden en te renderen.

Toevoegen aan favorieten

Op de detailpagina krijgt de gebruiker de mogelijkheid om de vacature aan zijn favorieten lijst toe te voegen. Het id van deze vacature wordt aan het user model toegevoegd in de database onder het veld "favorites". In favorites komt een array met objectId's te staan. Deze kunnen later op de profielpagina weer worden gerenderd.

app.post('/:id', checkAuthenticated, (req, res) => {
  const user = req.user.id;
  const offerId = req.params.id;
  
  Blog.findOneAndUpdate({
    _id: user
  }, {
    $push: {
      favorites: offerId
    }
  })
  .then((result) => {
    console.log(result)
    res.redirect('profile')
  })
})

In de app.post van de detailpagina worden allereerst het id van de gebruiker en desbetreffende vacature opgehaald. Om de juiste gebruiker in de database te vinden gebruiken we de methode .findOneAndUpdate(). Dit mede omdat we al aan een bestaande gebruiker data willen toevoegen. Om de juiste gebruiker te vinden wordt het eerder gedeclareerde user id bij het veld _id geplaatst. Hierdoor weet de database precies om welke gebruiker het gaat en kan nu daadwerkelijk het id van de vacature worden toegevoegd. Door de methode $push kunnen we het offerId toevoegen aan het favorites veld. Het updaten van data is een promise. De return is het result, dit is de vernieuwde versie van de data van de gebruiker. Na het succesvol voltooien wordt de gebruiker doorgeleid naar de profielpagina, waar hij zijn opgeslagen vacatures kan vinden.

Profielpagina

Direct vanuit de navigatie kan de gebruiker naar de profielpagina toe. Op de profielpagina kan de gebruiker zijn opgeslagen vacatures zien. Tevens kan die vanaf hier de DISC test starten om zo gepersonaliseerde vacatures te vinden. De DISC test wordt later bij de bijbehorende pagina technisch besproken.

Renderen van de favoriet gemarkeerde vacatures

Op de detailpagina van elke vacature kan de gebruiker de vacature opslaan. De vacature zijn objectId wordt toegevoegd aan de user favorites. Favorites is een array waar een ongelimiteerd aantal vacatures aan kan worden toegevoegd.

app.get('/profile', checkAuthenticated, (req, res, next) => {
  const user = req.user.id;
  Blog.findById(user).then(results => {

    const allResults = results.favorites.map(element => {
      return Offers.findById(element).exec();
    });

    Promise.all(allResults).then(data => {
      res.render('profile', {
        data: data,
        name: req.user.name
      })
    })
    
  })
  .catch((err) => {
    console.log(err);
  })
})

De favoriete vacatures worden opgehaald doordat allereerst het user id wordt opgevraagd. Met dit user id kan de .findById() methode worden gebruikt. Omdat we alleen de favorites nodig hebben gebruikten we de .map methode en .exec om dit daadwerkelijk uit te voeren. Omdat dit meerdere promises zijn kunnen we gebruik maken van de Promise.all methode om zo elke gemaakte promise af te handelen. Als dit allemaal voltooid is kan de pagina worden gerenderd en wordt de juiste data in een object meegegeven. Voor het renderen wordt dezelfde for loop gebruikt als voor het renderen van de index pagina.

Verwijderen van favoriet

Na het toevoegen van een vacature moet de gebruiker natuurlijk ook de optie krijgen om deze weer te verwijderen. Op de profielpagina is dit mogelijk. Om het juiste id op te vragen zit het ObjectId van de vacature in de verwijder button verstopt. Bij het klikken hiervan wordt door middel van body parser dit id vanuit de value opgehaald.

app.post('/profile', checkAuthenticated, async (req, res) => {
  const user = req.user.id;
  const objectId = req.body.delete;
  
  await Blog.findByIdAndUpdate(user, {
    $pull: {
      favorites: objectId
    }
  })
  res.redirect('profile')
})

Omdat beide id's van zowel de vacature als de gebruiker bekend zijn is het heel gemakkelijk om de vacature te verwijderen. Door middel van .findByIdAndUpdate() en $pull kan de juiste vacature uit de favorites array worden verwijdert.

DISC Test pagina

De DISC test is een formulier waarbij gebruikers verschillende opties krijgen, zij moeten aangeven welke zij het meeste bij zichzelf vinden passen. Deze data moet worden opgeslagen. Op basis van deze score worden er vacatures op basis van deze score gerenderd.

Opslaan resultaten DISC test

app.post('/disc', checkAuthenticated, (req, res) => {
  //declaring the point system
  let dpoints = 0;
  let ipoints = 0;
  let spoints = 0;
  let cpoints = 0;

  const intro1 = req.body.intro1;
  if(intro1 === 'direct') {
    dpoints++;
    ipoints++
  } else if(intro1 === 'indirect') {
    spoints++;
    cpoints++;
  }

  const intro2 = req.body.intro2;
  if(intro1 === 'mensgericht') {
    dpoints++;
    ipoints++
  } else if(intro2 === 'taakgericht') {
    spoints++;
    cpoints++;
  }
  const question1 = req.body.vraag1;
  if(question1 === 'dominant') {
    dpoints++
  } else if(question1 === 'interactive') {
    ipoints++
  } else if(question1 === 'stable') {
    spoints++
  } else if(question1 === 'conscientieus') {
    cpoints++
  }

  const question2 = req.body.vraag2;
  if(question2 === 'dominant') {
    dpoints++
  } else if(question2 === 'interactive') {
    ipoints++
  } else if(question2 === 'stable') {
    spoints++
  } else if(question2 === 'conscientieus') {
    cpoints++
  }

  const question3 = req.body.vraag3;
  if(question3 === 'dominant') {
    dpoints++
  } else if(question3 === 'interactive') {
    ipoints++
  } else if(question3 === 'stable') {
    spoints++
  } else if(question3 === 'conscientieus') {
    cpoints++
  }

  const question4 = req.body.vraag3;
  if(question4 === 'dominant') {
    dpoints++
  } else if(question4 === 'interactive') {
    ipoints++
  } else if(question4 === 'stable') {
    spoints++
  } else if(question4 === 'conscientieus') {
    cpoints++
  }

  const question5 = req.body.karakter5;
  if(question5 === 'dominant') {
    dpoints++
  } else if(question5 === 'interactive') {
    ipoints++
  } else if(question5 === 'stable') {
    spoints++
  } else if(question5 === 'conscientieus') {
    cpoints++
  }

  const question6 = req.body.karakter6;
  if(question6 === 'dominant') {
    dpoints++
  } else if(question6 === 'interactive') {
    ipoints++
  } else if(question6 === 'stable') {
    spoints++
  } else if(question6 === 'conscientieus') {
    cpoints++
  }

  const question7 = req.body.karakter7;
  if(question7 === 'dominant') {
    dpoints++
  } else if(question7 === 'interactive') {
    ipoints++
  } else if(question7 === 'stable') {
    spoints++
  } else if(question7 === 'conscientieus') {
    cpoints++
  }

  const question8 = req.body.karakter8;
  if(question8 === 'dominant') {
    dpoints++
  } else if(question8 === 'interactive') {
    ipoints++
  } else if(question8 === 'stable') {
    spoints++
  } else if(question8 === 'conscientieus') {
    cpoints++
  }

  console.log('d points:' + dpoints)
  console.log('i points:' + ipoints)
  console.log('s points:' + spoints)
  console.log('c points:' + cpoints)
  
  if(dpoints > ipoints ||  dpoints > spoints || dpoints > cpoints) {
      Offers.find({score: "dominant"})
      .then((dombo) => {
        console.log(dombo)
        res.render('results', {data: dombo})
      })
    }
      else if(ipoints > dpoints ||  ipoints > spoints || ipoints > cpoints) {
        Offers.find({score: "interactief"})
        .then(dombo => {
          console.log(dombo)
          res.render('results', {data: dombo})
        })
        } else if(spoints > dpoints ||  ipoints > spoints || cpoints > cpoints){
          Offers.find({score: "stabiel"})
          .then(dombo => {
            console.log(dombo)
            res.render('results', {data: dombo})
          })
        } else if(cpoints > dpoints ||  ipoints > spoints || spoints > cpoints) {
            Offers.find({score: "conscientieus"})
            .then(dombo => {
              console.log(dombo)
              res.render('results', {data: dombo})
            })
  }

Elke radiobutton heeft bij elke vraag een verschillende value. Deze kan dominant, interactief, stabiel of conscientieus zijn. Bij de submit van het formulier worden al deze waarden opgeslagen bij het juiste kernwoord. Door verschillende if statements worden alle woorden gecontroleerd om te kijken of zij de hoogste score hebben. Het woord met de hoogste score is uiteindelijk het woord waarop de vacatures gebaseerd moeten zijn. De vacatures uit de database hebben al een veld met score waarbij dit uit een van de vier worden bestaat. Door de .find() methode kan er gezocht worden naar vacatures waarbij het score veld gelijk staan aan het winnende woord. Hierdoor worden alleen deze vacatures gerenderd en zijn deze op basis van de resultaten van de DISC test.

Installeren van dit project

  • Clone de repository
  • Open de terminal in jouw code editor
  • Typ npm install
  • Typ npm start
  • Het project is klaar voor gebruik
⚠️ **GitHub.com Fallback** ⚠️