From 689923a16f79a8bc7d21aab8c0844e5fdf135230 Mon Sep 17 00:00:00 2001 From: Gustl22 Date: Tue, 8 Oct 2024 07:58:15 +0200 Subject: [PATCH] feat: Suggest to import from API (#74) --- .../lib/l10n/app_de.arb | 14 ++ .../lib/l10n/app_en.arb | 24 ++-- .../mocks/services/network/data_manager.dart | 15 ++ .../lib/services/network/data_manager.dart | 10 ++ .../lib/services/network/remote/rest.dart | 107 +++++++-------- .../overview/organization_overview.dart | 5 +- .../view/screens/overview/shared/actions.dart | 128 ++++++++++++++---- .../overview/team_match/league_overview.dart | 3 +- .../team_match/team_match_overview.dart | 4 +- .../view/screens/overview/team_overview.dart | 3 +- .../controllers/competition_controller.dart | 6 +- .../lib/controllers/entity_controller.dart | 14 ++ .../lib/controllers/league_controller.dart | 10 +- .../controllers/organization_controller.dart | 10 +- .../lib/controllers/team_controller.dart | 7 +- .../controllers/team_match_controller.dart | 10 +- .../lib/routes/api_route.dart | 7 + 17 files changed, 257 insertions(+), 120 deletions(-) diff --git a/wrestling_scoreboard_client/lib/l10n/app_de.arb b/wrestling_scoreboard_client/lib/l10n/app_de.arb index 3c98fa23..44b24df9 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_de.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_de.arb @@ -24,6 +24,20 @@ "apiProvider": "API-Anbieter", "importFromApiProvider": "Synchronisiere mit API-Anbieter", "warningImportFromApiProvider": "Diese Aktion importiert Objekte dieser Organisation und versucht diese zu integrieren. Bist du sicher, dass du fortfahren möchtest?", + "recommendFirstImportFromApiProvider": "Der letzte Import konnte nicht bestimmt werden. Möchtest du die Daten importieren?", + "recommendImportFromApiProvider": "Der letzte Import war am {date} um {time} Uhr. Möchtest du die Daten aktualisieren?", + "@recommendImportFromApiProvider": { + "placeholders": { + "date": { + "type": "DateTime", + "format": "yMd" + }, + "time": { + "type": "DateTime", + "format": "Hm" + } + } + }, "reportProvider": "Report-Anbieter", "database": "Datenbank", "exportDatabase": "Datenbank exportieren", diff --git a/wrestling_scoreboard_client/lib/l10n/app_en.arb b/wrestling_scoreboard_client/lib/l10n/app_en.arb index 1135b34e..d5f62131 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_en.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_en.arb @@ -27,6 +27,20 @@ "apiProvider": "API Provider", "importFromApiProvider": "Sync with API provider", "warningImportFromApiProvider": "This action imports objects of this organization and tries to integrate them. Are you sure, you want to continue?", + "recommendFirstImportFromApiProvider": "Cannot determine the last import. Would you like import the data?", + "recommendImportFromApiProvider": "The last import was on {date} at {time}. Would you like to update the data?", + "@recommendImportFromApiProvider": { + "placeholders": { + "date": { + "type": "DateTime", + "format": "yMd" + }, + "time": { + "type": "DateTime", + "format": "Hm" + } + } + }, "reportProvider": "Report Provider", "database": "Database", "exportDatabase": "Export database", @@ -34,12 +48,10 @@ "restoreDefaultDatabase": "Restore default database", "resetDatabase": "Reset database", "bellSound": "Bell sound", - "profile": "Profile", "username": "Username", "password": "Password", "email": "Email", - "auth_signIn": "Sign in", "auth_signInPrompt_phrase": "Already have an account? Sign In", "auth_signIntoAccount_phrase": "Sign in to your account", @@ -51,7 +63,6 @@ "auth_password_save_phrase": "Save password", "auth_change_password": "Change password", "auth_agreeTermsAndConditions_phrase": "I read and agree to Terms & Conditions", - "imprint": "Imprint", "imprint_phrase": "**Angaben gem. § 5 TMG:**\n\nOberhauser Dev\n\nAugust Oberhauser\n\nGroßhausener Str. 16\n\n86551 Aichach\n\n**Kontaktaufnahme:**\n\nE-Mail: info@oberhauser.dev\n\n**Umsatzsteuer-Identifikationsnummer gem. § 27 a Umsatzsteuergesetz:**\n\nDE XXX XXX XXX", "about": "About", @@ -63,7 +74,6 @@ "about_Development": "Development", "about_development_phrase": "August Oberhauser\n\nEmail: info@oberhauser.dev\n\nWebsite: [oberhauser.dev](https://oberhauser.dev)", "privacy_policy": "Privacy Policy", - "optionSelect": "select", "noneSelected": "None selected", "edit": "Edit", @@ -97,7 +107,6 @@ "warningPrefilledLineup": "The lineup was prefilled with values from a previous match!", "retry": "Retry", "networkTimeout": "Network timeout", - "event": "Event", "place": "Place", "date": "Date", @@ -105,7 +114,6 @@ "seconds": "Seconds", "startDate": "Beginning", "endDate": "End", - "wrestlingRulesPdf": "https://uww.org/sites/default/files/2019-12/wrestling_rules.pdf", "teamMatchTranscript": "Transcript for Team Matches", "teamMatchScoreSheet": "Score sheet for Team Matches", @@ -120,7 +128,6 @@ "numberAbbreviation": "No.", "total": "Total", "participantVacant": "vacant", - "weight": "Weight", "weightClass": "Weight Class", "weightClasses": "Weight Classes", @@ -190,7 +197,6 @@ "classificationPoints": "Classification Points", "participations": "Participations", "boutConfig": "Bout configuration", - "red": "Red", "blue": "Blue", "winner": "Winner", @@ -211,7 +217,6 @@ "activityDuration": "Activity duration", "injuryDuration": "Injury duration", "periodCount": "Number of periods", - "referee": "Referee", "refereeAbbr": "REF", "judge": "Judge", @@ -219,7 +224,6 @@ "timeKeeper": "Time Keeper", "transcriptionWriter": "Transcription Writer", "steward": "Steward", - "prename": "Prename", "surname": "Surname", "age": "Age", diff --git a/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart b/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart index eb5d262d..a885cb34 100644 --- a/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart +++ b/wrestling_scoreboard_client/lib/mocks/services/network/data_manager.dart @@ -463,6 +463,21 @@ class MockDataManager extends DataManager { throw UnimplementedError(); } + @override + Future organizationLastImportUtcDateTime(int id) => throw UnimplementedError(); + + @override + Future organizationLeagueLastImportUtcDateTime(int id) => throw UnimplementedError(); + + @override + Future organizationTeamMatchLastImportUtcDateTime(int id) => throw UnimplementedError(); + + @override + Future organizationCompetitionLastImportUtcDateTime(int id) => throw UnimplementedError(); + + @override + Future organizationTeamLastImportUtcDateTime(int id) => throw UnimplementedError(); + @override Future>> search({ required String searchTerm, diff --git a/wrestling_scoreboard_client/lib/services/network/data_manager.dart b/wrestling_scoreboard_client/lib/services/network/data_manager.dart index 2aa20f56..db80952f 100644 --- a/wrestling_scoreboard_client/lib/services/network/data_manager.dart +++ b/wrestling_scoreboard_client/lib/services/network/data_manager.dart @@ -94,6 +94,16 @@ abstract class DataManager implements AuthManager { Future organizationTeamMatchImport(int id, {AuthService? authService}); + Future organizationLastImportUtcDateTime(int id); + + Future organizationTeamLastImportUtcDateTime(int id); + + Future organizationLeagueLastImportUtcDateTime(int id); + + Future organizationCompetitionLastImportUtcDateTime(int id); + + Future organizationTeamMatchLastImportUtcDateTime(int id); + Future>> search({ required String searchTerm, Type? type, diff --git a/wrestling_scoreboard_client/lib/services/network/remote/rest.dart b/wrestling_scoreboard_client/lib/services/network/remote/rest.dart index 793faf09..826a784b 100644 --- a/wrestling_scoreboard_client/lib/services/network/remote/rest.dart +++ b/wrestling_scoreboard_client/lib/services/network/remote/rest.dart @@ -47,7 +47,7 @@ class RestDataManager extends DataManager { Uri.parse('$_apiUrl${_getPathFromType(T)}/$id').replace(queryParameters: isRaw ? rawQueryParameter : null); final response = await http.get(uri, headers: _headers); - if (response.statusCode == 200) { + if (response.statusCode < 400) { return jsonDecode(response.body); } else { throw RestException('Failed to READ single ${T.toString()}', response: response); @@ -67,7 +67,7 @@ class RestDataManager extends DataManager { Uri.parse('$_apiUrl$prepend${_getPathFromType(T)}s').replace(queryParameters: isRaw ? rawQueryParameter : null); final response = await http.get(uri, headers: _headers); - if (response.statusCode == 200) { + if (response.statusCode < 400) { final List json = jsonDecode(response.body); return json.map((e) => e as Map).toList(); // TODO check order } else { @@ -112,7 +112,7 @@ class RestDataManager extends DataManager { Future exportDatabase() async { final uri = Uri.parse('$_apiUrl/database/export'); final response = await http.get(uri, headers: _headers); - if (response.statusCode == 200) { + if (response.statusCode < 400) { return response.body; } else { throw RestException('Failed to export the database', response: response); @@ -123,7 +123,7 @@ class RestDataManager extends DataManager { Future resetDatabase() async { final uri = Uri.parse('$_apiUrl/database/reset'); final response = await http.post(uri, headers: _headers); - if (response.statusCode != 200) { + if (response.statusCode >= 400) { throw RestException('Failed to reset the database', response: response); } } @@ -132,7 +132,7 @@ class RestDataManager extends DataManager { Future restoreDefaultDatabase() async { final uri = Uri.parse('$_apiUrl/database/restore_default'); final response = await http.post(uri, headers: _headers); - if (response.statusCode != 200) { + if (response.statusCode >= 400) { throw RestException('Failed to restore the default database', response: response); } } @@ -141,7 +141,7 @@ class RestDataManager extends DataManager { Future restoreDatabase(String sqlDump) async { final uri = Uri.parse('$_apiUrl/database/restore'); final response = await http.post(uri, headers: _headers, body: sqlDump); - if (response.statusCode != 200) { + if (response.statusCode >= 400) { throw RestException('Failed to restore the database', response: response); } } @@ -151,81 +151,66 @@ class RestDataManager extends DataManager { _webSocketManager = manager; } - @override - Future organizationImport(int id, {AuthService? authService}) async { + Future _import(int id, String table, {AuthService? authService}) async { String? body; if (authService != null) { if (authService is BasicAuthService) { body = jsonEncode(singleToJson(authService, BasicAuthService, CRUD.update)); } } - final uri = Uri.parse('$_apiUrl/organization/$id/api/import'); + final uri = Uri.parse('$_apiUrl/$table/$id/api/import'); final response = await http.post(uri, body: body, headers: _headers); - if (response.statusCode != 200) { - throw RestException('Failed to import from organization $id', response: response); + if (response.statusCode >= 400) { + throw RestException('Failed to import from $table $id', response: response); } } @override - Future organizationLeagueImport(int id, {AuthService? authService}) async { - String? body; - if (authService != null) { - if (authService is BasicAuthService) { - body = jsonEncode(singleToJson(authService, BasicAuthService, CRUD.update)); - } - } - final uri = Uri.parse('$_apiUrl/league/$id/api/import'); - final response = await http.post(uri, body: body, headers: _headers); - if (response.statusCode != 200) { - throw RestException('Failed to import from league $id', response: response); - } - } + Future organizationImport(int id, {AuthService? authService}) => + _import(id, 'organization', authService: authService); @override - Future organizationTeamMatchImport(int id, {AuthService? authService}) async { - String? body; - if (authService != null) { - if (authService is BasicAuthService) { - body = jsonEncode(singleToJson(authService, BasicAuthService, CRUD.update)); - } - } - final uri = Uri.parse('$_apiUrl/team_match/$id/api/import'); - final response = await http.post(uri, body: body, headers: _headers); - if (response.statusCode != 200) { - throw RestException('Failed to import from team_match $id', response: response); - } - } + Future organizationLeagueImport(int id, {AuthService? authService}) => + _import(id, 'league', authService: authService); @override - Future organizationCompetitionImport(int id, {AuthService? authService}) async { - String? body; - if (authService != null) { - if (authService is BasicAuthService) { - body = jsonEncode(singleToJson(authService, BasicAuthService, CRUD.update)); - } - } - final uri = Uri.parse('$_apiUrl/competition/$id/api/import'); - final response = await http.post(uri, body: body, headers: _headers); - if (response.statusCode != 200) { - throw RestException('Failed to import from competition $id', response: response); - } - } + Future organizationTeamMatchImport(int id, {AuthService? authService}) => + _import(id, 'team_match', authService: authService); @override - Future organizationTeamImport(int id, {AuthService? authService}) async { - String? body; - if (authService != null) { - if (authService is BasicAuthService) { - body = jsonEncode(singleToJson(authService, BasicAuthService, CRUD.update)); - } - } - final uri = Uri.parse('$_apiUrl/team/$id/api/import'); - final response = await http.post(uri, body: body, headers: _headers); - if (response.statusCode != 200) { - throw RestException('Failed to import from team $id', response: response); + Future organizationCompetitionImport(int id, {AuthService? authService}) => + _import(id, 'competition', authService: authService); + + @override + Future organizationTeamImport(int id, {AuthService? authService}) => + _import(id, 'team', authService: authService); + + Future _lastImportUtcDateTime(int id, String table) async { + final uri = Uri.parse('$_apiUrl/$table/$id/api/last_import'); + final response = await http.get(uri, headers: _headers); + + if (response.statusCode < 400) { + return DateTime.tryParse(response.body); + } else { + throw RestException('Failed to get the last import date time for $table $id', response: response); } } + @override + Future organizationLastImportUtcDateTime(int id) => _lastImportUtcDateTime(id, 'organization'); + + @override + Future organizationLeagueLastImportUtcDateTime(int id) => _lastImportUtcDateTime(id, 'league'); + + @override + Future organizationTeamMatchLastImportUtcDateTime(int id) => _lastImportUtcDateTime(id, 'team_match'); + + @override + Future organizationCompetitionLastImportUtcDateTime(int id) => _lastImportUtcDateTime(id, 'competition'); + + @override + Future organizationTeamLastImportUtcDateTime(int id) => _lastImportUtcDateTime(id, 'team'); + @override Future>> search({ required String searchTerm, diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/organization_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/organization_overview.dart index 177dfa86..fe66c2b7 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/organization_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/organization_overview.dart @@ -72,7 +72,10 @@ class OrganizationOverview extends ConsumerWidget { dataObject: data, label: localizations.organization, details: data.name, - actions: [OrganizationImportAction(id: id, orgId: id, importType: OrganizationImportType.organization)], + actions: [ + ConditionalOrganizationImportAction( + id: id, organization: data, importType: OrganizationImportType.organization), + ], tabs: [ Tab(child: HeadingText(localizations.info)), Tab(child: HeadingText(localizations.organizations)), diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart b/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart index 35ec66b1..daf5acda 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/shared/actions.dart @@ -3,9 +3,34 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/auth.dart'; import 'package:wrestling_scoreboard_client/view/widgets/dialogs.dart'; +import 'package:wrestling_scoreboard_common/common.dart'; -class OrganizationImportAction extends ConsumerWidget { +class ConditionalOrganizationImportAction extends StatelessWidget { + final Organization organization; + final int id; + final OrganizationImportType importType; + + const ConditionalOrganizationImportAction({ + required this.id, + required this.organization, + required this.importType, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Visibility( + visible: organization.apiProvider != null, + child: Restricted( + privilege: UserPrivilege.write, + child: OrganizationImportAction(id: id, orgId: organization.id!, importType: importType), + )); + } +} + +class OrganizationImportAction extends ConsumerStatefulWidget { final int orgId; final int id; final OrganizationImportType importType; @@ -13,7 +38,50 @@ class OrganizationImportAction extends ConsumerWidget { const OrganizationImportAction({required this.id, required this.orgId, required this.importType, super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _OrganizationImportActionState(); +} + +class _OrganizationImportActionState extends ConsumerState { + @override + void initState() { + super.initState(); + ref.read(dataManagerNotifierProvider).then((dataManager) async { + DateTime? lastUpdated; + switch (widget.importType) { + case OrganizationImportType.organization: + lastUpdated = await dataManager.organizationLastImportUtcDateTime(widget.id); + case OrganizationImportType.team: + lastUpdated = await dataManager.organizationTeamLastImportUtcDateTime(widget.id); + case OrganizationImportType.league: + lastUpdated = await dataManager.organizationLeagueLastImportUtcDateTime(widget.id); + case OrganizationImportType.competition: + lastUpdated = await dataManager.organizationCompetitionLastImportUtcDateTime(widget.id); + case OrganizationImportType.teamMatch: + lastUpdated = await dataManager.organizationTeamMatchLastImportUtcDateTime(widget.id); + } + lastUpdated = lastUpdated?.toLocal(); + // TODO: make it configurable via Duration picker in settings + if (lastUpdated == null || lastUpdated.compareTo(DateTime.now().subtract(const Duration(hours: 48))) < 0) { + final context = this.context; + if (context.mounted) { + final localizations = AppLocalizations.of(context)!; + final result = await showOkCancelDialog( + context: context, + child: Text(lastUpdated == null + ? localizations.recommendFirstImportFromApiProvider + : localizations.recommendImportFromApiProvider(lastUpdated, lastUpdated)), + getResult: () => true, + ); + if (result == true && context.mounted) { + await _import(localizations); + } + } + } + }); + } + + @override + Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; return IconButton( tooltip: localizations.importFromApiProvider, @@ -24,37 +92,41 @@ class OrganizationImportAction extends ConsumerWidget { getResult: () => true, ); if (result == true && context.mounted) { - catchAsync( - context, - () => showLoadingDialog( - label: AppLocalizations.of(context)!.importFromApiProvider, - runAsync: (BuildContext context) async { - final dataManager = await ref.read(dataManagerNotifierProvider); - final authService = (await ref.read(orgAuthNotifierProvider))[orgId]; - switch (importType) { - case OrganizationImportType.team: - await dataManager.organizationImport(id, authService: authService); - case OrganizationImportType.organization: - await dataManager.organizationImport(id, authService: authService); - case OrganizationImportType.league: - await dataManager.organizationLeagueImport(id, authService: authService); - case OrganizationImportType.competition: - await dataManager.organizationCompetitionImport(id, authService: authService); - case OrganizationImportType.teamMatch: - await dataManager.organizationTeamMatchImport(id, authService: authService); - } - if (context.mounted) { - await showOkDialog(context: context, child: Text(localizations.actionSuccessful)); - } - }, - context: context, - ), - ); + await _import(localizations); } }, icon: const Icon(Icons.import_export), ); } + + Future _import(AppLocalizations localizations) async { + await catchAsync( + context, + () => showLoadingDialog( + label: AppLocalizations.of(context)!.importFromApiProvider, + runAsync: (BuildContext context) async { + final dataManager = await ref.read(dataManagerNotifierProvider); + final authService = (await ref.read(orgAuthNotifierProvider))[widget.orgId]; + switch (widget.importType) { + case OrganizationImportType.organization: + await dataManager.organizationImport(widget.id, authService: authService); + case OrganizationImportType.team: + await dataManager.organizationTeamImport(widget.id, authService: authService); + case OrganizationImportType.league: + await dataManager.organizationLeagueImport(widget.id, authService: authService); + case OrganizationImportType.competition: + await dataManager.organizationCompetitionImport(widget.id, authService: authService); + case OrganizationImportType.teamMatch: + await dataManager.organizationTeamMatchImport(widget.id, authService: authService); + } + if (context.mounted) { + await showOkDialog(context: context, child: Text(localizations.actionSuccessful)); + } + }, + context: context, + ), + ); + } } enum OrganizationImportType { diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/league_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/league_overview.dart index 1aaf7392..0872dba6 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/league_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/league_overview.dart @@ -68,7 +68,8 @@ class LeagueOverview extends ConsumerWidget { label: localizations.league, details: '${data.fullname}, ${data.startDate.year}', actions: [ - OrganizationImportAction(id: id, orgId: data.organization!.id!, importType: OrganizationImportType.league) + ConditionalOrganizationImportAction( + id: id, organization: data.organization!, importType: OrganizationImportType.league) ], tabs: [ Tab(child: HeadingText(localizations.info)), diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart index 88a8ab65..894a5c94 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart @@ -76,8 +76,8 @@ class TeamMatchOverview extends ConsumerWidget { label: localizations.match, details: '${match.home.team.name} - ${match.guest.team.name}', actions: [ - OrganizationImportAction( - id: id, orgId: match.organization!.id!, importType: OrganizationImportType.teamMatch), + ConditionalOrganizationImportAction( + id: id, organization: match.organization!, importType: OrganizationImportType.teamMatch), // TODO: replace with file_save when https://github.com/flutter/flutter/issues/102560 is merged, also replace in settings. IconButton( onPressed: () async { diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_overview.dart index d09048a1..59699ae2 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_overview.dart @@ -52,7 +52,8 @@ class TeamOverview extends ConsumerWidget { label: localizations.team, details: data.name, actions: [ - OrganizationImportAction(id: id, orgId: data.organization!.id!, importType: OrganizationImportType.team) + ConditionalOrganizationImportAction( + id: id, organization: data.organization!, importType: OrganizationImportType.team) ], tabs: [ Tab(child: HeadingText(localizations.info)), diff --git a/wrestling_scoreboard_server/lib/controllers/competition_controller.dart b/wrestling_scoreboard_server/lib/controllers/competition_controller.dart index afc6f76c..fc26ae1f 100644 --- a/wrestling_scoreboard_server/lib/controllers/competition_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/competition_controller.dart @@ -7,7 +7,7 @@ import 'package:wrestling_scoreboard_server/request.dart'; import 'bout_controller.dart'; import 'entity_controller.dart'; -class CompetitionController extends ShelfController { +class CompetitionController extends ShelfController with ImportController { static final CompetitionController _singleton = CompetitionController._internal(); factory CompetitionController() { @@ -28,7 +28,9 @@ class CompetitionController extends ShelfController { ); } - Future import(Request request, User? user, String teamId) async { + @override + Future import(Request request, User? user, String entityId) async { + updateLastImportUtcDateTime(entityId); return Response.notFound('This operation is not supported yet!'); } diff --git a/wrestling_scoreboard_server/lib/controllers/entity_controller.dart b/wrestling_scoreboard_server/lib/controllers/entity_controller.dart index cf9db915..897db3cb 100644 --- a/wrestling_scoreboard_server/lib/controllers/entity_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/entity_controller.dart @@ -55,6 +55,20 @@ Map typeDartToCodeMap = { // '_jsonb': psql.Type.jsonbArray, }; +mixin ImportController { + Map lastImportUtcDateTime = {}; + + Future requestLastImportUtcDateTime(Request request, User? user, String entityId) async { + return Response.ok(lastImportUtcDateTime[int.parse(entityId)]?.toIso8601String()); + } + + void updateLastImportUtcDateTime(String id) { + lastImportUtcDateTime[int.parse(id)] = DateTime.now().toUtc(); + } + + Future import(Request request, User? user, String entityId); +} + abstract class ShelfController extends EntityController { ShelfController({required super.tableName, super.primaryKeyName}); diff --git a/wrestling_scoreboard_server/lib/controllers/league_controller.dart b/wrestling_scoreboard_server/lib/controllers/league_controller.dart index dbfe5699..cf4a38dd 100644 --- a/wrestling_scoreboard_server/lib/controllers/league_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/league_controller.dart @@ -11,7 +11,7 @@ import 'package:wrestling_scoreboard_server/controllers/team_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_match_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; -class LeagueController extends OrganizationalController { +class LeagueController extends OrganizationalController with ImportController { static final LeagueController _singleton = LeagueController._internal(); factory LeagueController() { @@ -48,14 +48,15 @@ class LeagueController extends OrganizationalController { ); } - Future import(Request request, User? user, String leagueId) async { + @override + Future import(Request request, User? user, String entityId) async { try { final bool obfuscate = user?.obfuscate ?? true; - final league = await LeagueController().getSingle(int.parse(leagueId), obfuscate: false); + final league = await LeagueController().getSingle(int.parse(entityId), obfuscate: false); final organizationId = league.organization?.id; if (organizationId == null) { - throw Exception('No organization found for league $leagueId.'); + throw Exception('No organization found for league $entityId.'); } final apiProvider = await OrganizationController().initApiProvider(request, organizationId); @@ -99,6 +100,7 @@ class LeagueController extends OrganizationalController { } }); + updateLastImportUtcDateTime(entityId); return Response.ok('{"status": "success"}'); } catch (err, stackTrace) { return Response.internalServerError(body: '{"err": "$err", "stackTrace": "$stackTrace"}'); diff --git a/wrestling_scoreboard_server/lib/controllers/organization_controller.dart b/wrestling_scoreboard_server/lib/controllers/organization_controller.dart index 41216efc..6863d6d0 100644 --- a/wrestling_scoreboard_server/lib/controllers/organization_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/organization_controller.dart @@ -16,7 +16,7 @@ import 'package:wrestling_scoreboard_server/request.dart'; import 'bout_config_controller.dart'; import 'league_controller.dart'; -class OrganizationController extends ShelfController { +class OrganizationController extends ShelfController with ImportController { static final OrganizationController _singleton = OrganizationController._internal(); factory OrganizationController() { @@ -79,12 +79,13 @@ class OrganizationController extends ShelfController { authService: authService); } - Future import(Request request, User? user, String organizationId) async { + @override + Future import(Request request, User? user, String entityId) async { try { final bool obfuscate = user?.obfuscate ?? true; - final apiProvider = await initApiProvider(request, int.parse(organizationId)); + final apiProvider = await initApiProvider(request, int.parse(entityId)); if (apiProvider == null) { - throw Exception('No API provider selected for the organization $organizationId.'); + throw Exception('No API provider selected for the organization $entityId.'); } // apiProvider.isMock = true; @@ -117,6 +118,7 @@ class OrganizationController extends ShelfController { leagues = await LeagueController().getOrCreateManyOfOrg(leagues.toList(), obfuscate: obfuscate); }); + updateLastImportUtcDateTime(entityId); return Response.ok('{"status": "success"}'); } catch (err, stackTrace) { return Response.internalServerError(body: '{"err": "$err", "stackTrace": "$stackTrace"}'); diff --git a/wrestling_scoreboard_server/lib/controllers/team_controller.dart b/wrestling_scoreboard_server/lib/controllers/team_controller.dart index 08903e7a..703d003e 100644 --- a/wrestling_scoreboard_server/lib/controllers/team_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/team_controller.dart @@ -1,11 +1,12 @@ import 'package:shelf/shelf.dart'; import 'package:wrestling_scoreboard_common/common.dart'; import 'package:wrestling_scoreboard_server/controllers/auth_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/entity_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_match_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; -class TeamController extends OrganizationalController { +class TeamController extends OrganizationalController with ImportController { static final TeamController _singleton = TeamController._internal(); factory TeamController() { @@ -27,7 +28,9 @@ class TeamController extends OrganizationalController { isRaw: request.isRaw, sqlQuery: teamMatchesQuery, substitutionValues: {'id': id}, obfuscate: obfuscate); } - Future import(Request request, User? user, String teamId) async { + @override + Future import(Request request, User? user, String entityId) async { + updateLastImportUtcDateTime(entityId); return Response.notFound('This operation is not supported yet!'); } } diff --git a/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart b/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart index 8c330316..529f1422 100644 --- a/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart @@ -20,7 +20,7 @@ import 'package:wrestling_scoreboard_server/services/postgres_db.dart'; import 'bout_controller.dart'; import 'entity_controller.dart'; -class TeamMatchController extends OrganizationalController { +class TeamMatchController extends OrganizationalController with ImportController { static final TeamMatchController _singleton = TeamMatchController._internal(); factory TeamMatchController() { @@ -143,14 +143,15 @@ class TeamMatchController extends OrganizationalController { return {'comment': psql.Type.text}; } - Future import(Request request, User? user, String teamMatchId) async { + @override + Future import(Request request, User? user, String entityId) async { try { final bool obfuscate = user?.obfuscate ?? true; - final teamMatch = await TeamMatchController().getSingle(int.parse(teamMatchId), obfuscate: false); + final teamMatch = await TeamMatchController().getSingle(int.parse(entityId), obfuscate: false); final organizationId = teamMatch.organization?.id; if (organizationId == null) { - throw Exception('No organization found for league $teamMatchId.'); + throw Exception('No organization found for league $entityId.'); } final apiProvider = await OrganizationController().initApiProvider(request, organizationId); @@ -192,6 +193,7 @@ class TeamMatchController extends OrganizationalController { }); index++; } + updateLastImportUtcDateTime(entityId); return Response.ok('{"status": "success"}'); } catch (err, stackTrace) { return Response.internalServerError(body: '{"err": "$err", "stackTrace": "$stackTrace"}'); diff --git a/wrestling_scoreboard_server/lib/routes/api_route.dart b/wrestling_scoreboard_server/lib/routes/api_route.dart index 24dba59c..4ac3cc3a 100644 --- a/wrestling_scoreboard_server/lib/routes/api_route.dart +++ b/wrestling_scoreboard_server/lib/routes/api_route.dart @@ -75,6 +75,8 @@ class ApiRoute { final organizationController = OrganizationController(); router.restrictedPostOne('/organization//api/import', organizationController.import); + router.restrictedGetOne( + '/organization//api/last_import', organizationController.requestLastImportUtcDateTime); router.restrictedPost('/organization', organizationController.postSingle); router.restrictedGet('/organizations', organizationController.requestMany); router.restrictedGetOne('/organization/', organizationController.requestSingle); @@ -96,6 +98,7 @@ class ApiRoute { final leagueController = LeagueController(); router.restrictedPostOne('/league//api/import', leagueController.import); + router.restrictedGetOne('/league//api/last_import', leagueController.requestLastImportUtcDateTime); router.restrictedPost('/league', leagueController.postSingle); router.restrictedGet('/leagues', leagueController.requestMany); router.restrictedGetOne('/league/', leagueController.requestSingle); @@ -145,6 +148,7 @@ class ApiRoute { final teamController = TeamController(); router.restrictedPostOne('/team//api/import', teamController.import); + router.restrictedGetOne('/team//api/last_import', teamController.requestLastImportUtcDateTime); router.restrictedPost('/team', teamController.postSingle); router.restrictedGet('/teams', teamController.requestMany); router.restrictedGetOne('/team/', teamController.requestSingle); @@ -152,6 +156,7 @@ class ApiRoute { final matchController = TeamMatchController(); router.restrictedPostOne('/team_match//api/import', matchController.import); + router.restrictedGetOne('/team_match//api/last_import', matchController.requestLastImportUtcDateTime); router.restrictedPost('/team_match', matchController.postSingle); router.restrictedGet('/team_matchs', matchController.requestMany); router.restrictedGet('/team_matches', matchController.requestMany); @@ -167,6 +172,8 @@ class ApiRoute { final competitionController = CompetitionController(); router.restrictedPostOne('/competition//api/import', competitionController.import); + router.restrictedGetOne( + '/competition//api/last_import', competitionController.requestLastImportUtcDateTime); router.restrictedPost('/competition', competitionController.postSingle); router.restrictedGet('/competitions', competitionController.requestMany); router.restrictedGetOne('/competition/', competitionController.requestSingle);