diff --git a/lib/emitter/src/Controller.php b/lib/emitter/src/Controller.php
index 93b6271606d899b37563b684014e1fe02b2dfa1f..7bd641ff739bd58eafa4cafc6807f2af8e61324b 100755
--- a/lib/emitter/src/Controller.php
+++ b/lib/emitter/src/Controller.php
@@ -29,7 +29,8 @@ class Controller extends PhpObj {
         'module_viewed' => 'ModuleViewed',
         'attempt_started' => 'AttemptStarted',
         'attempt_abandoned' => 'AttemptCompleted',
-        'attempt_completed' => 'AttemptCompleted',
+        'attempt_submitted' => 'AttemptCompleted',
+        'attempt_reviewed' => 'AttemptReviewed',
         'attempt_question_completed' => 'QuestionAnswered',
         'user_loggedin' => 'UserLoggedin',
         'user_loggedout' => 'UserLoggedout',
diff --git a/lib/emitter/src/Events/AttemptReviewed.php b/lib/emitter/src/Events/AttemptReviewed.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a061efa5a1d0f813c54ca8bf5b2557d6710a725
--- /dev/null
+++ b/lib/emitter/src/Events/AttemptReviewed.php
@@ -0,0 +1,60 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace XREmitter\Events;
+
+defined('MOODLE_INTERNAL') || die();
+
+class AttemptReviewed extends Event {
+    protected static $verbdisplay = [
+        'en' => 'reviewed'
+    ];
+
+    /**
+     * Reads data for an event.
+     * @param [String => Mixed] $opts
+     * @return [String => Mixed]
+     * @override Event
+     */
+    public function read(array $opts) {
+        return array_merge_recursive(parent::read($opts), [
+            'verb' => [
+                'id' => 'http://activitystrea.ms/schema/1.0/review',
+                'display' => $this->read_verb_display($opts),
+            ],
+            'object' => [
+                'id' => $opts['attempt_url'],
+                'definition' => [
+                    'type' => $opts['attempt_type'],
+                    'name' => [
+                        $opts['context_lang'] => $opts['attempt_name'],
+                    ],
+                    'extensions' => [
+                        $opts['attempt_ext_key'] => $opts['attempt_ext']
+                    ],
+                ],
+            ],
+            'context' => [
+                'contextActivities' => [
+                    'grouping' => [
+                        $this->read_course($opts),
+                        $this->read_module($opts),
+                    ],
+                ],
+            ],
+        ]);
+    }
+}
diff --git a/lib/expander/src/Controller.php b/lib/expander/src/Controller.php
index 8a4f5b678ea446a4e0ecb19a8b2a4eb944fc7ba9..a4484726f663ed0217c3f6a7183e733639ad2b09 100644
--- a/lib/expander/src/Controller.php
+++ b/lib/expander/src/Controller.php
@@ -50,6 +50,7 @@ class Controller extends PhpObj {
         '\mod_quiz\event\attempt_preview_started' => 'AttemptEvent',
         '\mod_quiz\event\attempt_reviewed' => 'AttemptEvent',
         '\mod_quiz\event\attempt_viewed' => 'AttemptEvent',
+        '\mod_quiz\event\attempt_submitted' => 'AttemptEvent',
         '\core\event\user_loggedin' => 'Event',
         '\core\event\user_loggedout' => 'Event',
         '\mod_assign\event\submission_graded' => 'AssignmentGraded',
diff --git a/lib/translator/src/Controller.php b/lib/translator/src/Controller.php
index 5fc142a208e19fc84ee95776cc4acb6df57b4d8a..41b5960814ec9a04ab863e381df669f7c868b8fe 100644
--- a/lib/translator/src/Controller.php
+++ b/lib/translator/src/Controller.php
@@ -51,6 +51,7 @@ class Controller extends PhpObj {
         '\mod_quiz\event\attempt_preview_started' => 'AttemptStarted',
         '\mod_quiz\event\attempt_reviewed' => ['AttemptReviewed', 'QuestionSubmitted'],
         '\mod_quiz\event\attempt_viewed' => 'ModuleViewed',
+        '\mod_quiz\event\attempt_submitted' => ['AttemptSubmitted', 'QuestionSubmitted'],
         '\core\event\user_loggedin' => 'UserLoggedin',
         '\core\event\user_loggedout' => 'UserLoggedout',
         '\mod_assign\event\submission_graded' => 'AssignmentGraded',
diff --git a/lib/translator/src/Events/AttemptSubmitted.php b/lib/translator/src/Events/AttemptSubmitted.php
new file mode 100644
index 0000000000000000000000000000000000000000..44b042e6d46c5d229bce4309422476f9b7e894da
--- /dev/null
+++ b/lib/translator/src/Events/AttemptSubmitted.php
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace MXTranslator\Events;
+
+defined('MOODLE_INTERNAL') || die();
+
+class AttemptSubmitted extends AttemptStarted {
+    /**
+     * Reads data for an event.
+     * @param [String => Mixed] $opts
+     * @return [String => Mixed]
+     * @override AttemtStarted
+     */
+    public function read(array $opts) {
+        if (isset($opts['attempt']->timefinish)) {
+            $seconds = $opts['attempt']->timefinish - $opts['attempt']->timestart;
+            $duration = "PT".(string) $seconds."S";
+        } else {
+            $duration = "PT0S";
+        }
+
+        $scoreraw = isset($opts['attempt']->sumgrades) ? $opts['attempt']->sumgrades : 0;
+        $scoremin = (float) ($opts['grade_items']->grademin ?: 0);
+        $scoremax = (float) ($opts['grade_items']->grademax ?: 0);
+        $scorepass = (float) ($opts['grade_items']->gradepass ?: null);
+        $success = false;
+        // If there is no passing score then success is unknown.
+        if ($scorepass == null) {
+            $success = null;
+        } else if ($scoreraw >= $scorepass) {
+            $success = true;
+        }
+
+        // It's possible to configure Moodle quizzes such that you can score higher than the maximum grade.
+        // This is not allowed by xAPI, so cap the raw at the min/max.
+        if ($scoreraw > $scoremax) {
+            $scoreraw = $scoremax;
+        }
+        if ($scoreraw < $scoremin) {
+            $scoreraw = $scoremin;
+        }
+
+        // Calculate scaled score as the distance from zero towards the max (or min for negative scores).
+        if ($scoreraw >= 0) {
+            $scorescaled = $scoreraw / $scoremax;
+        } else {
+            $scorescaled = $scoreraw / $scoremin;
+        }
+
+        // Determine if the attempt was marked finished.
+        if (isset($opts['attempt']->state)) {
+            $completedstate = $opts['attempt']->state === 'finished';
+        } else {
+            $completedstate = false;
+        }
+
+        return [array_merge(parent::read($opts)[0], [
+            'recipe' => 'attempt_completed',
+            'attempt_score_raw' => $scoreraw,
+            'attempt_score_min' => $scoremin,
+            'attempt_score_max' => $scoremax,
+            'attempt_score_scaled' => $scorescaled,
+            'attempt_success' => $success,
+            'attempt_completed' => $completedstate,
+            'attempt_duration' => $duration,
+        ])];
+    }
+
+}
diff --git a/tests/attempt_submitted_test.php b/tests/attempt_submitted_test.php
new file mode 100644
index 0000000000000000000000000000000000000000..33614bda7b1df3ca1aa464e92d24e609dba0302a
--- /dev/null
+++ b/tests/attempt_submitted_test.php
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace Tests;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/quiz_attempt_testcase.php');
+
+class attempt_submitted_test extends quiz_attempt_testcase {
+    protected function construct_input() {
+        return array_merge(parent::construct_input(), [
+            'eventname' => '\mod_quiz\event\attempt_submitted',
+        ]);
+    }
+}