Technical Test – Javascript (Frontend) & PHP (API Backend)

Please complete the following technical task. I will be reviewing it to asses the following:

* Your ability to follow vague instructions and use your own initiative.
* Your ability to write documented PHP code.
* Your ability to design and abstract database tables.
* Your ability to ability to write a simple RESTful API.
* Your ability to use jQuery and ajax to interact with your API.
* Your ability to write integration documentation for your code.
* The overall robustness of your code (error handling, validation and security especially).

Here is the task:

* Create a simple RESTful API and PHP 7 application which serves to create, retrieve, update and delete employees to two fictional companies "Company A" and "Company B".

* A database with company and employee tables have been provided for you. Use these tables, but feel free to add any other tables or columns you need.

* The RESTful API must be the backend of your PHP app.

* Ensure that your API returns the proper HTTP status where required: 200/400/403/404/405/406/500 - this will be tested.

* In the frontend of your PHP app, create a simple list view to show all employees from all companies, but provide an option to filter these by company and sort them by first name or last name. Also include action buttons to add a new employee and update/delete existing employees.

* Use bootstrap components for style and responsive layout.

* Use a modal dialog to display the form which creates or updates a user.

* When deleting a user, display a message to ask the user if they are sure they want to proceed.

* When creating, updating or deleting a user, use jQuery ajax to call your API which in turn updates the database.

* When complete, put the project in a .git repository and add a readme.md to explain how to install and run the project on a vanilla linux box.

Source code: https://github.com/JonnyD/Technical-Test-Javascript-and-PHP

Employees:

You can filter by Company:

and sort by first name or last name:

You can read each employee:

Edit:

And Delete

Create employee:

Here is the MySQL to create and populate employees and companies:

CREATE DATABASE  IF NOT EXISTS `test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `test`;

--
-- Table structure for table `company`
--

DROP TABLE IF EXISTS `company`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `company` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

INSERT INTO `company` (`id`, `name`) VALUES (1, 'Company A');
INSERT INTO `company` (`id`, `name`) VALUES (2, 'Company B');

--
-- Table structure for table `employee`
--

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `job_title` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone` varchar(45) DEFAULT NULL,
  `address_line_1` varchar(45) DEFAULT NULL,
  `address_line_2` varchar(45) DEFAULT NULL,
  `town_city` varchar(45) DEFAULT NULL,
  `county_region` varchar(45) DEFAULT NULL,
  `country` varchar(45) DEFAULT NULL,
  `postcode` varchar(45) DEFAULT NULL,
  `company_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

ALTER TABLE employee ADD FOREIGN KEY (company_id) REFERENCES company(id);

INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (1, 'Jonathan', 'Devine', 'Software Developer', 'contact@jonnydevine.com', '0862041801', '23 Windmill Road', NULL, 'Drogheda', 'Louth', 'Ireland', 'ABC123', 1);
INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (2, 'Steven', 'Andrews', 'Front End Developer', 'contact@steven.com', '0862454944', '2 Fake Road', NULL, 'Dublin', 'Dublin', 'Ireland', 'DEF456', 2);

Here is an example of reading employees using Javascript:

$(document).ready(function(){

    // show list of employees on first load
    showEmployees();

    $(document).on('click', '#select-company', function(){
        showEmployees();
    });

    $(document).on('click', '.order-by', function(){
        showEmployees();
    });
});

// function to show list of employees
function showEmployees(){
    var order_by = $('input[name=orderBy]:checked').val();
    if (typeof order_by === 'undefined' || !order_by) {
        order_by = "first_name";
    }

    var company_id = $("#select-company").val();
    if (typeof company_id === 'undefined' || !company_id) {
        company_id = "All";
    }

    var order_by_html = "";
    if (order_by === "first_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name' checked> First Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name'> First Name";
    }
    if (order_by === "last_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name' checked> Last Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name'> Last Name";
    }

    // load list of companies
    $.getJSON("api/company/read.php", function(data) {
        // build companies option html
        // loop through returned list of data
        var companies_options_html = "";
        companies_options_html += "<select id='select-company' name='company_id' class='form-control pull-left'>";
        companies_options_html += "<option value='All'>All</option>";
        $.each(data, function (key, val) {
            if(val.id == company_id) {
                companies_options_html += "<option value='" + val.id + "' selected>" + val.name + "</option>";
            } else {
                companies_options_html += "<option value='" + val.id + "'>" + val.name + "</option>";
            }
        });
        companies_options_html += "</select>";

        // get list of employees from the API
        $.getJSON("api/employee/read.php?company_id=" + company_id + "&order_by=" + order_by, function(data){
            // html for listing employees
            read_employees_html = "";

            read_employees_html += "<div class='row'>";
                read_employees_html += "<div class='col-md-4'>" + companies_options_html + "</div>";

                read_employees_html += "<div class='col-md-4'>" + order_by_html + "</div>";

                // when clicked, it will load the create employee form
                read_employees_html += "<div class='col-md-4'><div id='create-employee' class='btn btn-primary pull-right create-employee-button' data-toggle='modal' data-target='#myModal'>";
                    read_employees_html += "<span class='glyphicon glyphicon-plus'></span> Create Employee";
                read_employees_html += "</div></div>";

            read_employees_html += "</div>";
            // start table
            read_employees_html += "<table class='table table-bordered table-hover'>";

                // creating our table heading
                read_employees_html += "<tr>";
                    read_employees_html += "<th class='w-25-pct'>First Name</th>";
                    read_employees_html += "<th class='w-10-pct'>Last Name</th>";
                    read_employees_html += "<th class='w-15-pct'>Company</th>";
                    read_employees_html += "<th class='w-25-pct text-align-center'>Action</th>";
                read_employees_html += "</tr>";

            // loop through returned list of data
            $.each(data, function(key, val) {

                // creating new table row per record
                read_employees_html += "<tr>";

                    read_employees_html += "<td>" + val.first_name + "</td>";
                    read_employees_html += "<td>" + val.last_name + "</td>";
                    read_employees_html += "<td>" + val.company.name + "</td>";

                    // 'action' buttons
                    read_employees_html += "<td>";
                        // read employee button
                        read_employees_html += "<button class='btn btn-primary read-one-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-eye-open'></span> Read";
                        read_employees_html += "</button>";

                        // edit button
                        read_employees_html += "<button class='btn btn-info update-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-edit'></span> Edit";
                        read_employees_html += "</button>";

                        // delete button
                        read_employees_html += "<button class='btn btn-danger delete-employee-button' data-id='" + val.id + "'>";
                            read_employees_html += "<span class='glyphicon glyphicon-remove'></span> Delete";
                        read_employees_html += "</button>";
                    read_employees_html += "</td>";

                read_employees_html += "</tr>";

            });

            // end table
            read_employees_html += "</table>";

            // inject to 'page-content' of our app
            $("#page-content").html(read_employees_html);

        });
    });
}

For more see here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/app/employees

An example of a backend API:

<?php
// required header
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// include files
include_once '../config/database.php';
include_once '../objects/employee.php';
include_once '../objects/company.php';
include_once '../repository/employee_repository.php';
include_once '../repository/company_repository.php';

// instantiate database
$database = new Database();
$db = $database->getConnection();

// instantiate employee repository
$employeeRepository = new EmployeeRepository($db);

// instantiate company repository
$companyRepository = new CompanyRepository($db);

// get company ID
$companyId = isset($_GET['company_id']) ? $_GET['company_id'] : null;

// get order by
$orderBy = isset($_GET['order_by']) ? $_GET['order_by'] : null;

// query employees
if ($companyId != "All" && $companyId != null) {
    $employees = $employeeRepository->readByCompany($companyId, $orderBy);
} else {
    $employees = $employeeRepository->read($orderBy);
}

// check if more than 0 records found
if(count($employees) > 0) {
    $employeesArray = [];

    foreach ($employees as $employee) {
        $company = $companyRepository->readOne($employee->getCompanyId());

        $employeesArr[] = [
            'id' => $employee->getId(),
            'first_name' => $employee->getFirstName(),
            'last_name' => $employee->getLastName(),
            'job_title' => $employee->getJobTitle(),
            'email' => $employee->getEmail(),
            'phone' => $employee->getPhone(),
            'address_line_1' => $employee->getAddressLine1(),
            'address_line_2' => $employee->getAddressLine2(),
            'town_city' => $employee->getTownCity(),
            'county_region' => $employee->getCountyReqion(),
            'country' => $employee->getCountry(),
            'postcode' => $employee->getPostcode(),
            'company' => [
                'id' => $company->getId(),
                'name' => $company->getName()
            ]
        ];
    }

    header("HTTP/1.1 200");
    echo json_encode($employeesArr);
} else {
    header("HTTP/1.1 404");
    echo json_encode([
        "message" => "No employees found."
    ]);
}
?>

More here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/api

EmployeeRepository:

<?php

class EmployeeRepository
{
    /**
     * @var string
     */
    private $tableName;

    /**
     * @var PDO
     */
    private $db;

    /**
     * @param PDO $db
     */
    public function __construct(PDO $db)
    {
        $this->tableName = "employee";
        $this->db = $db;
    }

    /**
     * @param string $orderBy
     * @return Employee[]
     */
    public function read(string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName;

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $companyId
     * @param string $orderBy
     * @return Employee[]
     */
    public function readByCompany(int $companyId, string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName . "
                    WHERE company_id = :companyId";

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->bindParam(":companyId", $companyId);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $id
     * @return Employee|null
     */
    public function readOne(int $id)
    {
        $query = "SELECT 
                    id,
                    first_name,
                    last_name,
                    job_title,
                    email,
                    phone,
                    address_line_1,
                    address_line_2,
                    town_city,
                    county_region,
                    country,
                    postcode,
                    company_id
				FROM " . $this->tableName . "
				WHERE id = :id
				LIMIT 0,1";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind selected record id
        $stmt->bindParam(":id", $id);

        // execute the query
        $stmt->execute();

        // get record details
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($stmt->rowCount() > 0) {
            // assign values to object properties
            $employee = new Employee();
            $employee->setId($row['id']);
            $employee->setFirstName($row['first_name']);
            $employee->setLastName($row['last_name']);
            $employee->setJobTitle($row['job_title']);
            $employee->setEmail($row['email']);
            $employee->setPhone($row['phone']);
            $employee->setAddressLine1($row['address_line_1']);
            $employee->setAddressLine2($row['address_line_2']);
            $employee->setTownCity($row['town_city']);
            $employee->setCountyReqion($row['county_region']);
            $employee->setCountry($row['country']);
            $employee->setPostcode($row['postcode']);
            $employee->setCompanyId($row['company_id']);

            return $employee;
        } else {
            return null;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function create(Employee $employee)
    {
        $query = "INSERT INTO
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind values
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute query
        if ($stmt->execute()) {
            return true;
        } else {
            echo "<pre>";
            print_r($stmt->errorInfo());
            echo "</pre>";

            return false;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function update(Employee $employee)
    {
        $query = "UPDATE
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id
				WHERE
					id = :id";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind new values
        $id = $employee->getId();
        $stmt->bindParam(':id', $id);
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute the query
        if ($stmt->execute()) {
            return true;
        }else{
            return false;
        }
    }

    /**
     * @param int $id
     * @return bool
     */
    public function delete(int $id)
    {
        $query = "DELETE FROM " . $this->tableName . " WHERE id = :id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind id of record to delete
        $stmt->bindParam(':id', $id);

        // execute query
        if ($stmt->execute()) {
            return true;
        }

        return false;

    }
}
 

Example of an API

View more here https://github.com/JonnyD/Greetup-API-PHP/tree/master/src/GU

<?php

namespace GU\GangBundle\Controller\API;

use GU\BaseBundle\Controller\BaseController;
use GU\GangBundle\Entity\Gang;
use GU\GangBundle\Entity\GangUser;
use GU\GangBundle\Entity\JoinRequest;
use GU\GangBundle\Enum\Role;
use GU\GangBundle\Form\GangType;
use GU\GangBundle\Service\GangService;
use GU\GangBundle\Service\GangUserService;
use GU\GangBundle\Service\JoinRequestService;
use GU\GangBundle\Specification\CanViewGang;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\QueryParam;

class GangController extends BaseController
{
    /**
     * @return Response
     */
    public function getGangsAction()
    {
        $gangService = $this->getGangService();
        $gangs = $gangService->getGangsWithinRadius(55.55555, 56.55555, 25);

        $canViewGangSpecification = $this->getCanViewGangSpecification();

        $gangsThatCanBeViewed = [];
        foreach ($gangs as $gang) {
            if ($canViewGangSpecification->isSatisfiedBy($gang)) {
                $gangsThatCanBeViewed[] = $gang;
            }
        }

        $response = $this->createApiResponse($gangsThatCanBeViewed);
        return $response;
    }

    /**
     * @param int $id
     * @return Response
     */
    public function getGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function postGangsAction(Request $request)
    {
        $data = json_decode($request->getContent(), true);

        $gang = new Gang();
        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);

            $gangUser = new GangUser();
            $gangUser->setUser($this->getLoggedInUser());
            $gangUser->setGang($gang);
            $gangUser->setRole(Role::FOUNDER);

            $gangUserService = $this->getGangUserService();
            $gangUserService->save($gangUser);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @param int $id
     * @return Response|NotFoundHttpException
     */
    public function putGangAction(Request $request, int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $data = json_decode($request->getContent(), true);

        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/join", name="join_gang")
     */
    public function joinGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser != null) {
            return $this->createNotFoundException("You are already a member");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestByGangAndUser($gang, $loggedInUser);

        if ($joinRequest != null) {
            return $this->createNotFoundException("You already requested to join this gang");
        }

        $joinRequest = new JoinRequest();
        $joinRequest->setGang($gang);
        $joinRequest->setUser($loggedInUser);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/accept-join-request/{join_request_id}", name="accept-join-request")
     */
    public function acceptJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $gangUser = new GangUser();
        $gangUser->setGang($joinRequest->getGang());
        $gangUser->setUser($joinRequest->getUser());
        $gangUser->setRole(Role::USER);

        $gangUserService->save($gangUser);
        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/reject-join-request/{join_request_id}", name="reject-join-request")
     */
    public function rejectJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/leave", name="leave_gang")
     */
    public function leaveGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService->remove($gangUser);

        return new Response(204);
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @GET("/gangs/{id}/actions/listMembers", name="list_members_gang")
     */
    public function listMembersAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUsers = $gangUserService->getGangUsersByGang($gang);

        $response = $this->createApiResponse($gangUsers);
        return $response;
    }

    /**
     * @return GangUserService
     */
    private function getGangUserService()
    {
        return $this->get('gu.gang_user_service');
    }

    /**
     * @return GangService
     */
    private function getGangService()
    {
        return $this->get('gu.gang_service');
    }

    /**
     * @return JoinRequestService
     */
    private function getJoinRequestService()
    {
        return $this->get('gu.join_request_service');
    }

    /**
     * @return CanViewGang
     */
    private function getCanViewGangSpecification()
    {
        return $this->get('gu.can_view_gang_specification');
    }
}
 

Add/Edit Standalone & Series Documentaries

Behold! A script to add/edit standalone & series documentaries plus using third party API’s such as IMDB and Youtube to auto populate data.

And here is the code. I’m relatively new to Angular (this is my second project) so I don’t know if I am doing things the right way, please leave feedback.

import { YoutubeService } from './../../../services/youtube.service';
import { UserService } from './../../../services/user.service';
import { DocumentaryService } from './../../../services/documentary.service';
import { YearService } from './../../../services/year.service';
import { CategoryService } from './../../../services/category.service';
import { HttpParams } from '@angular/common/http';
import { Documentary } from './../../../models/documentary.model';
import { FormGroup, FormControl, Validators, FormArray, FormBuilder } from '@angular/forms';
import { AngularEditorConfig } from '@kolkov/angular-editor';
import { Component, OnInit, ChangeDetectorRef, ViewChild } from '@angular/core';
import { VideoSourceService } from 'src/app/services/video-source.service';
import { Router, ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { IMDB } from 'src/app/models/imdb.model';
import { OMDBService } from 'src/app/services/omdb.service';
import { Location } from "@angular/common";

@Component({
  selector: 'app-documentary-add',
  templateUrl: './documentary-add.component.html',
  styleUrls: ['./documentary-add.component.css']
})
export class DocumentaryAddComponent implements OnInit {
  public slug;

  public editMode = false;

  public type;

  public activeIdString;

  public documentary: Documentary;
  public categories;
  public years;
  public videoSources;
  public posterImgURL;
  public wideImgURL;
  public imdb: IMDB;
  public thumbnailImgURLDict = {};

  private queryParamsSubscription;
  private routeParamsSubscription;
  private documentaryBySlugSubscription;
  private meSubscription;
  private getByImdbIdSubscription;

  public myStandaloneDocumentaries;
  public showStandaloneDocumentaries = false;

  public showStandaloneForm:boolean = false;
  public standaloneFormLoaded = false;
  public showStandaloneAddTitleButton = true;

  public showEpisodicForm = false;
  public showEpisodicPage = false;
  public showEpisodicDocumentaries = false;
  public showEpisodicAddTitleButton = true;
  public myEpisodicDocumentaries;
  
  public isFetchingEpisodicDocumentaries = false;

  public showStandalonePage = false;
  public showSearchedDocumentariesFromIMDB = false;
  public showSearchedDocumentaryFromIMDB = false;

  public isFetchingDocumentariesFromIMDB = false;
  public isFetchingDocumentaryFromIMDB = false;

  public searchedDocumentariesFromIMDB;
  public searchedDocumentaryFromIMDB;

  public searchedVideosFromYoutube;
  public isFetchingVideosFromYoutube = true;
  public showSearchedVideosFromYoutube = false;

  public isFetchingStandaloneDocumentaries = false;
  public isFetchingYears = false;
  public isFetchingVideoSources = false;
  public isFetchingCategories = false;
  
  public hasToggledStandaloneForm = false;

  public hasToggledEpisodicForm = false;

  public submitted = false;

  public posterError = false;

  public errors;

  public seasonNumber = 1;
  public episodeNumber = 1;

  standaloneForm: FormGroup;
  episodicForm: FormGroup;
  imdbForm: FormGroup;
  youtubeForm: FormGroup;

  standaloneConfig: any;
  episodicConfig: any;
  page;
  me;

  closeResult: string;

  editorConfig: AngularEditorConfig = {
    editable: true,
    spellcheck: true,
    height: '25rem',
    minHeight: '5rem',
    placeholder: 'Enter text here...',
    translate: 'no',
    uploadUrl: 'v1/images', // if needed
  };

  constructor(
    private categoryService: CategoryService,
    private yearService: YearService,
    private videoSourceService: VideoSourceService,
    private documentaryService: DocumentaryService,
    private omdbService: OMDBService,
    private userService: UserService,
    private youtubeService: YoutubeService,
    private router: Router,
    private location: Location,
    private cd: ChangeDetectorRef,
    private modalService: NgbModal,
    private route: ActivatedRoute,
    private fb: FormBuilder
  ) { }

  ngOnInit() {
    this.start('standalone');
  }

  start(type: string = null) {
    if (type != null) {
      this.type = type;
    }

    this.reset();

    this.documentary = new Documentary();

    this.queryParamsSubscription = this.route
      .queryParams
      .subscribe(params => {
        this.page = +params['page'] || 1;

        this.routeParamsSubscription = this.route.paramMap.subscribe(params => {
          if (type == null) {
            this.type = params['params']['type'];
          }
          this.slug = params['params']['slug'];
          this.editMode = this.slug != null;

          this.activeIdString = this.type;

          if (this.editMode) {
            this.documentaryBySlugSubscription = this.documentaryService.getDocumentaryBySlug(this.slug)
              .subscribe((result:any) => {
                this.documentary = result;
                console.log(result);

                if (this.documentary.type === 'standalone') {
                  console.log(this.documentary.type);
                  this.toggleStandaloneForm();
                  this.showStandalonePage = true;
                } else if (this.documentary.type === 'episodic') {
                  this.toggleEpisodicForm();
                  this.showEpisodicPage = true;
                }
              });
          } else {
            this.meSubscription = this.userService.getMe().subscribe(me => {
              this.me = me;

              if (this.type === 'standalone') {
                console.log("fdjksjfk");
                if (!this.hasToggledStandaloneForm) {
                  this.fetchStandaloneDocumentaries();
                  this.showStandalonePage = true;
                  console.log("dfff");
                }
              } else if (this.type === 'episodic') {
                console.log('episodic');
                if (!this.hasToggledEpisodicForm) {
                  this.fetchEpisodicDocumentaries();
                  this.showEpisodicPage = true;
                }
              }
            });
          }
        });
      });
  }

  reset() {
    this.hasToggledEpisodicForm = false;
    this.hasToggledStandaloneForm = false;
    this.showStandaloneForm = false;
    this.showEpisodicForm = false;
    this.showEpisodicPage = false;
    this.showStandalonePage = false;
    this.showEpisodicAddTitleButton = false;
    this.showStandaloneAddTitleButton = false;
    this.showStandaloneDocumentaries = false;
    this.showEpisodicDocumentaries = false;
  }

  tabChange(event) {
    console.log(event);
    this.start(event.nextId);
  }

  fetchStandaloneDocumentaries() {
    if (this.editMode) {
      this.showStandaloneDocumentaries = false;
      return;
    }

    this.isFetchingStandaloneDocumentaries = true;

    let params = new HttpParams();

    params = params.append('page', this.page.toString());

    this.location.go(this.router.url.split("?")[0], params.toString());
  
    this.documentaryService.getMyStandloneDocumentaries(params, this.me.username)
      .subscribe(result => {
        this.standaloneConfig = {
          itemsPerPage: 5,
          currentPage: this.page,
          totalItems: result['count_results']
        };
        console.log("result");
        console.log(result);
        this.myStandaloneDocumentaries = result['items'];

        this.isFetchingStandaloneDocumentaries = false;
        this.showStandaloneDocumentaries = true;
        this.showStandaloneAddTitleButton = true;
      })
  }

  fetchEpisodicDocumentaries() {

    this.isFetchingEpisodicDocumentaries = true;

    let params = new HttpParams();

    params = params.append('page', this.page.toString());

    this.location.go(this.router.url.split("?")[0], params.toString());
  
    this.documentaryService.getMyEpisodicDocumentaries(params, this.me.username)
      .subscribe(result => {
        this.episodicConfig = {
          itemsPerPage: 5,
          currentPage: this.page,
          totalItems: result['count_results']
        };
        this.myEpisodicDocumentaries = result['items'];

        this.isFetchingEpisodicDocumentaries = false;
        this.showEpisodicDocumentaries = true;
        this.showEpisodicAddTitleButton = true;
      })
  }

  toggleStandaloneForm() {
    console.log("toggle");
    this.showStandaloneAddTitleButton = false;

    this.showStandaloneDocumentaries = false;

    this.showStandaloneForm = !this.showStandaloneForm;

    this.initYears();
    this.initVideoSources();
    this.initCategories();
    this.initStandaloneForm();

    this.hasToggledStandaloneForm = true;
  }

  toggleEpisodicForm() {
    console.log("toggle");
    this.showEpisodicAddTitleButton = false;

    this.showEpisodicDocumentaries = false;

    this.showEpisodicForm = !this.showEpisodicForm;

    this.initYears();
    this.initVideoSources();
    this.initCategories();
    this.initEpisodicForm();

    this.hasToggledEpisodicForm = true;
  }

  initYears() {
    this.isFetchingYears = true;

    this.years = this.yearService.getAllYearsForForm();

    this.isFetchingYears = false;
  }

  initVideoSources() {
    this.isFetchingVideoSources = true;

    let params: HttpParams;
    this.videoSourceService.getAll(params)
      .subscribe(result => {
        this.videoSources = result;
        
        this.isFetchingVideoSources = false;
      });
  }

  initCategories() {
    this.isFetchingCategories = true;

    let params: HttpParams;
    this.categoryService.getAll(params)
      .subscribe(result => {
        this.categories = result;

        this.isFetchingCategories = false;
      });
  }

  initStandaloneForm() {
    let title = this.documentary.title;
    let category = null;
    if (this.documentary.category) {
      category = this.documentary.category.id;
    }
    let storyline = this.documentary.storyline;
    let summary = this.documentary.summary;
    let videoSource = null;
    if (this.documentary.videoSource) {
      videoSource = this.documentary.videoSource.id
    }
    let videoId = this.documentary.videoId;
    let year = this.documentary.year;
    let length = this.documentary.length;
    let poster = this.documentary.poster;
    this.posterImgURL = this.documentary.poster;
    let wideImage = this.documentary.wideImage;
    this.wideImgURL = this.documentary.wideImage;
    let imdbId = this.documentary.imdbId;

    this.standaloneForm = new FormGroup({
      'title': new FormControl(title, [Validators.required]),
      'category': new FormControl(category, [Validators.required]),
      'storyline': new FormControl(storyline, [Validators.required]),
      'summary': new FormControl(summary, [Validators.required]),
      'videoSource': new FormControl(videoSource, [Validators.required]),
      'videoId': new FormControl(videoId, [Validators.required]),
      'year': new FormControl(year, [Validators.required]),
      'length': new FormControl(length, [Validators.required]),
      'poster': new FormControl(poster, [Validators.required]),
      'wideImage': new FormControl(wideImage, [Validators.required]),
      'imdbId': new FormControl(imdbId)
    });
  }

  initEpisodicForm(seasons = null) {
    let title = this.documentary.title;
    let category = null;
    if (this.documentary.category) {
      category = this.documentary.category.id;
    }
    let storyline = this.documentary.storyline;
    let summary = this.documentary.summary;
    let year = this.documentary.year;
    let poster = this.documentary.poster;
    this.posterImgURL = this.documentary.poster;
    let wideImage = this.documentary.wideImage;
    this.wideImgURL = this.documentary.wideImage;
    let imdbId = this.documentary.imdbId;

    this.episodicForm = this.fb.group({
      'title': new FormControl(title, [Validators.required]),
      'category': new FormControl(category, [Validators.required]),
      'storyline': new FormControl(storyline, [Validators.required]),
      'summary': new FormControl(summary, [Validators.required]),
      'year': new FormControl(year, [Validators.required]),
      'poster': new FormControl(poster, [Validators.required]),
      'wideImage': new FormControl(wideImage),
      'imdbId': new FormControl(imdbId),
      'seasons': this.fb.array([], Validators.required)
    });

    if (seasons != null) {
      seasons.forEach(season => {
        this.addNewSeason(season);
      });
    }
  }

  addNewSeason(season = null) {
    let number = season.number
    if (number == null) {
      number = this.seasonNumber;
    }

    let control = <FormArray>this.episodicForm.controls.seasons;
    control.push(
      this.fb.group({
        'seasonNumber': new FormControl(number, [Validators.required]),
        'episodes': this.fb.array([], Validators.required)
      })
    );

    let episodes = season.episodes;
    if (season != null && episodes != null) {
      episodes.forEach(episode => {
        let episodesControl = control.at(number - 1).get('episodes');
        this.addNewEpisode(episodesControl, season, episode);
      })
    }

    if (season == null) {
      this.seasonNumber++;
    }
  }

  deleteSeason(index) {
    var seasonsFormArray = this.episodicForm.get("seasons") as FormArray;
    seasonsFormArray.removeAt(index);
  }

  addNewEpisode(control, season = null, episode = null) {
    let episodeNumber;
    let title;
    let storyline;
    let summary;
    let year;
    let length;
    let imdbId;
    let videoId;
    let videoSource;
    let poster;

    if (episode != null) {
      episodeNumber = episode.number;
      title = episode.title;
      imdbId = episode.imdbId;
      poster = episode.thumbnail;
      summary = episode.summary;
      storyline = episode.plot;
      year = episode.year;
      videoId = episode.videoId;
      videoSource = episode.videoSource;

      let seasonNumber = season.number;
      if (this.thumbnailImgURLDict[seasonNumber - 1] == undefined) {
        this.thumbnailImgURLDict[seasonNumber - 1] = {};
      }
      this.thumbnailImgURLDict[seasonNumber - 1][episodeNumber - 1] = poster;
    }

    control.push(
      this.fb.group({
        'episodeNumber': new FormControl(episodeNumber, [Validators.required]),
        'title': new FormControl(title, [Validators.required]),
        'imdbId': new FormControl(imdbId),
        'storyline': new FormControl(storyline, [Validators.required]),
        'summary': new FormControl(summary, [Validators.required]),
        'length': new FormControl(length, [Validators.required]),
        'year': new FormControl(year, [Validators.required]),
        'videoSource': new FormControl(videoSource, [Validators.required]),
        'videoId': new FormControl(videoId, [Validators.required]),
        'poster': new FormControl(poster, [Validators.required]),
      }));
  }
  
  deleteEpisode(seasonIndex, episodeIndex) {
    var seasonsFormArray = this.episodicForm.get("seasons") as FormArray;
    var episodesFormArray = seasonsFormArray.at(seasonIndex).get("episodes") as FormArray;
    episodesFormArray.removeAt(episodeIndex);
  }

  get fStandalone() { return this.standaloneForm.controls; }
  get fEpisodic() { return this.episodicForm.controls; }

  onPosterChange(event) {
    let reader = new FileReader();
 
    if (event.target.files && event.target.files.length) {
      const [file] = event.target.files;
      reader.readAsDataURL(file);
    
      reader.onload = () => {
        if (this.type == 'standalone') {
          this.standaloneForm.patchValue({
            poster: reader.result
          });
        } else {
          this.episodicForm.patchValue({
            poster: reader.result
          })
        }
        
        this.cd.markForCheck();

        this.posterImgURL = reader.result; 
      };
    }
  }

  onThumbnailChange(event, seasonNumber, episodeNumber) {
    console.log(event);
    let reader = new FileReader();
 
    if (event.target.files && event.target.files.length) {
      const [file] = event.target.files;
      reader.readAsDataURL(file);
    
      reader.onload = () => {
        var seasonsFormArray = this.episodicForm.get("seasons") as FormArray;
        var episodesFormArray = seasonsFormArray.at(seasonNumber).get("episodes") as FormArray;
        episodesFormArray.at(episodeNumber)['controls']['poster'].patchValue(reader.result);

        if (this.thumbnailImgURLDict[seasonNumber] == undefined) {
          this.thumbnailImgURLDict[seasonNumber] = {};
        }
        this.thumbnailImgURLDict[seasonNumber][episodeNumber] = reader.result;
      }
        
        this.cd.markForCheck();

      };
    }

  getThumbnailForSeasonAndEpsiode(seasonNumber, episodeNumber) {
    if (this.thumbnailImgURLDict[seasonNumber] == undefined) {
      this.thumbnailImgURLDict[seasonNumber] = {};
    }

    return this.thumbnailImgURLDict[seasonNumber][episodeNumber];
  }
  
  onWideImageChange(event) {
    let reader = new FileReader();
 
    if (event.target.files && event.target.files.length) {
      const [file] = event.target.files;
      reader.readAsDataURL(file);
    
      reader.onload = () => {
        if (this.type == 'standalone') {
          this.standaloneForm.patchValue({
            wideImage: reader.result
          });
        } else {
          this.episodicForm.patchValue({
            wideImage: reader.result
          });
        }
        
        // need to run CD since file load runs outside of zone
        this.cd.markForCheck();

        this.wideImgURL = reader.result; 
      };
    }
  }

  initIMDBFrom() {
    let title = null;

    if (this.type === 'standalone') {
      title = this.standaloneForm.value.title;
    } else if (this.type === 'episodic') {
      title = this.episodicForm.value.title;
    }

    this.imdbForm = new FormGroup({
      'title': new FormControl(title, [Validators.required])
    });

    if (title) {
      this.searchOMDB();
    }
  }

  initYoutubeForm() {
    let title = this.standaloneForm.value.title;

    this.youtubeForm = new FormGroup({
      'title': new FormControl(title, [Validators.required])
    });

    if (title) {
      this.searchYoutube();
    }
  }

  openIMDBModal(content) {
    this.initIMDBFrom();
    console.log(content);
    this.modalService.open(content, {ariaLabelledBy: 'modal-omdb'}).result.then((result) => {
      this.closeResult = `Closed with: ${result}`;
    }, (reason) => {
      this.closeResult = `Dismissed ${reason}`;
    });
  }

  openYoutubeModal(content) {
    this.initYoutubeForm();

    this.modalService.open(content, {ariaLabelledBy: 'modal-youtube'}).result.then((result) => {
      this.closeResult = `Closed with: ${result}`;
    }, (reason) => {
      this.closeResult = `Dismissed ${reason}`;
    });
  }

  imdbView(imdbId) {
    this.isFetchingDocumentaryFromIMDB = true;
    this.showSearchedDocumentariesFromIMDB = false;
    this.showSearchedDocumentaryFromIMDB = true;

    this.omdbService.getByImdbId(imdbId, 'movie')
      .subscribe((result: any) => {
        console.log(result);
        this.searchedDocumentaryFromIMDB = result;
        this.isFetchingDocumentaryFromIMDB = false;
      })
  }

  imdbSelect(selectedDocumentary) {
    this.documentary.title = selectedDocumentary.Title;

    if (this.documentary.imdbId != selectedDocumentary.imdbID) {
      this.documentary.imdbId = selectedDocumentary.imdbID;
      this.documentary.storyline = selectedDocumentary.Plot;
      this.documentary.year = selectedDocumentary.Year;
      this.documentary.poster = selectedDocumentary.Poster;
      this.posterImgURL = selectedDocumentary.Poster;
    }

    if (this.type === 'standaloine') {
      this.initStandaloneForm();
      this.modalService.dismissAll();  
    } else if (this.type === 'episodic') {
      this.getByImdbIdSubscription = this.omdbService.getByImdbId(selectedDocumentary.imdbID, this.type)
        .subscribe((result: any) => {
          console.log("result");
          console.log(result);
          let seasons = result['seasons'];
          this.initEpisodicForm(seasons);
          this.modalService.dismissAll();
      
      });
    }
  }

  youtubeSelect(selectedVideo) {
    this.modalService.dismissAll();

    if (!this.documentary.title) {
      this.documentary.title = selectedVideo.snippet.title;
    }

    if (!this.documentary.storyline) {
      this.documentary.storyline = selectedVideo.snippet.description;
    }

    if (!this.documentary.wideImage) {
      this.documentary.wideImage = selectedVideo.snippet.thumbnails.high.url;
      this.wideImgURL = selectedVideo.snippet.thumbnails.high.url;
    }

    this.documentary.videoId = selectedVideo.id.videoId;

    this.initStandaloneForm();
  }

  searchOMDB() {
    this.isFetchingDocumentariesFromIMDB = true;
    this.showSearchedDocumentaryFromIMDB = false;
    this.showSearchedDocumentariesFromIMDB = true;

    let title = this.imdbForm.value.title;
    let imdbType = 'movie';
    if (this.type === 'episodic') {
      imdbType = 'series';
    }
    this.omdbService.getSearchedDocumentaries(title, imdbType)
      .subscribe((result: any) => {
        console.log(result);
        this.searchedDocumentariesFromIMDB = result['Search'];
        this.isFetchingDocumentariesFromIMDB = false;
      });
  }

  searchYoutube() {
    this.isFetchingVideosFromYoutube = true;
    this.showSearchedVideosFromYoutube = true;

    let title = this.youtubeForm.value.title;
    this.youtubeService.getSearchedDocumentaries(title)
      .subscribe((result: any) => {
        this.searchedVideosFromYoutube = result['items'];
        this.isFetchingVideosFromYoutube = false;
      });
  }

  onStandaloneSubmit() {
    if (!this.standaloneForm.valid) {
      return;
    }

    this.submitted = true;
    this.errors = null;

    let values = this.standaloneForm.value;

    let formValue = this.standaloneForm.value;

    if (this.editMode) {
      this.documentaryService.editStandaloneDocumentary(this.documentary.id, formValue)
        .subscribe((result: any) => {
          this.reset();
          this.router.navigate(["/add/standalone"]);
        },
        (error) => {
          console.log(error);
          this.errors = error.error;
        });
    } else {
      this.documentaryService.createStandaloneDocumentary(formValue)
        .subscribe((result: any) => {
          this.reset();
          this.router.navigate(["/add/standalone"]);
      },
      (error) => {
        console.log(error);
        this.errors = error.error;
      });
    }
  }

  onEpisodicSubmit() {
    console.log(this.fEpisodic);
    console.log(this.episodicForm.value);

    if (!this.episodicForm.valid) {
      return;
    }

    this.submitted = true;
    this.errors = null;

    let values = this.episodicForm.value;

    let formValue = this.episodicForm.value;

    if (this.editMode) {
      this.documentaryService.editEpisodicDocumentary(this.documentary.id, formValue)
        .subscribe((result: any) => {
          this.reset();
          this.router.navigate(["/add/episodic"]);
        },
        (error) => {
          console.log(error);
          this.errors = error.error;
        });
    } else {
      this.documentaryService.createEpisodicDocumentary(formValue)
        .subscribe((result: any) => {
          this.reset();
          this.router.navigate(["/add/episodic"]);
      },
      (error) => {
        console.log(error);
        this.errors = error.error;
      });
    }
  }
  
  pageChanged(event) {
    console.log(event);
    this.standaloneConfig.currentPage = event;
    this.page = event;
    this.fetchStandaloneDocumentaries();
  }
  
  episodicPageChanged(event) {
    console.log(event);
    this.episodicConfig.currentPage = event;
    this.page = event;
    this.fetchEpisodicDocumentaries();
  }

  ngOnDestroy() {
    this.queryParamsSubscription.unsubscribe();
    this.routeParamsSubscription.unsubscribe();
    if (this.documentaryBySlugSubscription != null) {
      this.documentaryBySlugSubscription.unsubscribe();
    }
    if (this.meSubscription != null) {
      this.meSubscription.unsubscribe();
    }
    if (this.getByImdbIdSubscription != null) {
      this.getByImdbIdSubscription.unsubscribe();
    }
  }
}
 

Citadel v2 & Command Pattern

When I was in charge of implementing Version 2 of Citadel I was in charge of creating groups which allowed players in the group access reinforcements.

I also introduced the Command Pattern (which was later adopted by many plugin developers to easily manage the commands from the user’s console).

First we have to create the CommandHandler:

public class CommandHandler {

	private Map<String, Command> commands = new LinkedHashMap<String, Command>();
	private Map<String, Command> identifiers = new HashMap<String, Command>();

	public void addCommand(Command command){
		this.commands.put(command.getName().toLowerCase(), command);
		for(String ident : command.getIdentifiers()){
			this.identifiers.put(ident.toLowerCase(), command);
		}
	}

	public boolean dispatch(CommandSender sender, String label, String[] args){
		for(int argsIncluded = args.length; argsIncluded >= 0; argsIncluded--){
			StringBuilder identifier = new StringBuilder(label);
			for(int i = 0; i < argsIncluded; i++){
				identifier.append(" ").append(args[i]);
			}

			Command cmd = getCmdFromIdent(identifier.toString(), sender);
			if(cmd == null){
				continue;
			}
			String[] realArgs = (String[])Arrays.copyOfRange(args, argsIncluded, args.length);

			if(!cmd.isInProgress(sender)){
				if((realArgs.length < cmd.getMinArguments()) || (realArgs.length > cmd.getMaxArguments())){
					displayCommandHelp(cmd, sender);
					return true;
				}
				if((realArgs.length > 0) && (realArgs[0].equals("?"))){
					displayCommandHelp(cmd, sender);
					return true;
				}
			}

			cmd.execute(sender, realArgs);
			return true;
		}
		return true;
	}

	private void displayCommandHelp(Command cmd, CommandSender sender){
		sender.sendMessage(new StringBuilder().append("§cCommand:§e " ).append(cmd.getName()).toString());
		sender.sendMessage(new StringBuilder().append("§cDescription:§e " ).append(cmd.getDescription()).toString());
		sender.sendMessage(new StringBuilder().append("§cUsage:§e ").append(cmd.getUsage()).toString());
	}

	private Command getCmdFromIdent(String ident, CommandSender executor) {
		ident = ident.toLowerCase();
		if(this.identifiers.containsKey(ident)){
			return (Command)this.identifiers.get(ident);
		}

		for(Command cmd : this.commands.values()){
			if(cmd.isIdentifier(executor, ident)){
				return cmd;
			}
		}

		return null;
	}
}

Then a command:

public class PasswordCommand extends PlayerCommand {

	public PasswordCommand() {
		super("Set Group Password");
        setDescription("Sets the password for a group. Set password to \"null\" to make your group not joinable");
        setUsage("/ctpassword §8<group-name> <password>");
        setArgumentRange(2,2);
		setIdentifiers(new String[] {"ctpassword", "ctpw"});
	}

	public boolean execute(CommandSender sender, String[] args) {
		String groupName = args[0];
		GroupManager groupManager = Citadel.getGroupManager();
		Faction group = groupManager.getGroup(groupName);
		if(group == null){
			sendMessage(sender, ChatColor.RED, "Group doesn't exist");
			return true;
		}
		String playerName = sender.getName();
		if(!group.isFounder(playerName)){
			sendMessage(sender, ChatColor.RED, "Invalid permission to modify this group");
			return true;
		}
		String password = args[1];
		if(password.isEmpty() || password.equals("")){
			sendMessage(sender, ChatColor.RED, "Please enter a password");
			return true;
		}
		group.setPassword(password);
		groupManager.addGroup(group);
		sendMessage(sender, ChatColor.GREEN, "Changed password for %s to \"%s\"", groupName, password);
		return true;
	}

}

Notice the following:

super("Set Group Password");
        setDescription("Sets the password for a group. Set password to \"null\" to make your group not joinable");
        setUsage("/ctpassword §8<group-name> <password>");
        setArgumentRange(2,2);
		setIdentifiers(new String[] {"ctpassword", "ctpw"});

If there are parameters missing it will return a message from the information provided in the code above.
Notice in CommandHandler:

displayCommandHelp(cmd, sender);

Next we instantiate the commands in the singleton or main file called Citadel:

    public void registerCommands(){
    	commandHandler.addCommand(new AddModCommand());
    	commandHandler.addCommand(new AllowCommand());
    	commandHandler.addCommand(new BypassCommand());
    	commandHandler.addCommand(new CreateCommand());
    	commandHandler.addCommand(new DeleteCommand());
    	commandHandler.addCommand(new DisallowCommand());
    	commandHandler.addCommand(new FortifyCommand());
    	commandHandler.addCommand(new GroupCommand());
    	commandHandler.addCommand(new GroupsCommand());
    	commandHandler.addCommand(new InfoCommand());
    	commandHandler.addCommand(new JoinCommand());
    	commandHandler.addCommand(new LeaveCommand());
    	commandHandler.addCommand(new MaterialsCommand());
    	commandHandler.addCommand(new MembersCommand());
    	commandHandler.addCommand(new ModeratorsCommand());
    	commandHandler.addCommand(new NonReinforceableCommand());
    	commandHandler.addCommand(new OffCommand());
    	commandHandler.addCommand(new PasswordCommand());
    	commandHandler.addCommand(new PrivateCommand());
    	commandHandler.addCommand(new PublicCommand());
    	commandHandler.addCommand(new ReinforceCommand());
    	commandHandler.addCommand(new RemoveModCommand());
    	commandHandler.addCommand(new SecurableCommand());
    	commandHandler.addCommand(new StatsCommand());
    	commandHandler.addCommand(new TransferCommand());
    	commandHandler.addCommand(new VersionCommand());
    }
 

The Criteria Pattern

You’ve probably come across a Repository like the following:

class DocumentaryRepository
{
    public function findFeaturedDocumentaries() {
        //sql..
    }

    public function findPublishedDocumentariesInCategoryOrdededByCreated(Category $category) {
        //sql
    }

    public function findMostPopularDocumentaries() {
        //sql
    }

    public function findMostDiscussedDocumentaries() {
        //sql
    }

    public function findMostWatchlistedDocumentaries() {
        //sql
    }

    public function findRandomDocumentariesInCategory(Category $category) {
        //sql
    }

    public function findPublishedDocumentariesOrderedByCreated() {
        //sql
    }

    public function getLatestDocumentariesOrderedByCreated() {
        //sql
    }
}

This eventually can become cumbersome.

Another way to look at this is using the Criteria Pattern where you just have one function in the repository to handle the querying of the database.

First lets build our DocumentaryCriteria class:

<?php

namespace App\Criteria;

use App\Entity\Category;
use App\Entity\User;
use App\Entity\VideoSource;

class DocumentaryCriteria
{
    /**
     * @var bool
     */
    private $featured;

    /**
     * @var string
     */
    private $status;

    /**
     * @var Category
     */
    private $category;

    /**
     * @var VideoSource
     */
    private $videoSource;

    /**
     * @var int
     */
    private $year;

    /**
     * @var string
     */
    private $duration;

    /**
     * @var User
     */
    private $addedBy;

    /**
     * @var array
     */
    private $sort;

    /**
     * @var int
     */
    private $limit;

    //getter and setters
}

Another class required is DocumentaryOrderBy

<?php

namespace App\Enum;

class DocumentaryOrderBy
{
    const CREATED_AT = "createdAt";
    const UPDATED_AT = "updatedAt";
    const VIEWS = "views";
    const COMMENT_COUNT = "commentCount";
    const WATCHLIST_COUNT = "watchlistCount";
    const YEAR = "year";
}

and Order

<?php

namespace App\Enum;

class Order
{
    const ASC = "ASC";
    const DESC = "DESC";
}

Now lets look at the DocumentaryRepository and add the criteria function

/**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function findDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        $em = $this->getEntityManager();
        $qb = $em->createQueryBuilder();

        $qb->select('documentary')
            ->from('App\Entity\Documentary', 'documentary');

        if ($criteria->isFeatured() != null) {
            $qb->andWhere('documentary.featured = :featured')
                ->setParameter('featured', $criteria->isFeatured());
        }

        if ($criteria->getStatus()) {
            $qb->andWhere('documentary.status = :status')
                ->setParameter('status', $criteria->getStatus());
        }

        if ($criteria->getCategory()) {
            $qb->andWhere('documentary.category = :category')
                ->setParameter('category', $criteria->getCategory());
        }

        if ($criteria->getVideoSource()) {
            $qb->andWhere('documentary.videoSource = :videoSource')
                ->setParameter('videoSource', $criteria->getVideoSource());
        }

        if ($criteria->getAddedBy()) {
            $qb->andWhere('documentary.addedBy = :addedBy')
                ->setParameter('addedBy', $criteria->getAddedBy());
        }
        
        if ($criteria->getYear()) {
            $qb->andWhere('documentary.year = :year')
                ->setParameter('year', $criteria->getYear());
        }

        if ($criteria->getSort()) {
            foreach ($criteria->getSort() as $column => $direction) {
                $qb->addOrderBy($qb->getRootAliases()[0] . '.' . $column, $direction);
            }
        }

        if ($criteria->getLimit()) {
            $qb->setMaxResults($criteria->getLimit());
        }

        return $qb;
    }

We’re naming the function findDocumentariesByCriteriaQueryBuilder for a reason (which will come in handy later when we implement pagination).

Lets add another function:


    /**
     * @param DocumentaryCriteria $criteria
     * @return ArrayCollection|Documentary[]
     */
    public function findDocumentariesByCriteria(DocumentaryCriteria $criteria)
    {
        $qb = $this->findDocumentariesByCriteriaQueryBuilder($criteria);

        $query = $qb->getQuery();
        $result = $query->getResult();

        return $result;
    }

Now we can add DocumentaryService which will host more finely grained functions but it’s much simpler with DocumentaryCriteria:


    /**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function getDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        return $this->documentaryRepository->findDocumentariesByCriteriaQueryBuilder($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getFeaturedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setFeatured(true);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);

        $documentaries = $this->documentaryRepository->findDocumentariesByCriteria($criteria);
        shuffle($documentaries);
        return $documentaries;
    }

    /**
     * @param Category $category
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentariesInCategory(Category $category)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setCategory($category);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostPopularDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::VIEWS => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostDiscussedDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::COMMENT_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostWatchlistedDocumentaries(int $limit) : array
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::WATCHLIST_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getLatestDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setLimit($limit);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

Now you can see how the criteria can make your life easier and we can easily add pagination with the QueryBuilder and Pagerfanta.

 /**
     * @FOSRest\Get("/documentary", name="get_documentary_list", options={ "method_prefix" = false })
     *
     * @param Request $request
     * @throws \Doctrine\ORM\ORMException
     */
    public function listAction(Request $request)
    {
        $page = $request->query->get('page', 1);

        $criteria = new DocumentaryCriteria();

        $isRoleAdmin = $this->isGranted('ROLE_ADMIN');

        if ($isRoleAdmin) {
            $videoSourceId = $request->query->get('videoSource');
            if (isset($videoSourceId)) {
                $videoSource = $this->videoSourceService->getVideoSourceById($videoSourceId);
                $criteria->setVideoSource($videoSource);
            }

            $status = $request->query->get('status');
            if (isset($status)) {
                $criteria->setStatus($status);
            }

            $featured = $request->query->get('featured');
            if (isset($featured)) {
                $featured = $featured === 'true' ? true: false;
                $criteria->setFeatured($featured);
            }
        }

        if (!$isRoleAdmin) {
            $criteria->setStatus(DocumentaryStatus::PUBLISH);
        }

        $categorySlug = $request->query->get('category');
        if (isset($categorySlug)) {
            $category = $this->categoryService->getCategoryBySlug($categorySlug);
            $criteria->setCategory($category);
        }

        $year = $request->query->get('year');
        if (isset($year)) {
            $criteria->setYear($year);
        }

        $duration = $request->query->get('duration');
        if (isset($duration)) {
            $criteria->setDuration($duration);
        }

        $addedBy = $request->query->get('addedBy');
        if (isset($addedBy)) {
            $user = $this->userService->getUserByUsername($addedBy);
            $criteria->setAddedBy($user);
        }

        $sort = $request->query->get('sort');
        if (isset($sort)) {
            $exploded = explode("-", $sort);
            $sort = [$exploded[0] => $exploded[1]];
            $criteria->setSort($sort);
        } else {
            $criteria->setSort([
                DocumentaryOrderBy::CREATED_AT => Order::DESC
            ]);
        }

        $qb = $this->documentaryService->getDocumentariesByCriteriaQueryBuilder($criteria);

        $adapter = new DoctrineORMAdapter($qb, false);
        $pagerfanta = new Pagerfanta($adapter);
        $pagerfanta->setMaxPerPage($amountPerPage);
        $pagerfanta->setCurrentPage($page);

        $items = (array) $pagerfanta->getCurrentPageResults();

        $serialized = [];
        foreach ($items as $item) {
            $serialized[] = $this->serializeDocumentary($item);
        }

        $data = [
            'items'             => $serialized,
            'count_results'     => $pagerfanta->getNbResults(),
            'current_page'      => $pagerfanta->getCurrentPage(),
            'number_of_pages'   => $pagerfanta->getNbPages(),
            'next'              => ($pagerfanta->hasNextPage()) ? $pagerfanta->getNextPage() : null,
            'prev'              => ($pagerfanta->hasPreviousPage()) ? $pagerfanta->getPreviousPage() : null,
            'paginate'          => $pagerfanta->haveToPaginate(),
        ];

        return new JsonResponse($data, 200, array('Access-Control-Allow-Origin'=> '*'));
    }

Feedback appreciated.

 

Modulus Examples

aka Fizzbuzz alternatives.

getColumnsForCategories(categories) {
    let categoriesCount = 0;
    for (var key in categories) {
      categoriesCount++;
    }

    let half = Math.floor(categoriesCount / 2);
    let remainder = categoriesCount % half;

    let categoriesLeftColumnLength = half + remainder;
    let categoriesRightColumnLength = half;

    let categoriesLeftColumn = new Set();
    for (let i = 0; i < categoriesLeftColumnLength; i++) {
      categoriesLeftColumn.add(categories[i]);
    }

    let categoriesRighttColumn = new Set();
    for (let i = categoriesLeftColumnLength; i < (categoriesLeftColumnLength + categoriesRightColumnLength); i++) {
      categoriesRighttColumn.add(categories[i]);
    }

    let categoriesColumns = new Map();
    categoriesColumns.set('left', categoriesLeftColumn);
    categoriesColumns.set('right', categoriesRighttColumn);

    return categoriesColumns;
   }
convertArrayOfDocumentariesToMap(documentaries, amountPerRow, amountTotal) {
    let cardDecks = new Map();

    let counter = 0;
    for (let i in documentaries) {
        if (+i === amountTotal) {
          break;
        }

        let documentary = documentaries[i];

        let cardDeck = cardDecks.get(counter);
        if (cardDeck === undefined || !cardDeck) {
          cardDeck = new Set();
        } 

        cardDeck.add(documentary);
        cardDecks.set(counter, cardDeck);

        if (+i != 0 && (+i + 1) % amountPerRow === 0) {
          counter++;
        }
    }

    return cardDecks;
   }
 

Parent & Children Activity

private function convertActivityToArray(array $activity)
    {
        $activityArray = array();

        $previousGroupNumber = null;
        /** @var Activity $activityItem */
        foreach ($activity as $activityItem) {
            $type = $activityItem->getType();
            $groupNumber = $activityItem->getGroupNumber();
            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $data = $activityItem->getData();
            $created = $activityItem->getCreatedAt();

            $activityArray[$groupNumber]['type'] = $type;
            $activityArray[$groupNumber]['created'] = $created;

            if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

        return $activityArray;
    }
 

Greetapp – Specification Pattern Example

https://github.com/JonnyD/greetapp-api-java

Small example of the Specification pattern:

/**
     * GET  /greets-by-user/:groupId : get all the greets by group.
     *
     * @return the ResponseEntity with status 200 (OK) and the list of greets in body
     */
    @GetMapping("/greets-by-group/{groupId}")
    @Timed
    public Object getAllGreetsByGroup(@PathVariable Long groupId) {
        log.debug("REST request to get all Greets by user");
        List<Greet> greets = greetService.getByGang(groupId);

        Optional<Gang> optionalGang = this.gangService.getById(groupId);
        Gang gang = optionalGang.get();

        CanViewGang canViewGang = new CanViewGang(this.userService);
        if (!canViewGang.isSatisfiedBy(gang)) {
            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
        }

        CanViewGreetsByGang canViewGreetsByGang = new CanViewGreetsByGang(this.userRepository);

        List<Greet> greetsToShow = new ArrayList<Greet>();
        for (Greet greet : greets) {
            if (canViewGreetsByGang.isSatisfiedBy(greet)) {
                greetsToShow.add(greet);
            }
        }

        return greetsToShow;
    }
public class CanViewGreetsByGang extends AbstractSpecification<Greet> {
    private User loggedInUser;

    public CanViewGreetsByGang(UserRepository userRepository) {
        Optional<String> login = SecurityUtils.getCurrentUserLogin();
        this.loggedInUser = userRepository.findOneByLogin(login.get()).get();
    }

    public boolean isSatisfiedBy(Greet greet) {
        IsGangPublic isGangPublic = new IsGangPublic();
        IsHostOfGreet isHostOfGreet = new IsHostOfGreet(this.loggedInUser);
        IsMemberOfGang isMemberOfGang = new IsMemberOfGang(this.loggedInUser);

        boolean isGangPublicOrIsMemberOfGangBoolean = isGangPublic
            .or(isMemberOfGang)
            .isSatisfiedBy(greet.getGang());

        boolean isHostOfGreetBoolean = isHostOfGreet
            .isSatisfiedBy(greet);

        return isGangPublicOrIsMemberOfGangBoolean || isHostOfGreetBoolean;
    }
}
 

Introducing Peggy

PHP wrapper for 80legs.com API https://github.com/JonnyD/Peggy

$peggy = new Peggy\Client('<your api key>');

Crawls

// create crawl
$request = $peggy->crawl()->createCrawlRequest($crawlName, $appName, $urllist, $maxDepth, $maxUrls);
$peggy->crawl()->create($request);

// get crawl
$crawl = $peggy->crawl()->get($crawlName);
echo $crawl->getName();

// cancel crawl
$peggy->crawl()->cancel($crawlName);

// get all crawls
$allCrawls = $peggy->crawl()->all();
foreach ($allCrawls as $crawl) {
   echo $crawl->getName();
}

Results

// get result
$result = $peggy->result()->get($crawlName);
$urls = $result->getUrls(); // Returns the results of the crawl specified by CRAWL_NAME. This will return a 404 if no results have been posted. Example Url: "http://s3.amazonaws.com/results1"

Apps

// upload app
$request = $peggy->app()->createAppRequest($name, $filePath);
$peggy->app()->upload($request);

// get app
$app = $peggy->app()->get($appName); // this API is broken on 80legs.com

// remove app
$peggy->app()->remove($appName);

// get all apps
$allApps = $peggy->app()->all();
foreach ($allApps as $app) {
    echo $app->getName();
}

Url Lists

// create url list
$request = $peggy->urllist()->createUrllistRequest($name, $filePath);
$peggy->urllist()->upload($request);

// get url list
$urllist = $peggy->urllist()->get($name);
echo $urllist->getName();

// remove url list
$peggy->urllist->remove($name);

// get all url lists
$allUrllists = $peggy->urllist()->all();
foreach ($allUrllists as $urllist) {
    echo $urllist->getName();
}

User

// get me
$me = $peggy->user()->me();

// get user
$user = $peggy->user()->get($token);
 

Introducing DocumentaryWIRE v3

DocumentaryWIRE is a website dedicated to documentaries. Our mission is to help people find and share documentaries they love. http://www.documentarywire.com/

DocumentaryWIRE was built using the following technologies:

 

Introducing Skynet – A Minecraft Bot

Skynet is a Minecraft bot written with Node.js, MySQL, and Symfony2 for a Minecraft server called Civcraft. You can see it on Github:  https://github.com/JonnyD/Skynet/blob/v1.0.3/lib/skynet-minecraft.js

(Keep in mind it was my first time coding with Node.js so it may be a bit cringey but I did my best to make the code readable by using Async and other modules.

Civcraft is Minecraft server with the goal of leaving players as free as possible to generate their own political, social, and economic order within Minecraft through the use of several custom mods as well as some more general ones. That means no rulers but doesn’t mean no rules. Those rules are enforced by other players in the community. It’s part of a social experiment to see which ideas would win in a free society.

This bot was for my in-game security business that allows players, in combination with other plugins, to make their property more secure – to find out which players are using alts (alternative accounts), which players are trespassing on their property or stealing their property, which criminals are in the vicinity so you can avoid that area all without logging in.

var mineflayer = require('mineflayer');
var mysql = require('mysql');
var moment = require('moment');
var async = require('async');
var config = require('./config')

var connection = mysql.createConnection({
  host: config.mysql.host,
  user: config.mysql.user,
  password: config.mysql.password,
  database: config.mysql.database
});
connection.connect();

var bot;
var options = {
  host: config.mc.host,
  port: config.mc.port,
  username: config.mc.username,
  password: config.mc.password
};
startConnectionTimeout();

process.on('uncaughtException', function(exception) {
  console.log("caught exception " + exception);
});

var connected = false;
var connectionTimeout;
var afkTimeout;
var afk1MinuteTimeout;
var afk5MinutesTimeout;
var afk10MinutesTimeout;

function connect() {
  console.log("[" + getTimestamp() + "] Attempting to login");
  bot = mineflayer.createBot(options);
  bot.on('connect', function() {
    bindEvents(bot);
  });
}

function bindEvents(bot) {
  console.log("[" + getTimestamp() + "] Binding Events");

  bot.on('login', function() {
    console.log("[" + getTimestamp() + "] I logged in.");
    connected = true;
    stopConnectionTimeout();
  });

  bot.on('playerJoined', function(player) {
    loginPlayer(player);
  });

  bot.on('playerLeft', function(player) {
    logoutPlayer(player);
  });

  bot.on('whisper', function(username, message, rawMessage) {
    console.log("message", message, "rawMessage", rawMessage, "username", username);
    if (username === config.settings.owner) {
      if (message === " quit" || message === " restart") {
        connected = false;
        var timestamp = getTimestamp();
        clearTimeouts();
        async.series([
          function(callback) {
            bot.quit();
            console.log("logging out all players");
            setTimeout(function () {
              logoutAllPlayers(timestamp, function(finished) {
                console.log("logged out all players " + finished);
                callback();
              });
            }, 30 * 1000);
          },
          function(callback) {
            if (message === " restart") {
              callback();
            }
          }
        ]);
      }
    }
  });

  bot.on('chat', function(username, message) {
    console.log("chat " + username + " " + message);
  });

  bot.on('nonSpokenChat', function(message) {
    console.log("nonSpokenChat " + message);
    if (message.indexOf('AFK Plugin') >= 0) {
      if (message.indexOf('10 seconds') >= 0) {
        bot.chat(config.settings.antiAfkMessage);
      } else if (message.indexOf('1 minute') >= 0) {
        afk1MinuteTimeout = setTimeout(function () {
          bot.chat(config.settings.antiAfkMessage);
        }, 30 * 1000);
      } else if (message.indexOf('5 minutes') >= 0) {
        afk5MinutesTimeout = setTimeout(function () {
          bot.chat(config.settings.antiAfkMessage);
        }, 60 * 1000);
      } else if (message.indexOf('10 minutes') >= 0) {
        afk10MinutesTimeout = setTimeout(function () {
          bot.chat(config.settings.antiAfkMessage);
        }, 120 * 1000);
      } else {
        bot.chat(config.settings.antiAfkMessage);
      }
    }
  });

  bot.on('kicked', function(reason) {
    connected = false;
    stopConnectionTimeout();
    var timestamp = getTimestamp();
    console.log("[" + timestamp + "] I got kicked for", reason, "lol");
    clearTimeouts();
    async.series([
      function(callback) {
        console.log("logging out all players");
        setTimeout(function () {
          logoutAllPlayers(timestamp, function(finished) {
            console.log("logged out all players " + finished);
            callback();
          });
        }, 30 * 1000);
      },
      function(callback) {
        startConnectionTimeout();
        callback();
      }
    ]);
  });

  bot.on('spawn', function() {
    console.log("[" + getTimestamp() + "] I spawned");
    startAfkTimeout();
  });

  bot.on('death', function() {
    console.log("[" + getTimestamp() + "] I died x.x.");
  });
}

function getPlayer(username, timestamp, callback) {
  findPlayer(username, function(playerId) {
    if (typeof playerId === 'undefined') {
      createPlayer(username, timestamp, function(playerId) {
        callback(playerId);
      });
    } else {
      callback(playerId);
    }
  });
}

function addEvent(playerId, type, timestamp, callback) {
  var event = {player_id: playerId, event_type_id: type, timestamp: timestamp};
  connection.query('INSERT INTO event SET ?', event, function(err, result) {
    callback(result.insertId);
  });
}

function findEventTimestamp(eventId, callback) {
  connection.query('SELECT timestamp AS timestamp FROM event WHERE id = ' + eventId, function(err, rows, fields) {
    if (rows.length > 0) {
      callback(rows[0].timestamp);
    }
  });
}

function addSession(username, playerId, timestamp, loginEventId, callback) {
  var session = { player_id: playerId, login: loginEventId, login_timestamp: timestamp };
  connection.query('INSERT INTO session SET ?', session, function(err, result) {
    logVerbose("[" + getTimestamp() + "] Attempting to start session for " + username + " with eventId " + loginEventId);
    callback(result.insertId);
  });
}

function updateSession(sessionId, logoutEventId, logoutTimestamp, duration, callback) {
  connection.query("UPDATE session SET logout = ?, duration = ?, logout_timestamp = ? WHERE id = ?", [logoutEventId, duration, logoutTimestamp, sessionId], function(err, result) {
    callback(1);
  });
}

function findSession(playerId, callback) {
  connection.query('SELECT id AS session_id, login AS loginEvent FROM session WHERE player_id = ? AND logout IS NULL ORDER BY login_timestamp ASC LIMIT 1', [playerId], function(err, rows, fields) {
    if (rows.length > 0) {
      callback(rows[0].session_id, rows[0].loginEvent);
    }
  });
}

function findPlayer(username, callback) {
  connection.query('SELECT id from player where username = ?', [username], function(err, rows, fields) {
    var playerId;
    if (rows.length > 0) {
      playerId = rows[0].id;
    }
    callback(playerId);
  });
}

function findOnlinePlayers(callback) {
  connection.query('SELECT * FROM session s, player p WHERE p.id = s.player_id AND logout IS NULL', function(error, rows, fields) {
    if (rows.length > 0) {
      callback(rows);
    }
  });
}

function logoutAllPlayers(timestamp, callback) {
  var counter = 0;
  findOnlinePlayers(function(sessions) {
    logVerbose("[" + timestamp + "] Sessions found: " + sessions.length);
    if (sessions.length > 0) {
      sessions.forEach(function(session) {
        var playerId = session.player_id;
        var username = session.username;

        async.waterfall([
          function(callback) {
            addEvent(playerId, 2, timestamp, function(logoutEventId) {
              logVerbose("[" + timestamp + "] " + "Created logout: " + logoutEventId + " for " + username + " (" + playerId +")");
              callback(null, logoutEventId);
            });
          },
          function(logoutEventId, callback) {
            findSession(playerId, function(sessionId, loginEventId) {
              logVerbose("[" + timestamp + "] " + "Found session: " + sessionId +" for " + username + " (" + playerId +")");
              callback(null, sessionId, loginEventId, logoutEventId);
            });
          },
          function(sessionId, loginEventId, logoutEventId, callback) {
            findEventTimestamp(loginEventId, function(loginTimestamp) {
              findEventTimestamp(logoutEventId, function(logoutTimestamp) {
                var difference = diffBetweenTimestamps(loginTimestamp, logoutTimestamp);
                logVerbose("[" + timestamp + "] " + "Duration: " + difference + " for " + username);
                callback(null, sessionId, logoutEventId, difference);
              });
            });
          },
          function(sessionId, logoutEventId, difference, callback) {
            updateSession(sessionId, logoutEventId, timestamp, difference, function(finished) {
              logVerbose("[" + timestamp + "] Ended session: " + sessionId + " for " + username + " (" + playerId + ")");
              callback(null, finished);
            });1
          }
        ], function(err, result) {
          counter = counter + 1;
          if (counter === sessions.length) {
            callback(1);
          }
        });
      });
    }
  });
}

function createPlayer(username, timestamp, callback) {
  var newPlayer  = {username: username, timestamp: timestamp};
  connection.query('INSERT INTO player SET ?', newPlayer, function(err, result) {
    var playerId;
    playerId = result.insertId;
    logVerbose("[" + timestamp + "] " + "Created player: " + username + " (" + playerId + ")");
    callback(playerId);
  });
}

function loginPlayer(player) {
  var timestamp = getTimestamp();
  console.log("[" + timestamp + "] " + player.username + " joined");
  var username = player.username;
  async.waterfall([
    function(callback) {
      getPlayer(username, timestamp, function(playerId) {
        callback(null, playerId);
      });
    },
    function(playerId, callback) {
      addEvent(playerId, 1, timestamp, function(eventId) {
        logVerbose("[" + timestamp + "] " + "Created login: " + eventId + " for " + player.username + " (" + playerId +")");
        callback(null, playerId, eventId);
      });
    },
    function(playerId, eventId, callback) {
      addSession(player.username, playerId, timestamp, eventId, function(sessionId) {
        logVerbose("[" + timestamp + "] Started session: " + sessionId + " for " + player.username + " (" + playerId +")");
        callback(null, playerId, sessionId);
      });
    }
  ]);
}

function logoutPlayer(player) {
  var timestamp = getTimestamp();
  console.log("[" + timestamp + "] " + player.username + " left");

  var username = player.username;
  async.waterfall([
    function(callback) {
      findPlayer(username, function(playerId) {
        callback(null, playerId);
      });
    },
    function(playerId, callback) {
      addEvent(playerId, 2, timestamp, function(logoutEventId) {
        logVerbose("[" + timestamp + "] " + "Created logout: " + logoutEventId + " for " + username + " (" + playerId +")");
        callback(null, playerId, logoutEventId);
      });
    },
    function(playerId, logoutEventId, callback) {
      findSession(playerId, function(sessionId, loginEventId) {
        logVerbose("[" + timestamp + "] " + "Found session: " + sessionId +" for " + username + " (" + playerId +")");
        callback(null, playerId, sessionId, loginEventId, logoutEventId);
      });
    },
    function(playerId, sessionId, loginEventId, logoutEventId, callback) {
      findEventTimestamp(loginEventId, function(loginTimestamp) {
        findEventTimestamp(logoutEventId, function(logoutTimestamp) {
          var difference = diffBetweenTimestamps(loginTimestamp, logoutTimestamp);
          logVerbose("[" + timestamp + "] " + "Duration: " + difference + " for " + username);
          callback(null, playerId, sessionId, logoutEventId, difference);
        });
      });
    },
    function(playerId, sessionId, logoutEventId, difference, callback) {
      updateSession(sessionId, logoutEventId, timestamp, difference, function(updated) {
        logVerbose("[" + timestamp + "] " + "Ended session: " + sessionId + " for " + username + " (" + playerId +")");
      });
    }
  ]);
}

function getTimestamp() {
  var MyDate = new Date();
  var MyDateString;
  MyDateString = MyDate.getFullYear() + "-"
    + ('0' + (MyDate.getMonth()+1)).slice(-2) + "-"
    + ('0' + MyDate.getDate()).slice(-2) + " "
    + ('0' + MyDate.getHours()).slice(-2) + ":"
    + ('0' + MyDate.getMinutes()).slice(-2) + ":"
    + ('0' + MyDate.getSeconds()).slice(-2);
  return MyDateString;
}

function parseTimestamp(input) {
  var timestamp = input.split(" ");
  var date = timestamp[0];
  var time = timestamp[1];
  var dateParts = date.split("-");
  var timeParts = time.split(":");
  // new Date(year, month [, date [, hours[, minutes[, seconds[, ms]]]]])
  return moment([dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1], timeParts[2]]);
}

function diffBetweenTimestamps(timestamp1, timestamp2) {
  var timestamp1Parsed = moment(timestamp1);
  var timestamp2Parsed = moment(timestamp2);
  var difference = timestamp2Parsed.diff(timestamp1Parsed, 'seconds');
  return difference;
}

function clearTimeouts() {
  stopAfkTimeout();
  clearTimeout(afk1MinuteTimeout);
  clearTimeout(afk5MinutesTimeout);
  clearTimeout(afk10MinutesTimeout);
}

var startAfk;
var nextAtAfk;
function startAfkTimeout() {
  if (!startAfk) {
    startAfk = new Date().getTime();
    nextAtAfk = startAfk;
  }
  nextAtAfk += 30 * 1000;

  if (connected) {
    bot.setControlState('jump', true);
    bot.setControlState('jump', false);
    console.log("[" + getTimestamp() + "] I jumped");

    afkTimeout = setTimeout(startAfkTimeout, nextAtAfk - new Date().getTime());
  }
}

function stopAfkTimeout() {
  startAfk = null;
  nextAtAfk = null;
  clearTimeout(afkTimeout);
}

var startConnection;
var nextAtConnection;
function startConnectionTimeout() {
  if (!startConnection) {
    startConnection = new Date().getTime();
    nextAtConnection = startConnection;
  }
  nextAtConnection += 10 * 1000;

  if (!connected) {
    connect();

    afkTimeout = setTimeout(startConnectionTimeout, nextAtConnection - new Date().getTime());
  }
}

function stopConnectionTimeout() {
  startConnection = null;
  nextAtConnection = null;
  clearTimeout(connectionTimeout);
}

function logVerbose(message) {
  if (config.settings.verboseLogging) {
    console.log(message);
  }
}
 

Calculating Standings by Results

Using a PHP Framework called Symfony2, I am developing a web application for people who play the video game Fifa to compete against each other in an online competition. It’s on Github: https://github.com/JonnyD/Elite-Fifa-Leagues

In the picture above you can see the UI of the league standings. These values are calculated and stored in a Standings table after every confirmed match. There’s a problem with this and that is storing calculated values breaks normalization. The only time this could be acceptable is in cases where you want to improve performance by not having to re-calculate the values every time you need them.

However, what if I want to find out a teams standing by their Last X Games Played at Home, Last X Games Played Away, or Last X Games Played Combined?

Here’s how I could get Standings by home matches only:

SELECT team.name, home_team_id AS team_id,
    COUNT(*) AS played,
    SUM((CASE WHEN home_score > away_score THEN 1 ELSE 0 END)) AS won,
    SUM((CASE WHEN away_score > home_score THEN 1 ELSE 0 END)) AS lost,
    SUM((CASE WHEN home_score = away_score THEN 1 ELSE 0 END)) AS drawn,
    SUM(home_score) AS goalsFor,
    SUM(away_score) AS goalsAgainst,
    SUM(home_score - away_score) AS goalDifference,
    SUM((CASE WHEN home_score > away_score THEN 3 WHEN home_score = away_score THEN 1 ELSE 0 END)) AS points
FROM matches
INNER JOIN team ON matches.home_team_id = team.id
WHERE league_id = 94
    AND season_id = 82
    AND confirmed IS NOT NULL
GROUP BY home_team_id
ORDER BY POINTS DESC;

Here’s how I could get Standings by Away matches only:

```
SELECT team.name, away_team_id AS team_id,
    COUNT(*) AS played,
    SUM((CASE WHEN away_score > home_score THEN 1 ELSE 0 END)) AS won,
    SUM((CASE WHEN home_score > away_score THEN 1 ELSE 0 END)) AS lost,
    SUM((CASE WHEN home_score = away_score THEN 1 ELSE 0 END)) as drawn,
    SUM(away_score) AS goalsFor,
    SUM(home_score) AS goalsAgainst,
    SUM(away_score - home_score) AS goalDifference,
    SUM((CASE WHEN away_score > home_score THEN 3 WHEN away_score = home_score THEN 1 ELSE 0 END)) AS points
FROM matches
INNER JOIN team ON matches.away_team_id = team.id
WHERE league_id = 94
    AND season_id = 82
    AND confirmed IS NOT NULL
GROUP BY away_team_id
ORDER BY points DESC;

Here’s how I could get Standings by Home and Away matches combined:

SELECT team.name,
       team_id AS team_id,
       COUNT(*) AS played,
       SUM((CASE WHEN team_score > other_team_score THEN 1 ELSE 0 END)) AS won,
       SUM((CASE WHEN team_score < other_team_score THEN 1 ELSE 0 END)) AS lost,
       SUM((CASE WHEN team_score = other_team_score THEN 1 ELSE 0 END)) AS drawn,
       SUM(team_score) AS goalsFor,
       SUM(other_team_score) AS goalsAgainst,
       SUM(team_score - other_team_score) AS goalDifference,
       SUM((CASE WHEN team_score > other_team_score THEN 3
                 WHEN team_score = other_team_score THEN 1
                 ELSE 0 END)) AS points
FROM
    (
        -- LIST TEAM STATS WHEN PLAYED AS HOME_TEAM
        SELECT
             id,
             league_id,
             season_id,
             home_team_id as team_id,
             home_score   as team_score,
             away_score   as other_team_score,
             confirmed
        FROM    matches
        UNION ALL
        -- LIST TEAM STATS WHEN PLAYED AS AWAY_TEAM
        SELECT
             id,
             league_id,
             season_id,
             away_team_id as team_id,
             away_score   as team_score,
             home_score   as other_team_score,
             confirmed
        FROM matches
    ) matches
INNER JOIN team ON matches.team_id = team.id
WHERE league_id = 94
    AND season_id = 82
    AND confirmed IS NOT NULL
GROUP BY team.name, team_id
ORDER BY POINTS DESC;
 

Chrome Extension for Freedomain Radio

https://github.com/JonnyD/FreedomainRadio-Chrome-Extension

background.js

jQuery.support.cors = true;         
var feedItems = [];
var podcastUrl = "http://pipes.yahoo.com/pipes/pipe.run?_id=e5f849875fd5f61b23e5d7f79873d9c9&_render=json";
var videoUrl = "http://gdata.youtube.com/feeds/api/users/stefbot/uploads?v=2&alt=jsonc";

function initialise() {
    if (!localStorage.updateInterval) {
        localStorage.updateInterval = 5;
    }
    
    if (!localStorage.types) {
        localStorage.types = "podcast:true;video:true;topic:true";
    }
    
    chrome.browserAction.setBadgeBackgroundColor({color:[255, 102, 0, 255]});
    
    fetchFeeds();
}

function fetchPodcasts(callback) {
    $.ajax({
        url: podcastUrl,
        type: "GET",
        timeout: 30000,
        dataType: "json",
        success: function(data) {
            console.log(data);
            parsePodcasts(data.value, callback);
        },
        error: function(jqXHR, textStatus, ex) {
            console.log(textStatus + "," + ex + "," + jqXHR.responseText);
        }
    });    
}

function parsePodcasts(data, callback) {
    var podcasts = data.items;
    
    var newPodcastItems = [];
    for (var i = 0; i < podcasts.length; i++) {
        var podcast = podcasts[i];
        
        var item = {
            date: convertToUnix(podcast.pubDate), 
            title: podcast.title,
            description: podcast.description,
            thumbnail: '/img/podcast.jpg',
            link: podcast.link
        };      

        newPodcastItems.push(item);
    }
    
    callback(newPodcastItems);
}

function fetchVideos(callback) {
    var xhrFeed = new XMLHttpRequest();
    xhrFeed.onreadystatechange = function() {
        if (xhrFeed.readyState == 4 && xhrFeed.status == 200) {
            parseVideos(xhrFeed.responseText, callback);
        }   
    }
    xhrFeed.open("GET", videoUrl, true);
    xhrFeed.send();                
}

function parseVideos(data, callback) {
    var youtubeJSON = JSON.parse(data);
    var youtubeVideos = youtubeJSON.data.items;
    
    var newVideoItems = [];
    for (var i = 0; i < youtubeVideos.length; i++) {
        var video = youtubeVideos[i];
        
        var item = { 
            date: convertToUnix(video.uploaded), 
            title: video.title,
            description: video.description,
            thumbnail: video.thumbnail.hqDefault,
            link: "http://www.youtube.com/watch?v=" + video.id 
        };
        
        newVideoItems.push(item);
    }
    
    callback(newVideoItems);
}

function fetchFeeds() {
    fetchVideos(function(newVideoItems) {
        fetchPodcasts(function(newPodcastItems) {
            var newFeedItems = newVideoItems.concat(newPodcastItems);
            processLatestFeed(newFeedItems, function() {
              updateBadge();
            });
        });
    });
    
}

function processLatestFeed(newFeedItems, callback) {
    newFeedItems.sort(function(a,b) {return (a.date > b.date) ? -1 : ((b.date > a.date) ? 1 : 0);});
    
    for (var i = 0; i < 25; i++) {
        var newItem = newFeedItems[i];
        
        if (!isItemInFeed(newItem)) {
            newItem.featured = true;
            newItem.description = newItem.description.substring(0,320);
            feedItems.push(newItem);
        }
    }
    
    callback();
}

function isItemInFeed(item) {
    for (i in feedItems) {
        if (feedItems[i]["link"] == item.link) {
            return true;
        }
    }
    return false;
}

function convertToUnix(date) {
  return moment(date).unix()
}

function getFeaturedCount() {
    var featured = 0;
    
    for (i in feedItems) {
        var feedItem = feedItems[i];
        if (feedItem.featured == true) {
            featured++;
        }
    }
    
    return featured;
}

function updateBadge() {
  var featured = getFeaturedCount();
  
  if (featured > 0) {
      chrome.browserAction.setBadgeText({text: featured + ""});
      chrome.browserAction.setTitle({title: featured + " new item" + ((featured > 1) ? "s": "")});
  } else {
      chrome.browserAction.setBadgeText({text: ""});
      chrome.browserAction.setTitle({title: "No new items"});
  }
  
  console.log("badge updated");
}

initialise();

popup.js

document.addEventListener('DOMContentLoaded', function () {
    initialise();
});

function initialise() {
    var background = chrome.extension.getBackgroundPage();
    var latestItems = background.feedItems;
    
    createLatestFeed(latestItems);
    
    background.updateBadge();
}

function createLatestFeed(latestItems) {
    var content = document.getElementById("content");
    var itemView = document.createElement("div");
    itemView.setAttribute("class", "item");
    content.appendChild(itemView);
    
    for (var i = 0; i < 25; i++) {
        var item = latestItems[i];
        var detail = document.createElement("div");
        detail.setAttribute("class", "detail");
        
        if (item["featured"]) {
            detail.setAttribute("class", "featured");
            item["featured"] = false;
        }
        
        var thumbnailLink = document.createElement("a");
        
         if(item["type"] == "podcast"){
            thumbnailLink.setAttribute("href", "#");
            thumbnailLink.setAttribute("onClick", "javascript:window.open('http://fdrpodcast.com/player.php?id=" + item["linkId"] + "','podcastplayer','toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, width=375, height=125');");
        } else {
            thumbnailLink.setAttribute("href", item["link"]); // set link path
            thumbnailLink.setAttribute("target", "_blank");
        }
        
        var thumbnailImg = document.createElement("img");
        thumbnailImg.setAttribute("class", "thumbnail");
        thumbnailImg.setAttribute("onclick", "openTab('" + item["link"] + "');");
        thumbnailImg.setAttribute( "src", item["thumbnail"] );
        thumbnailLink.appendChild(thumbnailImg);
        detail.appendChild(thumbnailLink);
        
        var link = document.createElement("a");
        
        if(item["type"] == "podcast"){
            link.setAttribute("href", "#");
            link.setAttribute("onClick", "javascript:window.open('http://fdrpodcast.com/player.php?id=" + item["linkId"] + "','podcastplayer','toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, width=375, height=125');");
        } else {
            link.setAttribute("href", item["link"]);
            link.setAttribute("target", "_blank");
        }

        var titleNode = document.createElement("div");
        titleNode.setAttribute("class", "title");
        titleNode.appendChild(document.createTextNode(item["title"]));
        link.appendChild(titleNode);
        detail.appendChild(link);
        
        var descriptionNode = document.createElement("div");
        descriptionNode.setAttribute("class", "description");  
       descriptionNode.appendChild(document.createTextNode(item["description"]));
        detail.appendChild(descriptionNode);
        
        itemView.appendChild(detail);
    }
    
}
 

Updating Citadel Schema

Citadel is a Minecraft plugin

Before Update:

-- Citadel 3 Schema
 
CREATE TABLE groups_group (
 `id` INT AUTO_INCREMENT,
 `name` VARCHAR(255) UNIQUE,
 `password` VARCHAR(255),
 `personal` TINYINT(1) DEFAULT 0,
 `status` INT(2) DEFAULT 0,
 `updated` DATETIME,
 `created` DATETIME,
 PRIMARY KEY (id)
);
 
CREATE TABLE groups_member (
 `id` INT AUTO_INCREMENT,
 `name` VARCHAR(32) UNIQUE,
 `updated` DATETIME,
 `created` DATETIME,
 PRIMARY KEY (id)
);
 
CREATE TABLE groups_group_member (
 `member_id` INT,
 `group_id` INT,
 `role` INT DEFAULT 0,
 `updated` DATETIME,
 `created` DATETIME,
 PRIMARY KEY (member_id, group_id)
);
 
ALTER TABLE groups_group_member
ADD FOREIGN KEY (member_id)
REFERENCES groups_member (id);
 
ALTER TABLE groups_group_member
ADD FOREIGN KEY (group_id)
REFERENCES groups_group (id);
 
-- Citadel 3 Upgrade Script
 
-- Migrate members
INSERT INTO groups_member (name, updated, created)
SELECT name, now(), now() FROM member;
 
-- Migrate Groups
INSERT INTO groups_group(name, password, updated, created)
SELECT name, password, now(), now() FROM faction;
 
-- Migrate Personal Groups
UPDATE groups_group
SET personal = 1
WHERE name IN (SELECT name FROM personal_group);
 
-- Migrate Disciplined Groups
UPDATE groups_group g
JOIN faction f on g.name = f.name
SET status = 2
WHERE f.discipline_flags = 1;
 
-- Migrate Group Admins
INSERT INTO groups_group_member(member_id, group_id, role, updated, created)
SELECT gm.id AS member_id, g.id AS group_id, 0, now(), now()
FROM groups_member gm, faction f, groups_group g
WHERE gm.name = f.founder AND g.name = f.name;
 
-- Migrate Group Members
INSERT INTO groups_group_member(member_id, group_id, role, updated, created)
SELECT gm.id AS member_id, g.id AS group_id, 2, now(), now()
FROM faction_member fm, groups_member gm, groups_group g
WHERE gm.name = fm.memberName AND g.name = fm.factionName;
 
-- Migrate Group Moderators
INSERT INTO groups_group_member(member_id, group_id, role, updated, created)
SELECT gm.id AS member_id, g.id AS group_id, 1, now(), now()
FROM moderator m, groups_member gm, groups_group g
WHERE gm.name = m.memberName AND g.name = m.factionName;
 
-- Citadel 3 Schema Selects
SELECT * FROM groups_member;
SELECT * FROM groups_group;
SELECT * FROM groups_group_member;
 

A Finite State Machine in Minecraft

public String first(String text) {
return text.substring(0, 1);
}
 
@EventHandler(priority = EventPriority.HIGHEST)
public void blockPlace(BlockPlaceEvent bpe)
{
    // Block placed by player
    Block block = bpe.getBlock();
 
    // Get the blocks material relative to the block placed by the player
    Material main = block.getType();
    Material up1 = block.getRelative(BlockFace.UP, 1).getType();
    Material up2 = block.getRelative(BlockFace.UP, 2).getType();
    Material down1 = block.getRelative(BlockFace.DOWN, 1).getType();
    Material down2 = block.getRelative(BlockFace.DOWN, 2).getType();
 
    // Concatenate the first letters of each of the materials
    String materialFirstLetters = first(up1.toString()) + first(up2.toString()) +
                                  first(main.toString()) + first(down1.toString()) +
                                  first(down2.toString());
 
    // Regex pattern
    String regex = "DIJ..|I.DJ.|..IDJ";
 
    // Check if the first letters of the materials match the regex pattern
    if (materialFirstLetters.matches(regex))
    {
        System.out.println("Antenna Created");
    }
}