kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add low level timeline entry filtering capabilities
rodzic
f91080856f
commit
9e95427b9f
|
@ -0,0 +1,34 @@
|
|||
enum ComparisonType {
|
||||
containsCaseSensitive,
|
||||
containsCaseInsensitive,
|
||||
equals,
|
||||
equalsIgnoreCase,
|
||||
;
|
||||
|
||||
factory ComparisonType.parse(String? value) {
|
||||
return ComparisonType.values.firstWhere(
|
||||
(v) => v.name == value,
|
||||
orElse: () => equals,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StringFilter {
|
||||
final String filterString;
|
||||
final ComparisonType type;
|
||||
|
||||
const StringFilter({
|
||||
required this.filterString,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'filterString': filterString,
|
||||
'type': type,
|
||||
};
|
||||
|
||||
factory StringFilter.fromJson(Map<String, dynamic> json) => StringFilter(
|
||||
filterString: json['filterString'],
|
||||
type: ComparisonType.parse(json['type']),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import '../connection.dart';
|
||||
import 'string_filter.dart';
|
||||
|
||||
enum TimelineEntryFilterAction {
|
||||
hide,
|
||||
warn,
|
||||
;
|
||||
|
||||
factory TimelineEntryFilterAction.parse(String? value) {
|
||||
return TimelineEntryFilterAction.values.firstWhere(
|
||||
(v) => v.name == value,
|
||||
orElse: () => warn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineEntryFilter {
|
||||
final TimelineEntryFilterAction action;
|
||||
final String name;
|
||||
final List<StringFilter> authorFilters;
|
||||
final List<StringFilter> contentFilters;
|
||||
final List<StringFilter> hashtagFilters;
|
||||
|
||||
const TimelineEntryFilter({
|
||||
required this.action,
|
||||
required this.name,
|
||||
required this.authorFilters,
|
||||
required this.contentFilters,
|
||||
required this.hashtagFilters,
|
||||
});
|
||||
|
||||
factory TimelineEntryFilter.create({
|
||||
required TimelineEntryFilterAction action,
|
||||
required String name,
|
||||
List<Connection> authors = const [],
|
||||
List<String> keywords = const [],
|
||||
List<String> hashtags = const [],
|
||||
}) {
|
||||
return TimelineEntryFilter(
|
||||
action: action,
|
||||
name: name,
|
||||
authorFilters: authors
|
||||
.map((a) =>
|
||||
StringFilter(filterString: a.id, type: ComparisonType.equals))
|
||||
.toList(),
|
||||
contentFilters: keywords
|
||||
.map((k) => StringFilter(
|
||||
filterString: k, type: ComparisonType.containsCaseInsensitive))
|
||||
.toList(),
|
||||
hashtagFilters: hashtags
|
||||
.map((h) => StringFilter(
|
||||
filterString: h, type: ComparisonType.equalsIgnoreCase))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'action': action.name,
|
||||
'name': name,
|
||||
'authorFilters': authorFilters.map((f) => f.toJson()),
|
||||
'contentFilters': contentFilters.map((f) => f.toJson()),
|
||||
'hashtagFilters': hashtagFilters.map((f) => f.toJson()),
|
||||
};
|
||||
|
||||
factory TimelineEntryFilter.fromJson(Map<String, dynamic> json) =>
|
||||
TimelineEntryFilter(
|
||||
action: TimelineEntryFilterAction.parse(json['action']),
|
||||
name: json['name'],
|
||||
authorFilters: (json['authorFilters'] as List<dynamic>)
|
||||
.map((json) => StringFilter.fromJson(json))
|
||||
.toList(),
|
||||
contentFilters: (json['contentFilters'] as List<dynamic>)
|
||||
.map((json) => StringFilter.fromJson(json))
|
||||
.toList(),
|
||||
hashtagFilters: (json['hashtagFilters'] as List<dynamic>)
|
||||
.map((json) => StringFilter.fromJson(json))
|
||||
.toList(),
|
||||
);
|
||||
}
|
|
@ -48,6 +48,8 @@ class TimelineEntry {
|
|||
|
||||
final bool isFavorited;
|
||||
|
||||
final List<String> tags;
|
||||
|
||||
final List<LinkData> links;
|
||||
|
||||
final List<Connection> likes;
|
||||
|
@ -81,6 +83,7 @@ class TimelineEntry {
|
|||
this.externalLink = '',
|
||||
this.locationData = const LocationData(),
|
||||
this.isFavorited = false,
|
||||
this.tags = const [],
|
||||
this.links = const [],
|
||||
this.likes = const [],
|
||||
this.dislikes = const [],
|
||||
|
@ -112,6 +115,7 @@ class TimelineEntry {
|
|||
reshareAuthorId = 'Random parent author id ${randomId()}',
|
||||
locationData = LocationData.randomBuilt(),
|
||||
isFavorited = DateTime.now().second ~/ 2 == 0 ? true : false,
|
||||
tags = [],
|
||||
links = [],
|
||||
likes = [],
|
||||
dislikes = [],
|
||||
|
@ -140,6 +144,7 @@ class TimelineEntry {
|
|||
String? reshareAuthorId,
|
||||
LocationData? locationData,
|
||||
bool? isFavorited,
|
||||
List<String>? tags,
|
||||
List<LinkData>? links,
|
||||
List<Connection>? likes,
|
||||
List<Connection>? dislikes,
|
||||
|
@ -170,6 +175,7 @@ class TimelineEntry {
|
|||
reshareAuthorId: parentAuthorId ?? this.reshareAuthorId,
|
||||
locationData: locationData ?? this.locationData,
|
||||
isFavorited: isFavorited ?? this.isFavorited,
|
||||
tags: tags ?? this.tags,
|
||||
links: links ?? this.links,
|
||||
likes: likes ?? this.likes,
|
||||
dislikes: dislikes ?? this.dislikes,
|
||||
|
@ -213,6 +219,7 @@ class TimelineEntry {
|
|||
externalLink == other.externalLink &&
|
||||
locationData == other.locationData &&
|
||||
isFavorited == other.isFavorited &&
|
||||
tags == other.tags &&
|
||||
links == other.links &&
|
||||
likes == other.likes &&
|
||||
dislikes == other.dislikes &&
|
||||
|
@ -241,6 +248,7 @@ class TimelineEntry {
|
|||
externalLink.hashCode ^
|
||||
locationData.hashCode ^
|
||||
isFavorited.hashCode ^
|
||||
tags.hashCode ^
|
||||
links.hashCode ^
|
||||
likes.hashCode ^
|
||||
dislikes.hashCode ^
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import '../models/filters/string_filter.dart';
|
||||
import '../models/filters/timeline_entry_filter.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
|
||||
class FilterResult {
|
||||
final TimelineEntryFilterAction action;
|
||||
final bool isFiltered;
|
||||
|
||||
const FilterResult(this.isFiltered, this.action);
|
||||
|
||||
String toActionString() {
|
||||
return isFiltered ? action.name : 'show';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is FilterResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
action == other.action &&
|
||||
isFiltered == other.isFiltered;
|
||||
|
||||
@override
|
||||
int get hashCode => action.hashCode ^ isFiltered.hashCode;
|
||||
}
|
||||
|
||||
FilterResult runFilters(
|
||||
TimelineEntry entry,
|
||||
List<TimelineEntryFilter> filters,
|
||||
) {
|
||||
var isFiltered = false;
|
||||
var action = TimelineEntryFilterAction.warn;
|
||||
for (final filter in filters) {
|
||||
if (filter.isFiltered(entry)) {
|
||||
isFiltered = true;
|
||||
if (filter.action == TimelineEntryFilterAction.hide) {
|
||||
action = TimelineEntryFilterAction.hide;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FilterResult(isFiltered, action);
|
||||
}
|
||||
|
||||
extension StringFilterOps on StringFilter {
|
||||
bool isFiltered(String value) {
|
||||
switch (type) {
|
||||
case ComparisonType.containsCaseSensitive:
|
||||
return value.contains(filterString);
|
||||
case ComparisonType.containsCaseInsensitive:
|
||||
return value.toLowerCase().contains(filterString.toLowerCase());
|
||||
case ComparisonType.equals:
|
||||
return value == filterString;
|
||||
case ComparisonType.equalsIgnoreCase:
|
||||
return value.toLowerCase() == filterString.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineEntryFilterOps on TimelineEntryFilter {
|
||||
bool isFiltered(TimelineEntry entry) {
|
||||
if (authorFilters.isEmpty &&
|
||||
hashtagFilters.isEmpty &&
|
||||
contentFilters.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var authorFiltered = authorFilters.isEmpty ? true : false;
|
||||
for (final filter in authorFilters) {
|
||||
if (filter.isFiltered(entry.authorId)) {
|
||||
authorFiltered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var hashtagFiltered = hashtagFilters.isEmpty ? true : false;
|
||||
for (final filter in hashtagFilters) {
|
||||
for (final tag in entry.tags) {
|
||||
if (filter.isFiltered(tag)) {
|
||||
hashtagFiltered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contentFiltered = contentFilters.isEmpty ? true : false;
|
||||
for (final filter in contentFilters) {
|
||||
if (filter.isFiltered(entry.body)) {
|
||||
contentFiltered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return authorFiltered && hashtagFiltered && contentFiltered;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relatica/models/connection.dart';
|
||||
import 'package:relatica/models/filters/string_filter.dart';
|
||||
import 'package:relatica/models/filters/timeline_entry_filter.dart';
|
||||
import 'package:relatica/models/timeline_entry.dart';
|
||||
import 'package:relatica/utils/filter_runner.dart';
|
||||
|
||||
void main() {
|
||||
final entries = [
|
||||
TimelineEntry(body: 'Hello world', authorId: '1', tags: ['greeting']),
|
||||
TimelineEntry(body: 'Goodbye', authorId: '1', tags: ['SendOff']),
|
||||
TimelineEntry(body: 'Lorem ipsum', authorId: '1', tags: ['latin']),
|
||||
TimelineEntry(body: 'Hello world', authorId: '2', tags: ['greeting']),
|
||||
TimelineEntry(body: 'Goodbye', authorId: '2', tags: ['SendOff']),
|
||||
TimelineEntry(body: 'Lorem ipsum', authorId: '2', tags: ['LATIN']),
|
||||
TimelineEntry(body: 'Chao', authorId: '2', tags: ['sendoff']),
|
||||
];
|
||||
|
||||
group('Test StringFilter', () {
|
||||
test('Test equals', () {
|
||||
const filter = StringFilter(
|
||||
filterString: 'hello',
|
||||
type: ComparisonType.equals,
|
||||
);
|
||||
expect(filter.isFiltered('hello'), equals(true));
|
||||
expect(filter.isFiltered('Hello'), equals(false));
|
||||
expect(filter.isFiltered('hello!'), equals(false));
|
||||
expect(filter.isFiltered('help'), equals(false));
|
||||
});
|
||||
test('Test equalsIgnoreCase', () {
|
||||
const filter = StringFilter(
|
||||
filterString: 'hello',
|
||||
type: ComparisonType.equalsIgnoreCase,
|
||||
);
|
||||
expect(filter.isFiltered('hello'), equals(true));
|
||||
expect(filter.isFiltered('Hello'), equals(true));
|
||||
expect(filter.isFiltered('hello!'), equals(false));
|
||||
expect(filter.isFiltered('help'), equals(false));
|
||||
});
|
||||
test('Test containsCaseSensitive', () {
|
||||
const filter = StringFilter(
|
||||
filterString: 'hello',
|
||||
type: ComparisonType.containsCaseSensitive,
|
||||
);
|
||||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('Hello World'), equals(false));
|
||||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('help'), equals(false));
|
||||
});
|
||||
test('Test containsCaseInsensitive', () {
|
||||
const filter = StringFilter(
|
||||
filterString: 'hello',
|
||||
type: ComparisonType.containsCaseInsensitive,
|
||||
);
|
||||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('Hello World'), equals(true));
|
||||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('help'), equals(false));
|
||||
});
|
||||
});
|
||||
|
||||
group('Test TimelineEntryFilter', () {
|
||||
test('Empty Filter', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
);
|
||||
final expected = [false, false, false, false, false, false, false];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
test('Test Content Filter', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
keywords: ['hello', 'good'],
|
||||
);
|
||||
final expected = [true, true, false, true, true, false, false];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test Author Filter', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
authors: [Connection(id: '2')],
|
||||
);
|
||||
final expected = [false, false, false, true, true, true, true];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test Tag Filter', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
hashtags: ['latin', 'greet'],
|
||||
);
|
||||
final expected = [false, false, true, false, false, true, false];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test Author plus content', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
authors: [Connection(id: '2')],
|
||||
keywords: ['good'],
|
||||
);
|
||||
final expected = [false, false, false, false, true, false, false];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test Author plus tag', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
authors: [Connection(id: '2')],
|
||||
hashtags: ['latin', 'greet'],
|
||||
);
|
||||
final expected = [false, false, false, false, false, true, false];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test Content plus tag', () {
|
||||
final filter = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
keywords: ['chao'],
|
||||
hashtags: ['SENDOFF'],
|
||||
);
|
||||
final expected = [false, false, false, false, false, false, true];
|
||||
final actual = entries.map((e) => filter.isFiltered(e)).toList();
|
||||
expect(actual, equals(expected));
|
||||
});
|
||||
|
||||
test('Test all', () {
|
||||
final filter1 = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
authors: [Connection(id: '2'), Connection(id: '3')],
|
||||
keywords: ['chao'],
|
||||
hashtags: ['SENDOFF'],
|
||||
);
|
||||
final expected1 = [false, false, false, false, false, false, true];
|
||||
final actual1 = entries.map((e) => filter1.isFiltered(e)).toList();
|
||||
expect(actual1, equals(expected1));
|
||||
|
||||
final filter2 = TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'filter',
|
||||
authors: [Connection(id: '1'), Connection(id: '3')],
|
||||
keywords: ['chao'],
|
||||
hashtags: ['SENDOFF'],
|
||||
);
|
||||
final expected2 = [false, false, false, false, false, false, false];
|
||||
final actual2 = entries.map((e) => filter2.isFiltered(e)).toList();
|
||||
expect(actual2, equals(expected2));
|
||||
});
|
||||
});
|
||||
|
||||
test('Test runner', () {
|
||||
final runnerEntries = [
|
||||
...entries,
|
||||
TimelineEntry(body: 'User 3 Post #1', authorId: '3'),
|
||||
];
|
||||
final filters = [
|
||||
TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.warn,
|
||||
name: 'send-off-hide-filter',
|
||||
hashtags: ['SENDOFF'],
|
||||
),
|
||||
TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'author-3-hide',
|
||||
authors: [Connection(id: '3')],
|
||||
),
|
||||
TimelineEntryFilter.create(
|
||||
action: TimelineEntryFilterAction.hide,
|
||||
name: 'send-off-hide-filter',
|
||||
authors: [Connection(id: '1')],
|
||||
hashtags: ['SENDOFF'],
|
||||
)
|
||||
];
|
||||
|
||||
final expected = [
|
||||
'show',
|
||||
'hide',
|
||||
'show',
|
||||
'show',
|
||||
'warn',
|
||||
'show',
|
||||
'warn',
|
||||
'hide',
|
||||
];
|
||||
final actual = runnerEntries
|
||||
.map((e) => runFilters(e, filters).toActionString())
|
||||
.toList();
|
||||
expect(expected, equals(actual));
|
||||
});
|
||||
}
|
Ładowanie…
Reference in New Issue