From 72bf0c548abd2038b32e985d1568cf71d24bc007 Mon Sep 17 00:00:00 2001
From: David Curry <david.curry@totaralms.com>
Date: Tue, 24 Jun 2014 09:48:30 +1200
Subject: [PATCH] T-11185 appraisal: Add support for dynamic changes to
 assignments

Change-Id: Ia007149e3c6f39c61d0f2fa49089baeef12ce20a
---
 totara/appraisal/activation.php                    |   4 +-
 totara/appraisal/classes/observer.php              | 135 ++++
 totara/appraisal/cron.php                          |  13 +-
 totara/appraisal/db/events.php                     |   9 +-
 totara/appraisal/db/install.xml                    |  19 +-
 totara/appraisal/db/upgrade.php                    | 135 ++--
 totara/appraisal/general.php                       |   4 +
 totara/appraisal/lang/en/totara_appraisal.php      |  28 +-
 totara/appraisal/learners.php                      |  79 +-
 totara/appraisal/lib.php                           | 828 +++++++++++++++------
 totara/appraisal/lib/assign/ajax.php               |  25 +-
 totara/appraisal/lib/assign/lib.php                |  79 +-
 totara/appraisal/message.php                       |   7 +-
 totara/appraisal/missing.php                       |  72 ++
 .../rb_sources/lang/en/rb_source_appraisal.php     |   8 +
 .../appraisal/rb_sources/rb_source_appraisal.php   |  52 +-
 totara/appraisal/renderer.php                      | 250 ++++++-
 totara/appraisal/snapshot.php                      |  59 +-
 totara/appraisal/stage.php                         |   4 +
 totara/appraisal/styles.css                        |  15 +
 totara/appraisal/tests/appraisal_test.php          | 589 ++++++++++++++-
 totara/appraisal/tests/message_test.php            |   2 +-
 totara/appraisal/version.php                       |   2 +-
 totara/core/lib/assign/assigngroup_dialog.js       |  13 +-
 totara/core/lib/assign/groups/cohort.class.php     |  18 +
 totara/core/lib/assign/hierarchy.class.php         |  18 +
 totara/core/lib/assign/lib.php                     | 196 ++++-
 27 files changed, 2184 insertions(+), 479 deletions(-)
 create mode 100644 totara/appraisal/classes/observer.php
 create mode 100644 totara/appraisal/missing.php

diff --git a/totara/appraisal/activation.php b/totara/appraisal/activation.php
index 30344e8..e9f352e 100644
--- a/totara/appraisal/activation.php
+++ b/totara/appraisal/activation.php
@@ -45,7 +45,7 @@ $appraisal = new appraisal($id);
 
 switch ($action) {
     case 'activate':
-        $errors = $appraisal->validate();
+        list($errors, $warnings) = $appraisal->validate();
         if (empty($errors) && $confirm) {
             if (!confirm_sesskey()) {
                 print_error('confirmsesskeybad', 'error');
@@ -86,7 +86,7 @@ echo $output->header();
 switch ($action) {
     case 'activate':
         echo $output->heading($appraisal->name);
-        echo $output->confirm_appraisal_activation($appraisal, $errors);
+        echo $output->confirm_appraisal_activation($appraisal, $errors, $warnings);
         break;
     case 'close':
         echo $output->heading(get_string('closeappraisal', 'totara_appraisal'));
diff --git a/totara/appraisal/classes/observer.php b/totara/appraisal/classes/observer.php
new file mode 100644
index 0000000..972c73c
--- /dev/null
+++ b/totara/appraisal/classes/observer.php
@@ -0,0 +1,135 @@
+<?php
+/*
+ * This file is part of Totara LMS
+ *
+ * Copyright (C) 2010 onwards Totara Learning Solutions LTD
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author David Curry <david.curry@totaralms.com>
+ * @package totara
+ * @subpackage appraisal
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class totara_appraisal_observer {
+
+    /**
+     * Event that is triggered when a user is deleted.
+     *
+     * Checks for any appraisals roles the user may have had and archives them.
+     *
+     * @param \core\event\user_deleted $event
+     *
+     */
+    public static function user_deleted(\core\event\user_deleted $event) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/totara/appraisal/lib.php');
+
+        $userid = $event->objectid;
+        $transaction = $DB->start_delegated_transaction();
+
+        // Delete all user_assignments and associated data for the user.
+        appraisal::delete_learner_assignments($userid);
+
+        // Unassign all role_assignments for the user, but retain associated data.
+        appraisal::unassign_user_roles($userid);
+
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Activation message handler
+     * If message is not immediate - add scheduled event
+     * Also process stage_due as technically it's not an event but scheduled action
+     *
+     * @param \totara_appraisal\event\appraisal_activation $event
+     */
+    public static function appraisal_activation(\totara_appraisal\event\appraisal_activation $event) {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/totara/appraisal/lib.php'); // We should move all the classes into self loading ones.
+
+        $time = $event->other['time'];
+        $appraisalid = $event->objectid;
+
+        $sql = "SELECT id FROM {appraisal_event} WHERE triggered = 0 AND event IN (?, ?) AND appraisalid = ?";
+        $params = array(appraisal_message::EVENT_APPRAISAL_ACTIVATION, appraisal_message::EVENT_STAGE_DUE, $appraisalid);
+        $events = $DB->get_records_sql($sql, $params);
+        foreach ($events as $id => $eventdata) {
+            $eventmessage = new appraisal_message($id);
+            self::process_event($eventmessage, $time);
+        }
+    }
+
+    /**
+     * Stage complete message handler
+     *
+     * @param \totara_appraisal\event\appraisal_stage_completion $event
+     */
+    public static function appraisal_stage_completion(\totara_appraisal\event\appraisal_stage_completion $event) {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/totara/appraisal/lib.php'); // We should move all the classes into self loading ones.
+
+        $time = $event->other['time'];
+        $stageid = $event->other['stageid'];
+        $sql = "SELECT id FROM {appraisal_event} WHERE triggered = 0 AND event = ? AND appraisalstageid = ?";
+        $params = array(appraisal_message::EVENT_STAGE_COMPLETE, $stageid);
+        $events = $DB->get_records_sql($sql, $params);
+        foreach ($events as $id => $eventdata) {
+            self::process_event(new appraisal_message($id), $time);
+        }
+    }
+
+    /**
+     * Send or schedule messages according time and settings
+     *
+     * @param appraisal_message $event
+     * @param int $time
+     */
+    protected static function process_event(appraisal_message $event, $time) {
+        if ($event->is_immediate()) {
+            $event->send();
+        } else {
+            $event->schedule($event->get_schedule_from($time));
+            $event->save();
+        }
+    }
+
+
+    /**
+     * Get's all scheduled untriggered messages and send's them
+     *
+     * @param int $time current time
+     */
+    public static function send_scheduled($time) {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/totara/appraisal/lib.php'); // We should move all the classes into self loading ones.
+
+        $sql = "SELECT ae.id
+                FROM {appraisal_event} ae JOIN {appraisal} a ON (ae.appraisalid = a.id)
+                WHERE a.status = ? AND timescheduled > 0 AND triggered = 0";
+        $events = $DB->get_records_sql($sql, array(appraisal::STATUS_ACTIVE));
+        foreach ($events as $id => $eventdata) {
+            $event = new appraisal_message($id);
+            if ($event->is_time($time)) {
+                $event->send();
+            }
+        }
+    }
+
+}
diff --git a/totara/appraisal/cron.php b/totara/appraisal/cron.php
index 5488093..44fc485 100644
--- a/totara/appraisal/cron.php
+++ b/totara/appraisal/cron.php
@@ -28,11 +28,22 @@ require_once($CFG->dirroot.'/totara/appraisal/lib.php');
  * Run cron for appraisal
  */
 function appraisal_cron($time = 0) {
+    global $DB;
+
     // Execute the cron if Appraisals are not disabled.
     if (!totara_feature_disabled('appraisals')) {
+
+        // Update learner assignments for active appraisals.
+        $appraisals = $DB->get_records('appraisal', array('status' => appraisal::STATUS_ACTIVE));
+        foreach ($appraisals as $app) {
+            $appraisal = new appraisal($app->id);
+            $appraisal->check_assignment_changes();
+        }
+
+        // Send scheduled appraisals messages.
         if (!$time) {
             $time = time();
         }
-        appraisal_event_handler::send_scheduled($time);
+        totara_appraisal_observer::send_scheduled($time);
     }
 }
diff --git a/totara/appraisal/db/events.php b/totara/appraisal/db/events.php
index 187b769..facdb9e 100644
--- a/totara/appraisal/db/events.php
+++ b/totara/appraisal/db/events.php
@@ -34,17 +34,14 @@ if (!defined('MOODLE_INTERNAL')) {
 $observers = array(
     array(
         'eventname' => '\totara_appraisal\event\appraisal_activation',
-        'callback'=> 'appraisal_event_handler::appraisal_activation',
-        'includefile' => '/totara/appraisal/lib.php',
+        'callback' => 'totara_appraisal_observer::appraisal_activation',
     ),
     array(
         'eventname' => '\totara_appraisal\event\appraisal_stage_completion',
-        'callback'=> 'appraisal_event_handler::appraisal_stage_completion',
-        'includefile' => '/totara/appraisal/lib.php',
+        'callback' => 'totara_appraisal_observer::appraisal_stage_completion',
     ),
     array(
         'eventname' => '\core\event\user_deleted',
-        'callback'=> 'appraisal_event_handler::appraisal_user_deleted',
-        'includefile' => '/totara/appraisal/lib.php',
+        'callback' => 'totara_appraisal_observer::user_deleted',
     ),
 );
diff --git a/totara/appraisal/db/install.xml b/totara/appraisal/db/install.xml
index 92211fb..17526f7 100644
--- a/totara/appraisal/db/install.xml
+++ b/totara/appraisal/db/install.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="totara/appraisal/db" VERSION="20130730" COMMENT="XMLDB file for Moodle totara/appraisal"
+<XMLDB PATH="totara/appraisal/db" VERSION="20140602" COMMENT="XMLDB file for Moodle totara/appraisal"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -212,6 +212,7 @@
         <FIELD NAME="appraisalid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="activestageid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="timecompleted" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="status" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -227,6 +228,7 @@
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="appraisalrole" TYPE="int" LENGTH="3" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="activepageid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -309,5 +311,18 @@
         <KEY NAME="apprevenmess_app_fk" TYPE="foreign" FIELDS="appraisaleventmessageid" REFTABLE="appraisal_event_message" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="appraisal_role_changes" COMMENT="Store role changes for assignments">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userassignmentid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="originaluserid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="newuserid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="role" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
diff --git a/totara/appraisal/db/upgrade.php b/totara/appraisal/db/upgrade.php
index 1079ffc..b4ae13a 100644
--- a/totara/appraisal/db/upgrade.php
+++ b/totara/appraisal/db/upgrade.php
@@ -28,6 +28,8 @@
  */
 
 require_once($CFG->dirroot.'/totara/core/db/utils.php');
+require_once($CFG->dirroot.'/totara/appraisal/lib.php');
+
 /**
  * Local database upgrade script
  *
@@ -80,7 +82,7 @@ function xmldb_totara_appraisal_upgrade($oldversion) {
                             'mnethostid' => $user->mnethostid
                         )
                 ));
-                appraisal_event_handler::appraisal_user_deleted($event);
+                totara_appraisal_observer::user_deleted($event);
                 $i++;
                 $pbar->update($i, $usercount, "Fixing Appraisals for deleted users - {$i}/{$usercount}.");
             }
@@ -89,85 +91,68 @@ function xmldb_totara_appraisal_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2014061600, 'totara', 'appraisal');
     }
 
-    if ($oldversion < 2014062000) {
-        $users = $DB->get_records('user', array('deleted' => 1));
-        $users = array_keys($users);
-
-        $transaction = $DB->start_delegated_transaction();
-
-        if (!empty($users)) {
-            list($insql, $inparam) = $DB->get_in_or_equal($users);
-            $now = time();
-
-            // First try and complete the stage so the user can continue the appraisal.
-            $sql = "SELECT ara.*, aua.activestageid, aua.appraisalid, aua.userid AS subjectid
-                      FROM {appraisal_role_assignment} ara
-                      JOIN {appraisal_user_assignment} aua
-                        ON ara.appraisaluserassignmentid = aua.id
-                     WHERE ara.userid {$insql}";
-            $roleassignments = $DB->get_records_sql($sql, $inparam);
-
-            $completionsql = "SELECT 1
-                                FROM {appraisal_role_assignment} ara
-                           LEFT JOIN {appraisal_stage_data} asd
-                                  ON asd.appraisalroleassignmentid = ara.id
-                                 AND asd.appraisalstageid = ?
-                               WHERE ara.appraisaluserassignmentid = ?
-                                 AND ara.userid <> 0
-                                 AND asd.timecompleted IS NULL";
-
-            $todb = new stdClass();
-            $todb->timecompleted = $now;
-            foreach ($roleassignments as $role) {
-                $todb->appraisalroleassignmentid = $role->id;
-                $todb->appraisalstageid = $role->activestageid;
-                $DB->insert_record('appraisal_stage_data', $todb);
-
-                // Check if all assigned roles have completed the appraisal.
-                if (!$DB->record_exists_sql($completionsql, array($role->activestageid, $role->appraisaluserassignmentid))) {
-                    $stages = $DB->get_records('appraisal_stage', array('appraisalid' => $role->appraisalid), 'timedue, id');
-
-                    // Find the next stage.
-                    $currentstage = reset($stages);
-                    for ($i = 0; $i < count($stages) - 1; $i++) {
-                        if ($currentstage->id == $role->activestageid) {
-                            $currentstage = next($stages);
-                            $nextstageid = $currentstage->id;
-                            break;
-                        }
-                        $currentstage = next($stages);
-                    }
-
-                    // Move to the next stage or mark the appraisal as complete.
-                    if (!empty($nextstageid)) {
-                        $DB->set_field('appraisal_user_assignment', 'activestageid', $nextstageid,
-                            array('userid' => $role->subjectid, 'appraisalid' => $role->appraisalid));
-                        $nextstageid = 0;
-                    } else {
-                        // Mark the user as complete for this appraisal.
-                        $DB->set_field('appraisal_user_assignment', 'timecompleted', $now, array('id' => $role->appraisaluserassignmentid));
-
-                        // Check if all users are complete.
-                        $unfinished = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $role->appraisalid, 'timecompleted' => null));
-                        if (!$unfinished) {
-                            // Mark this appraisal as complete.
-                            $DB->set_field('appraisal', 'status', appraisal::STATUS_COMPLETED, array('id' => $role->appraisalid));
-                            $DB->set_field('appraisal', 'timefinished', $now, array('id' => $role->appraisalid));
-                        }
-                    }
-                }
+    if ($oldversion < 2014062300) {
+        $table = new xmldb_table('appraisal_user_assignment');
+        $field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
+
+        if (!$dbman->field_exists($table, $field)) {
+            $transaction = $DB->start_delegated_transaction();
+
+            $dbman->add_field($table, $field);
+
+            // Migrate status from appraisal status.
+            $appraisals = $DB->get_records('appraisal');
+            foreach ($appraisals as $appraisal) {
+                $sql = "UPDATE {appraisal_user_assignment} SET status = ? WHERE appraisalid = ?";
+                $params = array($appraisal->status, $appraisal->id);
+                $DB->execute($sql, $params);
             }
 
-            // Then flag all the role_assignments as empty.
-            $sql = "UPDATE {appraisal_role_assignment}
-                       SET userid = 0
-                       WHERE userid {$insql}";
-            $DB->execute($sql, $inparam);
+            // Finally set the status for all completed users at once.
+            $sql = "UPDATE {appraisal_user_assignment} SET status = ? WHERE timecompleted IS NOT NULL";
+            $params = array(appraisal::STATUS_COMPLETED);
+            $DB->execute($sql, $params);
+
+            $transaction->allow_commit();
         }
 
-        $transaction->allow_commit();
+        upgrade_plugin_savepoint(true, 2014062300, 'totara', 'appraisal');
+    }
+
+    if ($oldversion < 2014062301) {
+
+        // Define table appraisal_role_changes to be created.
+        $table = new xmldb_table('appraisal_role_changes');
+
+        // Adding fields to table appraisal_role_changes.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('userassignmentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('originaluserid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('newuserid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('role', XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
+
+        // Adding keys to table appraisal_role_changes.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for appraisal_role_changes.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        upgrade_plugin_savepoint(true, 2014062301, 'totara', 'appraisal');
+    }
+
+    if ($oldversion < 2014062302) {
+        // Adding a timecreated field to appraisals role assignments.
+        $table = new xmldb_table('appraisal_role_assignment');
+        $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
+
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
 
-        upgrade_plugin_savepoint(true, 2014062000, 'totara', 'appraisal');
+        upgrade_plugin_savepoint(true, 2014062302, 'totara', 'appraisal');
     }
 
     return true;
diff --git a/totara/appraisal/general.php b/totara/appraisal/general.php
index 9697ba6..2d069e1 100644
--- a/totara/appraisal/general.php
+++ b/totara/appraisal/general.php
@@ -79,6 +79,10 @@ if ($fromform = $mform->get_data()) {
     }
 }
 
+$title = $PAGE->title . ': ' . $appraisal->name;
+$PAGE->set_title($title);
+$PAGE->set_heading($appraisal->name);
+$PAGE->navbar->add($appraisal->name);
 $output = $PAGE->get_renderer('totara_appraisal');
 echo $output->header();
 if ($appraisal->id) {
diff --git a/totara/appraisal/lang/en/totara_appraisal.php b/totara/appraisal/lang/en/totara_appraisal.php
index 2530379..27c419e 100644
--- a/totara/appraisal/lang/en/totara_appraisal.php
+++ b/totara/appraisal/lang/en/totara_appraisal.php
@@ -54,8 +54,11 @@ $string['appraisalactivenochangesallowed'] = 'This appraisal is active, no chang
 $string['appraisalcloned'] = 'Appraisal Cloned';
 $string['appraisalclosed'] = 'Appraisal \'{$a}\' closed';
 $string['appraisalclosedalertssent'] = 'Appraisal \'{$a}\' closed and alert have been sent';
+$string['appraisalclosedforuser'] = 'This appraisal has been closed because {$a} is no longer assigned.';
+$string['appraisalclosednochangesallowed'] = 'This appraisal is closed, no changes can be made to learner assignments';
 $string['appraisalsdisabled'] = 'Appraisals are not enabled on this site';
 $string['appraisalfixerrors'] = 'You must fix the following errors prior to appraisal activation:';
+$string['appraisalfixwarnings'] = 'Fixing the following warnings is also recommended:';
 $string['appraisalhasstages'] = 'Appraisal has the following stages:';
 $string['appraisalhistory'] = 'appraisal history';
 $string['appraisalinvalid'] = 'Appraisal not ready for activation';
@@ -86,6 +89,8 @@ $string['assignments'] = 'Assignments';
 $string['assignnumusers'] = 'Learners';
 $string['assignsourcename'] = 'Assigned Group';
 $string['backtoappraisal'] = 'Back to appraisal';
+$string['cancelled'] = 'Cancelled';
+$string['changesnotlive'] = 'Changes within the assigned groups have occurred but are not yet live. Appraisal assignments will be updated on the next cron run';
 $string['changessaved'] = 'Changes saved';
 $string['close'] = 'Close';
 $string['closeappraisal'] = 'Close appraisal';
@@ -115,8 +120,10 @@ $string['configenableappraisals'] = 'This option will let you: Enable(show)/Hide
 * If Disable is chosen, appraisals will disappear from any menu on the site and will not be accessible.
 ';
 $string['confirmactivateappraisal'] = 'Do you really want to activate this appraisal?';
+$string['confirmactivatewarning'] = 'You can activate the appraisal without fixing these warnings, but users may encounter some issues. Do you really want to activate this appraisal?';
 $string['confirmcloseappraisal'] = 'Do you really want to close this appraisal?';
 $string['confirmdeleteappraisal'] = 'Do you really want to remove this appraisal?';
+$string['confirmdeletegroup'] = 'Do you really want to remove the {$a->grouptype} "{$a->groupname}" from appraisal "{$a->appraisalname}"? This will close assignments for any users not currently assigned via other group assignments.';
 $string['confirmdeletemessage'] = 'Do you really want to remove this message?';
 $string['confirmdeletepage'] = 'Do you really want to remove this page?';
 $string['confirmdeletestage'] = 'Do you really want to remove this stage?';
@@ -148,6 +155,7 @@ $string['downloadnow'] = 'Download now';
 $string['draft'] = 'Draft';
 $string['editpageheading'] = 'Edit page';
 $string['editstageheading'] = 'Edit stage';
+$string['emptyassignments'] = 'Due to be removed';
 $string['enableappraisals'] = 'Enable Appraisals';
 $string['example:appraisalname'] = 'Example appraisal';
 $string['example:appraisaldescription'] = '<p>This is an example appraisal. You can use this as a starting point for building your own appraisal or just browse around to see how the functionality works.</p><p>If you don\'t need it, just delete it and create your own.</p>';
@@ -198,10 +206,12 @@ $string['error:dialognotreeitemsevidencefromplan'] = 'There is no linked evidenc
 $string['error:dialognotreeitemsgoals'] = 'No goals in this framework';
 $string['error:dialognotreeitemsobjfromplan'] = 'There are no objectives in this plan';
 $string['error:dialognotreeitemsprogfromplan'] = 'There are no programs in this plan';
+$string['error:invalidgrouptype'] = 'Unrecognised group type';
 $string['error:messagetitleyrequired'] = 'Message title is required';
 $string['error:messagebodyrequired'] = 'Message body is required';
 $string['error:nopermissions'] = 'You do not have permissions to perform that action';
 $string['error:pagenotfound'] = 'Page not found';
+$string['error:pagepermissions'] = 'You do not have permissions to view this page';
 $string['error:redisplayoutoforder'] = 'Redisplay question must be placed on a page after the question it links to';
 $string['error:stagenotfound'] = 'Stage not found';
 $string['error:submitteddatainvalid'] = 'There were problems with the data you submitted';
@@ -262,6 +272,12 @@ $string['messageevent'] = 'Event';
 $string['messagetiming'] = 'Timing';
 $string['messagerecipients'] = 'Recipients';
 $string['messagesaved'] = 'Message saved';
+$string['missingroles'] = 'Some assigned users are missing important role assignments.';
+$string['missingrolesbelow'] = ' Users may not be able to complete the appraisal without these roles, they are listed below:';
+$string['missingrolesinfo'] = 'Some assigned users are missing important role assignments. {$a}';
+$string['missingroleslink'] = 'View full list of missing roles';
+$string['missingrolesnone'] = 'There are no missing roles for the user assignments of this appraisal.';
+$string['missingrolestitle'] = 'Appraisals Missing Roles - {$a}';
 $string['myappraisals'] = 'My Appraisals';
 $string['myteamappraisals'] = 'My Team\'s Appraisals';
 $string['name'] = 'Name';
@@ -313,18 +329,20 @@ $string['roleaccessnotice'] = 'If a role cannot answer or view other\'s answers
 $string['roleadministrator'] = 'Administrator';
 $string['roleappraiser'] = 'Appraiser';
 $string['roleall'] = 'All';
-$string['rolecurrentlyempty'] = 'Role currently empty';
-$string['rolelearner'] = 'Learner';
-$string['rolemanager'] = 'Manager';
-$string['roleteamlead'] = 'Manager\'s Manager';
 $string['rolecompleteyou'] = ' You must complete this stage';
 $string['rolecompleteyour'] = ' Your {$a} must complete this stage';
 $string['rolecompleteuser'] = ' {$a} must complete this stage';
 $string['rolecompleteusers'] = ' {$a->username}\'s {$a->rolename} must complete this stage';
+$string['rolecurrentlyempty'] = 'Role currently empty';
+$string['rolehaschanged'] = 'This role is due to change';
+$string['rolelearner'] = 'Learner';
+$string['rolemanager'] = 'Manager';
 $string['rolescananswer'] = 'Roles that can answer';
 $string['rolescananswer_help'] = 'These are the roles that can answer at least one question on a page within this stage.';
 $string['rolescanview'] = 'Roles that can view';
 $string['rolescanview_help'] = 'These are the roles that can view at least one other person\'s answer but cannot answer any questions themselves.';
+$string['rolesmissing'] = 'Warning: there are missing roles which may prevent this appraisal being completed.';
+$string['roleteamlead'] = 'Manager\'s Manager';
 $string['reports'] = 'Reports';
 $string['savechanges'] = 'Save changes';
 $string['savepdfsnapshot'] = 'Save PDF Snapshot';
@@ -355,6 +373,8 @@ $string['statusreport'] = 'Status report';
 $string['statusreportforx'] = '{$a} status report: ';
 $string['temporarypage'] = 'Temporary page';
 $string['unavailable'] = 'Unavailable';
+$string['update'] = 'Update';
+$string['updatenow'] = 'Update Now';
 $string['view'] = 'View';
 $string['viewother'] = 'View other role\'s answers';
 $string['viewreport'] = 'View report';
diff --git a/totara/appraisal/learners.php b/totara/appraisal/learners.php
index 3e35d7c..a0836cb 100644
--- a/totara/appraisal/learners.php
+++ b/totara/appraisal/learners.php
@@ -33,6 +33,7 @@ appraisal::check_feature_enabled();
 
 // Get the appraisal id.
 $appraisalid = required_param('appraisalid', PARAM_INT);
+$update = optional_param('update', false, PARAM_BOOL);
 $module = 'appraisal';
 $appraisal = new appraisal($appraisalid);
 $assign = new totara_assign_appraisal($module, $appraisal);
@@ -42,37 +43,82 @@ $systemcontext = context_system::instance();
 $canassign = has_capability('totara/appraisal:assignappraisaltogroup', $systemcontext);
 $canviewusers = has_capability('totara/appraisal:viewassignedusers', $systemcontext);
 
+admin_externalpage_setup('manageappraisals');
+$title = $PAGE->title . ': ' . $appraisal->name;
+$PAGE->set_title($title);
+$PAGE->set_heading($appraisal->name);
+$PAGE->navbar->add($appraisal->name);
+$output = $PAGE->get_renderer('totara_appraisal');
+
+$grouptypes = $assign->get_assignable_grouptype_names();
+$returnparams = array('appraisalid' => $appraisalid);
+$returnurl = new moodle_url('/totara/appraisal/learners.php', $returnparams);
+
 $deleteid = optional_param('deleteid', null, PARAM_ALPHANUMEXT);
-if ($deleteid && $canassign && ($appraisal->status == appraisal::STATUS_DRAFT)) {
+$confirm = optional_param('confirm', false, PARAM_BOOL);
+if ($deleteid && $canassign && (!appraisal::is_closed($appraisalid))) {
+    list($grp, $aid) = explode("_", $deleteid);
+
+    if (appraisal::is_active($appraisalid) && !$confirm) {
+
+        $deleteparams = array('appraisalid' => $appraisalid, 'deleteid' => $deleteid, 'confirm' => true, 'sesskey' => sesskey());
+        $deleteurl = new moodle_url('/totara/appraisal/learners.php', $deleteparams);
+
+        $confirmparams = new stdClass();
+        $confirmparams->grouptype = $grouptypes[$grp];
+        $confirmparams->groupname = $assign->get_group_instance_name($grp, $aid);
+        $confirmparams->appraisalname = $appraisal->name;
+        $confirmstr = get_string('confirmdeletegroup', 'totara_appraisal', $confirmparams);
+
+        echo $output->header();
+        echo $output->confirm($confirmstr, $deleteurl, $returnurl);
+        echo $output->footer();
+
+        exit();
+    } else {
+        if (!confirm_sesskey()) {
+            print_error('confirmsesskeybad', 'error');
+        }
+        $assign->delete_assigned_group($grp, $aid);
+        redirect($returnurl);
+    }
+}
+
+if ($update && $canassign) {
     if (!confirm_sesskey()) {
         print_error('confirmsesskeybad', 'error');
     }
-    list($grp, $aid) = explode("_", $deleteid);
-    $assign->delete_assigned_group($grp, $aid);
+
+    $appraisal->check_assignment_changes();
+    redirect($returnurl);
 }
 
-admin_externalpage_setup('manageappraisals');
+$notlivenotice = $output->display_notlive_notice($appraisalid, $canassign);
 // Setup the JS.
-totara_setup_assigndialogs($module, $appraisalid, $canviewusers);
-$output = $PAGE->get_renderer('totara_appraisal');
+totara_setup_assigndialogs($module, $appraisalid, $canviewusers, $notlivenotice);
 echo $output->header();
 if ($appraisal->id) {
     echo $output->heading($appraisal->name);
     echo $output->appraisal_additional_actions($appraisal->status, $appraisal->id);
 }
 
+if ($appraisal->status == appraisal::STATUS_ACTIVE) {
+    $warnings = $appraisal->validate_roles(true);
+    echo $output->display_learner_warnings($appraisal->id, $warnings, $canviewusers);
+}
+
 echo $output->appraisal_management_tabs($appraisal->id, 'learners');
 
 echo $output->heading(get_string('assigncurrentgroups', 'totara_appraisal'));
 
 if ($canassign) {
-    if ($appraisal->status == appraisal::STATUS_DRAFT) {
+    if ($appraisal->status == appraisal::STATUS_CLOSED) {
+        echo get_string('appraisalclosednochangesallowed', 'totara_appraisal');
+    } else {
         $options = array_merge(array("" => get_string('assigngroup', 'totara_core')),
-                $assign->get_assignable_grouptype_names());
+                $grouptypes);
         echo html_writer::select($options, 'groupselector', null, null,
                 array('class' => 'group_selector', 'itemid' => $appraisalid));
-    } else {
-        echo get_string('appraisalactivenochangesallowed', 'totara_appraisal');
     }
 }
 
@@ -82,8 +128,21 @@ echo $output->display_assigned_groups($currentassignments, $appraisalid);
 
 echo $output->heading(get_string('assigncurrentusers', 'totara_appraisal'));
 
+// If the appraisal is active notify the user that changes are not live.
+if ($appraisal->status == appraisal::STATUS_ACTIVE) {
+    $userassignments = $assign->get_current_users();
+    $groupassignments = $assign->get_current_users(null, null, null, true);
+    $differences = $appraisal->compare_assignments($userassignments, $groupassignments);
+    echo html_writer::start_tag('div', array('id' => 'notlivenotice'));
+    if ($differences) {
+        echo $notlivenotice;
+    }
+    echo html_writer::end_tag('div');
+}
+
 if ($canviewusers) {
     echo $output->display_user_datatable();
 }
 
+
 echo $output->footer();
diff --git a/totara/appraisal/lib.php b/totara/appraisal/lib.php
index 266e779..22ea9b7 100644
--- a/totara/appraisal/lib.php
+++ b/totara/appraisal/lib.php
@@ -207,8 +207,8 @@ class appraisal {
      */
     public function set_status($newstatus) {
         $allowedstatus = array(
-            self::STATUS_ACTIVE => array(self::STATUS_CLOSED, self::STATUS_COMPLETED),
-            self::STATUS_CLOSED => array(),
+            self::STATUS_ACTIVE => array(self::STATUS_CLOSED),
+            self::STATUS_CLOSED => array(self::STATUS_ACTIVE),
             self::STATUS_DRAFT => array(self::STATUS_ACTIVE),
             self::STATUS_COMPLETED => array()
         );
@@ -219,7 +219,7 @@ class appraisal {
             throw new appraisal_exception(get_string('error:cannotchangestatus', 'totara_appraisal', $a));
         } else {
             $this->status = $newstatus;
-            if ($newstatus == self::STATUS_COMPLETED || $newstatus == self::STATUS_CLOSED) {
+            if ($newstatus == self::STATUS_CLOSED) {
                 $this->timefinished = time();
             } else if ($newstatus == self::STATUS_ACTIVE) {
                 $this->timefinished = null;
@@ -282,7 +282,8 @@ class appraisal {
         if (is_null($time)) {
             $time = time();
         }
-        $err = array();
+        $err = array(); // Errors.
+        $war = array(); // Warnings.
 
         // Check that the current status is draft.
         if ($this->status != self::STATUS_DRAFT) {
@@ -296,7 +297,7 @@ class appraisal {
         }
 
         // Ensure each user has every required role.
-        $err += $this->validate_roles();
+        $war += $this->validate_roles();
 
         // Check that all stages are valid.
         $stages = appraisal_stage::fetch_appraisal($this->id);
@@ -310,32 +311,67 @@ class appraisal {
             $timesdue[] = $checkstage->timedue;
         }
 
-        return $err;
+        return array($err, $war);
+    }
+
+    /**
+     * Compare existing user assignments against group assignments to see if there have been changes
+     *
+     * @param $userassignments recordset    -   data from get_current_users()
+     * @param $groupassignments recordset   -   data from get_current_users()
+     * @return boolean                      -   true if there are differences
+     */
+    public function compare_assignments($userassignments, $groupassignments) {
+
+        // Expecting a recordset so we need to loop through to convert.
+        $ualist = array();
+        foreach ($userassignments as $ua) {
+            $ualist[$ua->id] = $ua->id;
+        }
+
+        // Expecting a recordset so we need to loop through to convert.
+        $galist = array();
+        foreach ($groupassignments as $ga) {
+            $galist[$ga->id] = $ga->id;
+        }
+
+        if (array_diff($ualist, $galist)) {
+            return true;
+        }
+
+        if (array_diff($galist, $ualist)) {
+            return true;
+        }
+
+        return false;
     }
 
     /**
-     * Check for problems activating an appraisal due to missing roles
+     * Check for potential problems with role assignments
+     * used on activation and updating of user assignments, and the warnings page.
      *
-     * @return $err Array Array of error strings to combine with any existing errors
+     * @param $live boolean     A flag to switch between live data and groups data.
+     * @return $war Array       Array of warning strings to combine with any existing warnings
      */
-    public function validate_roles() {
+    public function validate_roles($live = false) {
         global $DB, $CFG;
         require_once($CFG->dirroot . '/totara/hierarchy/prefix/position/lib.php');
 
         if (!defined('APPRAISAL_VALIDATION_MAX_BAD_ROLES')) {
             define('APPRAISAL_VALIDATION_MAX_BAD_ROLES', 500);
         }
-        $err = array();
+
+        $war = array(); // Warnings.
 
         $assign = new totara_assign_appraisal('appraisal', $this);
         $learnercount = $assign->get_current_users_count();
         if (!$learnercount) {
-            $err['learners'] = get_string('appraisalinvalid:learners', 'totara_appraisal');
-            return $err;
+            $war['learners'] = get_string('appraisalinvalid:learners', 'totara_appraisal');
+            return $war;
         }
 
-        // Get user info and roles for all users involved
-        // in this appraisal who have at least one role
+        // Get user info and roles for all users involved.
+        // in this appraisal who have at least one role.
         // missing.
         $usernamefields = get_all_user_name_fields(true, 'u');
         $select = "SELECT
@@ -353,8 +389,14 @@ class appraisal {
                 ON (pa.managerid = pa2.userid AND pa2.type = ?)";
         $params = array(POSITION_TYPE_PRIMARY, POSITION_TYPE_PRIMARY);
 
-        // Get SQL to limit to only users involved in this appraisal.
-        list($joinsql, $joinparams) = $assign->get_users_from_groups_sql('u', 'id');
+        if ($live) {
+            // User the appraisals user assignments instead of group assignments.
+            $joinsql = " JOIN ( SELECT aua.userid AS userid FROM {appraisal_user_assignment} aua WHERE aua.appraisalid = ? AND aua.status = ? ) liveusers ON liveusers.userid = u.id ";
+            $joinparams = array($this->id, self::STATUS_ACTIVE);
+        } else {
+            // Get SQL to limit to only users involved in this appraisal.
+            list($joinsql, $joinparams) = $assign->get_users_from_groups_sql('u', 'id');
+        }
 
         $params = array_merge($params, $joinparams);
 
@@ -374,7 +416,7 @@ class appraisal {
 
         // No roles to check, no need to run query.
         if (empty($rolestocheck)) {
-            return $err;
+            return $war;
         }
         $wheresql = ' WHERE ' . implode(' OR ', $rolestocheck);
 
@@ -382,7 +424,7 @@ class appraisal {
         $missingroles = $DB->get_records_sql($select.$from.$joinsql.$wheresql, $params, 0, APPRAISAL_VALIDATION_MAX_BAD_ROLES);
         $nummissing = $DB->count_records_sql($count.$from.$joinsql.$wheresql, $params);
 
-        // Add an error for each missing role.
+        // Add a warning for each missing role.
         $rolefieldmap = array(
             self::ROLE_LEARNER => 'id',
             self::ROLE_MANAGER => 'managerid',
@@ -396,20 +438,270 @@ class appraisal {
                     $a = new stdClass();
                     $a->user = fullname($missingrole);
                     $a->role = get_string($allroles[$role], 'totara_appraisal');
-                    $err['missingrole' . $allroles[$role] . $missingrole->id] =
+                    $war['missingrole' . $allroles[$role] . $missingrole->id] =
                         get_string('appraisalinvalid:missingrole',
                         'totara_appraisal', $a);
                 }
             }
         }
 
-        // If there were more records to show, add one more error listing how many have been truncated.
+        // If there were more records to show, add one more warning listing how many have been truncated.
         if ($nummissing > APPRAISAL_VALIDATION_MAX_BAD_ROLES) {
-            $err['missingrolesmore'] = get_string('xmoremissingroles', 'totara_appraisal',
+            $war['missingrolesmore'] = get_string('xmoremissingroles', 'totara_appraisal',
                 ($nummissing - APPRAISAL_VALIDATION_MAX_BAD_ROLES));
         }
 
-        return $err;
+        return $war;
+    }
+
+
+    /**
+     * Update user and role assignments for a live appraisal.
+     * Used via the cron and appraisals learners tab
+     */
+    public function check_assignment_changes() {
+        global $CFG, $DB;
+
+        $assign = new totara_assign_appraisal('appraisal', $this);
+
+        // Create user assignments and role assignments for new users.
+        $added = $assign->get_unstored_users();
+        if ($added->valid()) {
+            $assign->store_user_assignments($added);
+
+            // Set active stage for all new users.
+            $stages = appraisal_stage::get_stages($this->id);
+            $DB->set_field('appraisal_user_assignment', 'activestageid', reset($stages)->id, array('appraisalid' => $this->id, 'activestageid' => null));
+        }
+
+        // Find old user assignments that need to be reactivated.
+        list($assignjoinsql, $assignparams, $assignalias) = $assign->get_users_from_assignments_sql('u', 'id');
+        list($groupjoinsql, $groupparams, $groupalias) = $assign->get_users_from_groups_sql('u', 'id');
+
+        $sql = "UPDATE {appraisal_user_assignment}
+                   SET status = ?
+                 WHERE status = ?
+                   AND appraisalid = ?
+                   AND userid IN (
+                       SELECT u.id
+                         FROM {user} u
+                       " . $groupjoinsql . "
+                   )";
+        $params = array_merge(array(self::STATUS_ACTIVE, self::STATUS_CLOSED, $this->id), $groupparams);
+        $DB->execute($sql, $params);
+
+        // Find removed learners.
+        $removed = $assign->get_removed_users();
+        // Close user assignment for each user.
+        if ($removed->valid()) {
+            foreach ($removed as $user) {
+                $userassignment = new appraisal_user_assignment($user->userassignmentid);
+
+                $userassignment->close();
+            }
+        }
+
+        // Find role changes for appraisal assignments.
+        $changed = $this->get_changedrole_users();
+        $transaction = $DB->start_delegated_transaction();
+
+        // Check for existing data.
+        foreach ($changed as $rolechange) {
+
+            // Add record to new 'role changes' table.
+            $changerecord = new stdClass();
+            $changerecord->userassignmentid = $rolechange->userassignment;
+            $changerecord->originaluserid = $rolechange->olduserid;
+            $changerecord->newuserid = $rolechange->newuserid;
+            $changerecord->role = $rolechange->role;
+            $changerecord->timecreated = time();
+
+            $DB->insert_record('appraisal_role_changes', $changerecord);
+
+            // Switch user in role assignment.
+            $roleupdate = new stdClass();
+            $roleupdate->id = $rolechange->roleassignment;
+            $roleupdate->userid = is_null($rolechange->newuserid) ? 0 : $rolechange->newuserid;
+
+            $DB->update_record('appraisal_role_assignment', $roleupdate);
+        }
+
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Check for user assignments with missing roles.
+     *
+     * @return Array
+     */
+    public function get_missingrole_users() {
+        global $DB;
+
+        $assign = new totara_assign_appraisal('appraisal', $this);
+
+        $select = "SELECT
+            ara.id,
+            u.userid,
+            ara.appraisalrole,
+            pa.managerid,
+            pa2.managerid AS teamleadid,
+            pa.appraiserid";
+
+        $from = " FROM {appraisal_user_assignment} u
+            LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa
+                ON u.userid = pa.userid
+            LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa2
+                ON pa.managerid = pa2.userid
+            JOIN {appraisal_role_assignment} ara
+              ON u.id = ara.appraisaluserassignmentid ";
+
+        // Get SQL to limit to only users involved in this appraisal.
+        list($joinsql, $params) = $assign->get_users_from_assignments_sql('u', 'id');
+
+        // Only get rows that are missing data if we require that role.
+        $allroles = $this->get_roles();
+        $rolesinvolved = $this->get_roles_involved();
+        $rolestocheck = array();
+        foreach ($rolesinvolved as $roleinvolved) {
+            if ($roleinvolved == self::ROLE_MANAGER) {
+                $rolestocheck[] = 'pa.managerid IS NULL';
+            } else if ($roleinvolved == self::ROLE_TEAM_LEAD) {
+                $rolestocheck[] = 'pa2.managerid IS NULL';
+            } else if ($roleinvolved == self::ROLE_APPRAISER) {
+                $rolestocheck[] = 'pa.appraiserid IS NULL';
+            }
+        }
+
+        // No roles to check, no need to run query.
+        if (empty($rolestocheck)) {
+            return array();
+        }
+        $wheresql = 'WHERE u.appraisalid = ? AND ( ' . implode(' OR ', $rolestocheck) . ' )';
+        $params = array($this->id);
+
+        // Limit retrieved records but also count to see the total.
+        $missingroles = $DB->get_records_sql($select.$from.$wheresql, $params);
+
+        // Find each role that has changed.
+        $rolefieldmap = array(
+            self::ROLE_LEARNER => 'userid',
+            self::ROLE_MANAGER => 'managerid',
+            self::ROLE_TEAM_LEAD => 'teamleadid',
+            self::ROLE_APPRAISER => 'appraiserid'
+        );
+
+        $userassmissingroles = array();
+
+        foreach ($missingroles as $missingrole) {
+            $appraisalrole = array();
+            foreach ($rolesinvolved as $role) {
+                $field = $rolefieldmap[$role];
+
+                if (empty($missingrole->$field)) {
+                    $appraisalrole[] = $role;
+                }
+            }
+
+            $userassmissingroles[$missingrole->userid] = $appraisalrole;
+        }
+
+        return $userassmissingroles;
+    }
+
+
+    /**
+     * Get role changes for user assignments for the current appraisal
+     *
+     * @return Array
+     */
+    public function get_changedrole_users() {
+        global $DB;
+
+        $select = "SELECT
+            ara.id,
+            u.userid,
+            u.id as userassignmentid,
+            ara.userid as roleuserid,
+            ara.appraisalrole";
+
+        $from = " FROM {appraisal_user_assignment} u
+            LEFT JOIN {appraisal_role_assignment} ara
+                ON u.id = ara.appraisaluserassignmentid";
+
+        $rolesinvolved = $this->get_roles_involved();
+
+        $posjoin = false;
+        $posjoin2 = false;
+
+        foreach ($rolesinvolved as $roleinvolved) {
+            if ($roleinvolved == self::ROLE_MANAGER) {
+                $select .= ", CASE WHEN pa.managerid IS NULL THEN 0 ELSE pa.managerid END AS managerid";
+                $posjoin = true;
+            } else if ($roleinvolved == self::ROLE_TEAM_LEAD) {
+                $select .= ", CASE WHEN pa2.managerid IS NULL THEN 0 ELSE pa2.managerid END AS teamleader";
+                $posjoin2 = true;
+            } else if ($roleinvolved == self::ROLE_APPRAISER) {
+                $select .= ", CASE WHEN pa.appraiserid IS NULL THEN 0 ELSE pa.appraiserid END AS appraiserid";
+                $posjoin = true;
+            }
+        }
+
+        if ($posjoin) {
+            $from .= " JOIN {pos_assignment} pa
+                ON pa.userid = u.userid AND pa.type = 1 ";
+        }
+
+        if ($posjoin2) {
+            $from .= " LEFT JOIN {pos_assignment} pa2
+                ON pa2.userid = pa.managerid AND pa2.type = 1 ";
+        }
+
+        $wheresql = " WHERE u.status = 1 AND u.appraisalid = ?";
+
+        $currentroleassignments = $DB->get_records_sql($select.$from.$wheresql, array($this->id));
+
+        $changedroles = array();
+        foreach ($currentroleassignments as $roleassignment) {
+            if ($roleassignment->appraisalrole == self::ROLE_MANAGER) {
+                if ($roleassignment->roleuserid != $roleassignment->managerid) {
+                    $change = new stdClass();
+                    $change->userassignment  = $roleassignment->userassignmentid;
+                    $change->role = $roleassignment->appraisalrole;
+                    $change->roleassignment = $roleassignment->id;
+                    $change->olduserid = $roleassignment->roleuserid;
+                    $change->newuserid = $roleassignment->managerid;
+                    $change->timecreated = time();
+
+                    $changedroles[] = $change;
+                }
+            } else if ($roleassignment->appraisalrole == self::ROLE_TEAM_LEAD) {
+                if ($roleassignment->roleuserid != $roleassignment->teamleader) {
+                    $change = new stdClass();
+                    $change->userassignment  = $roleassignment->userassignmentid;
+                    $change->role = $roleassignment->appraisalrole;
+                    $change->roleassignment = $roleassignment->id;
+                    $change->olduserid = $roleassignment->roleuserid;
+                    $change->newuserid = $roleassignment->teamleader;
+                    $change->timecreated = time();
+
+                    $changedroles[] = $change;
+                }
+            } else if ($roleassignment->appraisalrole == self::ROLE_APPRAISER) {
+                if ($roleassignment->roleuserid != $roleassignment->appraiserid) {
+                    $change = new stdClass();
+                    $change->userassignment  = $roleassignment->userassignmentid;
+                    $change->role = $roleassignment->appraisalrole;
+                    $change->roleassignment = $roleassignment->id;
+                    $change->olduserid = $roleassignment->roleuserid;
+                    $change->newuserid = $roleassignment->appraiserid;
+                    $change->timecreated = time();
+
+                    $changedroles[] = $change;
+                }
+            }
+        }
+
+        return $changedroles;
     }
 
     /**
@@ -432,8 +724,8 @@ class appraisal {
             $alert->fullmessage = $formdata->alertbody;
             $alert->fullmessagehtml = $alert->fullmessage;
 
-            $learners = $DB->get_records('appraisal_user_assignment',
-                    array('appraisalid' => $formdata->id, 'timecompleted' => null), null, 'userid AS id');
+            $params = array('appraisalid' => $formdata->id, 'timecompleted' => null, 'status' => self::STATUS_ACTIVE);
+            $learners = $DB->get_records('appraisal_user_assignment', $params, null, 'userid AS id');
 
             foreach ($learners as $learner) {
                 $alert->userto = $learner;
@@ -453,10 +745,12 @@ class appraisal {
                         ON aua.userid = usr.id
                      WHERE aua.timecompleted IS NULL
                        AND aua.appraisalid = ?
+                       AND aua.status = ?
                        AND ara.appraisalrole != ?
+                       AND ara.userid <> 0
                      GROUP BY ara.userid';
 
-            $roleusers = $DB->get_records_sql($sql, array($formdata->id, self::ROLE_LEARNER));
+            $roleusers = $DB->get_records_sql($sql, array($formdata->id, self::STATUS_ACTIVE, self::ROLE_LEARNER));
 
             foreach ($roleusers as $roleuser) {
                 $formdata->staff = $roleuser->staff;
@@ -467,7 +761,13 @@ class appraisal {
             }
         }
 
+        // Mark the status as closed for the appraisal.
         $this->set_status(self::STATUS_CLOSED);
+
+        // Mark the status as closed for all user assignments.
+        $sql = "UPDATE {appraisal_user_assignment} SET status = ? WHERE status = ? AND appraisalid = ?";
+        $DB->execute($sql, array(self::STATUS_CLOSED, self::STATUS_ACTIVE, $this->id));
+
     }
 
 
@@ -569,6 +869,7 @@ class appraisal {
         if (($this->status == self::STATUS_CLOSED) || ($this->status == self::STATUS_COMPLETED)) {
             return true;
         }
+
         if (isset($assignment)) {
             if ($assignment instanceof appraisal_role_assignment) {
                 $userassignment = $assignment->get_user_assignment();
@@ -578,13 +879,18 @@ class appraisal {
                 throw new appraisal_exception('Wrong assignment class');
             }
         }
+
+        // Is appraisal locked for the user?
+        if (isset($userassignment) && $userassignment->is_closed()) {
+            return true;
+        }
+
         return isset($userassignment) && $userassignment->timecompleted > 0;
     }
 
 
     /**
      * Mark the appraisal as complete for the given user.
-     * Then check if all users are complete and if so then mark this appraisal as complete.
      *
      * @param int $subjectid
      */
@@ -595,20 +901,9 @@ class appraisal {
         $DB->set_field('appraisal_user_assignment', 'timecompleted', time(),
                 array('userid' => $subjectid, 'appraisalid' => $this->id));
 
-        // Check if all users are complete.
-        $unfinished = $DB->count_records('appraisal_user_assignment',
-                array('appraisalid' => $this->id, 'timecompleted' => null));
-        if (!$unfinished) {
-            // Mark this appraisal as complete.
-            $this->complete();
-        }
-    }
-
-    /**
-     * Mark this appraisal as complete.
-     */
-    public function complete() {
-        $this->set_status(self::STATUS_COMPLETED);
+        // Mark the user as complete for this appraisal.
+        $DB->set_field('appraisal_user_assignment', 'status', self::STATUS_COMPLETED,
+                array('userid' => $subjectid, 'appraisalid' => $this->id));
     }
 
     /**
@@ -656,11 +951,13 @@ class appraisal {
     public function count_incomplete_userassignments() {
         global $DB;
 
-        return $DB->count_records('appraisal_user_assignment', array('appraisalid' => $this->id, 'timecompleted' => null));
+        $params = array('appraisalid' => $this->id, 'timecompleted' => null, 'status' => self::STATUS_ACTIVE);
+        return $DB->count_records('appraisal_user_assignment', $params);
     }
 
     /**
      * Return array of roles involved in current appraisal
+     * array(appraisalrole => appraisalrole)
      *
      * @param int $rights count only roles that have certain rights
      * @return array of appraisalrole
@@ -694,6 +991,33 @@ class appraisal {
         return array_keys($out);
     }
 
+    /**
+     * Return array of roles involved in current appraisal linking to assigned users.
+     * array(appraisalrole => userid)
+     *
+     * @param subjectid the userid in the userassignment
+     * @return array
+     */
+    public function get_user_roles_involved($rights, $subjectid) {
+        global $DB;
+
+        $roles = $this->get_roles_involved($rights);
+        if (empty($roles)) {
+            return array();
+        }
+
+        list($insql, $inparam) = $DB->get_in_or_equal(array_keys($roles));
+        $missingsql = "SELECT ara.appraisalrole, ara.userid
+                         FROM {appraisal_role_assignment} ara
+                         JOIN {appraisal_user_assignment} aua
+                           ON ara.appraisaluserassignmentid = aua.id
+                        WHERE aua.userid = ?
+                          AND aua.appraisalid = ?
+                          AND appraisalrole {$insql}";
+        $missingparams = array_merge(array($subjectid, $this->appraisalid), $inparam);
+        return $DB->get_records_sql($missingsql, $missingparams);
+    }
+
 
     /**
      * Create answers table
@@ -753,7 +1077,7 @@ class appraisal {
 
 
     /**
-     * Save snapshot
+     * Save snapshot to the file system
      *
      * @param string $filepath snapshot /path/filename.pdf
      * @param int $roleassignmentid snapshot role assignment id
@@ -857,23 +1181,52 @@ class appraisal {
         $this->id = null;
     }
 
-
     /**
-     * Check if appraisal is in draft state
+     * Get the status of the appraisal.
      *
-     * @param mixed $appraisal appraisal.id or instance of appraisal
-     * @return bool
+     * @param mixed $appraisal  - appraisal.id or instance of appraisal
+     * @return int              - self::STATUS_DRAFT etc
      */
-    public static function is_draft($appraisal) {
+    public static function get_status($appraisal) {
         if (is_numeric($appraisal)) {
             $appraisal = new appraisal($appraisal);
         }
         if (!($appraisal instanceof appraisal)) {
             throw new appraisal_exception('Appraisal object not found', 2);
         }
-        return ($appraisal->status == self::STATUS_DRAFT);
+
+        return $appraisal->status;
     }
 
+    /**
+     * Check if appraisal is in draft state
+     *
+     * @param mixed $appraisal  - appraisal.id or instance of appraisal
+     * @return bool
+     */
+    public static function is_draft($appraisal) {
+        return (self::get_status($appraisal) == self::STATUS_DRAFT);
+    }
+
+    /**
+     * Check if appraisal is in an active state
+     *
+     * @param mixed $appraisal  - appraisal.id or instance of appraisal
+     * @return bool
+     */
+    public static function is_active($appraisal) {
+        return (self::get_status($appraisal) == self::STATUS_ACTIVE);
+    }
+
+    /**
+     * Check if appraisal has been closed.
+     *
+     * @param mixed $appraisal  - appraisal.id or instance of appraisal
+     * @return bool
+     */
+    public static function is_closed($appraisal) {
+        return (self::get_status($appraisal) == self::STATUS_CLOSED);
+    }
 
     /**
      * Get all individual roles supported by system (except two: 'Administrator' is reserved, 'All' is not a role.)
@@ -891,6 +1244,27 @@ class appraisal {
     }
 
     /**
+     * Get the current position assignments for a users appraisal role assignments
+     *
+     * @param int userid The id of the user to get the current assignments for
+     * @return array(appraisal::ROLE_* => userid)
+     */
+    public static function get_live_role_assignments($userid) {
+        $manager = totara_get_manager($userid);
+        $teamleader = totara_get_teamleader($userid);
+        $appraiser = totara_get_appraiser($userid);
+
+        $roles = array(
+            self::ROLE_LEARNER => $userid,
+            self::ROLE_MANAGER => $manager ? $manager->id : 0,
+            self::ROLE_TEAM_LEAD => $teamleader ? $teamleader->id : 0,
+            self::ROLE_APPRAISER => $appraiser ? $appraiser->id : 0,
+        );
+
+        return $roles;
+    }
+
+    /**
      * Get status name
      *
      * @param int $status
@@ -1147,7 +1521,7 @@ class appraisal {
         global $DB, $USER;
         $params = array($USER->id, $role);
         $sql = 'SELECT ' . $DB->sql_concat(sql_cast2char('a.id'), "'_'", sql_cast2char('aua.userid')) . ' AS uniqueid,
-                       a.id, aua.userid, a.name, a.timestarted, a.status, MAX(ast.timedue) AS timedue, aua.timecompleted,
+                       a.id, aua.userid, a.name, a.timestarted, aua.status, MAX(ast.timedue) AS timedue, aua.timecompleted,
                        ara.id as roleassignmentid
                   FROM {appraisal} a
                   JOIN {appraisal_user_assignment} aua
@@ -1166,8 +1540,8 @@ class appraisal {
             $sql .= ' AND a.status ' . $sqlstatus;
             $params = array_merge($params, $paramstatus);
         }
-        $sql .= ' GROUP BY a.id, aua.userid, a.name, a.timestarted, a.status, aua.timecompleted, ara.id
-                  ORDER BY CASE WHEN aua.timecompleted IS NULL AND a.status = ? THEN 0 ELSE 1 END, a.timestarted DESC';
+        $sql .= ' GROUP BY a.id, aua.userid, a.name, a.timestarted, aua.status, aua.timecompleted, ara.id
+                  ORDER BY CASE WHEN aua.timecompleted IS NULL AND aua.status = ? THEN 0 ELSE 1 END, a.timestarted DESC';
         $params[] = self::STATUS_ACTIVE;
         $appraisals = $DB->get_records_sql($sql, $params);
         return $appraisals;
@@ -1192,6 +1566,76 @@ class appraisal {
 
         return $appraisals;
     }
+
+    /**
+     * Deletes all user_assignments and associated data for a given user.
+     *
+     * @param int $userid   - The id of the user to run this for.
+     */
+    public static function delete_learner_assignments($userid) {
+        global $DB;
+
+        $context = context_system::instance();
+        $fs = get_file_storage();
+        $transaction = $DB->start_delegated_transaction();
+
+        $userassignments = $DB->get_records('appraisal_user_assignment', array('userid' => $userid));
+        foreach ($userassignments as $userassignment) {
+            $appraisalid = $userassignment->appraisalid;
+            $userassignmentid = $userassignment->id;
+            // Get all role assignments for this userassignment.
+            $roleassignments = $DB->get_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassignmentid));
+
+            foreach ($roleassignments as $roleassignment) {
+                $roleassignmentid = $roleassignment->id;
+                $DB->delete_records('appraisal_scale_data', array('appraisalroleassignmentid' => $roleassignmentid));
+                $DB->delete_records('appraisal_quest_data_' . $appraisalid, array('appraisalroleassignmentid' => $roleassignmentid));
+                $DB->delete_records('appraisal_stage_data', array('appraisalroleassignmentid' => $roleassignmentid));
+                $fs->delete_area_files($context->id, 'totara_appraisal', 'snapshot_', $appraisalid, $roleassignmentid);
+            }
+
+            $DB->delete_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassignmentid));
+            $DB->delete_records('appraisal_role_changes', array('userassignmentid' => $userassignmentid));
+        }
+        $DB->delete_records('appraisal_user_assignment', array('userid' => $userid));
+
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Unassign all role_assignments for a given user, but retain associated data.
+     *
+     * @param int $userid   - The id of the user to run this for.
+     */
+    public static function unassign_user_roles($userid) {
+        global $DB;
+
+        $transaction = $DB->start_delegated_transaction();
+
+        // Flag all data associated with other users assignments as deleted but keep the data.
+        $roles = $DB->get_records('appraisal_role_assignment', array('userid' => $userid));
+
+        // Create role changed records for all the roles.
+        $todb = new stdClass();
+        $todb->originaluserid = $userid;
+        $todb->newuserid = 0; // Deleted so there is no new user.
+        $todb->timecreated = time();
+
+        foreach ($roles as $role) {
+            $todb->role = $role->appraisalrole;
+            $todb->userassignmentid = $role->appraisaluserassignmentid;
+
+            $DB->insert_record('appraisal_role_changes', $todb);
+        }
+
+        // Unassign all the users role assignments.
+        $sql = "UPDATE {appraisal_role_assignment} SET userid = 0 WHERE userid = ?";
+        $DB->execute($sql, array($userid));
+
+        $transaction->allow_commit();
+    }
+
+
     /**
      * Get array of appraisals with their start dates and number of learners
      *
@@ -1200,17 +1644,18 @@ class appraisal {
     public static function get_manage_list() {
         global $DB;
 
-        $sql = 'SELECT a.id, a.name, a.timestarted, a.status, COUNT(aua.id) as lnum
-                FROM {appraisal} a
-                LEFT JOIN {appraisal_user_assignment} aua ON (aua.appraisalid = a.id)
-                GROUP BY a.id, a.name, a.timestarted, a.status
-                ORDER BY a.status, a.timestarted DESC, a.name, a.id';
-        $appraisals = $DB->get_records_sql($sql);
+        $appraisals = $DB->get_records('appraisal', null, 'status, timestarted DESC, name, id');
 
         foreach ($appraisals as $appraisal) {
             if ($appraisal->status == self::STATUS_DRAFT) {
                 $assign = new totara_assign_appraisal('appraisal', new appraisal($appraisal->id));
                 $appraisal->lnum = $assign->get_current_users_count();
+            } else if ($appraisal->status == self::STATUS_ACTIVE) {
+                $params = array('appraisalid' => $appraisal->id, 'status' => self::STATUS_ACTIVE);
+                $appraisal->lnum = $DB->count_records('appraisal_user_assignment', $params);
+            } else {
+                $params = array('appraisalid' => $appraisal->id);
+                $appraisal->lnum = $DB->count_records('appraisal_user_assignment', $params);
             }
         }
         return $appraisals;
@@ -1321,6 +1766,7 @@ class appraisal {
         $sql = 'SELECT app.*,
                        userstotal.userstotal,
                        userscomplete.userscomplete,
+                       userscancelled.userscancelled,
                        usersoverdue.usersoverdue
                   FROM {appraisal} app
                   LEFT JOIN (SELECT COUNT(aua.userid) AS userstotal, aua.appraisalid
@@ -1332,6 +1778,11 @@ class appraisal {
                               WHERE aua.timecompleted IS NOT NULL
                               GROUP BY aua.appraisalid) userscomplete
                     ON app.id = userscomplete.appraisalid
+                  LEFT JOIN (SELECT COUNT(aua.userid) AS userscancelled, aua.appraisalid
+                               FROM {appraisal_user_assignment} aua
+                               WHERE aua.status = ?
+                            GROUP BY aua.appraisalid) userscancelled
+                    ON app.id = userscancelled.appraisalid
                   LEFT JOIN (SELECT COUNT(aua.userid) AS usersoverdue, ast.appraisalid
                                FROM {appraisal_user_assignment} aua
                                JOIN {appraisal_stage} ast
@@ -1341,7 +1792,7 @@ class appraisal {
                               GROUP BY ast.appraisalid) usersoverdue
                     ON app.id = usersoverdue.appraisalid
                  WHERE app.status = ?';
-        $appraisals = $DB->get_records_sql($sql, array(time(), self::STATUS_ACTIVE));
+        $appraisals = $DB->get_records_sql($sql, array(self::STATUS_CLOSED, time(), self::STATUS_ACTIVE));
 
         return $appraisals;
     }
@@ -1908,7 +2359,7 @@ class appraisal_stage {
         global $DB;
 
         if (!$DB->record_exists('appraisal_stage_data',
-                array('appraisalroleassignmentid' => $roleassignment->id, 'appraisalstageid' => $this->id))) {
+            array('appraisalroleassignmentid' => $roleassignment->id, 'appraisalstageid' => $this->id))) {
             // Mark the role as complete.
             $stage_data = new stdClass();
             $stage_data->appraisalroleassignmentid = $roleassignment->id;
@@ -1917,7 +2368,7 @@ class appraisal_stage {
             $DB->insert_record('appraisal_stage_data', $stage_data);
 
             // Check if all involved roles are complete for this user and stage.
-            $rolescompletion = $this->get_completion($roleassignment->subjectid, appraisal::ACCESS_CANANSWER);
+            $rolescompletion = $this->get_mandatory_completion($roleassignment->subjectid);
             $complete = true;
             foreach ($rolescompletion as $rolecompletion) {
                 if (!isset($rolecompletion->timecompleted)) {
@@ -2007,21 +2458,57 @@ class appraisal_stage {
                  WHERE asp.appraisalstageid = ?
                  ORDER BY aqfr.appraisalrole";
         $params[] = $this->id;
-        return $DB->get_records_sql($sql, $params);
-    }
+        $rolesrecords = $DB->get_records_sql($sql, $params);
 
+        $out = array();
+        foreach ($rolesrecords as $rolerecord) {
+            $out[$rolerecord->appraisalrole] = 1;
+        }
+        return array_keys($out);
+    }
 
     /**
-     * Get completion data for all roles involved in this stage, for the given user.
+     * Much the same as get_roles_involved() but linking to the appropriate userid
      *
-     * @param int $subjectid
-     * @param int $rights Only if they have the specified rights.
+     * @param subjectid the userid in the userassignment
      * @return array
      */
-    public function get_completion($subjectid, $rights = 0) {
+    public function get_user_roles_involved($rights, $subjectid) {
         global $DB;
 
         $roles = $this->get_roles_involved($rights);
+        if (!empty($roles)) {
+            list($insql, $inparam) = $DB->get_in_or_equal($roles);
+            $missingsql = "SELECT ara.appraisalrole, ara.userid
+                             FROM {appraisal_role_assignment} ara
+                             JOIN {appraisal_user_assignment} aua
+                               ON ara.appraisaluserassignmentid = aua.id
+                            WHERE aua.userid = ?
+                              AND aua.appraisalid = ?
+                              AND appraisalrole {$insql}";
+            $missingparams = array_merge(array($subjectid, $this->appraisalid), $inparam);
+            return $DB->get_records_sql($missingsql, $missingparams);
+        }
+        return array();
+    }
+
+    public function get_mandatory_completion($subjectid) {
+        global $DB;
+
+        $cananswer = $this->get_user_roles_involved(appraisal::ACCESS_CANANSWER, $subjectid);
+        $mustanswer = $this->get_user_roles_involved(appraisal::ACCESS_MUSTANSWER, $subjectid);
+
+        $roles = array();
+
+        foreach ($mustanswer as $role) {
+            $roles[$role->appraisalrole] = $role->appraisalrole;
+        }
+
+        foreach ($cananswer as $role) {
+            if (!empty($role->userid)) {
+                $roles[$role->appraisalrole] = $role->appraisalrole;
+            }
+        }
 
         $sql = 'SELECT ara.appraisalrole, ara.userid, ara.activepageid, asd.timecompleted
                   FROM {appraisal_role_assignment} ara
@@ -2032,7 +2519,6 @@ class appraisal_stage {
                     ON ara.id = asd.appraisalroleassignmentid
                  WHERE aua.userid = ?
                    AND aua.appraisalid = ?
-                   AND ara.userid <> 0
                  ORDER BY ara.appraisalrole';
         $completiondata = $DB->get_records_sql($sql, array($this->id, $subjectid, $this->appraisalid));
         return array_intersect_key($completiondata, $roles);
@@ -3336,16 +3822,12 @@ class appraisal_question extends question_storage {
                 $isviewonlyquestion = false;
                 // Show role user icon.
                 $subjectid = $otherassignments[$eachrole]->userid;
-                if ($subjectid == 0) {
-                    continue;
-                }
-
-                $subject = $DB->get_record('user', array('id' => $subjectid));
 
                 // Add information about other roles to element.
-                $questioninfo = new question_manager($subject->id, $otherassignments[$eachrole]->id);
+                $questioninfo = new question_manager($subjectid, $otherassignments[$eachrole]->id);
                 $questioninfo->viewonly = true;
-                if (!$nouserpic) {
+                if (!$nouserpic && $subjectid != 0) {
+                    $subject = $DB->get_record('user', array('id' => $subjectid));
                     $questioninfo->userimage = $OUTPUT->user_picture($subject);
                 } else {
                    $questioninfo->userimage = '';
@@ -3919,14 +4401,30 @@ class appraisal_message {
      */
     public function send() {
         global $DB, $CFG;
+
         if ($this->triggered) {
             return false;
         }
-        $learners = $DB->get_records('appraisal_user_assignment', array('appraisalid' => $this->appraisalid));
+
+        $where = "appraisalid = ?";
+        $params = array($this->appraisalid);
+
+        // If this gets any more complex we might want to replace it with get_in_or_equal().
+        if ($this->type == 'appraisal_stage_completion') {
+            $where .= " AND (status = ? OR status = ?)";
+            $params[] = appraisal::STATUS_ACTIVE;
+            $params[] = appraisal::STATUS_COMPLETED;
+        } else {
+            $where .= " AND status = ?";
+            $params[] = appraisal::STATUS_ACTIVE;
+        }
+
+        $learners = $DB->get_records_select('appraisal_user_assignment', $where, $params);
         $sentaddress = array();
+
         foreach ($learners as $learner) {
             $params = array('appraisaluserassignmentid' => $learner->id);
-            $assignedroles = $DB->get_records('appraisal_role_assignment', $params, '', 'appraisalrole, userid');
+            $assignedroles = $DB->get_records('appraisal_role_assignment', $params, '', 'appraisalrole, id, userid');
             foreach ($this->roles as $role) {
                 if (isset($assignedroles[$role])) {
                     $rcptuserid = $assignedroles[$role]->userid;
@@ -3934,8 +4432,12 @@ class appraisal_message {
                     if ($this->type == self::EVENT_STAGE_DUE && $this->stageiscompleted != 0) {
                         // Get stage completion.
                         $stage = new appraisal_stage($this->stageid);
-                        $roleassignment = appraisal_role_assignment::get_role($this->appraisalid, $learner->userid, $rcptuserid, $role);
-                        $complete = $stage->is_completed($roleassignment);
+                        if ($role == appraisal::ROLE_LEARNER) {
+                            $approle = new appraisal_user_assignment($learner->id);
+                        } else {
+                            $approle = new appraisal_role_assignment($assignedroles[$role]->id);
+                        }
+                        $complete = $stage->is_completed($approle);
                         // Skip completed if set "only to incompleted" and contra versa.
                         if ($this->stageiscompleted == 1 && !$complete ||
                             $this->stageiscompleted == -1 && $complete) {
@@ -4124,140 +4626,6 @@ class appraisal_message {
     }
 }
 
-
-/**
- * Listener for appraisal specific events.
- */
-class appraisal_event_handler {
-    /**
-     * Activation message handler
-     * If message is not immediate - add sheduled event
-     * Also process stage_due as technically it's not an event but scheduled action
-     *
-     * @param \totara_appraisal\event\appraisal_activation $event
-     */
-    public static function appraisal_activation(\totara_appraisal\event\appraisal_activation $event) {
-        global $DB;
-
-        $time = $event->other['time'];
-        $appraisalid = $event->objectid;
-
-        $sql = "SELECT id FROM {appraisal_event} WHERE triggered = 0 AND event IN (?, ?) AND appraisalid = ?";
-        $params = array(appraisal_message::EVENT_APPRAISAL_ACTIVATION, appraisal_message::EVENT_STAGE_DUE, $appraisalid);
-        $events = $DB->get_records_sql($sql, $params);
-        foreach ($events as $id => $eventdata) {
-            $eventmessage = new appraisal_message($id);
-            self::process_event($eventmessage, $time);
-        }
-    }
-
-    /**
-     * User deleted message handler
-     *
-     * @param \totara_appraisal\event\appraisal_user_deleted $event
-     */
-    public static function appraisal_user_deleted(\core\event\user_deleted $event) {
-        global $DB;
-
-        $userid = $event->objectid;
-        $transaction = $DB->start_delegated_transaction();
-        $context = context_system::instance();
-        $fs = get_file_storage();
-        // Wipe data in appraisals assigned to this user.
-        $userassignments = $DB->get_records('appraisal_user_assignment', array('userid' => $userid));
-        foreach ($userassignments as $userassignment) {
-            $appraisalid = $userassignment->appraisalid;
-            $userassignmentid = $userassignment->id;
-            // Get all role assignments for this userassignment.
-            $roleassignments = $DB->get_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassignmentid));
-            foreach ($roleassignments as $roleassignment) {
-                $roleassignmentid = $roleassignment->id;
-                $DB->delete_records('appraisal_scale_data', array('appraisalroleassignmentid' => $roleassignmentid));
-                $DB->delete_records('appraisal_quest_data_' . $appraisalid, array('appraisalroleassignmentid' => $roleassignmentid));
-                $DB->delete_records('appraisal_stage_data', array('appraisalroleassignmentid' => $roleassignmentid));
-                $fs->delete_area_files($context->id, 'totara_appraisal', 'snapshot_', $appraisalid, $roleassignmentid);
-            }
-            $DB->delete_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassignmentid));
-        }
-        $DB->delete_records('appraisal_user_assignment', array('userid' => $userid));
-
-        // First mark the stages as complete for the users role assignments.
-        $sql = "SELECT ara.id, aua.activestageid
-                  FROM {appraisal_role_assignment} ara
-                  JOIN {appraisal_user_assignment} aua
-                    ON ara.appraisaluserassignmentid = aua.id
-                 WHERE ara.userid = ?";
-        $roleassignments = $DB->get_records_sql($sql, array($userid));
-
-        foreach ($roleassignments as $roleid => $stage) {
-            $stage = new appraisal_stage($stage->activestageid);
-            $roleassignment = new appraisal_role_assignment($roleid);
-
-            $stage->complete_for_role($roleassignment);
-        }
-
-        // Then flag their role assignments as deleted, but keep any existing data.
-        $sql = "UPDATE {appraisal_role_assignment} SET userid = 0 WHERE userid = ?";
-        $DB->execute($sql, array($userid));
-
-        $transaction->allow_commit();
-    }
-
-    /**
-     * Stage complete message handler
-     *
-     * @param \totara_appraisal\event\appraisal_stage_completion $event
-     */
-    public static function appraisal_stage_completion(\totara_appraisal\event\appraisal_stage_completion $event) {
-        global $DB;
-
-        $time = $event->other['time'];
-        $stageid = $event->other['stageid'];
-        $sql = "SELECT id FROM {appraisal_event} WHERE triggered = 0 AND event = ? AND appraisalstageid = ?";
-        $params = array(appraisal_message::EVENT_STAGE_COMPLETE, $stageid);
-        $events = $DB->get_records_sql($sql, $params);
-        foreach ($events as $id => $eventdata) {
-            self::process_event(new appraisal_message($id), $time);
-        }
-    }
-
-    /**
-     * Send or schedule messages according time and settings
-     *
-     * @param appraisal_message $event
-     * @param int $time
-     */
-    protected static function process_event(appraisal_message $event, $time) {
-        if ($event->is_immediate()) {
-            $event->send();
-        } else {
-            $event->schedule($event->get_schedule_from($time));
-            $event->save();
-        }
-    }
-
-
-    /**
-     * Get's all scheduled untriggered messages and send's them
-     *
-     * @param int $time current time
-     */
-    public static function send_scheduled($time) {
-        global $DB;
-        $sql = "SELECT ae.id
-                FROM {appraisal_event} ae JOIN {appraisal} a ON (ae.appraisalid = a.id)
-                WHERE a.status = ? AND timescheduled > 0 AND triggered = 0";
-        $events = $DB->get_records_sql($sql, array(appraisal::STATUS_ACTIVE));
-        foreach ($events as $id => $eventdata) {
-            $event = new appraisal_message($id);
-            if ($event->is_time($time)) {
-                $event->send();
-            }
-        }
-    }
-}
-
-
 /**
  * Serves the folder files.
  *
@@ -4642,7 +5010,13 @@ class appraisal_role_assignment {
                  AND ara.userid = ?
                  AND ara.appraisalrole = ?';
         $record = $DB->get_record_sql($sql, array($appraisalid, $subjectid, $userid, $role));
-        $roleassignment = new appraisal_role_assignment($record->id);
+
+        if ($record) {
+            $roleassignment = new appraisal_role_assignment($record->id);
+        } else {
+            $roleassignment = null;
+        }
+
         return $roleassignment;
     }
 
@@ -4729,6 +5103,12 @@ class appraisal_user_assignment {
     protected $user = null;
 
     /**
+     * User status
+     * @var int
+     */
+    protected $status = 0;
+
+    /**
      * Load role assignment
      * @param int $id
      */
@@ -4766,6 +5146,7 @@ class appraisal_user_assignment {
         $this->appraisalid = $userass->appraisalid;
         $this->activestageid = $userass->activestageid;
         $this->timecompleted = $userass->timecompleted;
+        $this->status = $userass->status;
         $this->preview = false;
         $this->user = $DB->get_record('user', array('id' => $this->userid));
     }
@@ -4797,6 +5178,27 @@ class appraisal_user_assignment {
         $userassignment = new appraisal_user_assignment($record->id);
         return $userassignment;
     }
+
+    /**
+     * Closes the appraisal for a user.
+     */
+    public function close() {
+        global $DB;
+
+        $this->status = appraisal::STATUS_CLOSED;
+
+        $todb = new stdClass();
+        $todb->id = $this->id;
+        $todb->status = $this->status;
+        $DB->update_record('appraisal_user_assignment', $todb);
+    }
+
+    /**
+     *
+     */
+    public function is_closed() {
+        return $this->status == appraisal::STATUS_CLOSED;
+    }
 }
 
 /**
diff --git a/totara/appraisal/lib/assign/ajax.php b/totara/appraisal/lib/assign/ajax.php
index 1769523..0e5e049 100644
--- a/totara/appraisal/lib/assign/ajax.php
+++ b/totara/appraisal/lib/assign/ajax.php
@@ -45,6 +45,7 @@ $assignclass = new $assignclassname($module, $appraisal);
 $users = $assignclass->get_current_users($ssearch, $idisplaystart, $idisplaylength);
 $igrandtotal = $assignclass->get_current_users_count();
 $idisplaytotal = $assignclass->get_current_users_count($ssearch);
+$emptyassignments = get_string('emptyassignments', 'totara_appraisal');
 
 // Since we only have one page we can save to an array.
 $userdata = array();
@@ -55,21 +56,25 @@ $users->close();
 
 // Get group info for the users on this page.
 $groupinfo = $assignclass->get_group_assignedvia_details(array_keys($userdata));
+
 $aadata = array();
 foreach ($userdata as $userid => $user) {
-    // Check for deleted users.
-    if (isset($groupinfo[$userid])) {
-        $url = new moodle_url('/user/view.php', array('id' => $user->id));
-        $link = html_writer::link($url, fullname($user));
-        $assignvia = array();
-        if (!empty($groupinfo)) {
-            foreach ($groupinfo[$userid] as $groupid => $groupstring) {
-                $assignvia[] = $groupstring;
-            }
+    $url = new moodle_url('/user/view.php', array('id' => $user->id));
+    $link = html_writer::link($url, fullname($user));
+
+    $assignvia = array();
+    if (!empty($groupinfo[$userid])) {
+        foreach ($groupinfo[$userid] as $groupid => $groupstring) {
+            $assignvia[] = $groupstring;
         }
+    }
+    if (empty($assignvia)) {
+        $assignviastring = $emptyassignments;
+    } else {
         $assignviastring = implode(', ', $assignvia);
-        $aadata[] = array($link, $assignviastring);
     }
+
+    $aadata[] = array($link, $assignviastring);
 }
 
 $output = array(
diff --git a/totara/appraisal/lib/assign/lib.php b/totara/appraisal/lib/assign/lib.php
index 240fa8b..5f2cee5 100644
--- a/totara/appraisal/lib/assign/lib.php
+++ b/totara/appraisal/lib/assign/lib.php
@@ -30,8 +30,17 @@ class totara_assign_appraisal extends totara_assign_core {
 
     protected static $module = 'appraisal';
 
-    public function store_user_assignments() {
-        parent::store_user_assignments();
+    public function store_user_assignments($newusers = null, $processor = null) {
+        // Define a processor function to format the data for appraisals.
+        $processor = function($record, $modulekey, $moduleinstanceid) {
+            $todb = new stdClass();
+            $todb->$modulekey = $moduleinstanceid;
+            $todb->userid = $record->id;
+            $todb->status = appraisal::STATUS_ACTIVE;
+            return $todb;
+        };
+
+        parent::store_user_assignments($newusers, $processor);
 
         $this->store_role_assignments();
     }
@@ -45,9 +54,6 @@ class totara_assign_appraisal extends totara_assign_core {
     private function store_role_assignments() {
         global $DB;
 
-        // Clear any existing records (so that we don't create duplicates).
-        $this->delete_role_assignments();
-
         // Get all roles required for this appraisal.
         $appraisal = new appraisal($this->moduleinstanceid);
         $roles = $appraisal->get_roles_involved();
@@ -57,43 +63,53 @@ class totara_assign_appraisal extends totara_assign_core {
                 case appraisal::ROLE_LEARNER:
                     $select = "SELECT ua.id, ua.userid, {$role}
                                  FROM {appraisal_user_assignment} ua
-                                WHERE ua.appraisalid = {$this->moduleinstanceid}";
+                               WHERE ua.appraisalid = {$this->moduleinstanceid}
+                                 AND NOT EXISTS (
+                                     SELECT id FROM {appraisal_role_assignment} ara WHERE ara.appraisaluserassignmentid = ua.id AND userid = ua.userid)";
+                    $params = array();
                     break;
                 case appraisal::ROLE_MANAGER:
-                    $select = "SELECT ua.id, pa.managerid, {$role}
+                    $select = "SELECT ua.id, CASE WHEN pa.managerid IS NULL THEN 0 ELSE pa.managerid END, ?
                                  FROM (SELECT * FROM {appraisal_user_assignment}
-                                        WHERE appraisalid = {$this->moduleinstanceid}) ua
-                                 JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa
+                                        WHERE appraisalid = ?) ua
+                                 LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = ?) pa
                                    ON ua.userid = pa.userid
-                                WHERE pa.managerid IS NOT NULL";
+                                 WHERE NOT EXISTS (
+                                       SELECT id FROM {appraisal_role_assignment} ara WHERE ara.appraisaluserassignmentid = ua.id AND ara.appraisalrole = ?)";
+                    $params = array($role, $this->moduleinstanceid, POSITION_TYPE_PRIMARY, $role);
                     break;
                 case appraisal::ROLE_TEAM_LEAD:
-                    $select = "SELECT ua.id, pa2.managerid, {$role}
+                    $select = "SELECT ua.id, CASE WHEN pa2.managerid IS NULL THEN 0 ELSE pa2.managerid END, ?
                                  FROM (SELECT * FROM {appraisal_user_assignment}
-                                        WHERE appraisalid = {$this->moduleinstanceid}) ua
-                                 JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa
+                                        WHERE appraisalid = ?) ua
+                                 LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = ?) pa
                                    ON ua.userid = pa.userid
-                                 JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa2
+                                 LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = ?) pa2
                                    ON pa.managerid = pa2.userid
-                                WHERE pa2.managerid IS NOT NULL";
+                                 WHERE NOT EXISTS (
+                                       SELECT id FROM {appraisal_role_assignment} ara WHERE ara.appraisaluserassignmentid = ua.id AND ara.appraisalrole = ?)";
+                    $params = array($role, $this->moduleinstanceid, POSITION_TYPE_PRIMARY, POSITION_TYPE_PRIMARY, $role);
                     break;
                 case appraisal::ROLE_APPRAISER:
-                    $select = "SELECT ua.id, pa.appraiserid, {$role}
+                    $select = "SELECT ua.id, CASE WHEN pa.appraiserid IS NULL THEN 0 ELSE pa.appraiserid END, ?
                                  FROM (SELECT * FROM {appraisal_user_assignment}
-                                        WHERE appraisalid = {$this->moduleinstanceid}) ua
-                                 JOIN (SELECT * FROM {pos_assignment} WHERE type = 1) pa
+                                        WHERE appraisalid = ?) ua
+                                 LEFT JOIN (SELECT * FROM {pos_assignment} WHERE type = ?) pa
                                    ON ua.userid = pa.userid
-                                WHERE pa.appraiserid IS NOT NULL";
+                                 WHERE NOT EXISTS (
+                                        SELECT id FROM {appraisal_role_assignment} ara WHERE ara.appraisaluserassignmentid = ua.id AND ara.appraisalrole = ?)";
+                    $params = array($role, $this->moduleinstanceid, POSITION_TYPE_PRIMARY, $role);
                     break;
                 default:
                     // Error.
                     continue 2;
                     break;
             }
+
             $sql = "INSERT INTO {appraisal_role_assignment}
                         (appraisaluserassignmentid, userid, appraisalrole)
                         ({$select})";
-            $DB->execute($sql);
+            $DB->execute($sql, $params);
         }
     }
 
@@ -143,7 +159,7 @@ class totara_assign_appraisal extends totara_assign_core {
      * @return bool
      */
     public function is_locked() {
-        return !appraisal::is_draft($this->moduleinstanceid);;
+        return appraisal::is_closed($this->moduleinstanceid);;
     }
 
     /**
@@ -155,4 +171,25 @@ class totara_assign_appraisal extends totara_assign_core {
         return ($this->moduleinstance->status != appraisal::STATUS_DRAFT);
     }
 
+    /**
+     * Over ride in module code to add specific module related search queries.
+     *
+     * @param  $useralias       string  The alias of the user table.
+     * @param  $joinalias       string  The alias of the joined table.
+     * @param  $liveassignments boolean Flags which $joinalias table is being used.
+     * @return string  The additional where statement.
+     */
+    public function get_user_extra_search_where_sql($useralias, $joinalias, $liveassignments) {
+        $sql = "";
+        $params = array();
+
+        if ($liveassignments) {
+            // Extra search where clause on the user_assignments table.
+            $sql = "{$joinalias}.status <> ?";
+            $params = array(appraisal::STATUS_CLOSED);
+        }
+
+        return array($sql, $params);
+    }
+
 }
diff --git a/totara/appraisal/message.php b/totara/appraisal/message.php
index 2c28f68..3944f39 100644
--- a/totara/appraisal/message.php
+++ b/totara/appraisal/message.php
@@ -135,6 +135,11 @@ switch ($action) {
 }
 
 $output = $PAGE->get_renderer('totara_appraisal');
+
+$title = $PAGE->title . ': ' . $appraisal->name;
+$PAGE->set_title($title);
+$PAGE->set_heading($appraisal->name);
+$PAGE->navbar->add($appraisal->name);
 echo $output->header();
 echo $output->heading($appraisal->name);
 echo $output->appraisal_additional_actions($appraisal->status, $appraisal->id);
@@ -162,4 +167,4 @@ switch ($action) {
         }
         echo $output->appraisal_message_table($messages);
 }
-echo $output->footer();
\ No newline at end of file
+echo $output->footer();
diff --git a/totara/appraisal/missing.php b/totara/appraisal/missing.php
new file mode 100644
index 0000000..0f90f30
--- /dev/null
+++ b/totara/appraisal/missing.php
@@ -0,0 +1,72 @@
+<?php
+/*
+ * This file is part of Totara LMS
+ *
+ * Copyright (C) 2010 onwards Totara Learning Solutions LTD
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author David Curry <david.curry@totaralms.com>
+ * @package totara
+ * @subpackage appraisal
+ */
+
+require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->dirroot . '/totara/appraisal/lib.php');
+
+// Check if Appraisals are enabled.
+appraisal::check_feature_enabled();
+
+// Get the appraisal id.
+$appraisalid = required_param('appraisalid', PARAM_INT);
+
+// Capability checks.
+$systemcontext = context_system::instance();
+$canassign = has_capability('totara/appraisal:assignappraisaltogroup', $systemcontext);
+$canviewusers = has_capability('totara/appraisal:viewassignedusers', $systemcontext);
+$appraisal = new appraisal($appraisalid);
+
+admin_externalpage_setup('manageappraisals');
+$PAGE->set_heading($appraisal->name);
+$PAGE->set_title(get_string('missingrolestitle', 'totara_appraisal', $appraisal->name));
+
+$output = $PAGE->get_renderer('totara_appraisal');
+echo $output->header();
+echo $output->heading($appraisal->name);
+
+if ($canviewusers || $canassign) {
+    $errors = $appraisal->validate_roles(true);
+    if (!empty($errors)) {
+        $explaination = get_string('missingroles', 'totara_appraisal');
+        $explaination .= get_string('missingrolesbelow', 'totara_appraisal');
+        echo html_writer::tag('p', $explaination);
+
+        $warndesc = array();
+        foreach ($errors as $warn) {
+            $warndesc[] = html_writer::tag('li', $warn);
+        }
+        echo html_writer::tag('ul', implode('', $warndesc), array('class' => 'appraisalerrorlist'));
+
+    } else {
+        // Looks like all the roles are filled.
+        echo html_writer::tag('p', get_string('missingrolesnone', 'totara_appraisal'));
+
+    }
+} else {
+    // You should not be viewing this page.
+    print_error('error:pagepermissions', 'totara_appraisal');
+}
+
+echo $output->footer();
diff --git a/totara/appraisal/rb_sources/lang/en/rb_source_appraisal.php b/totara/appraisal/rb_sources/lang/en/rb_source_appraisal.php
index 5e00ce6..5f12f3a 100644
--- a/totara/appraisal/rb_sources/lang/en/rb_source_appraisal.php
+++ b/totara/appraisal/rb_sources/lang/en/rb_source_appraisal.php
@@ -24,12 +24,17 @@
 
 $string['appraisalnamecolumn'] = 'Appraisal Name';
 $string['appraisalnameheading'] = 'Appraisal name';
+$string['appraisalstatusactive'] = 'Appraisal Active';
+$string['appraisalstatusclosed'] = 'Appraisal Closed';
 $string['appraisalstatuscolumn'] = 'Appraisal Status';
+$string['appraisalstatuscomplete'] = 'Appraisal Complete';
+$string['appraisalstatusdraft'] = 'Appraisal Draft';
 $string['appraisalstatusheading'] = 'Appraisal status';
 $string['appraisaltimefinishedcolumn'] = 'Appraisal Time Finished';
 $string['appraisaltimefinishedheading'] = 'Appraisal finished';
 $string['appraisaltimestartedcolumn'] = 'Appraisal Time Started';
 $string['appraisaltimestartedheading'] = 'Appraisal started';
+$string['cancelled'] = 'Assignment Cancelled';
 $string['completiondate'] = 'The completion date';
 $string['embeddedactivestagenameheading'] = 'Active stage';
 $string['embeddedmanagerheading'] = 'Manager';
@@ -40,7 +45,10 @@ $string['embeddedpreviousstagetimecompletedheading'] = 'Previous stage time comp
 $string['embeddedstatusheading'] = 'Status';
 $string['selectanappraisal'] = 'To view this report, first <a href="{$a}">select an appraisal</a> you want to view.';
 $string['sourcetitle'] = 'Appraisal Status';
+$string['statuscancelled'] = 'Assignment Cancelled';
+$string['statusclosed'] = 'Appraisal Closed';
 $string['statuscomplete'] = 'Complete';
+$string['statusdraft'] = 'Pending';
 $string['statusincomplete'] = 'Incomplete';
 $string['statusontarget'] = 'On Target';
 $string['statusoverdue'] = 'Overdue';
diff --git a/totara/appraisal/rb_sources/rb_source_appraisal.php b/totara/appraisal/rb_sources/rb_source_appraisal.php
index 9f10e2a..b78a090 100644
--- a/totara/appraisal/rb_sources/rb_source_appraisal.php
+++ b/totara/appraisal/rb_sources/rb_source_appraisal.php
@@ -113,6 +113,9 @@ class rb_source_appraisal extends rb_base_source {
     }
 
     protected function define_columnoptions() {
+        global $CFG;
+        require_once($CFG->dirroot . '/totara/appraisal/lib.php');
+
         $columnoptions = array(
             new rb_column_option(
                 'userappraisal',
@@ -142,10 +145,13 @@ class rb_source_appraisal extends rb_base_source {
                 'userappraisal',
                 'status',
                 get_string('userappraisalstatuscolumn', 'rb_source_appraisal'),
-                "CASE WHEN base.timecompleted IS NOT NULL THEN 'statuscomplete' " .
-                     "WHEN appraisal.status = 2 THEN 'statusincomplete' " .
-                     "WHEN activestage.timedue < " . time() . " THEN 'statusoverdue' " .
-                     "ELSE 'statusontarget' " .
+                "CASE WHEN base.status = " . appraisal::STATUS_COMPLETED . " AND base.timecompleted IS NOT NULL THEN 'statuscomplete' " .
+                     "WHEN base.status = " . appraisal::STATUS_CLOSED . " AND appraisal.status = " . appraisal::STATUS_ACTIVE . " THEN 'statuscancelled' " .
+                     "WHEN base.status = " . appraisal::STATUS_CLOSED . " AND (appraisal.status = " . appraisal::STATUS_CLOSED .
+                            " OR appraisal.status = " . appraisal::STATUS_COMPLETED . " ) THEN 'statusincomplete' " .
+                     "WHEN base.status = " . appraisal::STATUS_ACTIVE . " AND activestage.timedue < " . time() . " THEN 'statusoverdue' " .
+                     "WHEN base.status = " . appraisal::STATUS_ACTIVE . " AND activestage.timedue >= " . time() . " THEN 'statusontarget' " .
+                     "ELSE 'statusdraft' " .
                 "END",
                 array('joins' => array('appraisal', 'activestage'),
                       'displayfunc' => 'status',
@@ -231,7 +237,14 @@ class rb_source_appraisal extends rb_base_source {
                 get_string('userappraisalstatuscolumn', 'rb_source_appraisal'),
                 'select',
                 array('selectfunc' => 'status')
-            )
+            ),
+            new rb_filter_option(
+                'appraisal',
+                'status',
+                get_string('appraisalstatuscolumn', 'rb_source_appraisal'),
+                'select',
+                array('selectfunc' => 'appraisalstatus')
+            ),
         );
 
         $this->add_user_fields_to_filters($filteroptions);
@@ -245,11 +258,14 @@ class rb_source_appraisal extends rb_base_source {
         $paramoptions = array(
             new rb_param_option('appraisalid', 'base.appraisalid'),
             new rb_param_option('filterstatus',
-                "CASE WHEN base.timecompleted IS NOT NULL THEN 'statuscomplete'
-                      WHEN appraisal.status = 2 THEN 'statusincomplete'
-                      WHEN activestage.timedue < " . time() . " THEN 'statusoverdue'
-                      ELSE 'statusontarget'
-                 END", array('appraisal', 'activestage'), 'string')
+                "CASE WHEN base.status = " . appraisal::STATUS_COMPLETED . " AND base.timecompleted IS NOT NULL THEN 'statuscomplete' " .
+                     "WHEN base.status = " . appraisal::STATUS_CLOSED . " AND appraisal.status = " . appraisal::STATUS_ACTIVE . " THEN 'statuscancelled' " .
+                     "WHEN base.status = " . appraisal::STATUS_CLOSED . " AND (appraisal.status = " . appraisal::STATUS_CLOSED .
+                            " OR appraisal.status = " . appraisal::STATUS_COMPLETED . " ) THEN 'statusincomplete' " .
+                     "WHEN base.status = " . appraisal::STATUS_ACTIVE . " AND activestage.timedue < " . time() . " THEN 'statusoverdue' " .
+                     "WHEN base.status = " . appraisal::STATUS_ACTIVE . " AND activestage.timedue >= " . time() . " THEN 'statusontarget' " .
+                     "ELSE 'statusdraft' " .
+                "END", array('appraisal', 'activestage'), 'string')
         );
 
         return $paramoptions;
@@ -303,7 +319,7 @@ class rb_source_appraisal extends rb_base_source {
         return get_string($status, 'rb_source_appraisal');
     }
 
-     /**
+    /**
      * Convert appraisal status code string to human readable string.
      * @param string $status status code string
      * @param object $row other fields in the record (unused)
@@ -358,6 +374,7 @@ class rb_source_appraisal extends rb_base_source {
         $statuses = array();
 
         $statuses['statuscomplete'] = get_string('statuscomplete', 'rb_source_appraisal');
+        $statuses['statuscancelled'] = get_string('statuscancelled', 'rb_source_appraisal');
         $statuses['statusincomplete'] = get_string('statusincomplete', 'rb_source_appraisal');
         $statuses['statusoverdue'] = get_string('statusoverdue', 'rb_source_appraisal');
         $statuses['statusontarget'] = get_string('statusontarget', 'rb_source_appraisal');
@@ -365,6 +382,19 @@ class rb_source_appraisal extends rb_base_source {
         return $statuses;
     }
 
+    public function rb_filter_appraisalstatus() {
+        global $CFG;
+        require_once($CFG->dirroot . "/totara/appraisal/lib.php");
+
+        $statuses = array();
+        $statuses[appraisal::STATUS_DRAFT] = appraisal::display_status(appraisal::STATUS_DRAFT);
+        $statuses[appraisal::STATUS_ACTIVE] = appraisal::display_status(appraisal::STATUS_ACTIVE);
+        $statuses[appraisal::STATUS_CLOSED] = appraisal::display_status(appraisal::STATUS_CLOSED);
+        $statuses[appraisal::STATUS_COMPLETED] = appraisal::display_status(appraisal::STATUS_COMPLETED);
+
+        return $statuses;
+    }
+
     protected function define_defaultcolumns() {
         $defaultcolumns = array(
             array(
diff --git a/totara/appraisal/renderer.php b/totara/appraisal/renderer.php
index cc94409..fe722db 100644
--- a/totara/appraisal/renderer.php
+++ b/totara/appraisal/renderer.php
@@ -81,6 +81,8 @@ class totara_appraisal_renderer extends plugin_renderer_base {
                     array('id' => $appraisal->id, 'action' => 'close'));
             $editurl = new moodle_url('/totara/appraisal/general.php',
                     array('id' => $appraisal->id));
+            $assignurl = new moodle_url('/totara/appraisal/learners.php',
+                    array('appraisalid' => $appraisal->id));
             $deleteurl = new moodle_url('/totara/appraisal/manage.php',
                     array('id' => $appraisal->id, 'action' => 'delete'));
             $cloneurl = new moodle_url('/totara/appraisal/manage.php',
@@ -103,7 +105,7 @@ class totara_appraisal_renderer extends plugin_renderer_base {
             $options = '';
             if (has_capability('totara/appraisal:manageappraisals', $systemcontext, $userid)) {
                 if ($appraisal->status == appraisal::STATUS_ACTIVE) {
-                    $options .= $this->output->pix_icon('/t/edit_gray', $strnoteditable, 'moodle', array('class' => 'disabled iconsmall'));
+                    $options .= $this->output->action_icon($assignurl, new pix_icon('/t/edit', $strsettings, 'moodle'));
                     $options .= $this->output->action_icon($cloneurl, new pix_icon('/t/copy', $strclone, 'moodle'));
                     $options .= $this->output->pix_icon('/t/delete_gray', $strcannotdelete, 'moodle', array('class' => 'disabled iconsmall'));
                 } else {
@@ -283,6 +285,7 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         $tableheader = array(get_string('name', 'totara_appraisal'),
                              get_string('overdue', 'totara_appraisal'),
                              get_string('ontarget', 'totara_appraisal'),
+                             get_string('cancelled', 'totara_appraisal'),
                              get_string('complete', 'totara_appraisal'),
                              get_string('reports', 'totara_appraisal'));
 
@@ -311,7 +314,7 @@ class totara_appraisal_renderer extends plugin_renderer_base {
             if (!($appraisal->usersoverdue > 0)) {
                 $appraisal->usersoverdue = 0;
             }
-            $appraisal->usersontarget = $appraisal->userstotal - $appraisal->userscomplete - $appraisal->usersoverdue;
+            $appraisal->usersontarget = $appraisal->userstotal - $appraisal->userscomplete - $appraisal->usersoverdue - $appraisal->userscancelled;
             if ($appraisal->usersoverdue > 0) {
                 $statusreporturl->param('filterstatus', 'statusoverdue');
                 $row[] = html_writer::link($statusreporturl, $appraisal->usersoverdue);
@@ -326,6 +329,13 @@ class totara_appraisal_renderer extends plugin_renderer_base {
                 $row[] = '0';
             }
 
+            if ($appraisal->userscancelled > 0) {
+                $statusreporturl->param('filterstatus', 'statuscancelled');
+                $row[] = html_writer::link($statusreporturl, $appraisal->userscancelled);
+            } else {
+                $row[] = '0';
+            }
+
             if ($appraisal->userscomplete > 0) {
                 $statusreporturl->param('filterstatus', 'statuscomplete');
                 $row[] = html_writer::link($statusreporturl, $appraisal->userscomplete);
@@ -532,7 +542,7 @@ class totara_appraisal_renderer extends plugin_renderer_base {
                 $row[] = get_string('none', 'totara_appraisal');
             }
 
-            if (($appraisal->status == appraisal::STATUS_ACTIVE) && !empty($appraisal->timecompleted)) {
+            if (!empty($appraisal->timecompleted)) {
                 $row[] = appraisal::display_status(appraisal::STATUS_COMPLETED);
             } else {
                 $row[] = appraisal::display_status($appraisal->status);
@@ -952,16 +962,26 @@ class totara_appraisal_renderer extends plugin_renderer_base {
      * @param array $errors
      * @return string HTML
      */
-    public function confirm_appraisal_activation($appraisal, $errors) {
+    public function confirm_appraisal_activation($appraisal, $errors, $warnings) {
         $out = '';
         if (!empty($errors)) {
             $out .= $this->heading(get_string('appraisalinvalid', 'totara_appraisal'));
+            // Output the errors to the screen.
             $out .= html_writer::tag('p', get_string('appraisalfixerrors', 'totara_appraisal'));
             $errordesc = array();
             foreach ($errors as $error) {
                 $errordesc[] = html_writer::tag('li', $error);
             }
             $out .= html_writer::tag('ul', implode('', $errordesc), array('class' => 'appraisalerrorlist'));
+            // Output the warnings to the screen.
+            if (!empty($warnings)) {
+                $out .= html_writer::tag('p', get_string('appraisalfixwarnings', 'totara_appraisal'));
+                $warndesc = array();
+                foreach ($warnings as $warn) {
+                    $warndesc[] = html_writer::tag('li', $warn);
+                }
+                $out .= html_writer::tag('ul', implode('', $warndesc), array('class' => 'appraisalerrorlist'));
+            }
             $buttons = array();
             $buttons[] = $this->output->single_button(new moodle_url('/totara/appraisal/stage.php',
                     array('appraisalid' => $appraisal->id)), get_string('backtoappraisal', 'totara_appraisal',
@@ -969,7 +989,17 @@ class totara_appraisal_renderer extends plugin_renderer_base {
             $out .= html_writer::tag('div', implode(' ', $buttons), array('class' => 'buttons'));
         } else {
             $out .= html_writer::tag('p', get_string('appraisallastwarning', 'totara_appraisal'));
-            $out .= html_writer::tag('p', get_string('confirmactivateappraisal', 'totara_appraisal'));
+            // Output the warnings to the screen.
+            if (!empty($warnings)) {
+                $out .= html_writer::tag('p', get_string('confirmactivatewarning', 'totara_appraisal'));
+                $warndesc = array();
+                foreach ($warnings as $warn) {
+                    $warndesc[] = html_writer::tag('li', $warn);
+                }
+                $out .= html_writer::tag('ul', implode('', $warndesc), array('class' => 'appraisalerrorlist'));
+            } else {
+                $out .= html_writer::tag('p', get_string('confirmactivateappraisal', 'totara_appraisal'));
+            }
             $buttons = array();
             $buttons[] = $this->output->single_button(new moodle_url('/totara/appraisal/activation.php',
                     array('action' => 'activate', 'confirm' => 1, 'id' => $appraisal->id)),
@@ -1157,7 +1187,7 @@ class totara_appraisal_renderer extends plugin_renderer_base {
                 $row[] = new html_table_cell($includechildren);
                 $row[] = new html_table_cell($assign->groupusers);
                 // Only show delete if appraisal is draft status.
-                if ($appraisal->status == appraisal::STATUS_DRAFT) {
+                if (!appraisal::is_closed($itemid)) {
                     $delete = $this->output->action_icon(
                             new moodle_url('/totara/appraisal/learners.php',
                             array('appraisalid' => $itemid, 'deleteid' => $assign->id, 'sesskey' => sesskey())),
@@ -1190,6 +1220,66 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         return $out;
     }
 
+    /**
+     * Returns the markup for the learner assignments not live notification.
+     *
+     * @param   int         The id of the appraisal we are display warnings for.
+     * @param   boolean     Flag whether the user should see the update assignments button.
+     * @return  string      HTML
+     */
+    public function display_notlive_notice($appraisalid, $canassign = false) {
+
+        // Not live notification text.
+        $out = get_string('changesnotlive', 'totara_appraisal');
+
+        if ($canassign) {
+            // Update learner assignments button.
+            $updatestr = get_string('updatenow', 'totara_appraisal');
+            $updateparams = array('appraisalid' => $appraisalid, 'update' => true, 'sesskey' => sesskey());
+            $updateurl = new moodle_url('/totara/appraisal/learners.php', $updateparams);
+            $updatebutton = new single_button($updateurl, $updatestr, 'get');
+            $updatebutton->class .= ' update_assignment_records';
+            $out .= $this->render($updatebutton);
+        }
+
+        return $this->container($out, 'notifynotice');
+    }
+
+    /**
+     * Returns the markup for the learner assignments error warning.
+     *
+     * @param   int         The id of the appraisal we are display warnings for.
+     * @param   array       An array of the warnings we are displaying.
+     * @param   boolean     Flag whether the user should see the link to more information.
+     * @return  string      HTML
+     */
+    public function display_learner_warnings($appraisalid, $warnings, $canview = false) {
+
+        if (empty($warnings)) {
+            return '';
+        }
+
+        if (!empty($warnings['learners'])) {
+            // Display the no learners assigned warning.
+            $out = get_string('appraisalinvalid:learners', 'totara_appraisal');
+        } else if (!empty($warnings)) {
+            // The only other warnings are role warnings.
+            if ($canview) {
+                // Add the link to list all the role warings.
+                $infostr = get_string('missingroleslink', 'totara_appraisal');
+                $infourl = new moodle_url('/totara/appraisal/missing.php', array('appraisalid' => $appraisalid));
+                $infolink = html_writer::link($infourl->out(), $infostr, array('class' => 'missingroleslink'));
+
+                $out = get_string('missingrolesinfo', 'totara_appraisal', $infolink);
+            } else {
+                // Just the plain warning without a link.
+                $out = get_string('missingroles', 'totara_appraisal');
+            }
+        }
+
+        return $this->container($out, 'notifyproblem');
+    }
+
 
     /**
      * Displays the header for the given appraisal. Includes appraisal status for the given user.
@@ -1206,35 +1296,84 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         $out = html_writer::tag('h3', format_string($appraisal->name));
 
         // Display the list of participants.
-        $rolestringkeys = Appraisal::get_roles();
+        $rolestringkeys = appraisal::get_roles();
+        $actual = appraisal::get_live_role_assignments($userassignment->userid);
         $participants = array();
         foreach ($appraisal->get_roles_involved() as $role) {
             $rolestring = get_string($rolestringkeys[$role], 'totara_appraisal');
             if (empty($roleassignments[$role]) || empty($roleassignments[$role]->userid)) {
-                $participants[] = $rolestring . ": " . get_string('rolecurrentlyempty', 'totara_appraisal');
+                $participant = $rolestring . ": " . get_string('rolecurrentlyempty', 'totara_appraisal');
             } else {
                 $user = $DB->get_record('user', array('id' => $roleassignments[$role]->userid));
-                $participants[] = $rolestring . ": " . fullname($user);
+                $participant = $rolestring . ": " . fullname($user);
+            }
+
+            if ($appraisal->status == appraisal::STATUS_ACTIVE && $roleassignments[$role]->userid != $actual[$role]) {
+                $participant .= $this->container(get_string('rolehaschanged', 'totara_appraisal'), 'rolechangedwarning');
             }
+
+            $participants[] = $participant;
         }
         $out .= html_writer::tag('div', html_writer::tag('label', get_string('participants', 'totara_appraisal')) .
                     html_writer::alist($participants), array('class' => 'appraisal-participants'));
 
         if (!$preview) {
-            if ($appraisal->status == appraisal::STATUS_CLOSED) {
-                $icon = $this->output->pix_icon('i/invalid', get_string('closed', 'totara_appraisal'));
-                $out .= html_writer::tag('p', $icon . get_string('closedon', 'totara_appraisal',
-                        userdate($appraisal->timefinished, get_string('strftimedate', 'langconfig'))));
-            } else if (!empty($userassignment->timecompleted)) {
+            if (!empty($userassignment->timecompleted)) {
                 $icon = $this->output->pix_icon('i/valid', get_string('completed', 'totara_appraisal'));
                 $out .= html_writer::tag('p', $icon . get_string('completedon', 'totara_appraisal',
-                        userdate($userassignment->timecompleted, get_string('strftimedate', 'langconfig'))));
+                    userdate($userassignment->timecompleted, get_string('strftimedate', 'langconfig'))));
+            } else if ($appraisal->status == appraisal::STATUS_CLOSED) {
+                $icon = $this->output->pix_icon('i/invalid', get_string('closed', 'totara_appraisal'));
+                $out .= html_writer::tag('p', $icon . get_string('closedon', 'totara_appraisal',
+                    userdate($appraisal->timefinished, get_string('strftimedate', 'langconfig'))));
+            } else if ($userassignment->status == appraisal::STATUS_CLOSED) {
+                $icon = $this->output->pix_icon('i/invalid', get_string('closed', 'totara_appraisal'));
+                $userfullname = fullname($userassignment->user);
+                $out .= html_writer::tag('p', $icon . get_string('appraisalclosedforuser', 'totara_appraisal', $userfullname));
             }
+
         }
 
         return html_writer::tag('div', $out, array('class' => 'appraisal-title'));
     }
 
+    public function display_missing_roles($userassignmentid, $appraisalid) {
+        global $DB;
+
+        // Get roles required for the appraisal.
+        $appraisal = new appraisal($appraisalid);
+        $requiredroles = $appraisal->get_roles_involved(appraisal::ACCESS_MUSTANSWER);
+
+        if (empty($requiredroles)) {
+            return "";
+        }
+
+        // Check required roles are not empty.
+        list($insql, $inparam) = $DB->get_in_or_equal($requiredroles);
+        $missingsql = "SELECT *
+                         FROM {appraisal_role_assignment}
+                        WHERE appraisaluserassignmentid = ?
+                        AND userid = 0
+                        AND appraisalrole {$insql}";
+        $missingparams = array_merge(array($userassignmentid), $inparam);
+        $missingroles = $DB->get_records_sql($missingsql, $missingparams);
+
+        if (empty($missingroles)) {
+            return "";
+        }
+
+        $out = html_writer::tag('p', get_string('rolesmissing', 'totara_appraisal'));
+
+        $allroles = appraisal::get_roles();
+        $out .= html_writer::start_tag('ul');
+        foreach ($missingroles as $role) {
+            $out .= html_writer::tag('li', get_string($allroles[$role->appraisalrole], 'totara_appraisal'));
+        }
+        $out .= html_writer::end_tag('ul');
+
+        return $this->container($out, 'missingroles notifyproblem');
+    }
+
 
     /**
      * Displays the actions for the given appraisal. Intended to be used beside the appraisal header.
@@ -1304,7 +1443,8 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         if ($preview) {
             $info .= html_writer::tag('h4', get_string('inprogress', 'totara_appraisal'));
             $stagestatus = 'appraisal-stage-inprogress';
-        } else if ($appraisal->status == appraisal::STATUS_CLOSED && !$stage->is_completed($userassignment)) {
+        } else if ((($appraisal->status == appraisal::STATUS_CLOSED && !$stage->is_completed($userassignment)) ||
+                    ($userassignment->is_closed() && !$stage->is_completed($userassignment)))) {
             $info .= html_writer::tag('h4', get_string('incomplete', 'totara_appraisal'));
             if ($stageinprogress) {
                 $stagestatus = 'appraisal-stage-completed';
@@ -1332,8 +1472,9 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         if ($preview) {
             $rolescompletion = $stage->get_roles_involved(appraisal::ACCESS_CANANSWER);
         } else {
-            $rolescompletion = $stage->get_completion($userassignment->userid, appraisal::ACCESS_CANANSWER);
+            $rolescompletion = $stage->get_mandatory_completion($userassignment->userid);
         }
+
         $lines = '';
         foreach ($rolescompletion as $rolecompletion) {
             // Icon.
@@ -1387,7 +1528,11 @@ class totara_appraisal_renderer extends plugin_renderer_base {
             $pagesurl->param('stageid', $stage->id);
             $button = new single_button($pagesurl, get_string('preview', 'totara_appraisal'), 'get');
             $action .= $this->output->render($button);
+        } else if ($userassignment->is_closed()) {
+            $button = new single_button($pagesurl, get_string('view', 'totara_appraisal'), 'get');
+            $action .= $this->output->render($button);
         } else if ($stage->is_completed($userassignment)) {
+
             if (isset($stage->firstpage)) {
                 $pagesurl->param('pageid', $stage->firstpage);
                 $button = new single_button($pagesurl, get_string('view', 'totara_appraisal'), 'get');
@@ -1487,14 +1632,21 @@ class totara_appraisal_renderer extends plugin_renderer_base {
             return $out . get_string('nostages', 'totara_appraisal');
         }
 
+        $out .= html_writer::start_tag('div', array('class' => 'stagelist'));
+
+        // If appropriate display the missing roles warning above the stages.
+        if ($userassignment->status == appraisal::STATUS_ACTIVE) {
+            $out .= $this->display_missing_roles($userassignment->id, $userassignment->appraisalid);
+        }
+
         // Stages list.
         $stagelist = array();
         foreach ($stages as $stage) {
             $stageactions = $this->display_stage_actions_for_stages($appraisal, $stage, $userassignment, $roleassignment,
                     $urlparams, $preview);
-            $stagelist[] = $this->display_stage($appraisal, $stage, $userassignment, $roleassignment, $stageactions, $preview);
+            $out .= $this->display_stage($appraisal, $stage, $userassignment, $roleassignment, $stageactions, $preview);
         }
-        $out .= html_writer::tag('div', implode("\n", $stagelist), array('class' => 'stagelist'));
+        $out .= html_writer::end_tag('div');
 
         return $out;
     }
@@ -1647,4 +1799,64 @@ class totara_appraisal_renderer extends plugin_renderer_base {
         // Combine and display.
         return html_writer::tag('div', $heading . $rolecontrol . $description, array('class' => "plan_box notifymessage", 'id' => 'preview-appraisal-notification'));
     }
+
+    /**
+     * Returns the base markup for a snapshot export.
+     *
+     * @return string HTML
+     */
+    public function display_snapshot($appraisal, $subject, $userassignment, $roleassignment, $spaces = 0) {
+        global $CFG, $TEXTAREA_OPTIONS;
+
+        require_once($CFG->libdir . '/dompdf/lib.php');
+        require_once($CFG->dirroot . '/totara/appraisal/appraisal_forms.php');
+
+        // Set up.
+        $out = "";
+        $role = $roleassignment->appraisalrole;
+        $roles = $appraisal::get_roles();
+        $assignments = $appraisal->get_all_assignments($subject->id);
+        $otherassignments = $assignments;
+        $nouserpic = true;
+
+        $out .= $this->display_appraisal_header($appraisal, $userassignment, $assignments);
+
+        $appdesc = new stdClass();
+        $appdesc->description = $appraisal->description;
+        $appdesc->descriptionformat = FORMAT_HTML;
+        $appdesc = file_prepare_standard_editor($appdesc, 'description', $TEXTAREA_OPTIONS, $TEXTAREA_OPTIONS['context'],
+                'totara_appraisal', 'appraisal', $appraisal->id);
+        $out .= $appdesc->description_editor['text'];
+        $out .= $this->display_viewing_appraisal_header($subject, 'youareprintingxsappraisal', $role);
+        $stageslist = appraisal_stage::get_stages($appraisal->id, array($role));
+
+        foreach ($stageslist as $stageid => $stagedata) {
+            if (empty($printstages) || in_array($stageid, $printstages)) {
+                // Print stage.
+                $stage = new appraisal_stage($stageid);
+                $out .= $this->display_stage($appraisal, $stage, $userassignment, $roleassignment, '', false);
+
+                $pages = appraisal_page::get_applicable_pages($stageid, $role, 0, false);
+                foreach ($pages as $page) {
+                    // Print page.
+                    $out .= $this->heading($page->name);
+
+                    // Print form.
+                    $form = new appraisal_answer_form(null, array('appraisal' => $appraisal, 'page' => $page,
+                    'userassignment' => $userassignment, 'roleassignment' => $roleassignment,
+                    'otherassignments' => $otherassignments, 'spaces' => $spaces, 'nouserpic' => $nouserpic,
+                    'action' => 'print', 'preview' => false, 'islastpage' => false, 'readonly' => true));
+
+                    foreach ($assignments as $assignment) {
+                        $form->set_data($appraisal->get_answers($page->id, $assignment));
+                    }
+
+                    $out .= $form->render();
+                    $form->reset_form_sent();
+                }
+            }
+        }
+
+        return $out;
+    }
 }
diff --git a/totara/appraisal/snapshot.php b/totara/appraisal/snapshot.php
index 53aab68..4fb06f5 100644
--- a/totara/appraisal/snapshot.php
+++ b/totara/appraisal/snapshot.php
@@ -53,6 +53,7 @@ $stageschecked = (isset($_REQUEST['stages']) && is_array($_REQUEST['stages'])) ?
 $printstages = array_keys(array_filter($stageschecked));
 $action = optional_param('action', '', PARAM_ACTION);
 
+$subject = $DB->get_record('user', array('id' => $subjectid));
 if ($action == 'stages') {
     if ($subjectid == $USER->id) {
         require_capability('totara/appraisal:printownappraisals', $systemcontext);
@@ -85,6 +86,7 @@ if (!$appraisal->can_access($roleassignment)) {
 }
 $assignments = $appraisal->get_all_assignments($subjectid);
 $otherassignments = $assignments;
+
 unset($otherassignments[$roleassignment->appraisalrole]);
 
 $PAGE->set_url(new moodle_url('/totara/appraisal/snapshot.php', array('role' => $role,
@@ -103,67 +105,19 @@ $PAGE->set_heading($heading);
 $nouserpic = false;
 
 if ($action == 'snapshot') {
-    $ob_was = 0;
     $nouserpic = true;
-    $ob_level = ob_get_level();
-    // IIS always return true for ob_get_level().
-    while ($ob_level != $ob_was && $ob_level > 0) {
-        $ob_was = $ob_level;
-        ob_flush();
-        $ob_level = ob_get_level();
-    }
     require_once($CFG->libdir . '/dompdf/lib.php');
     set_time_limit('300');
-    ob_start();
 }
 
-echo $renderer->header();
-
-// Print appraisal header.
-$roleassignments = $appraisal->get_all_assignments($userassignment->userid);
-echo $renderer->display_appraisal_header($appraisal, $userassignment, $roleassignments);
-$appdesc = new stdClass();
-$appdesc->description = $appraisal->description;
-$appdesc->descriptionformat = FORMAT_HTML;
-$appdesc = file_prepare_standard_editor($appdesc, 'description', $TEXTAREA_OPTIONS, $TEXTAREA_OPTIONS['context'],
-        'totara_appraisal', 'appraisal', $appraisal->id);
-echo $appdesc->description_editor['text'];
-$subject = $DB->get_record('user', array('id' => $subjectid));
-echo $renderer->display_viewing_appraisal_header($subject, 'youareprintingxsappraisal', $role);
-$stageslist = appraisal_stage::get_stages($appraisal->id, array($role));
-
-foreach ($stageslist as $stageid => $stagedata) {
-    if (empty($printstages) || in_array($stageid, $printstages)) {
-        // Print stage.
-        $stage = new appraisal_stage($stageid);
-        echo $renderer->display_stage($appraisal, $stage, $userassignment, $roleassignment, '', false);
-
-        $pages = appraisal_page::get_applicable_pages($stageid, $role, 0, false);
-        foreach ($pages as $page) {
-            // Print page.
-            echo $renderer->heading($page->name);
-
-            // Print form.
-            $form = new appraisal_answer_form(null, array('appraisal' => $appraisal, 'page' => $page,
-            'userassignment' => $userassignment, 'roleassignment' => $roleassignment,
-            'otherassignments' => $otherassignments, 'spaces' => $spaces, 'nouserpic' => $nouserpic,
-            'action' => 'print', 'preview' => false, 'islastpage' => false, 'readonly' => true));
-
-            foreach ($assignments as $assignment) {
-                $form->set_data($appraisal->get_answers($page->id, $assignment));
-            }
-            $form->display();
-            $form->reset_form_sent();
-        }
-    }
-}
+$out = "";
+$out .= $renderer->header();
+$out .= $renderer->display_snapshot($appraisal, $subject, $userassignment, $roleassignment, $spaces);
 
 if ($action == 'snapshot') {
-    $html = ob_get_contents();
-    ob_end_clean();
     $filename = 'appraisal_'.$appraisal->id.'_'.date("Y-m-d_His").'_'.$roles[$role].'.pdf';
     $pdf = new totara_dompdf();
-    $pdf->load_html($html);
+    $pdf->load_html($out);
     $pdf->render();
 
     file_put_contents($CFG->tempdir.'/'.$filename, $pdf->output());
@@ -180,6 +134,7 @@ if ($action == 'snapshot') {
     echo html_writer::tag('div', get_string('snapshotdone', 'totara_appraisal', $strsource),
             array('class'=>'notifysuccess dialog-nobind'));
 } else {
+    echo $out;
     echo $renderer->footer();
 }
 
diff --git a/totara/appraisal/stage.php b/totara/appraisal/stage.php
index 7d4cdcd..e081d39 100644
--- a/totara/appraisal/stage.php
+++ b/totara/appraisal/stage.php
@@ -157,6 +157,10 @@ switch ($action) {
 }
 
 $output = $PAGE->get_renderer('totara_appraisal');
+$title = $PAGE->title . ': ' . $appraisal->name;
+$PAGE->set_title($title);
+$PAGE->set_heading($appraisal->name);
+$PAGE->navbar->add($appraisal->name);
 
 local_js();
 
diff --git a/totara/appraisal/styles.css b/totara/appraisal/styles.css
index 001d482..22c5530 100644
--- a/totara/appraisal/styles.css
+++ b/totara/appraisal/styles.css
@@ -496,3 +496,18 @@ form.print-stages-form .fitem .fitemtitle {
 #preview-appraisal-notification {
     min-height: 115px
 }
+.update_assignment_records {
+    margin-top: -5px;
+    float: right;
+}
+
+.dir-rtl .update_assignment_records {
+    float: left;
+}
+
+.rolechangedwarning {
+    display: inline-block;
+    margin-left: 20px;
+    padding-left: 20px;
+    background: url([[pix:theme|notifynotice]]) no-repeat;
+}
diff --git a/totara/appraisal/tests/appraisal_test.php b/totara/appraisal/tests/appraisal_test.php
index 4c127d8..ef29117 100644
--- a/totara/appraisal/tests/appraisal_test.php
+++ b/totara/appraisal/tests/appraisal_test.php
@@ -113,7 +113,9 @@ class appraisal_test extends appraisal_testcase {
         $this->setAdminUser();
 
         list($appraisal1) = $this->prepare_appraisal_with_users();
-        $this->assertEmpty($appraisal1->validate());
+        list($errors, $warnings) = $appraisal1->validate();
+        $this->assertEmpty($errors);
+        $this->assertEmpty($warnings);
 
         $appraisal1->activate();
 
@@ -137,12 +139,14 @@ class appraisal_test extends appraisal_testcase {
         $this->setAdminUser();
 
         list($appraisal) = $this->prepare_appraisal_with_users();
-        $this->assertEmpty($appraisal->validate());
+        list($errors, $warnings) = $appraisal->validate();
+        $this->assertEmpty($errors);
+        $this->assertEmpty($warnings);
         $appraisal->activate();
 
-        $res = $appraisal->validate();
-        $this->assertCount(1, $res);
-        $this->assertEquals(array('status'), array_keys($res));
+        list($errors, $warnings) = $appraisal->validate();
+        $this->assertCount(1, $errors);
+        $this->assertEquals(array('status'), array_keys($errors));
 
     }
 
@@ -157,8 +161,8 @@ class appraisal_test extends appraisal_testcase {
             ))
         ));
         list($appraisal) = $this->prepare_appraisal_with_users($def);
-        $res = $appraisal->validate();
-        $this->assertArrayHasKey('roles', $res);
+        list($errors, $warnings) = $appraisal->validate();
+        $this->assertArrayHasKey('roles', $errors);
     }
 
     public function test_appraisal_answers() {
@@ -226,7 +230,9 @@ class appraisal_test extends appraisal_testcase {
         $this->assertEquals(0, $appraisal->count_incomplete_userassignments());
         $this->assertTrue($appraisal->is_locked($updateduserassignment));
         $this->assertTrue($appraisal->is_locked($updateduser2assignment));
-        $this->assertEquals(appraisal::STATUS_COMPLETED, $appraisal->status);
+
+        // Appraisals no longer auto complete due to dynamic assignments, check it is still open.
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
     }
 
     public function test_appraisal_role_involved() {
@@ -297,4 +303,571 @@ class appraisal_test extends appraisal_testcase {
         $this->assertEmpty($users3managappr);
         $this->assertEmpty($user4appr);
     }
+
+    public function test_active_appraisal_add_group () {
+        global $DB;
+
+        // Set up active appraisal.
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        list($appraisal) = $this->prepare_appraisal_with_users();
+        list($errors, $warnings) = $appraisal->validate();
+        $this->assertEmpty($errors);
+        $this->assertEmpty($warnings);
+
+        $this->assertEquals(appraisal::STATUS_DRAFT, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(0, $count);
+
+        $appraisal->activate();
+
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, $count);
+
+        // Set up group.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        cohort_add_member($cohort->id, $user1->id);
+        cohort_add_member($cohort->id, $user2->id);
+
+        // Add group.
+        $urlparams = array('includechildren' => false, 'listofvalues' => array($cohort->id));
+        $assign = new totara_assign_appraisal('appraisal', $appraisal);
+        $grouptypeobj = $assign->load_grouptype('cohort');
+        $grouptypeobj->handle_item_selector($urlparams);
+
+        // There should still only be 2 user assignments.
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, $count);
+
+        // Force user assignments update.
+        $appraisal->check_assignment_changes();
+
+        // Check users have now gone up to 4.
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(4, $count);
+    }
+
+    public function test_active_appraisal_remove_group () {
+        global $DB;
+
+        // Set up active appraisal.
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        list($appraisal) = $this->prepare_appraisal_with_users();
+        list($errors, $warnings) = $appraisal->validate();
+        $this->assertEmpty($errors);
+        $this->assertEmpty($warnings);
+
+        // Set up group.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        cohort_add_member($cohort->id, $user1->id);
+        cohort_add_member($cohort->id, $user2->id);
+
+        // Add group.
+        $urlparams = array('includechildren' => false, 'listofvalues' => array($cohort->id));
+        $assign = new totara_assign_appraisal('appraisal', $appraisal);
+        $grouptypeobj = $assign->load_grouptype('cohort');
+        $grouptypeobj->handle_item_selector($urlparams);
+
+        $this->assertEquals(appraisal::STATUS_DRAFT, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(0, $count);
+
+        $appraisal->activate();
+
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(4, $count);
+
+        // Remove one of the groups.
+        $assign->delete_assigned_group('cohort', $cohort->id);
+
+        // Force user assignments update.
+        $appraisal->check_assignment_changes();
+
+        // Check appraisal is still active, and total user assignments are still 4.
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(4, $count);
+        // Only 2 user assignments should be active.
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id, 'status' => appraisal::STATUS_ACTIVE));
+        $this->assertEquals(2, $count);
+        // There should be 2 closed user assignments, the removed 2.
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id, 'status' => appraisal::STATUS_CLOSED));
+        $this->assertEquals(2, $count);
+    }
+
+    /**
+     * Test activating an appraisal when an assigned user is missing required roles.
+     *
+     * User position assignment structure
+     * $user1 ------| Manager   | 0
+     *              | Teamlead  | 0
+     *              | Appraiser | 0
+     */
+    public function test_activation_with_missing_roles() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Set up.
+        $roles = array();
+        $roles[appraisal::ROLE_LEARNER] = 6;
+        $roles[appraisal::ROLE_MANAGER] = 6;
+        $roles[appraisal::ROLE_TEAM_LEAD] = 6;
+        $roles[appraisal::ROLE_APPRAISER] = 6;
+
+        $def = array('name' => 'Appraisal', 'stages' => array(
+            array('name' => 'Stage', 'timedue' => time() + 86400, 'pages' => array(
+                array('name' => 'Page', 'questions' => array(
+                    array('name' => 'Text', 'type' => 'text', 'roles' => $roles)
+                ))
+            ))
+        ));
+
+        $user1 = $this->getDataGenerator()->create_user();
+
+        list($appraisal) = $this->prepare_appraisal_with_users($def, array($user1));
+        list($errors, $warnings) = $appraisal->validate();
+
+        // This should only generate role warnings not errors.
+        $this->assertEmpty($errors);
+
+        // There should be a warning for manager, teamlead and appraiser.
+        $this->assertEquals(3, count($warnings));
+
+        $this->assertEquals(appraisal::STATUS_DRAFT, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(0, $count);
+
+        $appraisal->activate();
+
+        $this->assertEquals(appraisal::STATUS_ACTIVE, $appraisal->status);
+        $count = $DB->count_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(1, $count);
+    }
+
+    /**
+     * Test removing the position assignments of an assigned user
+     * while they are assigned to an active appraisal.
+     *
+     * User position assignment structure
+     * $manager ----| Manager   | $teamlead1
+     *
+     * $user1 ------| Manager   | $manager     -> 0
+     *              | Teamlead  | $teamlead    -> 0
+     *              | Appraiser | $appraiser   -> 0
+     *
+     * $user2 ------| Manager   | $manager
+     *              | Teamlead  | $teamlead
+     *              | Appraiser | $appraiser
+     */
+    public function test_active_appraisal_role_removal() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Set up appraisal.
+        $roles = array();
+        $roles[appraisal::ROLE_LEARNER] = 6;
+        $roles[appraisal::ROLE_MANAGER] = 6;
+        $roles[appraisal::ROLE_TEAM_LEAD] = 6;
+        $roles[appraisal::ROLE_APPRAISER] = 6;
+
+        $def = array('name' => 'Appraisal', 'stages' => array(
+            array('name' => 'Stage', 'timedue' => time() + 86400, 'pages' => array(
+                array('name' => 'Page', 'questions' => array(
+                    array('name' => 'Text', 'type' => 'text', 'roles' => $roles)
+                ))
+            ))
+        ));
+        $appraisal = appraisal::build($def);
+
+        // Set up group.
+        $teamlead = $this->getDataGenerator()->create_user();
+        $manager = $this->getDataGenerator()->create_user();
+        $appraiser = $this->getDataGenerator()->create_user();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $assignment = new position_assignment(array('userid' => $manager->id, 'type' => 1));
+        $assignment->managerid = $teamlead->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user1->id, 'type' => 1));
+        $assignment->managerid = $manager->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user2->id, 'type' => 1));
+        $assignment->managerid = $manager->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        cohort_add_member($cohort->id, $user1->id);
+        cohort_add_member($cohort->id, $user2->id);
+
+        // Assign group and activate.
+        $urlparams = array('includechildren' => false, 'listofvalues' => array($cohort->id));
+        $assign = new totara_assign_appraisal('appraisal', $appraisal);
+        $grouptypeobj = $assign->load_grouptype('cohort');
+        $grouptypeobj->handle_item_selector($urlparams);
+
+        $appraisal->activate();
+
+        // That should have created 2 user assignments.
+        $userassignments = $DB->get_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, count($userassignments));
+
+        // And 4 role assignments per userassignment.
+        foreach ($userassignments as $aua) {
+            $countrole = $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $aua->id));
+            $this->assertEquals(4, $countrole);
+        }
+
+        // Now Change user1s position assignment.
+        $assignment = new position_assignment(array('userid' => $user1->id, 'type' => 1));
+        $assignment->managerid = 0;
+        $assignment->appraiserid = 0;
+        assign_user_position($assignment, true);
+
+        // User1 should now be missing all roles (except learner).
+        $missing = $appraisal->get_missingrole_users(true);
+        $this->assertEquals(1, count($missing));
+        $this->assertEquals(3, count($missing[$user1->id]));
+
+        // Check that the changedrole function finds the 3 roles that have changed.
+        $changedroles = $appraisal->get_changedrole_users();
+        $this->assertEquals(3, count($changedroles));
+
+        $appraisal->check_assignment_changes();
+
+        // Check that the changedrole function now resolves to 0.
+        $changedroles = $appraisal->get_changedrole_users();
+        $this->assertEquals(0, count($changedroles));
+
+        // There should still be 2 user assignments.
+        $userassignments = $DB->get_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, count($userassignments));
+
+        // Each with the corresponding 4 role assignments.
+        foreach ($userassignments as $aua) {
+            $countrole = $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $aua->id));
+            $this->assertEquals(4, $countrole);
+        }
+
+        // But user1s role assignments should have the userid set to 0.
+        $userassig = $userassignments[1]->userid == $user1->id ? $userassignments[1] : $userassignments[2];
+        $countrole = $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassig->id, 'userid' => 0));
+        $this->assertEquals(3, $countrole);
+    }
+
+    /**
+     * Test changing the position assignments of an assigned user
+     * while they are assigned to an active appraisal.
+     *
+     * User position assignment structure
+     * $manager1 ---| Manager   | $teamlead1
+     *
+     * $manager2 ---| Manager   | $teamlead2
+     *
+     * $user1 ------| Manager   | $manager1     -> $manager2
+     *              | Teamlead  | $teamlead1    -> $teamlead2
+     *              | Appraiser | $appraiser1   -> $appraiser2
+     *
+     * $user2 ------| Manager   | $manager1
+     *              | Teamlead  | $teamlead1
+     *              | Appraiser | $appraiser1
+     */
+    public function test_active_appraisal_role_reassignment() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Set up appraisal.
+        $roles = array();
+        $roles[appraisal::ROLE_LEARNER] = 6;
+        $roles[appraisal::ROLE_MANAGER] = 6;
+        $roles[appraisal::ROLE_TEAM_LEAD] = 6;
+        $roles[appraisal::ROLE_APPRAISER] = 6;
+
+        $def = array('name' => 'Appraisal', 'stages' => array(
+            array('name' => 'Stage', 'timedue' => time() + 86400, 'pages' => array(
+                array('name' => 'Page', 'questions' => array(
+                    array('name' => 'Text', 'type' => 'text', 'roles' => $roles)
+                ))
+            ))
+        ));
+        $appraisal = appraisal::build($def);
+
+        // Set up group.
+        $teamlead = $this->getDataGenerator()->create_user();
+        $manager = $this->getDataGenerator()->create_user();
+        $appraiser = $this->getDataGenerator()->create_user();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $teamlead2 = $this->getDataGenerator()->create_user();
+        $manager2 = $this->getDataGenerator()->create_user();
+        $appraiser2 = $this->getDataGenerator()->create_user();
+
+        $assignment = new position_assignment(array('userid' => $manager->id, 'type' => 1));
+        $assignment->managerid = $teamlead->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $manager2->id, 'type' => 1));
+        $assignment->managerid = $teamlead2->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user1->id, 'type' => 1));
+        $assignment->managerid = $manager->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user2->id, 'type' => 1));
+        $assignment->managerid = $manager->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        cohort_add_member($cohort->id, $user1->id);
+        cohort_add_member($cohort->id, $user2->id);
+
+        // Assign group and activate.
+        $urlparams = array('includechildren' => false, 'listofvalues' => array($cohort->id));
+        $assign = new totara_assign_appraisal('appraisal', $appraisal);
+        $grouptypeobj = $assign->load_grouptype('cohort');
+        $grouptypeobj->handle_item_selector($urlparams);
+
+        $appraisal->activate();
+
+        // That should have created 2 user assignments.
+        $userassignments = $DB->get_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, count($userassignments));
+
+        // And 4 role assignments per userassignment.
+        foreach ($userassignments as $aua) {
+            $countrole = $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $aua->id));
+            $this->assertEquals(4, $countrole);
+        }
+
+        // Now Change user1s position assignment.
+        $assignment = new position_assignment(array('userid' => $user1->id, 'type' => 1));
+        $assignment->managerid = $manager2->id;
+        $assignment->appraiserid = $appraiser2->id;
+        assign_user_position($assignment, true);
+
+        // There should be no missing roles.
+        $missing = $appraisal->get_missingrole_users(true);
+        $this->assertEquals(0, count($missing));
+
+        // Check that the changedrole function finds the 3 roles that have changed.
+        $changedroles = $appraisal->get_changedrole_users();
+        $this->assertEquals(3, count($changedroles));
+
+        $appraisal->check_assignment_changes();
+
+        // Check that the changedrole function now resolves to 0.
+        $changedroles = $appraisal->get_changedrole_users();
+        $this->assertEquals(0, count($changedroles));
+
+        // There should still be 2 user assignments.
+        $userassignments = $DB->get_records('appraisal_user_assignment', array('appraisalid' => $appraisal->id));
+        $this->assertEquals(2, count($userassignments));
+
+        // Each with the corresponding 4 role assignments.
+        foreach ($userassignments as $aua) {
+            $countrole = $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $aua->id));
+            $this->assertEquals(4, $countrole);
+        }
+
+        // Check user1s role assignments have been swapped to the new users.
+        $userassig = $userassignments[1]->userid == $user1->id ? $userassignments[1] : $userassignments[2];
+        $roles = $DB->get_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $userassig->id));
+        foreach ($roles as $role) {
+            switch ($role->appraisalrole) {
+                case appraisal::ROLE_LEARNER :
+                    $this->assertEquals($user1->id, $role->userid);
+                    break;
+                case appraisal::ROLE_MANAGER :
+                    $this->assertEquals($manager2->id, $role->userid);
+                    break;
+                case appraisal::ROLE_TEAM_LEAD :
+                    $this->assertEquals($teamlead2->id, $role->userid);
+                    break;
+                case appraisal::ROLE_APPRAISER :
+                    $this->assertEquals($appraiser2->id, $role->userid);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Test deleting an assigned user while they are assigned to an active appraisal
+     *
+     * User position assignment structure
+     * $manager ----| Manager   | $teamlead
+     *
+     * $user1   ----| Manager   | $manager      -> null
+     *              | Teamlead  | $teamlead     -> null
+     *              | Appraiser | $appraiser    -> null
+     *
+     * $user2   ----| Manager   | $user1        -> 0
+     *              | Teamlead  | $manager      -> 0
+     *              | Appraiser | $appraiser
+     */
+    public function test_active_appraisal_user_deletion() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Set up appraisal.
+        $roles = array();
+        $roles[appraisal::ROLE_LEARNER] = 6;
+        $roles[appraisal::ROLE_MANAGER] = 6;
+        $roles[appraisal::ROLE_TEAM_LEAD] = 6;
+        $roles[appraisal::ROLE_APPRAISER] = 6;
+
+        $def = array('name' => 'Appraisal', 'stages' => array(
+            array('name' => 'Stage', 'timedue' => time() + 86400, 'pages' => array(
+                array('name' => 'Page', 'questions' => array(
+                    array('name' => 'Text', 'type' => 'text', 'roles' => $roles)
+                ))
+            ))
+        ));
+        $appraisal = appraisal::build($def);
+        $answertable = 'appraisal_quest_data_'.$appraisal->id;
+
+        // Create users.
+        $teamlead = $this->getDataGenerator()->create_user();
+        $manager = $this->getDataGenerator()->create_user();
+        $appraiser = $this->getDataGenerator()->create_user();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        // Set up position assignments.
+        $assignment = new position_assignment(array('userid' => $manager->id, 'type' => 1));
+        $assignment->managerid = $teamlead->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user1->id, 'type' => 1));
+        $assignment->managerid = $manager->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        $assignment = new position_assignment(array('userid' => $user2->id, 'type' => 1));
+        $assignment->managerid = $user1->id;
+        $assignment->appraiserid = $appraiser->id;
+        assign_user_position($assignment, true);
+
+        // Create group and assign users.
+        $cohort = $this->getDataGenerator()->create_cohort();
+        cohort_add_member($cohort->id, $user1->id);
+        cohort_add_member($cohort->id, $user2->id);
+
+        // Assign group and activate.
+        $urlparams = array('includechildren' => false, 'listofvalues' => array($cohort->id));
+        $assign = new totara_assign_appraisal('appraisal', $appraisal);
+        $grouptypeobj = $assign->load_grouptype('cohort');
+        $grouptypeobj->handle_item_selector($urlparams);
+
+        $appraisal->activate();
+
+        $ua1 = $DB->get_record('appraisal_user_assignment', array('appraisalid' => $appraisal->id, 'userid' => $user1->id));
+        $ua2 = $DB->get_record('appraisal_user_assignment', array('appraisalid' => $appraisal->id, 'userid' => $user2->id));
+
+        // This should have created 2 user assignments and 8 role assignments.
+        $this->assertEquals(2, $DB->count_records('appraisal_user_assignment'));
+        $this->assertEquals(8, $DB->count_records('appraisal_role_assignment'));
+        $this->assertEquals(0, $DB->count_records('appraisal_quest_data_'.$appraisal->id));
+
+        // Now create some answer data for user1.
+        $user1roles = array();
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user1->id, $user1->id,
+            appraisal::ROLE_LEARNER);
+        $user1roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user1->id, $manager->id,
+                appraisal::ROLE_MANAGER);
+        $user1roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user1->id, $teamlead->id,
+                appraisal::ROLE_TEAM_LEAD);
+        $user1roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user1->id, $appraiser->id,
+                appraisal::ROLE_APPRAISER);
+        $user1roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+
+        // Now create some answer data for user2.
+        $user2roles = array();
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user2->id, $user2->id,
+                appraisal::ROLE_LEARNER);
+        $user2roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user2->id, $user1->id,
+                appraisal::ROLE_MANAGER);
+        $user2roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user2->id, $manager->id,
+                appraisal::ROLE_TEAM_LEAD);
+        $user2roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+        $roleassignment = appraisal_role_assignment::get_role($appraisal->id, $user2->id, $appraiser->id,
+                appraisal::ROLE_APPRAISER);
+        $user2roles[] = $roleassignment->id;
+        $this->answer_question($appraisal, $roleassignment, 0, 'completestage');
+
+        list($u1insql, $u1param) = $DB->get_in_or_equal($user1roles);
+        $u1sql = "SELECT COUNT(*) FROM {{$answertable}} where appraisalroleassignmentid " . $u1insql;
+        list($u2insql, $u2param) = $DB->get_in_or_equal($user2roles);
+        $u2sql = "SELECT COUNT(*) FROM {{$answertable}} where appraisalroleassignmentid " . $u2insql;
+
+        // There should now be 8 answer records, 4 per user_assignment.
+        $this->assertEquals(8, $DB->count_records($answertable));
+        $this->assertEquals(4, $DB->count_records_sql($u1sql, $u1param));
+        $this->assertEquals(4, $DB->count_records_sql($u2sql, $u2param));
+
+        // First half of the delete, remove user_assignment records.
+        appraisal::delete_learner_assignments($user1->id);
+
+        // This should have deleted user1's user assignment and associated role assignments.
+        $this->assertEquals(1, $DB->count_records('appraisal_user_assignment'));
+        $this->assertEquals(0, $DB->count_records('appraisal_user_assignment', array('userid' => $user1->id)));
+        $this->assertEquals(4, $DB->count_records('appraisal_role_assignment'));
+        $this->assertEquals(0, $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $ua1->id)));
+        $this->assertEquals(4, $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $ua2->id)));
+        $this->assertEquals(4, $DB->count_records($answertable));
+        $this->assertEquals(0, $DB->count_records_sql($u1sql, $u1param));
+        $this->assertEquals(4, $DB->count_records_sql($u2sql, $u2param));
+
+        // Second half of the delete, unassign role_assignment records.
+        appraisal::unassign_user_roles($user1->id);
+
+        // This should have left the role_assignments and associated data alone but set the userid to 0.
+        $this->assertEquals(1, $DB->count_records('appraisal_user_assignment'));
+        $this->assertEquals(1, $DB->count_records('appraisal_user_assignment', array('userid' => $user2->id)));
+        $this->assertEquals(4, $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $ua2->id)));
+        $this->assertEquals(1, $DB->count_records('appraisal_role_assignment', array('appraisaluserassignmentid' => $ua2->id, 'userid' => 0)));
+        $this->assertEquals(4, $DB->count_records($answertable));
+        $this->assertEquals(0, $DB->count_records_sql($u1sql, $u1param));
+        $this->assertEquals(4, $DB->count_records_sql($u2sql, $u2param));
+    }
 }
diff --git a/totara/appraisal/tests/message_test.php b/totara/appraisal/tests/message_test.php
index 28fefb0..aec44bb 100644
--- a/totara/appraisal/tests/message_test.php
+++ b/totara/appraisal/tests/message_test.php
@@ -468,4 +468,4 @@ class appraisal_message_test extends appraisal_testcase {
 
         ini_set('error_log', $oldlog);
     }
-}
\ No newline at end of file
+}
diff --git a/totara/appraisal/version.php b/totara/appraisal/version.php
index 36f0bf8..b2cf37a 100644
--- a/totara/appraisal/version.php
+++ b/totara/appraisal/version.php
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version  = 2014062000;       // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2014062302;       // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2012120305;       // Requires this Moodle version.
 $plugin->cron = 0;                    // Period for cron to check this module (secs).
 $plugin->component = 'totara_appraisal';   // To check on upgrade, that module sits in correct place.
diff --git a/totara/core/lib/assign/assigngroup_dialog.js b/totara/core/lib/assign/assigngroup_dialog.js
index 34306df..019dee5 100644
--- a/totara/core/lib/assign/assigngroup_dialog.js
+++ b/totara/core/lib/assign/assigngroup_dialog.js
@@ -52,9 +52,8 @@ M.totara_assigngroupdialog = M.totara_assigngroupdialog || {
                 }
             }
         }
-
         // check jQuery dependency is available
-        if (typeof $ === 'undefined') {
+        if (typeof $ == 'undefined') {
             throw new Error('M.totara_assigngroupdialog.init()-> jQuery dependency required for this module.');
         }
 
@@ -126,9 +125,12 @@ M.totara_assigngroupdialog = M.totara_assigngroupdialog || {
 var assigngroup_handler_responsefunc = function(response) {
 
     if (response.substr(0,4) == 'DONE') {
+        if (response.lastIndexOf('updatestr') != -1) {
+            var start = response.lastIndexOf('updatestr');
+        }
+
         // Get all root elements in response
         var els = $(response.substr(4));
-
         // If we're adding a new group, insert it
         if (this.responsetype == 'newgroup') {
             this.responsegoeshere.replaceWith(els);
@@ -140,6 +142,11 @@ var assigngroup_handler_responsefunc = function(response) {
             }
         }
 
+        var notice = document.getElementById('notlivenotice');
+        if (notice !== null ) {
+            notice.innerHTML = M.totara_assigngroupdialog.config.notice;
+        }
+
         // Close dialog
         this._dialog.hide();
     } else {
diff --git a/totara/core/lib/assign/groups/cohort.class.php b/totara/core/lib/assign/groups/cohort.class.php
index cc65714..75ea414 100644
--- a/totara/core/lib/assign/groups/cohort.class.php
+++ b/totara/core/lib/assign/groups/cohort.class.php
@@ -63,6 +63,24 @@ class totara_assign_core_grouptype_cohort extends totara_assign_core_grouptype {
     }
 
     /**
+     * Gets the name of a cohort based off the group assignment id
+     *
+     * @param  int  $instanceid The id of the group assignment record
+     * @return string
+     */
+    public function get_instance_name($instanceid) {
+        global $DB;
+
+        $sql = "SELECT c.name
+                  FROM {{$this->tablename}} grp
+                  JOIN {cohort} c
+                    ON grp.cohortid = c.id
+                 WHERE grp.id = ?";
+
+        return format_string($DB->get_field_sql($sql, array($instanceid)));
+    }
+
+    /**
      * Instantiate and display the dialog class content
      * @access public
      * @param $urlparams array Parameters to be passed to the code handling the dialog submission
diff --git a/totara/core/lib/assign/hierarchy.class.php b/totara/core/lib/assign/hierarchy.class.php
index 067b35c..c265e01 100644
--- a/totara/core/lib/assign/hierarchy.class.php
+++ b/totara/core/lib/assign/hierarchy.class.php
@@ -59,6 +59,24 @@ class totara_assign_core_grouptype_hierarchy extends totara_assign_core_grouptyp
     }
 
     /**
+     * Gets the name of a hierarchy pos/org based off the group assignment id
+     *
+     * @param  int  $instanceid The id of the group assignment record
+     * @return string
+     */
+    public function get_instance_name($instanceid) {
+        global $DB;
+
+        $sql = "SELECT h.fullname
+                  FROM {{$this->tablename}} grp
+                  JOIN {{$this->grouptype}} h
+                    ON grp.{$this->grouptype}id = h.id
+                 WHERE grp.id = ?";
+
+        return format_string($DB->get_field_sql($sql, array($instanceid)));
+    }
+
+    /**
      * Instantiate and display the dialog class content
      * @access public
      * @param $urlparams array Parameters to be passed to the code handling the dialog submission
diff --git a/totara/core/lib/assign/lib.php b/totara/core/lib/assign/lib.php
index f179184..e834672 100644
--- a/totara/core/lib/assign/lib.php
+++ b/totara/core/lib/assign/lib.php
@@ -32,6 +32,7 @@
 require_once($CFG->dirroot.'/totara/reportbuilder/lib.php');
 
 class totara_assign_core {
+
     /**
      * Reference to the module.
      *
@@ -147,6 +148,24 @@ class totara_assign_core {
     }
 
     /**
+     * Get the name of a group assignment
+     *
+     * @param  string  grouptype
+     * @param  int     groupid    The id of the group assignment
+     * @return string
+     */
+    public function get_group_instance_name($grouptype, $groupid) {
+        $groups = $this->get_assignable_grouptypes();
+
+        if (!in_array($grouptype, $groups)) {
+            print_error('error:invalidgrouptype', 'totara_core');
+        }
+
+        $group = $this->load_grouptype($grouptype);
+        return $group->get_instance_name($groupid);
+    }
+
+    /**
      * Delete an assigned group
      * @access  public
      * @param $grouptype string grouptype prefix e.g. 'org'
@@ -240,26 +259,38 @@ class totara_assign_core {
      * @param $search string A search string to limit the results by (matches user firstname or lastname).
      * @param $limitfrom int
      * @param $limitnum int
+     * @param $active boolean A flag that makes the function return only the active users.
      * @return recordset Containing basic information about users.
      */
-    public function get_current_users($search=null, $limitfrom=null, $limitnum=null) {
+    public function get_current_users($search=null, $limitfrom=null, $limitnum=null, $forcegroup=false) {
         global $DB;
         // How the current user list is calculated depends of the status.
         // It could be static (from a table) or dynamic (calculated).
-        if ($this->assignments_are_stored()) {
-            list($joinsql, $params) = $this->get_users_from_assignments_sql('u', 'id');
+        $liveassignments = $this->assignments_are_stored() && !$forcegroup;
+        if ($liveassignments) {
+            list($joinsql, $params, $joinalias) = $this->get_users_from_assignments_sql('u', 'id');
         } else {
-            list($joinsql, $params) = $this->get_users_from_groups_sql('u', 'id');
+            list($joinsql, $params, $joinalias) = $this->get_users_from_groups_sql('u', 'id');
         }
 
         // Get WHERE clause to restrict by search if required.
         list($searchsql, $searchparams) = $this->get_user_search_where_sql($search, 'u');
+        $searchsql = !empty($searchsql) ? $searchsql : "1 = 1";
+
+        // Get WHERE clause for any further restrictions.
+        list($extrasql, $extraparams) = $this->get_user_extra_search_where_sql('u', $joinalias, $liveassignments);
+        $extrasql = !empty($extrasql) ? $extrasql : "1 = 1";
+
+        // Combine the two WHERE clauses.
+        $wheresql = "WHERE {$searchsql} AND {$extrasql}";
+        $whereparams = array_merge($searchparams, $extraparams);
 
         $usernamefields = get_all_user_name_fields(true, 'u');
-        $sql = "SELECT u.id, {$usernamefields} FROM {user} u {$joinsql} {$searchsql}";
-        $params = array_merge($params, $searchparams);
+        $sql = "SELECT u.id, {$usernamefields} FROM {user} u {$joinsql} {$wheresql}";
+        $params = array_merge($params, $whereparams);
 
         $sql .= " ORDER BY u.firstname, u.lastname";
+
         return $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
     }
 
@@ -276,18 +307,28 @@ class totara_assign_core {
         global $DB;
         // How the current user list is calculated depends of the status.
         // It could be static (from a table) or dynamic (calculated).
-        if ($this->assignments_are_stored()) {
-            list($joinsql, $params) = $this->get_users_from_assignments_sql('u', 'id');
+        $liveassignments = $this->assignments_are_stored();
+        if ($liveassignments) {
+            list($joinsql, $params, $joinalias) = $this->get_users_from_assignments_sql('u', 'id');
         } else {
-            list($joinsql, $params) = $this->get_users_from_groups_sql('u', 'id');
+            list($joinsql, $params, $joinalias) = $this->get_users_from_groups_sql('u', 'id');
         }
 
         // Get WHERE clause to restrict by search if required.
         list($searchsql, $searchparams) = $this->get_user_search_where_sql($search, 'u');
+        $searchsql = !empty($searchsql) ? $searchsql : "1 = 1";
+
+        // Get WHERE clause for any further restrictions.
+        list($extrasql, $extraparams) = $this->get_user_extra_search_where_sql('u', $joinalias, $liveassignments);
+        $extrasql = !empty($extrasql) ? $extrasql : "1 = 1";
+
+        // Combine the two WHERE clauses.
+        $wheresql = "WHERE {$searchsql} AND {$extrasql}";
+        $whereparams = array_merge($searchparams, $extraparams);
 
         // Do the count.
-        $sql = "SELECT COUNT(*) FROM {user} u {$joinsql} {$searchsql}";
-        $params = array_merge($params, $searchparams);
+        $sql = "SELECT COUNT(*) FROM {user} u {$joinsql} {$wheresql}";
+        $params = array_merge($params, $whereparams);
 
         return $DB->count_records_sql($sql, $params);
     }
@@ -311,7 +352,7 @@ class totara_assign_core {
 
         $likeparam = '%' . $DB->sql_like_escape($search) . '%';
 
-        $sql = " WHERE (" .
+        $sql = "(" .
             $DB->sql_like("{$useralias}.firstname", '?', false, false) .
             " OR " .
             $DB->sql_like("{$useralias}.lastname", '?', false, false) .
@@ -322,6 +363,31 @@ class totara_assign_core {
     }
 
     /**
+     * Over ride in module code to add specific module related search queries.
+     * It should look something like this.
+     *
+     * @param  $useralias       string  The alias of the user table.
+     * @param  $joinalias       string  The alias of the joined table.
+     * @param  $liveassignments boolean Flags which $joinalias table is being used.
+     *
+     * @return string  The additional where statement.
+     */
+    public function get_user_extra_search_where_sql($useralias, $joinalias, $liveassignments) {
+
+        // If the extra search is on the $useralias table it is safe to put here.
+        $sql = "";
+        $params = array();
+
+        if ($liveassignments) {
+            // Extra search where clause on the $joinalias user_assignments table.
+        } else {
+            // Extra search where clause on the $joinalias group_assignments table.
+        }
+
+        return array($sql, $params);
+    }
+
+    /**
      * Returns the SQL to join to a table via userid, such that only records belonging to users
      * who are currently in the assignment will be left.
      *
@@ -330,7 +396,7 @@ class totara_assign_core {
      *
      * Example usage:
      *
-     * list($assignsql, $assignparams) = $assign->get_users_from_assignments('u', 'id');
+     * list($assignsql, $assignparams, tablealias) = $assign->get_users_from_assignments('u', 'id');
      *
      * // return names of all users currently assigned
      * $sql = "SELECT u.firstname,u.lastname FROM {user} u $assignsql";
@@ -340,7 +406,7 @@ class totara_assign_core {
      *                       You MUST use a table alias, not just the table's name.
      * @param $field  string The name of the field containing user ids in the table you are joining to.
      *
-     * @return array  An array containing an SQL snippet and parameters to restrict the users.
+     * @return array  An array containing an SQL snippet, parameters to restrict the users and the table alias.
      */
     public function get_users_from_assignments_sql($table, $field) {
         $module = self::$module;
@@ -359,7 +425,7 @@ class totara_assign_core {
 
         $params = array($this->moduleinstanceid);
 
-        return array($sql, $params);
+        return array($sql, $params, $uaalias);
     }
 
 
@@ -373,7 +439,7 @@ class totara_assign_core {
      *
      * Example usage:
      *
-     * list($assignsql, $assignparams) = $assign->get_users_from_groups_sql('u', 'id');
+     * list($assignsql, $assignparams, $tablealias) = $assign->get_users_from_groups_sql('u', 'id');
      *
      * // return names of all users currently assigned
      * $sql = "SELECT u.firstname,u.lastname FROM {user} u $assignsql";
@@ -383,7 +449,7 @@ class totara_assign_core {
      *                       You MUST use a table alias, not just the table's name.
      * @param $field  string The name of the field containing user ids in the table you are joining to.
      *
-     * @return array  An array containing an SQL snippet and parameters to restrict the users.
+     * @return array  An array containing an SQL snippeti, parameters to restrict the users and table alias.
      */
     public function get_users_from_groups_sql($table, $field) {
         $module = self::$module;
@@ -401,7 +467,7 @@ class totara_assign_core {
         if (empty($assignedgroups)) {
             $sql = "JOIN (SELECT 0 AS userid) {$sqalias}
                 ON {$sqalias}.userid = {$table}.{$field}";
-            return array($sql, array());
+            return array($sql, array(), $sqalias);
         }
 
         $sqls = array();
@@ -423,7 +489,7 @@ class totara_assign_core {
         // Now join to the UNIONed queries as a subquery.
         $sql = " JOIN (\n{$allgroupsql}\n) {$sqalias}
             ON {$sqalias}.userid = {$table}.{$field}";
-        return array($sql, $params);
+        return array($sql, $params, $sqalias);
     }
 
 
@@ -465,17 +531,60 @@ class totara_assign_core {
 
 
     /**
+     * Get all users that are currently assigned (in group tables) but have not been stored (in assignment table).
+     *
+     * @access public
+     * @param $limitfrom int
+     * @param $limitnum int
+     * @return recordset Containing basic information about users.
+     */
+    public function get_unstored_users($limitfrom=null, $limitnum=null) {
+        global $DB;
+
+        list($assignjoinsql, $assignparams) = $this->get_users_from_assignments_sql('u', 'id');
+        list($groupjoinsql, $groupparams) = $this->get_users_from_groups_sql('u', 'id');
+
+        $params = array_merge($groupparams, $assignparams);
+
+        $sql = "SELECT u.id, u.firstname, u.lastname FROM {user} u " . $groupjoinsql . "
+                WHERE u.id NOT IN (SELECT u.id FROM {user} u " . $assignjoinsql . ")";
+
+        return $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
+    }
+
+
+    /**
+     * Get all users that are currently stored (in assignment table) but are not assigned (in group tables).
+     *
+     * @access public
+     * @param $limitfrom int
+     * @param $limitnum int
+     * @return recordset Containing basic information about users.
+     */
+    public function get_removed_users($limitfrom=null, $limitnum=null) {
+        global $DB;
+
+        list($assignjoinsql, $assignparams, $assignalias) = $this->get_users_from_assignments_sql('u', 'id');
+        list($groupjoinsql, $groupparams, $groupalias) = $this->get_users_from_groups_sql('u', 'id');
+
+        $params = array_merge($assignparams, $groupparams);
+
+        $sql = "SELECT u.id, u.firstname, u.lastname, {$assignalias}.id as userassignmentid
+                FROM {user} u " . $assignjoinsql . "
+                WHERE u.id NOT IN (SELECT u.id FROM {user} u " . $groupjoinsql . ")";
+
+        return $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
+    }
+
+
+    /**
      * Get the users according to the current state of the assigned groups and store to module_user_assignment
      * @access public
      * @return void
      */
-    public function store_user_assignments() {
+    public function store_user_assignments($newusers = null, $processor = null) {
         global $DB;
 
-        if ($this->is_locked()) {
-            print_error('error:assignmentmoduleinstancelocked', 'totara_core');
-        }
-
         $module = self::$module;
         $modulekey = "{$module}id";
         $moduleinstanceid = $this->moduleinstanceid;
@@ -483,19 +592,25 @@ class totara_assign_core {
 
         $transaction = $DB->start_delegated_transaction();
 
-        // Clear out the user assignment table first to prevent duplicates.
-        $DB->delete_records($tablename, array($modulekey => $moduleinstanceid));
+        if (!$newusers) {
+            // Clear out the user assignment table first to prevent duplicates.
+            $DB->delete_records($tablename, array($modulekey => $moduleinstanceid));
 
-        // Get recordset containing current user ids.
-        $allusers = $this->get_current_users();
+            // Get recordset containing current user ids.
+            $users = $this->get_current_users();
+        } else {
+            $users = $newusers;
+        }
 
-        // Define a processor function to reformat the data on the fly.
-        $processor = function($record, $modulekey, $moduleinstanceid) {
-            $todb = new stdClass();
-            $todb->$modulekey = $moduleinstanceid;
-            $todb->userid = $record->id;
-            return $todb;
-        };
+        if (empty($processor)) {
+            // Define a default processor function to reformat the data on the fly.
+            $processor = function($record, $modulekey, $moduleinstanceid) {
+                $todb = new stdClass();
+                $todb->$modulekey = $moduleinstanceid;
+                $todb->userid = $record->id;
+                return $todb;
+            };
+        }
 
         // Pass required data into the processor as an argument.
         $processordata = array(
@@ -504,9 +619,9 @@ class totara_assign_core {
         );
 
         // Accept the recordset and save to user_assignment table in batches.
-        $DB->insert_records_via_batch($tablename, $allusers, $processor, $processordata);
+        $DB->insert_records_via_batch($tablename, $users, $processor, $processordata);
 
-        $allusers->close();
+        $users->close();
 
         $transaction->allow_commit();
     }
@@ -586,6 +701,7 @@ abstract class totara_assign_core_grouptype {
 
     abstract public function generate_item_selector($hidden=array(), $groupinstanceid=false);
     abstract public function handle_item_selector($data);
+    abstract public function get_instance_name($instanceid);
 
     public function __construct($assignobject) {
         // Store the whole assignment object from totara_assign or child class of totara_assign.
@@ -915,9 +1031,10 @@ class totara_assign_ui_picker_hierarchy extends totara_dialog_content_hierarchy_
  * @param   $module string The Totara module
  * @param   $itemid int id of the object the dialogs will be assigning groups to
  * @param   $datatable boolean Whether to start up the Javascript for a datatable
+ * @param   $notice string The html output of a notice to display on change
  * @return  void
  */
-function totara_setup_assigndialogs($module, $itemid, $datatable = false) {
+function totara_setup_assigndialogs($module, $itemid, $datatable = false, $notice = "") {
     global $CFG, $PAGE;
     // Setup custom javascript.
     $jselements = array(
@@ -935,7 +1052,8 @@ function totara_setup_assigndialogs($module, $itemid, $datatable = false) {
         'name' => 'totara_assigngroups',
         'fullpath' => '/totara/core/lib/assign/assigngroup_dialog.js',
         'requires' => array('json'));
-    $args = array('args' => '{"module":"'.$module.'","sesskey":"'.sesskey().'"}');
+    $args = array('args' => '{"module":"'.$module.'","sesskey":"'.sesskey().'","notice":"'.addslashes_js($notice).'"}');
+
     $PAGE->requires->js_init_call('M.totara_assigngroupdialog.init', $args, false, $jsmodule);
 
     if ($datatable) {
-- 
1.8.3.2

