Search in attachment file names and attachments notes.

This commit is contained in:
James Cole
2022-03-28 07:54:52 +02:00
parent 2be7813a67
commit e5a08d2cf1
10 changed files with 555 additions and 209 deletions

View File

@@ -0,0 +1,284 @@
<?php
namespace FireflyIII\Helpers\Collector\Extensions;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Log;
/**
* Trait AttachmentCollection
*/
trait AttachmentCollection
{
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameContains(string $name): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($name): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
$result = str_contains(strtolower($attachment['filename']), strtolower($name)) || str_contains(strtolower($attachment['title']), strtolower($name));
if (true === $result) {
return true;
}
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasAttachments(): GroupCollectorInterface
{
Log::debug('Add filter on attachment ID.');
$this->joinAttachmentTables();
$this->query->whereNotNull('attachments.attachable_id');
return $this;
}
/**
* Join table to get attachment information.
*/
private function joinAttachmentTables(): void
{
if (false === $this->hasJoinedAttTables) {
// join some extra tables:
$this->hasJoinedAttTables = true;
$this->query->leftJoin('attachments', 'attachments.attachable_id', '=', 'transaction_journals.id')
->where(
static function (EloquentBuilder $q1) {
$q1->where('attachments.attachable_type', TransactionJournal::class);
//$q1->where('attachments.uploaded', true);
$q1->orWhereNull('attachments.attachable_type');
}
);
}
}
/**
* @inheritDoc
*/
public function withAttachmentInformation(): GroupCollectorInterface
{
$this->fields[] = 'attachments.id as attachment_id';
$this->fields[] = 'attachments.filename as attachment_filename';
$this->fields[] = 'attachments.title as attachment_title';
$this->fields[] = 'attachments.uploaded as attachment_uploaded';
$this->joinAttachmentTables();
return $this;
}
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameEnds(string $name): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($name): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
$result = str_ends_with(strtolower($attachment['filename']), strtolower($name)) || str_ends_with(strtolower($attachment['title']), strtolower($name));
if (true === $result) {
return true;
}
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameIs(string $name): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($name): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
$result = $attachment['filename'] === $name || $attachment['title'] === $name;
if (true === $result) {
return true;
}
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameStarts(string $name): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($name): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
$result = str_starts_with(strtolower($attachment['filename']), strtolower($name)) || str_starts_with(strtolower($attachment['title']), strtolower($name));
if (true === $result) {
return true;
}
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesAre(string $value): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($value): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
/** @var Attachment $object */
$object = auth()->user()->attachments()->find($attachment['id']);
$notes = (string) $object->notes()?->first()?->text;
return $notes !== '' && $notes === $value;
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesContains(string $value): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($value): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
/** @var Attachment $object */
$object = auth()->user()->attachments()->find($attachment['id']);
$notes = (string) $object->notes()?->first()?->text;
return $notes !== '' && str_contains(strtolower($notes), strtolower($value));
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesEnds(string $value): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($value): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
/** @var Attachment $object */
$object = auth()->user()->attachments()->find($attachment['id']);
$notes = (string) $object->notes()?->first()?->text;
return $notes !== '' && str_ends_with(strtolower($notes), strtolower($value));
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesStarts(string $value): GroupCollectorInterface
{
$this->hasAttachments();
$this->withAttachmentInformation();
$filter = function (int $index, array $object) use ($value): bool {
/** @var array $transaction */
foreach ($object['transactions'] as $transaction) {
/** @var array $attachment */
foreach ($transaction['attachments'] as $attachment) {
/** @var Attachment $object */
$object = auth()->user()->attachments()->find($attachment['id']);
$notes = (string) $object->notes()?->first()?->text;
return $notes !== '' && str_starts_with(strtolower($notes), strtolower($value));
}
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasNoAttachments(): GroupCollectorInterface
{
Log::debug('Add filter on no attachments.');
$this->joinAttachmentTables();
$this->query->whereNull('attachments.attachable_id');
return $this;
}
}

View File

@@ -32,7 +32,6 @@ use FireflyIII\Models\Tag;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Log;
/**
* Trait MetaCollection

View File

@@ -85,6 +85,18 @@ trait TimeCollection
return $this;
}
/**
* @inheritDoc
*/
public function withMetaDate(string $field): GroupCollectorInterface
{
$this->joinMetaDataTables();
$this->query->where('journal_meta.name', '=', $field);
$this->query->whereNotNull('journal_meta.data');
return $this;
}
/**
* @param string $day
* @param string $field
@@ -492,18 +504,6 @@ trait TimeCollection
return $this;
}
/**
* @inheritDoc
*/
public function withMetaDate(string $field): GroupCollectorInterface
{
$this->joinMetaDataTables();
$this->query->where('journal_meta.name', '=', $field);
$this->query->whereNotNull('journal_meta.data');
return $this;
}
/**
* @param Carbon $start
* @param Carbon $end

View File

@@ -29,6 +29,7 @@ use Closure;
use Exception;
use FireflyIII\Helpers\Collector\Extensions\AccountCollection;
use FireflyIII\Helpers\Collector\Extensions\AmountCollection;
use FireflyIII\Helpers\Collector\Extensions\AttachmentCollection;
use FireflyIII\Helpers\Collector\Extensions\CollectorProperties;
use FireflyIII\Helpers\Collector\Extensions\MetaCollection;
use FireflyIII\Helpers\Collector\Extensions\TimeCollection;
@@ -49,7 +50,7 @@ use Log;
*/
class GroupCollector implements GroupCollectorInterface
{
use CollectorProperties, AccountCollection, AmountCollection, TimeCollection, MetaCollection;
use CollectorProperties, AccountCollection, AmountCollection, TimeCollection, MetaCollection, AttachmentCollection;
/**
* Group collector constructor.
@@ -399,11 +400,18 @@ class GroupCollector implements GroupCollectorInterface
if (0 !== $attachmentId && $uploaded) {
$result['attachments'][$attachmentId] = [
'id' => $attachmentId,
'filename' => $augumentedJournal['attachment_filename'],
'title' => $augumentedJournal['attachment_title'],
];
}
}
// unset various fields:
unset($result['tag_id'], $result['meta_data'], $result['meta_name'], $result['tag_name'], $result['tag_date'], $result['tag_description'], $result['tag_latitude'], $result['tag_longitude'], $result['tag_zoom_level']);
unset($result['tag_id'], $result['meta_data'], $result['meta_name'],
$result['tag_name'], $result['tag_date'], $result['tag_description'],
$result['tag_latitude'], $result['tag_longitude'], $result['tag_zoom_level'],
$result['attachment_filename'], $result['attachment_id']
);
return $result;
}
@@ -591,52 +599,6 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasAttachments(): GroupCollectorInterface
{
Log::debug('Add filter on attachment ID.');
$this->joinAttachmentTables();
$this->query->whereNotNull('attachments.attachable_id');
return $this;
}
/**
* Join table to get attachment information.
*/
private function joinAttachmentTables(): void
{
if (false === $this->hasJoinedAttTables) {
// join some extra tables:
$this->hasJoinedAttTables = true;
$this->query->leftJoin('attachments', 'attachments.attachable_id', '=', 'transaction_journals.id')
->where(
static function (EloquentBuilder $q1) {
$q1->where('attachments.attachable_type', TransactionJournal::class);
//$q1->where('attachments.uploaded', true);
$q1->orWhereNull('attachments.attachable_type');
}
);
}
}
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasNoAttachments(): GroupCollectorInterface
{
Log::debug('Add filter on no attachments.');
$this->joinAttachmentTables();
$this->query->whereNull('attachments.attachable_id');
return $this;
}
/**
* Limit results to a specific currency, either foreign or normal one.
@@ -858,16 +820,4 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
/**
* @inheritDoc
*/
public function withAttachmentInformation(): GroupCollectorInterface
{
$this->fields[] = 'attachments.id as attachment_id';
$this->fields[] = 'attachments.uploaded as attachment_uploaded';
$this->joinAttachmentTables();
return $this;
}
}

View File

@@ -66,6 +66,54 @@ interface GroupCollectorInterface
*/
public function amountMore(string $amount): GroupCollectorInterface;
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameContains(string $name): GroupCollectorInterface;
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameEnds(string $name): GroupCollectorInterface;
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameIs(string $name): GroupCollectorInterface;
/**
* @param string $name
* @return GroupCollectorInterface
*/
public function attachmentNameStarts(string $name): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesAre(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesContains(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesEnds(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function attachmentNotesStarts(string $value): GroupCollectorInterface;
/**
* @param string $day
* @return GroupCollectorInterface

View File

@@ -40,6 +40,7 @@ use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Services\Internal\Destroy\TransactionGroupDestroyService;
use FireflyIII\Services\Internal\Update\GroupUpdateService;
use FireflyIII\Support\NullArrayObject;
@@ -100,6 +101,8 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
*/
public function getAttachments(TransactionGroup $group): array
{
$repository = app(AttachmentRepositoryInterface::class);
$repository->setUser($this->user);
$journals = $group->transactionJournals->pluck('id')->toArray();
$set = Attachment::whereIn('attachable_id', $journals)
->where('attachable_type', TransactionJournal::class)
@@ -113,6 +116,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
$result[$journalId] = $result[$journalId] ?? [];
$current = $attachment->toArray();
$current['file_exists'] = true;
$current['notes'] = $repository->getNoteText($attachment);
$current['journal_title'] = $attachment->attachable->description; // @phpstan-ignore-line
$result[$journalId][] = $current;

View File

@@ -907,6 +907,31 @@ class OperatorQuerySearch implements SearchInterface
case 'internal_reference_ends':
$this->collector->internalReferenceEnds($value);
break;
case 'attachment_name_is':
$this->collector->attachmentNameIs($value);
break;
case 'attachment_name_contains':
$this->collector->attachmentNameContains($value);
break;
case 'attachment_name_starts':
$this->collector->attachmentNameStarts($value);
break;
case 'attachment_name_ends':
$this->collector->attachmentNameEnds($value);
break;
case 'attachment_notes_are':
$this->collector->attachmentNotesAre($value);
break;
case 'attachment_notes_contains':
$this->collector->attachmentNotesContains($value);
break;
case 'attachment_notes_starts':
$this->collector->attachmentNotesStarts($value);
break;
case 'attachment_notes_ends':
$this->collector->attachmentNotesEnds($value);
break;
}
return true;

View File

@@ -188,5 +188,20 @@ return [
'foreign_amount_max' => ['alias' => true, 'alias_for' => 'foreign_amount_less', 'needs_context' => true,],
'foreign_amount_more' => ['alias' => false, 'needs_context' => true,],
'foreign_amount_min' => ['alias' => true, 'alias_for' => 'foreign_amount_more', 'needs_context' => true,],
'attachment_name_is' => ['alias' => false, 'needs_context' => true],
'attachment' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true],
'attachment_is' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true],
'attachment_name' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true],
'attachment_name_contains' => ['alias' => false, 'needs_context' => true],
'attachment_name_starts' => ['alias' => false, 'needs_context' => true],
'attachment_name_ends' => ['alias' => false, 'needs_context' => true],
'attachment_notes' => ['alias' => true, 'alias_for' => 'attachment_notes_are', 'needs_context' => true],
'attachment_notes_are' => ['alias' => false, 'needs_context' => true],
'attachment_notes_contains' => ['alias' => false, 'needs_context' => true],
'attachment_notes_contain' => ['alias' => true, 'alias_for' => 'attachment_notes_contains', 'needs_context' => true],
'attachment_notes_starts' => ['alias' => false, 'needs_context' => true],
'attachment_notes_start' => ['alias' => true, 'alias_for' => 'attachment_notes_starts', 'needs_context' => true],
'attachment_notes_ends' => ['alias' => false, 'needs_context' => true],
'attachment_notes_end' => ['alias' => true, 'alias_for' => 'attachment_notes_ends', 'needs_context' => true],
],
];

View File

@@ -490,7 +490,14 @@ return [
'search_modifier_updated_at_on' => 'Transaction was updated on ":value"',
'search_modifier_updated_at_before' => 'Transaction was updated on or before ":value"',
'search_modifier_updated_at_after' => 'Transaction was updated on or after ":value"',
'search_modifier_attachment_name_is' => 'Any attachment\'s name is ":value"',
'search_modifier_attachment_name_contains' => 'Any attachment\'s name contains ":value"',
'search_modifier_attachment_name_starts' => 'Any attachment\'s name starts with ":value"',
'search_modifier_attachment_name_ends' => 'Any attachment\'s name ends with ":value"',
'search_modifier_attachment_notes_are' => 'Any attachment\'s notes are ":value"',
'search_modifier_attachment_notes_contains' => 'Any attachment\'s notes contain ":value"',
'search_modifier_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"',
'search_modifier_attachment_notes_ends' => 'Any attachment\'s notes end is ":value"',
'update_rule_from_query' => 'Update rule ":rule" from search query',
'create_rule_from_query' => 'Create new rule from search query',
'rule_from_search_words' => 'The rule engine has a hard time handling ":string". The suggested rule that fits your search query may give different results. Please verify the rule triggers carefully.',
@@ -826,7 +833,22 @@ return [
'rule_trigger_foreign_amount_less' => 'Foreign amount is less than ":trigger_value"',
'rule_trigger_foreign_amount_more_choice' => 'Foreign amount is more than..',
'rule_trigger_foreign_amount_more' => 'Foreign amount is more than ":trigger_value"',
'rule_trigger_attachment_name_is_choice' => 'Any attachment\'s name is..',
'rule_trigger_attachment_name_is' => 'Any attachment\'s name is ":trigger_value"',
'rule_trigger_attachment_name_contains_choice' => 'Any attachment\'s name contains..',
'rule_trigger_attachment_name_contains' => 'Any attachment\'s name contains ":trigger_value"',
'rule_trigger_attachment_name_starts_choice' => 'Any attachment\'s name starts with..',
'rule_trigger_attachment_name_starts' => 'Any attachment\'s name starts with ":trigger_value"',
'rule_trigger_attachment_name_ends_choice' => 'Any attachment\'s name ends with..',
'rule_trigger_attachment_name_ends' => 'Any attachment\'s name ends with ":trigger_value"',
'rule_trigger_attachment_notes_are_choice' => 'Any attachment\'s notes are..',
'rule_trigger_attachment_notes_are' => 'Any attachment\'s notes are ":trigger_value"',
'rule_trigger_attachment_notes_contains_choice' => 'Any attachment\'s notes contain..',
'rule_trigger_attachment_notes_contains' => 'Any attachment\'s notes contain ":trigger_value"',
'rule_trigger_attachment_notes_starts_choice' => 'Any attachment\'s notes start with..',
'rule_trigger_attachment_notes_starts' => 'Any attachment\'s notes start with ":trigger_value"',
'rule_trigger_attachment_notes_ends_choice' => 'Any attachment\'s notes end with..',
'rule_trigger_attachment_notes_ends' => 'Any attachment\'s notes end with ":trigger_value"',
// actions
'rule_action_delete_transaction_choice' => 'DELETE transaction (!)',
@@ -1668,7 +1690,6 @@ return [
'overview' => 'Overview',
'saveOnAccount' => 'Save on account',
'unknown' => 'Unknown',
'daily' => 'Daily',
'monthly' => 'Monthly',
'profile' => 'Profile',
'errors' => 'Errors',

View File

@@ -27,8 +27,8 @@
{% endif %}
</a>
({{ attachment.size|filesize }})
{% if null != attachment.notes_text and '' != attachment.notes_text %}
{{ attachment.notes_text|default('')|markdown }}
{% if null != attachment.notes and '' != attachment.notes %}
{{ attachment.notes|default('')|markdown }}
{% endif %}
{% endif %}
{% if not attachment.file_exists %}