Part 4: Implementing Result Upload By Admin

Photo by Carlos Muza on Unsplash

Part 4: Implementing Result Upload By Admin

ยท

9 min read

This is the fourth article in my blog series "Building A Simple Web Application With YII2" and in this article we'll be working on the admin login.

In the last article we worked on admin login. Now we are going to implement result upload feature.

The full code for this article can be found on my github: https://github.com/mrfola/yii-result-portal-tutorial/tree/article-4

To check the previous article kindly go here: Part 3: Building Admin Login Functionality With Yii2.

Requirements

In order to get the best from this article, you need to have knowledge of certain things. The details of the requirements can be found here: Building A Simple Web Application With YII2

Create Migrations.

In order to implement result upload we need to have subjects table and results table. We'll create that using migrations.

Subject Table Migration.

In your CLI, type:

yii migrate/create create_subjects_table

You'll receive a confirmation prompt, type "yes" and your migration will be created.

Navigate to your subject table migration and modify your "SafeUp" method to look like this:

   public function safeUp()
    {
        $this->createTable('{{%subjects}}', [
            'id' => $this->primaryKey(),
            'name' => $this->string(120),
            'added_by' => $this->integer(),
            'created_at' => $this->dateTime()->defaultValue(Date('Y-m-d H:i:s')),
            'updated_at' => $this->dateTime()->defaultValue(Date('Y-m-d H:i:s')),
        ]);

        //Foreign Key Contraint for Added By (id for admin who added the subject)
        $this->addForeignKey('FK_admin_subject', 'subjects', 'added_by', 'users', 'id', 'CASCADE', 'CASCADE');
    }

After, ensure your "safeDown" method should looks like this:

 public function safeDown()
    {
        $this->dropTable('{{%subjects}}');
    }

Now let's create our results table.

Results Table Migration.

In your CLI, type:

yii migrate/create create_results_table

You'll receive a confirmation prompt, type "yes" and your migration will be created.

Navigate to your migration file and modify your "safeUp" method to look like this:

  public function safeUp()
    {
        $this->createTable('{{%results}}', [
            'id' => $this->primaryKey(),
            'user_id' => $this->integer(),
            'score' => $this->integer()->defaultValue(0),
            'subject_id' => $this->integer(),
            'created_at' => $this->dateTime()->defaultValue(Date('Y-m-d H:i:s')),
            'updated_at' => $this->dateTime()->defaultValue(Date('Y-m-d H:i:s')),
        ]);

        //Foreign Key Constraint for User Id
        $this->addForeignKey('FK_user_result', 'results', 'user_id',   'users',  'id', 'CASCADE', 'CASCADE');
        //Foreign Key Constraint for Subject Id
        $this->addForeignKey('FK_user_subject', 'results', 'subject_id', 'subjects', 'id', 'CASCADE', 'CASCADE');
    }

After that, ensure your "safeDown" method looks like this:

public function safeDown()
    {
        $this->dropTable('{{%results}}');
    }

Run your migration to create the appropriate tables. Type the following in your CLI:

yii migrate

You should receive a confirmation prompt. Type "yes" and your migrations should be successful.

With that done, let us setup our models.

Create Result Model.

Navigate to "models" folder, create a "Result.php" file and type the following code there:

<?php

namespace app\models;

use Yii;
use app\models\User;
use app\models\Subject;
use yii\db\ActiveRecord;

class Result extends ActiveRecord
{

    public static function tableName()
    {
        return 'results';
    }

    public function rules()
    {
        return [
            [['score', 'user_id', 'subject_id'], 'required'],
            [['score', 'user_id', 'subject_id'], 'integer'],
            [['created_at', 'updated_at'], 'safe'],
        ];
    }

    public function getUser()
    {
        return $this->hasOne(User::className(), ["id" => "user_id"]);
    }

    public function getSubject()
    {
        return $this->hasOne(Subject::className(), ["id" => "subject_id"]);
    }
}

Let me explain what each of the methods do in this model.

"tableName" tells YII that we want to connect the "Result" model to the "results" (note the plural) database table. By default YII will connect "Result" model to the "result" table. That function helps us override that.

"rules" are a set of validation rules that we want to apply on our model to ensure the correct information is passed into the model anytime we want to create result.

"getUser" and "getSubject" help us connect related models to the Result model in this case "Users" and "Result" respectively.

Now, let's create our "Subject" model.

Create Subject Model.

Navigate to "models" folder, create a "Subject.php" file and type the following code there:

<?php

namespace app\models;

use Yii;
use app\models\Result;
use yii\db\ActiveRecord;

class Subject extends ActiveRecord
{

    public static function tableName()
    {
        return 'subjects';
    }

    public function rules()
    {
        return [
            [['name', 'added_by'], 'required'],
            ['added_by', 'integer'],
            ['name', 'unique'],
            [['created_at', 'updated_at'], 'safe'],
        ];
    }

    public function getResults()
    {
        return $this->hasMany(Result::className(), ["id" => "result_id"]);
    }

}

The methods here are similar to the methods found in the "Result" model.

With that done, let's add some new useful methods to our User class.

Add Methods to User Class.

We are going to add some new methods to the User Model, that we'll be using in our AdminController when we start getting and updating results.

Navigate to models/User.php.

Insert the following at the top of the file after your "namespace app\models" :

use app\models\Result;
use app\models\Subject;

At the end of the file (before the last closing braces), insert the following:

public function getResults()
    {
        return $this->hasMany(Result::className(), ['user_id' => 'id']);
    }

    public function createDefaultUserResult($id)
    {   
        //get all subjects
        $subjects = Subject::find()->all();

         //save result for each subject
         foreach($subjects as $subject)
         {
            $result = new Result();
            $result->user_id = $id;
            $result->score = 0;
            $result->subject_id = $subject->id;
            $result->save();
         }
    }

    public static function getAllResults()
    {
        $students = self::find()->where(["user_type" => "user"])->all();

        $studentResults = [];
        foreach($students as $index => $student)
        {
            $studentResult["id"] = $student->id;
            $studentResult["name"] = $student->name;

            //get individual result and add it to new array
            foreach($student->results as $result)
            {
                $studentResult["subjects"][$result->subject->name] = 
                [
                    "subject_id" => $result->subject->id,
                    "score" => $result->score 
                ];

            }

            //add new student array to the larger array containing all students
            $studentResults[] = $studentResult;
        }

        return $studentResults;
    }

The "getResults" method helps connect the User model to the Result model.

The "createDefaultUserResult" method creates a default set of result for all available subjects, for a specific user.

The "getAllResults" method gets all the available student and their corresponding records. It also get some extra data and puts it in a format that we can use in our AdminController.

Now, let's edit our AdminController to allow admins to view and update results.

Edit Admin Controller

Firstly, let's edit our "actionDashboard" method, to pass the appropriate data. Replace the current "actionDashboard" with this:

    public function actionDashboard()
    {
        if (\Yii::$app->user->can('viewResultDashboard'))
        {
            //get all students from db
            $studentResults = User::getAllResults();

            $data = ["students" => $studentResults];

            return $this->render('dashboard', $data);
        }else
        {
            return "You are not authorized to view this page.";
        }
    }

Secondly, lets add a new method to allow us update User results.

Add this method in your Admin Controller:

    public function actionUpdateUserResult()
    {
        if (\Yii::$app->user->can('updateResult'))
        {
        $request = Yii::$app->request->post();
        $user_id = $request["user_id"];
        unset($request["user_id"]);

        //loop through all subjects and update each one.
        foreach($request as $subject_id => $score)
        {
            //to ignore the csrf field
            if(!is_int($subject_id))
            {
                continue;
            }

            $result = Result::findOne([
                "user_id" => $user_id,
                "subject_id" => $subject_id
            ]);

            $result->score = $score;
            $result->save();
        }

        $session = Yii::$app->session;
        $session->setFlash('successMessage', "Result Updated");
        return $this->redirect(["admin/dashboard"]);

        }else
        {
            return "You are not authorized to view this page.";
        }

    }

Don't forget to include your Result model at the top of your file (after "namespace app\controllers") :

use app\models\Result;

With that done, let's edit our dashboard view to display and edit results.

Edit Dashboard View.

Navigate to views/admin/dashboard.php.

Replace the code there with the following:

<?php
    use yii\bootstrap4\ActiveForm;
    use yii\helpers\Html;
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Dashboard</title>
</head>
<body>                        
    <div class="container dashboard">

        <h1>Admin Dashboard</h1>
        <div class="student-results">

            <?php
            $session = Yii::$app->session;
            $errors = $session->getFlash('errorMessages');
            $success = $session->getFlash('successMessage');
            if(isset($errors) && (count($errors) > 0))
            {
                foreach($session->getFlash('errorMessages') as $error)
                {
                    echo "<div class='alert alert-danger' role='alert'>$error[0]</div>";
                }
            }

            if(isset($success))
            {
                echo "<div class='alert alert-success' role='alert'>$success</div>";
            }
            ?>

        <table class="table table-striped">
        <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Name</th>
                <th scope="col">Maths</th>
                <th scope="col">English</th>
                <th scope="col">History</th>
                <th scope="col">Mental Studies</th>
                <th scope="col">Negotation Studies</th>
                <th scope="col">Action</th>
            </tr>
        </thead>
        <tbody>
            <?php 

                foreach($students as $index => $student)
                {
            ?>
            <tr>
            <th scope="row"><?=  $index+1; ?></th>
                <td><?= $student["name"]; ?></td>
                <td><?= $student["subjects"]["Maths"]["score"]; ?></td>
                <td><?= $student["subjects"]["English"]["score"]; ?></td>
                <td><?= $student["subjects"]["History"]["score"]; ?></td>
                <td><?= $student["subjects"]["Mental Studies"]["score"]; ?></td>
                <td><?= $student["subjects"]["Negotiation Studies"]["score"]; ?></td>
                <td>
                <button type="button" class="btn btn-primary" data-toggle="modal" data-target="<?= '#updateModal'.$index; ?>">
                Update
                </button>
                </td>
            </tr>


            <!-- Update Modal -->
            <div class="modal fade" id="<?= 'updateModal'.$index; ?>" tabindex="-1" aria-labelledby="<?= 'updateModal'.$index; ?>" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="<?= 'updateModal'.$index; ?>"><?= 'Update Result For '.$student["name"]; ?></h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <?php
                    ActiveForm::begin([
                        "action" => ["admin/update-user-result"],
                        "method" => "post"
                    ]);
                ?>
                <div class="modal-body">

                    <div class="form-group mx-2">

                        <?php

                        //hidden field containing student id
                        echo Html::hiddenInput('user_id', $student['id']);

                        foreach($student["subjects"] as $subjectName => $subject)
                        {
                        ?>
                            <label for="exampleFormControlSelect1"><?= $subjectName;?></label>
                            <input 
                            name = "<?= $subject["subject_id"];?>"
                            type="number"
                            min="0" 
                            max="100" 
                            class="form-control" 
                            id="score" 
                            placeholder="Enter score"
                            value = "<?= $subject["score"]; ?>"
                            >
                        <?php
                        }
                        ?>

                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-primary">Update</button>
                </div>

                <?php
                    ActiveForm::end();
                ?>

                </div>
  </div>

            <?php
                }
            ?>

        </tbody>
        </table>
        </div>
    </div>
</div>
</body>
</html>

Explanation:

In the view, I looped through all the student records, then displayed the scores for each subject. I also have an update button to update individual student record.

A new student automatically gets 0 on all subjects, but admins can change that through the update.

The update button opens up a modal with the current user results which the admin can change and then update.

On successful update, a success message is sent to the dashboard and the information is updated.

Seed Your Database

If you've followed carefully to this point, you should be able to see all user results and update them.

To test it out, we'll seed our database with some default subjects and users.

Navigate commands/SeedController.php.

We'll be adding more methods to the "actionAdmin" that is already there.

Replace the current code with the following:

<?php

namespace app\commands;

use yii\console\Controller;
use app\models\Result;
use app\models\Subject;
use app\models\User;
use Yii;

class SeedController extends Controller
{

    protected $subjects = ["Maths", "English", "History", "Mental Studies", "Negotiation Studies"];

    public function actionAll()
    {
        $this->actionAdmin(); //create default admins
        $this->actionSubjects(); //create default subjects based on protected $subjects
        $this->actionUser(); //create default users
    }

    public function actionAdmin()
    {
        $admin = new User();
        $admin->name = "admin";
        $admin->email = "admin@gmail.com";
        $admin->password = Yii::$app->getSecurity()->generatePasswordHash("password");
        $admin->user_type = "admin";

        if($admin->save())
        {
            echo "Admins successfully seeded";
        }else
        {
            foreach($admin->getErrors() as $error)
            {
                echo $error[0]." ";
            }
        }   
    }

    public function actionSubjects()
    {
        //get admin
        if(!$admin_id = User::findOne(["user_type" => "admin"])->id)
        {
            $this->actionAdmin();//create test admin
            $admin_id = User::findOne(["user_type" => "admin"])->id;
        }

        foreach ($this->subjects as $subjectName)
        {
            $subject = new Subject();
            $subject->name = $subjectName;
            $subject->added_by = $admin_id;
            if($subject->save())
            {
                echo $subjectName." Subject successfully seeded. ";

            }else
            {
                foreach($subject->getErrors() as $error)
                {
                    echo $error[0]." ";
                }
            }
        }
    }


    public function actionUser()
    {
        $user = new User;
        $user->name = "Test";
        $user->email = "test@gmail.com";
        $user->password =  Yii::$app->getSecurity()->generatePasswordHash("password");
        if($user->save())
        {
            echo "User successfully seeded";        

            //get all subjects
            $subjects = Subject::find()->all();

            //create user result for each subject
            $this->createUserResults($user->id, $subjects);      
        }else
        {
            foreach($user->getErrors() as $error)
            {
                echo $error[0]." ";
            }
        }

        $user2 = new User;
        $user2->name = "Test 2";
        $user2->email = "test2@gmail.com";
        $user2->password =  Yii::$app->getSecurity()->generatePasswordHash("password");
        if($user2->save())
        {
            echo "User successfully seeded";

            //get all subjects
            $subjects = Subject::find()->all();

            //create user result for each subject
            $this->createUserResults($user2->id, $subjects);  

        }else
        {
            foreach($user2->getErrors() as $error)
            {
                echo $error[0]." ";
            }
        }
    }

    public function createUserResults($user_id, $subjects)
    {
        //save result for each subject
        foreach($subjects as $subject)
        {
        $result = new Result();
        $result->user_id = $user_id;
        $result->score = rand(1,100);
        $result->subject_id = $subject->id;

        if($result->save())
        {
            echo "Result for " . $subject->name . " added. ";

        }else
        {
            foreach($result->getErrors() as $error)
            {
                echo $error[0]." ";
            }
        }
        }
    }

 }

Here's what we're doing:

"actionAdmin" method creates a default admin

"actionSubjects" method seeds the "subjects" table with a list of subjects according to what we have in our protected $subjects variable.

"actionUsers" method seeds the "users" table with 2 new users and it automatically creates random results for them.

"actionAll" methods calls all other methods in order, so we don't have to call each method manually from our CLI.

So to seed the database, simple type the following in your CLI:

yii seed/all

You should get a series of success messages for all the methods available.

Now it's time to test your application.

Testing Time ....

Serve your application:

yii serve

Navigate to your admin dashboard and try updating the results you see.

It should work perfectly. Viola. We're done with the result upload by Admin.๐ŸŽ‰

If you have any questions or comments kindly leave them below. Thanks.

Don't forget that the full code for this article can be found on my github: https://github.com/mrfola/yii-result-portal-tutorial/tree/article-4