Index: locale/en/mlx.po
===================================================================
--- locale/en/mlx.po	(revision 764)
+++ locale/en/mlx.po	(revision 769)
@@ -1,3 +1,3 @@
-# English-language texts for the MAVA Logger X
+
 #
 # István Váradi <ivaradi@gmail.com>, 2012
@@ -2311,2 +2311,103 @@
 "the exam earlier. Note, that its status is updated only a few minutes "
 "after passing the exam."
+
+msgid "student_check_flight_status"
+msgstr "Check flight:"
+
+msgid "student_check_flight_passed"
+msgstr "<span color=\"darkgreen\"><b>Passed</b></span>"
+
+msgid "student_check_flight_not_passed"
+msgstr "<span color=\"orange\"><b>Not yet passed</b></span>"
+
+msgid "student_check_flight"
+msgstr "Perform the _check flight"
+
+msgid "student_check_flight_tooltip"
+msgstr ""
+"Click this button to perform the check flight, after you have selected "
+"the airfract type to fly with. You will then have to perform a flight "
+"departing from and arriving at LHBP (Budapest Ferihegy Airport) with a "
+"proper SID and arrival transition."
+
+msgid "student_fo"
+msgstr "Congratulations!"
+
+msgid "student_fo_secondary"
+msgstr ""
+"You have received the rank of <b>First Officer</b> (FO).\n\n"
+"By clicking OK below, you will be taken to the flight selection page, "
+"from which you can perform your first real MAVA flight!"
+
+msgid "chkfinish_title"
+msgstr "Check flight finished"
+
+msgid "chkfinish_passed_begin"
+msgstr ""
+"<b><span size=\"xx-large\">Congratulations!</span></b>\n"
+"\n"
+"You have successfully completed your check flight."
+
+msgid "chkfinish_passed_fo"
+msgstr ""
+" \n\n"
+"You have received the rank of <b>First Officer</b> (FO)."
+
+msgid "chkfinish_passed_end"
+msgstr ""
+" \n\n"
+"You may wish to review the log if you have made any mistakes."
+
+msgid "chkfinish_failed"
+msgstr ""
+"Unfortunately, you have <b>failed</b> your check flight.\n"
+"\n"
+"You can review the log to check what mistakes you have committed."
+
+msgid "chkfinish_savepirep"
+msgstr ""
+" \n\n"
+"If you have further questions, you can save your PIREP\n"
+"and send the file to your mentor for a review and\n"
+"to receive helpful advice."
+
+msgid "chkfinish_next_student_begin"
+msgstr ""
+" \n\n"
+"The <i>Next</i> button takes you back to the <i>Student</i> page,"
+
+msgid "chkfinish_next_student_nothing"
+msgstr ""
+" \n"
+"where you can initiate another test flight or take your entry exam."
+
+msgid "chkfinish_next_student_no_exam"
+msgstr ""
+" \n"
+"where you can take your entry exam."
+
+msgid "chkfinish_next_student_no_flight"
+msgstr ""
+" \n"
+"where you can initiate another test flight."
+
+msgid "chkfinish_next"
+msgstr ""
+" \n\n"
+"The <i>Next</i> button takes you to the flight selection page,\n"
+"from which you can perform your first real MAVA flight!"
+
+msgid "chkfinish_updateweb_busy"
+msgstr "Updating check flight status..."
+
+msgid "chkfinish_passedset_failed"
+msgstr "Failed to update the check flight status"
+
+msgid "chkfinish_passedset_failed_title"
+msgstr "Update failure"
+
+msgid "chkfinish_passedset_failed_secondary"
+msgstr ""
+"You can retry later, or save your PIREP and send it "
+"to your mentor at MAVA, who can arrange for your flight "
+"to be accepted."
Index: locale/hu/mlx.po
===================================================================
--- locale/hu/mlx.po	(revision 764)
+++ locale/hu/mlx.po	(revision 769)
@@ -781,5 +781,5 @@
 msgstr "Emelke_dési profil:"
 
-msgid "simbrief_climb_profile_tootip"
+msgid "simbrief_climb_profile_tooltip"
 msgstr "Válaszd ki az emelkedési profilt"
 
@@ -787,5 +787,5 @@
 msgstr "_Utazó profil:"
 
-msgid "simbrief_cruise_profile_tootip"
+msgid "simbrief_cruise_profile_tooltip"
 msgstr "Válaszd ki az utazó profilt"
 
@@ -793,5 +793,5 @@
 msgstr "Süll_yedési profil:"
 
-msgid "simbrief_descent_profile_tootip"
+msgid "simbrief_descent_profile_tooltip"
 msgstr "Válaszd ki a süllyedési profilt"
 
@@ -2254,14 +2254,14 @@
 msgstr ""
 "Gratulálunk! Ezennel a MAVA pilótája lettél\n"
-"STU (student - tanuló) státuszban.\n"
+"STU (student - jelölt) státuszban.\n"
 "\n"
 "Pilóta azonosítód: <b>%s</b>.\n"
 "\n"
-"Most a tanulók oldala fog megjelenni,\n"
+"Most a jelöltek oldala fog megjelenni,\n"
 "ahol kezdeményezheted a belépő teszt\n"
 "kitöltését vagy elindíthatod az ellenőrző\n"
 "repülést.\n"
 "\n"
-"A tanulók oldalára bármikor visszatérhetsz\n"
+"A jelöltek oldalára bármikor visszatérhetsz\n"
 "ha elindítod a programot és bejelentkezel."
 
@@ -2269,5 +2269,5 @@
 msgstr ""
 "Gratulálunk! Ezennel a MAVA pilótája lettél\n"
-"STU (student - tanuló) státuszban.\n"
+"STU (student - jelölt) státuszban.\n"
 "\n"
 "Pilóta azonosítód: <b>%s</b>.\n"
@@ -2300,5 +2300,5 @@
 
 msgid "student_title"
-msgstr "Tanuló"
+msgstr "Jelölt"
 
 msgid "student_help"
@@ -2327,2 +2327,104 @@
 "Ha a gomb inaktív, a vizsgát korábban már sikeresen teljesítetted. "
 "A gomb állapota csak néhány perccel a vizsga teljesítése után frissül."
+
+msgid "student_check_flight_status"
+msgstr "Ellenőrző repülés:"
+
+msgid "student_check_flight_passed"
+msgstr "<span color=\"darkgreen\"><b>Sikeres</b></span>"
+
+msgid "student_check_flight_not_passed"
+msgstr "<span color=\"orange\"><b>Még nem hajtottad végre</b></span>"
+
+msgid "student_check_flight"
+msgstr "Ellenőrző _repülés"
+
+msgid "student_check_flight_tooltip"
+msgstr ""
+"Miután kiválasztottad a kívánt repülőgép típust, erre a gombra kattintva "
+"elindíthatod az ellenőrző repülést. Ennek során egy LHBP-ről "
+"(a ferihegyi repülőtérről) induló és oda érkező repülést kell végezned "
+"megfelelő SID és érkezési transition alkalmazásával."
+
+msgid "student_fo"
+msgstr "Gratulálunk!"
+
+msgid "student_fo_secondary"
+msgstr ""
+"Megkaptad az <b>első tiszt</b> (FO) rangot.\n\n"
+"Ha az OK-ra kattintasz, a járatválasztó oldalra kerülsz, "
+"ahonnan teljesítheted az első igazi MAVA járatodat!"
+
+msgid "chkfinish_title"
+msgstr "Az ellenőrző repülés vége"
+
+msgid "chkfinish_passed_begin"
+msgstr ""
+"<b><span size=\"xx-large\">Gratulálunk!</span></b>\n"
+"\n"
+"Sikeresen teljesítetted az ellenőrző repülést."
+
+msgid "chkfinish_passed_fo"
+msgstr ""
+" \n\n"
+"A rangod <b>első tiszt</b> (FO) lett."
+
+msgid "chkfinish_passed_end"
+msgstr ""
+" \n\n"
+"Ha kívánod, a naplóban megnézheted, hogy vétettél-e valamilyen hibát."
+
+msgid "chkfinish_failed"
+msgstr ""
+"Sajnos az ellenőrző repülésed <b>sikertelen</b>.\n"
+"\n"
+"A naplóban megnézheted, milyen hibákat vétettel."
+
+msgid "chkfinish_savepirep"
+msgstr ""
+" \n\n"
+"Ha lennének további kérdéseid, a PIREP-et elmentheted, \n"
+"és elküldheted mentorodnak, hogy ő megnézhesse, és\n"
+"hasznos tanácsokkal szolgálhasson."
+
+msgid "chkfinish_next_student_begin"
+msgstr ""
+" \n\n"
+"Az <i>Előre</i> gomb visszavisz a <i>Jelölt</i> oldalra,"
+
+msgid "chkfinish_next_student_nothing"
+msgstr ""
+" \n"
+"ahol kezdeményezheted egy újabb ellenőrző repülés végrehajtását, "
+"vagy az elméleti vizsga letételét.."
+
+msgid "chkfinish_next_student_no_exam"
+msgstr ""
+" \n"
+"ahol kezdeményezheted az elméleti vizsga letételét."
+
+msgid "chkfinish_next_student_no_flight"
+msgstr ""
+" \n"
+"ahol egy újabb ellenőrző repülést kezdeményezhetsz."
+
+msgid "chkfinish_next"
+msgstr ""
+" \n\n"
+"Az <i>Előre</i> gombbal a járatválasztó oldara kerülsz,\n"
+"ahonnan elindíthatod első igazi MAVA járatod teljesítését!"
+
+msgid "chkfinish_updateweb_busy"
+msgstr "Az ellenőrző repülés státuszának frissítése..."
+
+msgid "chkfinish_passedset_failed"
+msgstr "Nem sikerült frissíteni az ellenőrző repülés státuszát"
+
+msgid "chkfinish_passedset_failed_title"
+msgstr "Frissítési hiba"
+
+msgid "chkfinish_passedset_failed_secondary"
+msgstr ""
+"Megpróbálhatod később, vagy elmentheted a PIREP-et, és "
+"elküldheted a mentorodnak, aki tesz róla, hogy elfogadják az "
+"ellenőrző repülésedet."
Index: src/mlx/gui/flight.py
===================================================================
--- src/mlx/gui/flight.py	(revision 764)
+++ src/mlx/gui/flight.py	(revision 769)
@@ -169,5 +169,6 @@
 
     def addButton(self, label, default = False, sensitive = True,
-                  tooltip = None, clicked = None, padding = 4):
+                  tooltip = None, clicked = None, padding = 4,
+                  clickedArg = None):
         """Add a button with the given label.
 
@@ -183,5 +184,8 @@
             button.set_tooltip_text(tooltip)
         if clicked is not None:
-            button.connect("clicked", clicked)
+            if clickedArg is None:
+                button.connect("clicked", clicked)
+            else:
+                button.connect("clicked", clicked, clickedArg)
         return button
 
@@ -1376,10 +1380,13 @@
                                           xstr("student_help"))
 
+
+        self._getEntryExamStatusCancelled = False
+
         alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
                                   xscale = 0.5, yscale = 0.0)
 
-        table = gtk.Table(2, 2)
+        table = gtk.Table(6, 4)
         table.set_row_spacings(4)
-        table.set_col_spacings(32)
+        table.set_col_spacings(0)
         table.set_homogeneous(False)
         alignment.add(table)
@@ -1391,6 +1398,8 @@
                                        xscale=0.0, yscale = 0.0)
         label = gtk.Label(xstr("student_entry_exam_status"))
+        label.set_alignment(0.0, 0.5)
         labelAlignment.add(label)
-        table.attach(labelAlignment, 0, 1, row, row + 1, xoptions = 0)
+        labelAlignment.resize_children()
+        table.attach(labelAlignment, 0, 1, row, row + 1, xoptions = FILL)
 
         alignment = gtk.Alignment(xalign=0.0, yalign = 0.5,
@@ -1400,5 +1409,6 @@
         self._entryExamStatus.set_alignment(0.0, 0.5)
         alignment.add(self._entryExamStatus)
-        table.attach(alignment, 1, 2, row, row + 1)
+        alignment.resize_children()
+        table.attach(alignment, 1, 4, row, row + 1)
 
         row += 1
@@ -1411,15 +1421,84 @@
 
         buttonAlignment.add(button)
-        table.attach(buttonAlignment, 0, 2, row, row + 1,
+        table.attach(buttonAlignment, 0, 4, row, row + 1, xoptions = FILL,
                      ypadding = 4)
+
+        row += 3
+
+        labelAlignment = gtk.Alignment(xalign=0.0, yalign = 0.5,
+                                       xscale=0.0, yscale = 0.0)
+        label = gtk.Label(xstr("student_check_flight_status"))
+        labelAlignment.add(label)
+        table.attach(labelAlignment, 0, 1, row, row + 1, xoptions = FILL)
+
+        alignment = gtk.Alignment(xalign=0.0, yalign = 0.5,
+                                  xscale=1.0, yscale = 0.0)
+        self._checkFlightStatus = gtk.Label()
+        self._checkFlightStatus.set_use_markup(True)
+        self._checkFlightStatus.set_alignment(0.0, 0.5)
+        alignment.add(self._checkFlightStatus)
+        table.attach(alignment, 1, 4, row, row + 1)
+
+        row += 1
+
+        alignment = gtk.Alignment(xalign=0.0, xscale=1.0)
+
+        hbox = gtk.HBox()
+        hbox.set_homogeneous(False)
+        hbox.set_spacing(0)
+
+        aircraftTypesModel = gtk.ListStore(str, int)
+        for aircraftType in web.BookedFlight.checkFlightTypes:
+            aircraftTypesModel.append([aircraftNames[aircraftType],
+                                       aircraftType])
+
+        aircraftTypeAlignment = gtk.Alignment(xalign = 0.0, xscale = 1.0)
+
+        self._aircraftType = gtk.ComboBox(model = aircraftTypesModel)
+        renderer = gtk.CellRendererText()
+        self._aircraftType.pack_start(renderer, True)
+        self._aircraftType.add_attribute(renderer, "text", 0)
+        self._aircraftType.set_tooltip_text(xstr("student_check_flight_type_tooltip"))
+        self._aircraftType.set_active(0)
+
+        aircraftTypeAlignment.add(self._aircraftType)
+
+        hbox.pack_start(aircraftTypeAlignment, False, False, 0)
+
+        buttonAlignment = gtk.Alignment(xalign=0.0, xscale=1.0)
+        button = self._checkFlightButton = gtk.Button(xstr("student_check_flight"))
+        button.set_use_underline(True)
+        button.connect("clicked", self._checkFlightClicked)
+        button.set_tooltip_text(xstr("student_check_flight_tooltip"))
+
+        hbox.pack_start(button, True, True, 0)
+
+        alignment.add(hbox)
+        table.attach(alignment, 0, 4, row, row + 1, xoptions = FILL)
+
+    @property
+    def aircraftType(self):
+        """Get the type of the aircraft used to perform the check flight."""
+        index = self._aircraftType.get_active()
+        return self._aircraftType.get_model()[index][1]
 
     def activate(self):
         """Activate the student page."""
+        print "StudentPage.activate"
+        self._getEntryExamStatusCancelled = False
+
         loginResult = self._wizard.loginResult
         self._entryExamLink = loginResult.entryExamLink
 
         self._updateEntryExamStatus(loginResult.entryExamPassed)
-
         self._getEntryExamStatus()
+
+        # FIXME: call with real value
+        self._updateCheckFlightStatus(self._wizard.loginResult.checkFlightStatus)
+
+    def finalize(self):
+        """Finalize the page."""
+        print "StudentPage.finalize"
+        self._getEntryExamStatusCancelled = True
 
     def _entryExamClicked(self, button):
@@ -1429,7 +1508,8 @@
     def _getEntryExamStatus(self):
         """Initiate the query of the entry exam status after the interval."""
-        gobject.timeout_add(StudentPage._entryExamStatusQueryInterval,
-                            lambda: self._wizard.gui.webHandler. \
-                            getEntryExamStatus(self._entryExamStatusCallback))
+        if not self._getEntryExamStatusCancelled:
+            gobject.timeout_add(StudentPage._entryExamStatusQueryInterval,
+                                lambda: self._wizard.gui.webHandler. \
+                                getEntryExamStatus(self._entryExamStatusCallback))
 
     def _entryExamStatusCallback(self, returned, result):
@@ -1440,8 +1520,11 @@
         """Called when the entry exam status is availabe."""
         print "_handleEntryExamStatus", returned, result
-        if returned:
+        if returned and not self._getEntryExamStatusCancelled:
             self._entryExamLink = result.entryExamLink
             self._updateEntryExamStatus(result.entryExamPassed)
-            self._getEntryExamStatus()
+            if result.madeFO:
+                self._madeFO()
+            else:
+                self._getEntryExamStatus()
 
     def _updateEntryExamStatus(self, passed):
@@ -1452,4 +1535,42 @@
         self._entryExamStatus.set_use_markup(True)
         self._entryExamButton.set_sensitive(not passed)
+
+    def _checkFlightClicked(self, button):
+        """Called when the check flight button is clicked."""
+        aircraftType = self.aircraftType
+        self._wizard._bookedFlight = \
+            web.BookedFlight.forCheckFlight(aircraftType)
+        self._wizard.gui.enableFlightInfo(aircraftType)
+        self._wizard.jumpPage("connect")
+
+    def _updateCheckFlightStatus(self, passed):
+        """Update the status of the check flight."""
+        self._aircraftType.set_sensitive(not passed)
+        self._checkFlightStatus.set_text(xstr("student_check_flight_passed")
+                                         if passed else
+                                         xstr("student_check_flight_not_passed"))
+        self._checkFlightStatus.set_use_markup(True)
+        self._checkFlightButton.set_sensitive(not passed)
+
+    def _madeFO(self):
+        """Handle the event when the pilot has become a first officer."""
+        wizard = self._wizard
+        loginResult = wizard.loginResult
+        loginResult.rank = "FO"
+
+        gui = wizard.gui
+
+        dialog = gtk.MessageDialog(parent = gui.mainWindow,
+                                   type = MESSAGETYPE_INFO,
+                                   message_format = xstr("student_fo"))
+
+        dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
+        dialog.set_title(WINDOW_TITLE_BASE)
+        secondary = xstr("student_fo_secondary")
+        dialog.format_secondary_markup(secondary)
+        dialog.run()
+        dialog.hide()
+
+        gui.reset()
 
 #-----------------------------------------------------------------------------
@@ -1575,5 +1696,6 @@
         self.addCancelFlightButton()
 
-        self.addPreviousButton(clicked = self._backClicked)
+        self._previousButton = \
+          self.addPreviousButton(clicked = self._backClicked)
 
         self._button = self.addButton(xstr("button_connect"), default = True,
@@ -1609,4 +1731,6 @@
             self._selectMSFS.set_active(config.defaultMSFS)
             self._selectXPlane.set_active(not config.defaultMSFS)
+
+        self._previousButton.set_sensitive(not self._wizard.entranceExam)
 
     def finalize(self):
@@ -2125,5 +2249,5 @@
         alternate = self._alternate.get_text()
         self._button.set_sensitive(cruiseLevel>=50 and self._getRoute()!="" and
-                                   len(alternate)==4)
+                                   (len(alternate)==4 or self._wizard.entranceExam))
 
     def _cruiseLevelChanged(self, *arg):
@@ -4224,15 +4348,18 @@
     def _forwardClicked(self, button):
         """Called when the forward button is clicked."""
-        aircraft = self._wizard.gui.flight.aircraft
+        wizard = self._wizard
+
+        aircraft = wizard.gui.flight.aircraft
         aircraft.updateVRef()
         aircraft.updateLandingAntiIce()
-        if self._wizard.gui.config.onlineGateSystem and \
-           self._wizard.loggedIn and not self._completed and \
-           self._wizard.bookedFlight.arrivalICAO=="LHBP" and \
-           not self._wizard.entranceExam:
-            self._wizard.getFleet(callback = self._fleetRetrieved,
-                                  force = True)
+        if wizard.gui.config.onlineGateSystem and \
+           wizard.loggedIn and not self._completed and \
+           wizard.bookedFlight.arrivalICAO=="LHBP" and \
+           not wizard.entranceExam:
+            wizard.getFleet(callback = self._fleetRetrieved, force = True)
+        elif wizard.entranceExam:
+            self._handleEntranceExamDone()
         else:
-            self._wizard.nextPage()
+            wizard.nextPage()
 
     def _fleetRetrieved(self, fleet):
@@ -4260,4 +4387,146 @@
             self._updatingMETAR = False
 
+    def _handleEntranceExamDone(self):
+        """Handle the end of the entrance exam.
+
+        If the there was a NO-GO fault, notify the user that exam is a failure
+        and take them back to the student page. Otherwise congratulate, update
+        the database to reflect that the exam has been taken and go back to the
+        student page."""
+        self._wizard.jumpPage("chkfinish")
+
+#-----------------------------------------------------------------------------
+
+class PIREPSaveHelper(object):
+    """A helper to use for saving PIREPs."""
+    def __init__(self, wizard):
+        """Construct the helper."""
+        super(PIREPSaveHelper, self).__init__()
+
+        self._wizard = wizard
+
+        self._lastSavePath = None
+        self._savePIREPDialog = None
+
+    def addButton(self, page):
+        """Add a button to save the PIREP to the given page."""
+        return page.addButton(xstr("finish_save"), sensitive = False,
+                              clicked = self._saveClicked,
+                              tooltip = xstr("finish_save_tooltip"),
+                              clickedArg = page)
+
+    def autoSavePIREP(self):
+        """Perform the automatic saving of the PIREP."""
+        self._lastSavePath = os.path.join(self._wizard.gui.config.pirepDirectory,
+                                          self._getDefaultPIREPName())
+        self._lastSavePath = text2unicode(self._lastSavePath)
+        self._savePIREP(automatic = True)
+
+    def _getDefaultPIREPName(self):
+        """Get the default name of the PIREP."""
+        gui = self._wizard.gui
+
+        bookedFlight = gui.bookedFlight
+        tm = time.gmtime()
+
+        pilotID = self._wizard.pilotID
+        if pilotID: pilotID += " "
+        return "%s%s %02d%02d %s-%s.pirep" % \
+               (pilotID, str(bookedFlight.departureTime.date()),
+                tm.tm_hour, tm.tm_min,
+                bookedFlight.departureICAO, bookedFlight.arrivalICAO)
+
+    def _saveClicked(self, button, page):
+        """Called when the Save PIREP button is clicked."""
+        gui = self._wizard.gui
+
+        fileName = self._getDefaultPIREPName()
+
+        dialog = self._getSaveDialog()
+
+        if self._lastSavePath is None:
+            pirepDirectory = gui.config.pirepDirectory
+            if pirepDirectory is not None:
+                dialog.set_current_folder(pirepDirectory)
+        else:
+            dialog.set_current_folder(os.path.dirname(self._lastSavePath))
+
+        dialog.set_current_name(fileName)
+        result = dialog.run()
+        dialog.hide()
+
+        if result==RESPONSETYPE_OK:
+            self._lastSavePath = text2unicode(dialog.get_filename())
+            self._savePIREP(page)
+
+    def _savePIREP(self, page, automatic = False):
+        """Perform the saving of the PIREP."""
+
+        gui = self._wizard.gui
+
+        if automatic:
+            gui.beginBusy(xstr("finish_autosave_busy"))
+
+        pirep = PIREP(gui.flight)
+        error = pirep.save(self._lastSavePath)
+
+        if automatic:
+            gui.endBusy()
+
+        if error:
+            type = MESSAGETYPE_ERROR
+            message = xstr("finish_save_failed")
+            secondary = xstr("finish_save_failed_sec") % (text2unicode(error),)
+        else:
+            type = MESSAGETYPE_INFO
+            message = xstr("finish_save_done")
+            if automatic:
+                secondary = xstr("finish_save_done_sec") % (self._lastSavePath,)
+            else:
+                secondary = None
+            page.setPIREPSaved()
+
+        dialog = gtk.MessageDialog(parent = gui.mainWindow,
+                                   type = type, message_format = message)
+        dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
+        dialog.set_title(WINDOW_TITLE_BASE)
+        if secondary is not None:
+            dialog.format_secondary_markup(secondary)
+
+        dialog.run()
+        dialog.hide()
+
+        return pirepSaved
+
+    def _getSaveDialog(self):
+        """Get the PIREP saving dialog.
+
+        If it does not exist yet, create it."""
+        if self._savePIREPDialog is None:
+            gui = self._wizard.gui
+            dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
+                                           xstr("finish_save_title"),
+                                           action = FILE_CHOOSER_ACTION_SAVE,
+                                           buttons = (gtk.STOCK_CANCEL,
+                                                      RESPONSETYPE_CANCEL,
+                                                      gtk.STOCK_OK, RESPONSETYPE_OK),
+                                           parent = gui.mainWindow)
+            dialog.set_modal(True)
+            dialog.set_do_overwrite_confirmation(True)
+
+            filter = gtk.FileFilter()
+            filter.set_name(xstr("file_filter_pireps"))
+            filter.add_pattern("*.pirep")
+            dialog.add_filter(filter)
+
+            filter = gtk.FileFilter()
+            filter.set_name(xstr("file_filter_all"))
+            filter.add_pattern("*.*")
+            dialog.add_filter(filter)
+
+            self._savePIREPDialog = dialog
+
+        return self._savePIREPDialog
+
 #-----------------------------------------------------------------------------
 
@@ -4269,5 +4538,5 @@
                      ("flighttype_charter", const.FLIGHTTYPE_CHARTER) ]
 
-    def __init__(self, wizard):
+    def __init__(self, wizard, saveHelper):
         """Construct the finish page."""
         help = xstr("finish_help") + xstr("finish_help_goodtime")
@@ -4456,10 +4725,6 @@
         self.addPreviousButton(clicked = self._backClicked)
 
-        self._saveButton = self.addButton(xstr("finish_save"),
-                                          sensitive = False,
-                                          clicked = self._saveClicked,
-                                          tooltip = xstr("finish_save_tooltip"))
-        self._savePIREPDialog = None
-        self._lastSavePath = None
+        self._saveHelper = saveHelper
+        self._saveButton = saveHelper.addButton(self)
 
         self._tooBigTimeDifference = False
@@ -4561,5 +4826,5 @@
         if gui.config.pirepAutoSave and sensitive and not wasSensitive:
             if gui.isWizardActive():
-                self._autoSavePIREP()
+                self._saveHelper.autoSavePIREP()
             else:
                 self._deferredAutoSave = True
@@ -4576,13 +4841,10 @@
         super(FinishPage, self).grabDefault()
         if self._deferredAutoSave:
-            self._autoSavePIREP()
+            self._saveHelper.autoSavePIREP()
             self._deferredAutoSave = False
 
-    def _autoSavePIREP(self):
-        """Perform the automatic saving of the PIREP."""
-        self._lastSavePath = os.path.join(self._wizard.gui.config.pirepDirectory,
-                                          self._getDefaultPIREPName())
-        self._lastSavePath = text2unicode(self._lastSavePath)
-        self._savePIREP(automatic = True)
+    def setPIREPSaved(self):
+        """Mark the PIREP as saved."""
+        self._pirepSaved = True
 
     def _backClicked(self, button):
@@ -4616,110 +4878,4 @@
 
         gui.reset()
-
-    def _getDefaultPIREPName(self):
-        """Get the default name of the PIREP."""
-        gui = self._wizard.gui
-
-        bookedFlight = gui.bookedFlight
-        tm = time.gmtime()
-
-        pilotID = self._wizard.pilotID
-        if pilotID: pilotID += " "
-        return "%s%s %02d%02d %s-%s.pirep" % \
-               (pilotID, str(bookedFlight.departureTime.date()),
-                tm.tm_hour, tm.tm_min,
-                bookedFlight.departureICAO, bookedFlight.arrivalICAO)
-
-
-    def _saveClicked(self, button):
-        """Called when the Save PIREP button is clicked."""
-        gui = self._wizard.gui
-
-        fileName = self._getDefaultPIREPName()
-
-        dialog = self._getSaveDialog()
-
-        if self._lastSavePath is None:
-            pirepDirectory = gui.config.pirepDirectory
-            if pirepDirectory is not None:
-                dialog.set_current_folder(pirepDirectory)
-        else:
-            dialog.set_current_folder(os.path.dirname(self._lastSavePath))
-
-        dialog.set_current_name(fileName)
-        result = dialog.run()
-        dialog.hide()
-
-        if result==RESPONSETYPE_OK:
-            self._lastSavePath = text2unicode(dialog.get_filename())
-            self._savePIREP()
-
-    def _savePIREP(self, automatic = False):
-        """Perform the saving of the PIREP."""
-
-        gui = self._wizard.gui
-
-        if automatic:
-            gui.beginBusy(xstr("finish_autosave_busy"))
-
-        pirep = PIREP(gui.flight)
-        error = pirep.save(self._lastSavePath)
-
-        if automatic:
-            gui.endBusy()
-
-        if error:
-            type = MESSAGETYPE_ERROR
-            message = xstr("finish_save_failed")
-            secondary = xstr("finish_save_failed_sec") % (text2unicode(error),)
-        else:
-            type = MESSAGETYPE_INFO
-            message = xstr("finish_save_done")
-            if automatic:
-                secondary = xstr("finish_save_done_sec") % (self._lastSavePath,)
-            else:
-                secondary = None
-            self._pirepSaved = True
-
-        dialog = gtk.MessageDialog(parent = gui.mainWindow,
-                                   type = type, message_format = message)
-        dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
-        dialog.set_title(WINDOW_TITLE_BASE)
-        if secondary is not None:
-            dialog.format_secondary_markup(secondary)
-
-        dialog.run()
-        dialog.hide()
-
-    def _getSaveDialog(self):
-        """Get the PIREP saving dialog.
-
-        If it does not exist yet, create it."""
-        if self._savePIREPDialog is None:
-            gui = self._wizard.gui
-            dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
-                                           xstr("finish_save_title"),
-                                           action = FILE_CHOOSER_ACTION_SAVE,
-                                           buttons = (gtk.STOCK_CANCEL,
-                                                      RESPONSETYPE_CANCEL,
-                                                      gtk.STOCK_OK, RESPONSETYPE_OK),
-                                           parent = gui.mainWindow)
-            dialog.set_modal(True)
-            dialog.set_do_overwrite_confirmation(True)
-
-            filter = gtk.FileFilter()
-            filter.set_name(xstr("file_filter_pireps"))
-            filter.add_pattern("*.pirep")
-            dialog.add_filter(filter)
-
-            filter = gtk.FileFilter()
-            filter.set_name(xstr("file_filter_all"))
-            filter.add_pattern("*.*")
-            dialog.add_filter(filter)
-
-            self._savePIREPDialog = dialog
-
-        return self._savePIREPDialog
-
 
     def _sendClicked(self, button):
@@ -4816,4 +4972,121 @@
 #-----------------------------------------------------------------------------
 
+class CheckFlightFinishPage(Page):
+    """Finish page for a check flight."""
+    def __init__(self, wizard, saveHelper):
+        """Construct the check flight finish page."""
+        super(CheckFlightFinishPage, self).__init__(wizard,
+                                                    "chkfinish",
+                                                    xstr("chkfinish_title"),
+                                                    "")
+
+        alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
+                                  xscale = 1.0, yscale = 1.0)
+        self._label = gtk.Label()
+        alignment.add(self._label)
+
+        self.setMainWidget(alignment)
+
+        self._saveHelper = saveHelper
+        self._saveButton = saveHelper.addButton(self)
+
+        self._button = self.addNextButton(sensitive = False,
+                                          clicked =  self._forwardClicked)
+
+    def activate(self):
+        """Activate the page."""
+        wizard = self._wizard
+        loginResult = wizard.loginResult
+        gui = wizard.gui
+        rating = gui.flight.logger.getRating()
+
+        if rating>=0:
+            loginResult.checkFlightStatus = True
+
+        firstOfficer = \
+          loginResult.entryExamPassed and loginResult.checkFlightStatus
+
+        if firstOfficer:
+            loginResult.rank = "FO"
+
+        if rating<0:
+            mainMessage = xstr("chkfinish_failed")
+        else:
+            mainMessage = xstr("chkfinish_passed_begin")
+            if firstOfficer:
+                mainMessage += xstr("chkfinish_passed_fo")
+            mainMessage += xstr("chkfinish_passed_end")
+
+        if firstOfficer:
+            nextMessage = xstr("chkfinish_next")
+        else:
+            nextMessage = xstr("chkfinish_next_student_begin")
+            if not loginResult.entryExamPassed and \
+               not loginResult.checkFlightStatus:
+                nextMessage += xstr("chkfinish_next_student_nothing")
+            elif loginResult.entryExamPassed and \
+                 not loginResult.checkFlightStatus:
+                nextMessage += xstr("chkfinish_next_student_no_flight")
+            elif not loginResult.entryExamPassed and \
+                 loginResult.checkFlightStatus:
+                nextMessage += xstr("chkfinish_next_student_no_exam")
+
+        self._label.set_text(mainMessage +
+                             xstr("chkfinish_savepirep") +
+                             nextMessage)
+        self._label.set_use_markup(True)
+        self._label.set_alignment(0.5, 0.0)
+
+        self._saveButton.set_sensitive(True)
+        self._button.set_sensitive(True)
+
+    def _forwardClicked(self, button):
+        """Jump to the student page if there are some tasks to do,
+        or to the flight selection page, if the pilot is allowed to perform
+        MAVA flights."""
+        wizard = self._wizard
+        gui = wizard.gui
+
+        loginResult = wizard.loginResult
+        if loginResult.checkFlightStatus:
+            gui.beginBusy(xstr("chkfinish_updateweb_busy"))
+            gui.webHandler.setCheckFlightPassed(self._checkFlightPassedSetCallback,
+                                                wizard.checkFlightAircraftType)
+        else:
+            self._resetGUI()
+
+    def _checkFlightPassedSetCallback(self, returned, result):
+        """Called when the check flight status has been set."""
+        gobject.idle_add(self._checkFlightPassedSet, returned, result)
+
+    def _checkFlightPassedSet(self, returned, result):
+        """Handle the result of an attempt to set the check flight status."""
+        gui = self._wizard.gui
+
+        gui.endBusy()
+
+        if returned:
+            self._resetGUI()
+        else:
+            dialog = gtk.MessageDialog(parent = gui.mainWindow,
+                                       type = MESSAGETYPE_ERROR,
+                                       message_format =
+                                       xstr("chkfinish_passedset_failed"))
+            dialog.set_title(WINDOW_TITLE_BASE + " - " +
+                             xstr("chkfinish_passedset_failed_title"))
+            dialog.format_secondary_markup(xstr("chkfinish_passedset_failed_secondary"))
+
+            dialog.add_button(xstr("button_ok"), 0)
+
+            dialog.run()
+            dialog.hide()
+
+    def _resetGUI(self):
+        """Reset the GUI."""
+        gui = self._wizard.gui
+        gui.reset()
+
+#-----------------------------------------------------------------------------
+
 class Wizard(gtk.VBox):
     """The flight wizard."""
@@ -4832,5 +5105,6 @@
         self._pages.append(GateSelectionPage(self))
         self._pages.append(RegisterPage(self))
-        self._pages.append(StudentPage(self))
+        self._studentPage = StudentPage(self)
+        self._pages.append(self._studentPage)
         self._pages.append(ConnectPage(self))
         self._payloadPage = PayloadPage(self)
@@ -4856,6 +5130,10 @@
         self._landingPage = LandingPage(self)
         self._pages.append(self._landingPage)
-        self._finishPage = FinishPage(self)
+
+        pirepSaveHelper = PIREPSaveHelper(self)
+
+        self._finishPage = FinishPage(self, pirepSaveHelper)
         self._pages.append(self._finishPage)
+        self._pages.append(CheckFlightFinishPage(self, pirepSaveHelper))
 
         self._requestedWidth = None
@@ -4916,4 +5194,9 @@
         """Get the login result."""
         return self._loginResult
+
+    @property
+    def checkFlightAircraftType(self):
+        """Get the type of the aircraft used to perform the check flight."""
+        return self._studentPage.aircraftType
 
     def setCurrentPage(self, index, finalize = False, fromPageShift = None):
@@ -5218,5 +5501,16 @@
     def reloadFlights(self, callback):
         """Reload the flights from the MAVA server."""
-        self.login(callback, None, None, None)
+        self.login(callback, None, None)
+
+    def cancelFlight(self, reloadCallback):
+        """Cancel the flight.
+
+        If it is an entry exam flight, we go back to the student page.
+        Otherwise we reload the flights."""
+        if self.entranceExam:
+            self.reset(None)
+            self.jumpPage("student")
+        else:
+            self.reloadFlights(reloadCallback)
 
     def cruiseLevelChanged(self):
Index: src/mlx/gui/gui.py
===================================================================
--- src/mlx/gui/gui.py	(revision 755)
+++ src/mlx/gui/gui.py	(revision 769)
@@ -581,5 +581,5 @@
 
         if self.loggedIn:
-            self._wizard.reloadFlights(self._handleReloadResult)
+            self._wizard.cancelFlight(self._handleReloadResult)
         else:
             self._wizard.reset(None)
Index: src/mlx/rpc.py
===================================================================
--- src/mlx/rpc.py	(revision 764)
+++ src/mlx/rpc.py	(revision 769)
@@ -262,5 +262,5 @@
                                   self._server.getEntryExamStatus(sessionID))
         return (value["entryExamPassed"], value["entryExamLink"],
-                value["checkFlightStatus"])
+                value["checkFlightStatus"], value["madeFO"])
 
     def getFleet(self):
@@ -292,4 +292,9 @@
         self._performCall(lambda sessionID:
                           self._server.updateOnlineACARS(sessionID, acars))
+
+    def setCheckFlightPassed(self, type):
+        """Mark the check flight of the user passed with the given type."""
+        self._performCall(lambda sessionID:
+                          self._server.setCheckFlightPassed(sessionID, type))
 
     def _performCall(self, callFn, acceptResults = []):
Index: src/mlx/web.py
===================================================================
--- src/mlx/web.py	(revision 764)
+++ src/mlx/web.py	(revision 769)
@@ -91,4 +91,7 @@
                       const.AIRCRAFT_B462  : "146" }
 
+    checkFlightTypes = [ const.AIRCRAFT_B736, const.AIRCRAFT_B737,
+                         const.AIRCRAFT_B738, const.AIRCRAFT_DH8D ]
+
     @staticmethod
     def getDateTime(date, time):
@@ -96,4 +99,36 @@
         return datetime.datetime.strptime(date + " " + time,
                                           "%Y-%m-%d %H:%M:%S")
+
+    @staticmethod
+    def forCheckFlight(aircraftType):
+        """Create a booked flight for a check flight with the given aircraft
+        type."""
+        flight = BookedFlight()
+
+        flight.departureICAO = "LHBP"
+        flight.arrivalICAO = "LHBP"
+
+        flight.aircraftType = aircraftType
+        flight.aircraftTypeName = BookedFlight.TYPE2TYPECODE[aircraftType]
+
+        # FIXME: perhaps find one for the type
+        flight.tailNumber = "HA-CHK"
+        flight.callsign = "HA-CHK"
+
+        flight.numPassengers = 0
+        flight.numCrew = 2
+        flight.bagWeight = 0
+        flight.cargoWeight = 0
+        flight.mailWeight = 0
+        flight.route = "DCT"
+
+        t = datetime.datetime.now() + datetime.timedelta(minutes = 20)
+        flight.departureTime = datetime.datetime(t.year, t.month, t.day,
+                                                 t.hour, t.minute)
+        t = flight.departureTime + datetime.timedelta(minutes = 30)
+        flight.arrivalTime = datetime.datetime(t.year, t.month, t.day,
+                                               t.hour, t.minute)
+
+        return flight
 
     def __init__(self, id = None):
@@ -741,4 +776,6 @@
                 result.entryExamLink = reply[1]
                 result.checkFlightStatus = reply[2]
+                if reply[3]:
+                    result.rank = "FO"
 
         return result
@@ -761,4 +798,5 @@
         result.entryExamLink = reply[1]
         result.checkFlightStatus = reply[2]
+        result.madeFO = reply[3]
 
         return result
@@ -1156,4 +1194,19 @@
 #------------------------------------------------------------------------------
 
+class SetCheckFlightPassed(RPCRequest):
+    """A request to mark the user as one having passed the check flight."""
+    def __init__(self, client, callback, aircraftType):
+        """Construct the request for the given type."""
+        super(SetCheckFlightPassed, self).__init__(client, callback)
+        self._aircraftType = aircraftType
+
+    def run(self):
+        """Perform the update."""
+        aircraftType = BookedFlight.TYPE2TYPECODE[self._aircraftType]
+        self._client.setCheckFlightPassed(aircraftType)
+        return Result()
+
+#------------------------------------------------------------------------------
+
 class Handler(threading.Thread):
     """The handler for the web services.
@@ -1232,4 +1285,9 @@
         self._addRequest(SendBugReport(callback, summary, description, email))
 
+    def setCheckFlightPassed(self, callback, aircraftType):
+        """Mark the check flight as passed."""
+        self._addRequest(SetCheckFlightPassed(self._rpcClient,
+                                              callback, aircraftType))
+
     def run(self):
         """Process the requests."""
