Add ability to create a feed from the UI. Still TODO: make it not look like utter shite.

This commit is contained in:
Lewis Dale 2025-01-02 11:41:59 +00:00
parent cee4f9944e
commit 0e06d5587f
11 changed files with 946 additions and 633 deletions

View File

@ -24,7 +24,8 @@
"ext-simplexml": "*",
"ext-curl": "*",
"ext-dom": "*",
"php": ">=8.3"
"php": ">=8.3",
"doctrine/migrations": "^3.8"
},
"require-dev": {
"phpunit/phpunit": "^10.0"

1422
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,11 @@ declare(strict_types=1);
namespace Lewisdale\App\Controllers;
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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@ -16,7 +21,8 @@ class FeedController
public function __construct(
private readonly Twig $view,
private readonly LoggerInterface $logger,
private readonly FeedRepository $feedRepository
private readonly FeedRepository $feedRepository,
private readonly FeedFilterRepository $feedFilterRepository,
) {}
public function get(ServerRequestInterface $request, ResponseInterface $response)
@ -50,9 +56,47 @@ class FeedController
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$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();
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
@ -64,6 +108,6 @@ class FeedController
}
$this->feedRepository->delete($feed);
return $response->withStatus(201)->withHeader('Location', '/feed');
return $response->withStatus(302)->withHeader('Location', '/feed');
}
}

View File

@ -48,6 +48,10 @@ class Feed
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: true)]
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
{
if (Robots::allowed($this->url)) {

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Lewisdale\App\Models\Data;
use Doctrine\Common\Collections\Collection;
use Lewisdale\App\Models\Traits\AutoUpdate;
use Lewisdale\App\Models\View;
use Doctrine\ORM\Mapping as ORM;
@ -26,6 +27,12 @@ class User
#[ORM\Column(type: 'string')]
private string $salt;
/**
* @var Collection<Feed>
*/
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Feed::class, cascade: ['persist', 'remove'])]
public Collection $feeds;
function __construct(
string $password,
string $email,

View File

@ -15,4 +15,10 @@ class FeedFilterRepository extends EntityRepository
{
parent::__construct($em, $em->getClassMetadata(FeedFilter::class));
}
public function save(FeedFilter $filter): void
{
$this->getEntityManager()->persist($filter);
$this->getEntityManager()->flush();
}
}

View File

@ -15,7 +15,7 @@ class LoginMiddleware implements MiddlewareInterface {
private readonly UserRepository $users,
) {}
private function redirectToLogin() {
private function redirectToLogin(): ResponseInterface {
$response = new Response(302);
return $response->withHeader('Location', '/account/login');
}

View File

@ -3,6 +3,8 @@
use Lewisdale\App\Controllers\FeedController;
use Lewisdale\App\Controllers\HomeController;
use Lewisdale\App\Controllers\LoginController;
use Lewisdale\App\Session\LoginMiddleware;
use Slim\Routing\RouteCollectorProxy;
use Slim\Views\TwigMiddleware;
ini_set('user_agent', 'Baleen/1.0 (https://baleen.lewisdale.dev)');
@ -24,13 +26,20 @@ $app->add('csrf');
$app->get("/", [HomeController::class, 'get']);
$app->group('/feed', function (\Slim\Routing\RouteCollectorProxy $group) use ($app) {
$group->get('[/]', [FeedController::class, 'get'])->add(\Lewisdale\App\Session\LoginMiddleware::class);
$app->group('/feed', function (RouteCollectorProxy $group) use ($app) {
$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}/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->post('/login', [LoginController::class, 'login']);
});

View 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
View 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>

View File

@ -9,7 +9,11 @@
{% for feed in feeds %}
<tr>
<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>
{% endfor %}
</tbody>