Initial commit

This commit is contained in:
Oliver Davies 2020-03-13 11:17:16 +00:00
commit e1517ecd9d
36 changed files with 2093 additions and 0 deletions

29
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Lint README.md
on:
push:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run prettier
run: |
npm install
npx prettier README.md --write
- name: Commit changes
run: |
if [ -n "$(git status --short README.md)" ]; then
git config --local user.name "Oliver Davies"
git config --local user.email "339813+opdavies@users.noreply.github.com"
git commit -a -m "Run prettier"
fi
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/node_modules/

1337
README.md Normal file

File diff suppressed because it is too large Load diff

BIN
docs/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
docs/images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
docs/images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

12
package-lock.json generated Normal file
View file

@ -0,0 +1,12 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"prettier": "^1.19.1"
}
}

18
prettier.config.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
arrowParens: 'avoid',
bracketSpacing: false,
endOfLine: 'lf',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxBracketSameLine: false,
jsxSingleQuote: false,
printWidth: 80,
proseWrap: 'always',
quoteProps: 'as-needed',
requirePragma: false,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
}

View file

@ -0,0 +1,5 @@
name: My Module
type: module
core: 8.x
core_version_requirement: ^8 || ^9
package: Custom

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class MyModuleTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
/** @test */
public function the_front_page_loads_for_anonymous_users() {
$this->drupalGet('<front>');
$this->assertResponse(Response::HTTP_OK);
}
}

View file

@ -0,0 +1,5 @@
name: My Module
type: module
core: 8.x
core_version_requirement: ^8 || ^9
package: Custom

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class MyModuleTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
/** @test */
public function the_front_page_loads_for_anonymous_users() {
$this->drupalGet('<front>');
$this->assertResponse(Response::HTTP_OK);
}
/** @test */
public function the_admin_page_is_not_accessible_to_anonymous_users() {
$this->drupalGet('admin');
$this->assertResponse(Response::HTTP_FORBIDDEN);
}
/** @test */
public function the_admin_page_is_accessible_by_admin_users() {
$adminUser = $this->createUser([
'access administration pages',
]);
$this->drupalLogin($adminUser);
$this->drupalGet('/admin');
$this->assertResponse(Response::HTTP_OK);
}
}

View file

@ -0,0 +1,5 @@
name: My Module
type: module
core: 8.x
core_version_requirement: ^8 || ^9
package: Custom

View file

@ -0,0 +1,7 @@
blog.page:
path: /blog
defaults:
_controller: Drupal\my_module\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\my_module\Controller;
use Drupal\Core\StringTranslation\StringTranslationTrait;
class BlogPageController {
use StringTranslationTrait;
public function __invoke(): array {
return [
'#markup' => $this->t('Welcome to my blog!'),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Drupal\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class BlogPageTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = [
'node',
'my_module',
];
/** @test */
public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text() {
$this->drupalGet('/blog');
$session = $this->assertSession();
$session->statusCodeEquals(Response::HTTP_OK);
$session->responseContains('<h1>Blog</h1>');
$session->pageTextContains('Welcome to my blog!');
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class MyModuleTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
/** @test */
public function the_front_page_loads_for_anonymous_users() {
$this->drupalGet('<front>');
$this->assertResponse(Response::HTTP_OK);
}
}

View file

@ -0,0 +1,5 @@
name: My Module
type: module
core: 8.x
core_version_requirement: ^8 || ^9
package: Custom

View file

@ -0,0 +1,7 @@
blog.page:
path: /blog
defaults:
_controller: Drupal\my_module\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content

View file

@ -0,0 +1,6 @@
services:
Drupal\my_module\Controller\BlogPageController:
autowire: true
Drupal\my_module\Repository\ArticleRepository:
autowire: true

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\my_module\Controller;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_module\Repository\ArticleRepository;
class BlogPageController {
use StringTranslationTrait;
/**
* @var \Drupal\my_module\Repository\ArticleRepository
*/
private $articleRepository;
/**
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
private $nodeViewBuilder;
public function __construct(
EntityTypeManagerInterface $entityTypeManager,
ArticleRepository $articleRepository
) {
$this->nodeViewBuilder = $entityTypeManager->getViewBuilder('node');
$this->articleRepository = $articleRepository;
}
public function __invoke(): array {
$build = [];
$articles = $this->articleRepository->getAll();
foreach ($articles as $article) {
$build[] = $this->nodeViewBuilder->view($article, 'teaser');
}
return [
'#markup' => render($build),
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\my_module\Repository;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
class ArticleRepository {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
private $nodeStorage;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->nodeStorage = $entityTypeManager->getStorage('node');
}
public function getAll(): array {
$articles = $this->nodeStorage->loadByProperties([
'status' => Node::PUBLISHED,
'type' => 'article',
]);
uasort($articles, function (NodeInterface $a, NodeInterface $b): bool {
return $a->getCreatedTime() < $b->getCreatedTime();
});
return $articles;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class BlogPageTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = [
'node',
'my_module',
];
/** @test */
public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text() {
$this->drupalGet('/blog');
$session = $this->assertSession();
$session->statusCodeEquals(Response::HTTP_OK);
$session->responseContains('<h1>Blog</h1>');
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class MyModuleTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
/** @test */
public function the_front_page_loads_for_anonymous_users() {
$this->drupalGet('<front>');
$this->assertResponse(Response::HTTP_OK);
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\Tests\my_module\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\my_module\Repository\ArticleRepository;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Traits\NodeCreationTrait;
class ArticleRepositoryTest extends EntityKernelTestBase {
use NodeCreationTrait;
public static $modules = [
'node',
'my_module',
];
protected function setUp() {
parent::setUp();
$this->installSchema('node', ['node_access']);
$this->installConfig([
'filter',
]);
}
/** @test */
public function nodes_that_are_not_articles_are_not_returned() {
$this->createNode(['type' => 'article'])->save();
$this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
$this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
$this->assertCount(5, Node::loadMultiple());
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
}
/** @test */
public function only_published_articles_are_returned() {
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
}
/** @test */
public function nodes_are_ordered_by_date_and_returned_newest_first() {
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]);
$repository = $this->container->get(ArticleRepository::class);
$nodes = $repository->getAll();
$nodeIds = array_keys($nodes);
$this->assertSame([3, 1, 2, 5, 4], $nodeIds);
}
}

View file

@ -0,0 +1,5 @@
name: My Module
type: module
core: 8.x
core_version_requirement: ^8 || ^9
package: Custom

View file

@ -0,0 +1,7 @@
blog.page:
path: /blog
defaults:
_controller: Drupal\my_module\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content

View file

@ -0,0 +1,6 @@
services:
Drupal\my_module\Controller\BlogPageController:
autowire: true
Drupal\my_module\Repository\ArticleRepository:
autowire: true

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\my_module\Controller;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_module\Repository\ArticleRepository;
class BlogPageController {
use StringTranslationTrait;
/**
* @var \Drupal\my_module\Repository\ArticleRepository
*/
private $articleRepository;
/**
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
private $nodeViewBuilder;
public function __construct(
EntityTypeManagerInterface $entityTypeManager,
ArticleRepository $articleRepository
) {
$this->nodeViewBuilder = $entityTypeManager->getViewBuilder('node');
$this->articleRepository = $articleRepository;
}
public function __invoke(): array {
$build = [];
$articles = $this->articleRepository->getAll();
foreach ($articles as $article) {
$build[] = $this->nodeViewBuilder->view($article, 'teaser');
}
return [
'#markup' => render($build),
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\my_module\Repository;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
class ArticleRepository {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
private $nodeStorage;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->nodeStorage = $entityTypeManager->getStorage('node');
}
public function getAll(): array {
$articles = $this->nodeStorage->loadByProperties([
'status' => Node::PUBLISHED,
'type' => 'article',
]);
uasort($articles, function (NodeInterface $a, NodeInterface $b): bool {
return $a->getCreatedTime() < $b->getCreatedTime();
});
return $articles;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\my_module\Wrapper;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\node\NodeInterface;
class ArticleWrapper {
private $article;
public function __construct(TimeInterface $time, NodeInterface $node) {
$this->verifyNodeType($node);
$this->time = $time;
$this->article = $node;
}
public function getOriginal(): NodeInterface {
return $this->article;
}
private function verifyNodeType(NodeInterface $node): void {
if ($node->bundle() != 'article') {
throw new \InvalidArgumentException(sprintf(
'%s is not an article',
$node->bundle()
));
}
}
public function isPublishable(): bool {
$created = $this->article->getCreatedTime();
$difference = $this->time->getRequestTime() - $created;
return $difference >= 60 * 60 * 24 * 3;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class BlogPageTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = [
'node',
'my_module',
];
/** @test */
public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text() {
$this->drupalGet('/blog');
$session = $this->assertSession();
$session->statusCodeEquals(Response::HTTP_OK);
$session->responseContains('<h1>Blog</h1>');
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class MyModuleTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
/** @test */
public function the_front_page_loads_for_anonymous_users() {
$this->drupalGet('<front>');
$this->assertResponse(Response::HTTP_OK);
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\Tests\my_module\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\my_module\Repository\ArticleRepository;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Traits\NodeCreationTrait;
class ArticleRepositoryTest extends EntityKernelTestBase {
use NodeCreationTrait;
public static $modules = [
'node',
'my_module',
];
protected function setUp() {
parent::setUp();
$this->installSchema('node', ['node_access']);
$this->installConfig([
'filter',
]);
}
/** @test */
public function nodes_that_are_not_articles_are_not_returned() {
$this->createNode(['type' => 'article'])->save();
$this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
$this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
$this->assertCount(5, Node::loadMultiple());
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
}
/** @test */
public function only_published_articles_are_returned() {
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
}
/** @test */
public function nodes_are_ordered_by_date_and_returned_newest_first() {
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]);
$repository = $this->container->get(ArticleRepository::class);
$nodes = $repository->getAll();
$nodeIds = array_keys($nodes);
$this->assertSame([3, 1, 2, 5, 4], $nodeIds);
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Drupal\Tests\my_module\Unit\Wrapper;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\my_module\Wrapper\ArticleWrapper;
use Drupal\node\NodeInterface;
use Drupal\Tests\UnitTestCase;
class ArticleWrapperTest extends UnitTestCase {
private $time;
protected function setUp() {
$this->time = $this->createMock(TimeInterface::class);
}
/** @test */
public function it_returns_the_article() {
$article = $this->createMock(NodeInterface::class);
$article->method('id')->willReturn(5);
$article->method('bundle')->willReturn('article');
$articleWrapper = new ArticleWrapper($this->time, $article);
$this->assertInstanceOf(NodeInterface::class, $articleWrapper->getOriginal());
$this->assertSame(5, $articleWrapper->getOriginal()->id());
$this->assertSame('article', $articleWrapper->getOriginal()->bundle());
}
/** @test */
public function it_throws_an_exception_if_the_node_is_not_an_article() {
$this->expectException(\InvalidArgumentException::class);
$page = $this->createMock(NodeInterface::class);
$page->method('bundle')->willReturn('page');
new ArticleWrapper($this->time, $page);
}
/**
* @test
* @dataProvider articleCreatedDateProvider
*/
public function articles_created_less_than_3_days_ago_are_not_publishable(
string $offset,
bool $expected
) {
$this->time->method('getRequestTime')->willReturn(
(new \DateTime())->getTimestamp()
);
$article = $this->createMock(NodeInterface::class);
$article->method('bundle')->willReturn('article');
$article->method('getCreatedTime')->willReturn(
(new \DateTime())->modify($offset)->getTimestamp()
);
$articleWrapper = new ArticleWrapper($this->time, $article);
$this->assertSame($expected, $articleWrapper->isPublishable());
}
public function articleCreatedDateProvider() {
return [
['-1 day', FALSE],
['-2 days 59 minutes', FALSE],
['-3 days', TRUE],
['-1 week', TRUE],
];
}
}