Add ability to create a feed from the UI. Still TODO: make it not look like utter shite.
This commit is contained in:
parent
cee4f9944e
commit
0e06d5587f
@ -24,7 +24,8 @@
|
|||||||
"ext-simplexml": "*",
|
"ext-simplexml": "*",
|
||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"php": ">=8.3"
|
"php": ">=8.3",
|
||||||
|
"doctrine/migrations": "^3.8"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^10.0"
|
"phpunit/phpunit": "^10.0"
|
||||||
|
1422
composer.lock
generated
1422
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace Lewisdale\App\Controllers;
|
namespace Lewisdale\App\Controllers;
|
||||||
|
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Lewisdale\App\Models\Data\Feed;
|
||||||
|
use Lewisdale\App\Models\Data\FeedFilter;
|
||||||
|
use Lewisdale\App\Models\Data\FilterTarget;
|
||||||
|
use Lewisdale\App\Models\Data\FilterType;
|
||||||
|
use Lewisdale\App\Models\Repositories\FeedFilterRepository;
|
||||||
use Lewisdale\App\Models\Repositories\FeedRepository;
|
use Lewisdale\App\Models\Repositories\FeedRepository;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@ -16,7 +21,8 @@ class FeedController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Twig $view,
|
private readonly Twig $view,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly FeedRepository $feedRepository
|
private readonly FeedRepository $feedRepository,
|
||||||
|
private readonly FeedFilterRepository $feedFilterRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function get(ServerRequestInterface $request, ResponseInterface $response)
|
public function get(ServerRequestInterface $request, ResponseInterface $response)
|
||||||
@ -50,9 +56,47 @@ class FeedController
|
|||||||
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
{
|
{
|
||||||
$this->logger->info("FeedController::create() called");
|
$this->logger->info("FeedController::create() called");
|
||||||
|
|
||||||
|
$feed = $this->feedRepository->find($request->getAttribute('id'));
|
||||||
|
|
||||||
|
return $this->view->render($response, 'feed/edit.twig.html', ['FilterTypes' => FilterType::cases(), 'FilterTargets' => FilterTarget::cases(), 'feed' => $feed ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->logger->info("FeedController::save() called");
|
||||||
|
|
||||||
$body = $request->getParsedBody();
|
$body = $request->getParsedBody();
|
||||||
|
|
||||||
return $this->view->render($response, 'create.twig.html');
|
$id = $request->getAttribute('id');
|
||||||
|
|
||||||
|
$feed = $id ? $this->feedRepository->find($request->getAttribute('id')) : new Feed();
|
||||||
|
|
||||||
|
$feed->title = $body['title'];
|
||||||
|
$feed->url = $body['url'];
|
||||||
|
|
||||||
|
$this->feedRepository->save($feed);
|
||||||
|
|
||||||
|
foreach ($body['feedFilters'] as $filter) {
|
||||||
|
$this->logger->info("Creating filter", ['filter' => $filter, 'post' => $_POST]);
|
||||||
|
|
||||||
|
$f = $filter['id'] ? $this->feedFilterRepository->find($filter['id']) : new FeedFilter(
|
||||||
|
FilterTarget::from($filter['target']),
|
||||||
|
FilterType::from($filter['type']),
|
||||||
|
$filter['value'],
|
||||||
|
$feed
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($f !== null && $filter['id']) {
|
||||||
|
$f->filter = FilterType::from($filter['type']);
|
||||||
|
$f->target = FilterTarget::from($filter['target']);
|
||||||
|
$f->value = $filter['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->feedFilterRepository->save($f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->withStatus(302)->withHeader('Location', "/feed");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
public function delete(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
@ -64,6 +108,6 @@ class FeedController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->feedRepository->delete($feed);
|
$this->feedRepository->delete($feed);
|
||||||
return $response->withStatus(201)->withHeader('Location', '/feed');
|
return $response->withStatus(302)->withHeader('Location', '/feed');
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -48,6 +48,10 @@ class Feed
|
|||||||
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: true)]
|
||||||
public \DateTimeInterface|null $lastFetched;
|
public \DateTimeInterface|null $lastFetched;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class, cascade: ['persist', 'remove'], inversedBy: 'feeds')]
|
||||||
|
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||||
|
public User|null $user;
|
||||||
|
|
||||||
public function fetch(): void
|
public function fetch(): void
|
||||||
{
|
{
|
||||||
if (Robots::allowed($this->url)) {
|
if (Robots::allowed($this->url)) {
|
||||||
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Lewisdale\App\Models\Data;
|
namespace Lewisdale\App\Models\Data;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Lewisdale\App\Models\Traits\AutoUpdate;
|
use Lewisdale\App\Models\Traits\AutoUpdate;
|
||||||
use Lewisdale\App\Models\View;
|
use Lewisdale\App\Models\View;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -26,6 +27,12 @@ class User
|
|||||||
#[ORM\Column(type: 'string')]
|
#[ORM\Column(type: 'string')]
|
||||||
private string $salt;
|
private string $salt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<Feed>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Feed::class, cascade: ['persist', 'remove'])]
|
||||||
|
public Collection $feeds;
|
||||||
|
|
||||||
function __construct(
|
function __construct(
|
||||||
string $password,
|
string $password,
|
||||||
string $email,
|
string $email,
|
||||||
|
@ -15,4 +15,10 @@ class FeedFilterRepository extends EntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($em, $em->getClassMetadata(FeedFilter::class));
|
parent::__construct($em, $em->getClassMetadata(FeedFilter::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function save(FeedFilter $filter): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist($filter);
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ class LoginMiddleware implements MiddlewareInterface {
|
|||||||
private readonly UserRepository $users,
|
private readonly UserRepository $users,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private function redirectToLogin() {
|
private function redirectToLogin(): ResponseInterface {
|
||||||
$response = new Response(302);
|
$response = new Response(302);
|
||||||
return $response->withHeader('Location', '/account/login');
|
return $response->withHeader('Location', '/account/login');
|
||||||
}
|
}
|
||||||
|
17
src/app.php
17
src/app.php
@ -3,6 +3,8 @@
|
|||||||
use Lewisdale\App\Controllers\FeedController;
|
use Lewisdale\App\Controllers\FeedController;
|
||||||
use Lewisdale\App\Controllers\HomeController;
|
use Lewisdale\App\Controllers\HomeController;
|
||||||
use Lewisdale\App\Controllers\LoginController;
|
use Lewisdale\App\Controllers\LoginController;
|
||||||
|
use Lewisdale\App\Session\LoginMiddleware;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
use Slim\Views\TwigMiddleware;
|
use Slim\Views\TwigMiddleware;
|
||||||
|
|
||||||
ini_set('user_agent', 'Baleen/1.0 (https://baleen.lewisdale.dev)');
|
ini_set('user_agent', 'Baleen/1.0 (https://baleen.lewisdale.dev)');
|
||||||
@ -24,13 +26,20 @@ $app->add('csrf');
|
|||||||
|
|
||||||
$app->get("/", [HomeController::class, 'get']);
|
$app->get("/", [HomeController::class, 'get']);
|
||||||
|
|
||||||
$app->group('/feed', function (\Slim\Routing\RouteCollectorProxy $group) use ($app) {
|
$app->group('/feed', function (RouteCollectorProxy $group) use ($app) {
|
||||||
$group->get('[/]', [FeedController::class, 'get'])->add(\Lewisdale\App\Session\LoginMiddleware::class);
|
$group->get('[/]', [FeedController::class, 'get'])->add(LoginMiddleware::class);
|
||||||
|
$group->post('[/]', [FeedController::class, 'save'])->add(LoginMiddleware::class);
|
||||||
|
|
||||||
|
$group->get('/new', [FeedController::class, 'create'])->add(LoginMiddleware::class);
|
||||||
|
|
||||||
$group->get('/{id}', [FeedController::class, 'get_feed']);
|
$group->get('/{id}', [FeedController::class, 'get_feed']);
|
||||||
$group->get('/{id}/delete', [FeedController::class, 'delete'])->add(\Lewisdale\App\Session\LoginMiddleware::class);
|
$group->post('/{id}', [FeedController::class, 'save'])->add(LoginMiddleware::class);
|
||||||
|
|
||||||
|
$group->get('/{id}/delete', [FeedController::class, 'delete'])->add(LoginMiddleware::class);
|
||||||
|
$group->get('/{id}/edit', [FeedController::class, 'create'])->add(LoginMiddleware::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->group('/account', function (\Slim\Routing\RouteCollectorProxy $group) use ($app, $container) {
|
$app->group('/account', function (RouteCollectorProxy $group) use ($app, $container) {
|
||||||
$group->get('/login', [LoginController::class, 'index']);
|
$group->get('/login', [LoginController::class, 'index']);
|
||||||
$group->post('/login', [LoginController::class, 'login']);
|
$group->post('/login', [LoginController::class, 'login']);
|
||||||
});
|
});
|
||||||
|
23
views/_includes/filterBlock.twig.html
Normal file
23
views/_includes/filterBlock.twig.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% macro filterBlock(ctx, idx, type, target, value, id) %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Filter</legend>
|
||||||
|
<input type="hidden" name="feedFilters[{{ idx }}][id]" value="{{ id }}"/>
|
||||||
|
|
||||||
|
<label for="target">Filter target:</label>
|
||||||
|
<select name="feedFilters[{{ idx }}][target]" id="target">
|
||||||
|
{% for filterTarget in ctx.FilterTargets %}
|
||||||
|
<option value="{{ filterTarget.value }}" {% if filterTarget.value is same as(target) %}selected{% endif %}>{{ filterTarget.name | capitalize }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="type">Filter type: </label>
|
||||||
|
<select name="feedFilters[{{ idx }}][type]" id="type">
|
||||||
|
{% for filterType in ctx.FilterTypes %}
|
||||||
|
<option value="{{ filterType.value }}" {% if filterType is same as(type) %}selected{% endif %}>{{ filterType.name | capitalize }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="value">Filter value:</label>
|
||||||
|
<input type="text" name="feedFilters[{{ idx }}][value]" id="value" value="{{ value }}" />
|
||||||
|
</fieldset>
|
||||||
|
{% endmacro %}
|
37
views/feed/edit.twig.html
Normal file
37
views/feed/edit.twig.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% from "_includes/filterBlock.twig.html" import filterBlock %}
|
||||||
|
|
||||||
|
<h1>Create feed</h1>
|
||||||
|
|
||||||
|
<form method="POST" action="/feed/{{ feed.id }}">
|
||||||
|
{{ csrf() | raw }}
|
||||||
|
|
||||||
|
<input type="hidden" name="id" value="{{ feed.id }}"/>
|
||||||
|
|
||||||
|
<label for="url">
|
||||||
|
Feed URL
|
||||||
|
</label>
|
||||||
|
<input type="text" inputmode="url" name="url" value="{{ feed.url }}" id="url"/>
|
||||||
|
|
||||||
|
<label for="title">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<input type="text" inputmode="title" name="title" value="{{ feed.title }}" id="title"/>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Filters</legend>
|
||||||
|
|
||||||
|
{% for filter in feed.feedFilters %}
|
||||||
|
{{ filterBlock(_context, loop.index0, filter.filter, filter.target, filter.value, filter.id) }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{{ filterBlock(_context, (feed.feedFilters | length)) }}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button type="submit">Save feed</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
</script>
|
@ -9,7 +9,11 @@
|
|||||||
{% for feed in feeds %}
|
{% for feed in feeds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ feed.title }}</td>
|
<td>{{ feed.title }}</td>
|
||||||
<td><a href="/feed/{{ feed.id }}">Link to feed</a><a href="/feed/{{ feed.id }}/delete">Delete</a></td>
|
<td>
|
||||||
|
<a href="/feed/{{ feed.id }}">Link to feed</a>
|
||||||
|
<a href="/feed/{{ feed.id }}/edit">Edit feed</a>
|
||||||
|
<a href="/feed/{{ feed.id }}/delete">Delete</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
Loading…
Reference in New Issue
Block a user