Adding Automate Certificate Delivering Functionality
#controllers/login.py
import config, os, uuid, pydf
from copy import deepcopy
from bottle import Bottle, template, request, response, redirect
from verify_email import verify_email
from models import userdb
from models.certificate import Certificate

class Login(Bottle):
  def __init__(self):
    super().__init__()
    self.get('/', callback=self.index)
    self.post('/user', callback=self.postUser)
    self.get('/update', callback=self.updateUser)
    self.get('/logout', callback=self.logout)

    self.userdb = userdb.Userdb()
    self.template = Certificate()
    
  def index(self):
    kdict = deepcopy(config.kdict)
    kdict['blogTitle'] = "ចុះឈ្មោះ"
    return template('login', data=kdict)

  def postUser(self):
    kdict = deepcopy(config.kdict)
    username = request.forms.getunicode('fusername')
    password = request.forms.getunicode('fpassword')
    email = request.forms.getunicode('femail')

    checkEmail = verify_email(email)

    if checkEmail and username and password:
      result = self.userdb.checkUser(username, password, email)
      if result:
        response.set_cookie('logged-in', result[0], path='/', secret=kdict['secretKey'])
        redirect('/')
      else:
        result = self.userdb.checkUsername(username)
        if not result:
          response.set_cookie('logged-in', username, path='/', secret=kdict['secretKey'])
          self.userdb.insert(username, password, email, 1, False)
          redirect('/')
        else:
          kdict['message'] = 'ឈ្មោះ​អ្នក​ប្រើប្រាស់​នេះ​ត្រូវ​បាន​គេប្រើ​រួច​ហើយ​។'
          return template('login', data=kdict)
    else:
      if not checkEmail:
        kdict['message'] = 'Email របស់​លោក​អ្នក​មិនត្រឹមត្រូវ​ទេ។'
        return template('login', data=kdict)
      elif not (username or password):
        kdict['message'] = 'ត្រូវ​មាន​ឈ្មោះ​អ្នក​ប្រើប្រាស់​និង​ពាក្យ​សំងាត់​។'
        return template('login', data=kdict)

  def logout(self):
    kdict = deepcopy(config.kdict)
    username = request.get_cookie('logged-in', secret=kdict['secretKey'])
    if username:
      self.userdb.deleteUser(username)
      
    response.delete_cookie('logged-in', path='/', secret=kdict['secretKey'])
    redirect('/')

  def updateUser(self):
    kdict = deepcopy(config.kdict)
    username = request.get_cookie('logged-in', secret=kdict['secretKey'])
    if username:
      grade = self.userdb.checkUsername(username)
      if grade[2] < 8:
        self.userdb.updateUser(username)
        return {'grade':grade[2]}
      elif grade[2] == 8:
        pdfFile = self.createPdf(username)
        return {'grade':grade[2], 'pdf':pdfFile}
      else:
        return {'grade':grade[2]}
      
  def createPdf(self, username=0):
    id = str(uuid.uuid4().int)
    pdfFile = '/static/pdfs/'+id+'.pdf'
    template = self.template.substitute()
    options = {
      'page-size': 'Letter',
      'margin-top': '0',
      'margin-right': '0',
      'margin-bottom': '0',
      'margin-left': '0',
      'encoding': "UTF-8",
      'orientation': 'Landscape'
    }
    
    if 'DYNO' in os.environ:
      pdf = pydf.generate_pdf(template, **options)
      with open('public/pdfs/'+ id +'.pdf', 'wb') as f:
        f.write(pdf)
        f.close()

    else:
      import pdfkit
      pdf = pdfkit.from_string(template, False, options=options)
      with open('public/pdfs/'+ id +'.pdf', 'wb') as f:
        f.write(pdf)
        f.close()

    return pdfFile
//public/js/practice.js
class Typing{
  constructor(practice){
    this.letters = practice;
    this.counter = Math.floor(Math.random() * (this.letters).length);
    this.usedCounter = this.counter;
    this.nextKey = this.letters[this.counter][0];
    this.pressedKey = 0;
    this.mistake = 0;
    this.scoreLetter = 0;
    this.numLetters = 0;
    this.setClock();
    this.setColor(this.nextKey);
  }

  setColor(nextKey){
    var rightShift = {'A':1,'S':1,'D':1,'F':1,'G':1,'Z':1,'X':1,'C':1,'V':1,'B':1,'Q':1,'W':1,'E':1,'R':1,'T':1,
                      '~':1,'!':1,'@':1,'#':1,'$':1,'%':1};
    var leftShift = {'H':1,'J':1,'K':1,'L':1,':':1,'"':1,'N':1,'M':1,'<':1,'>':1,'?':1,'Y':1,'U':1,'I':1,'O':1,'P':1,
                    '{':1,'}':1,'|':1,'^':1,'&':1,'*':1,'(':1,')':1,'_':1,'+':1};

    var keys = $(".keyboard-base").children().css({'color':'black'});
    
    for(var index in keys){
      var key = keys[index].innerHTML;

      if(key){
        var data = keys[index].getAttribute("data-l");
        if(key == "'"){
          data += '"';
        }

        if((nextKey == data[5]) || (nextKey == data[6])){
          keys[index].focus();
          $(keys[index]).css({'color':'teal'});

          if(this.nextKey in rightShift){
            $('.rightshift').css({'color':'teal'});
          }else if(this.nextKey in leftShift){
            $('.leftshift').css({'color':'teal'});
          }

          break;
        }
      }
    }
    
    $('#letter').html(this.letters[this.counter][2]);
  }

  checkKey(key){
    if(key == this.nextKey){
      this.scoreLetter += 1;
      while(true){
        this.counter = Math.floor(Math.random() * (this.letters).length);
        if(this.counter != this.usedCounter){
          this.usedCounter = this.counter
          break;
        }
      }

      this.pressedKey = this.nextKey;
      this.nextKey = this.letters[this.counter][0];
      this.setColor(this.nextKey);
    }else{
      document.getElementById('beep').play();
      $('#mistake span').html(this.toKhNum(++this.mistake));
    }

    $('#letters span').html(this.toKhNum(++this.numLetters))
  }

  toKhNum(number){
    const khNum = {'0':'០', '1':'១', '2':'២', '3':'៣', '4':'៤', '5':'៥', '6':'៦', '7':'៧', '8':'៨', '9':'៩'};
    var stringNum = number.toString();
    var khNumString = '';
   
    for(var i in stringNum){
      khNumString += khNum[stringNum[i]];
    }
   
    return khNumString;
  }

  setClock(){
    var second = 0;
    var minute = 0;
    var hour = 0;
    this.minuteTest = 0;
    
    this.clock = setInterval(() => {
      $('#timelapse .second').html(this.toKhNum(++second));
      if(second == 60){
        second = 0;
        $('#timelapse .minute').html(this.toKhNum(++minute));
      }

      if(minute == 60){
        minute = 0;
        ++this.minuteTest;
        $('#timelapse .hour').html(this.toKhNum(++hour));
      }

      if((this.minuteTest <= 2) && (this.scoreLetter >= 5) && (this.mistake <= 10))
        this.updateLevel();
    }, 1000);
  }

  updateLevel(){
    clearInterval(this.clock)
    $.get("login/update",
      function(data, status){
        if(status == "success"){
          var grade = typing.toKhNum(data.grade);
          var message = `<div>សូម​អបអរ​សាទ​ដោយ​អ្នក​បាន​​ឆ្លង​ចូល​កំរឹត​ទី ${grade} ហើយ!!</div>`;
          if(data.grade === 8){
            message = `<div>សូម​អបអរ​សាទ​ដោយ​អ្នក​បានបញ្ចប់​​កំរឹត​ទី ${grade} ដែល​ជា​កំរឹត​ចុង​ក្រោយ!!</div>
                      <a target="_blank" href="${data.pdf}">នេះ​ជា​លិខិត​បញ្ជាក​សមត្ថភាព​របស់​អ្នក​</a>`;
          }
          $('#info').html(message);
          $('#level span').html(grade);
          document.getElementById('level').children[0].innerHTML = grade;
        }
     });
  }
}//end of class

GitHub: https://khmerweb-typing.herokuapp.com
Heroku: https://khmerweb-typing.herokuapp.com/