Skip to content

Commit

Permalink
feat: fitbit_credentials table and integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ibrahimozkn committed Feb 19, 2025
1 parent c1d4ff5 commit 25c45c1
Show file tree
Hide file tree
Showing 17 changed files with 661 additions and 90 deletions.
4 changes: 2 additions & 2 deletions app/lib/util/fitbit_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class FitbitHandler {
static Future<fitbitter.FitbitCredentials?> _obtainCredentials(
Study study,
) async {
final fitbitCreds = study.fitbitCredentials;
final fitbitCreds = study.fitbitCredentials?.fitbitCredentials;

if (fitbitCreds == null) {
StudyULogger.error('Study is missing Fitbit credentials.');
Expand Down Expand Up @@ -427,7 +427,7 @@ class FitbitHandler {
}
return _getFitbitData(
question.types,
study.fitbitCredentials!,
study.fitbitCredentials!.fitbitCredentials,
credentials,
taskId,
subject,
Expand Down
1 change: 1 addition & 0 deletions core/lib/src/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export 'study_schedule/study_schedule.dart';
export 'tables/app_config.dart';
export 'tables/repo.dart';
export 'tables/study.dart';
export 'tables/study_fitbit_credentials.dart';
export 'tables/study_invite.dart';
export 'tables/study_subject.dart';
export 'tables/subject_progress.dart';
Expand Down
18 changes: 16 additions & 2 deletions core/lib/src/models/tables/study.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum StudyStatus {
closed;

String toJson() => name;

static StudyStatus fromJson(String json) => values.byName(json);
}

Expand All @@ -22,6 +23,7 @@ enum Participation {
invite;

String toJson() => name;

static Participation fromJson(String json) => values.byName(json);
}

Expand All @@ -31,6 +33,7 @@ enum ResultSharing {
organization;

String toJson() => name;

static ResultSharing fromJson(String json) => values.byName(json);
}

Expand All @@ -46,8 +49,6 @@ class Study extends SupabaseObjectFunctions<Study>
String id;
String? title;
String? description = '';
@JsonKey(name: 'fitbit_credentials')
FitbitCredentials? fitbitCredentials;
@JsonKey(name: 'user_id')
String userId;
Participation participation = Participation.invite;
Expand Down Expand Up @@ -82,6 +83,9 @@ class Study extends SupabaseObjectFunctions<Study>
@JsonKey(name: 'registry_published', defaultValue: false)
late bool registryPublished = false;

@JsonKey(includeToJson: false, includeFromJson: false)
StudyFitbitCredentials? fitbitCredentials;

@JsonKey(includeToJson: false, includeFromJson: false)
int participantCount = 0;
@JsonKey(includeToJson: false, includeFromJson: false)
Expand Down Expand Up @@ -150,6 +154,14 @@ class Study extends SupabaseObjectFunctions<Study>
factory Study.fromJson(Map<String, dynamic> json) {
final study = _$StudyFromJson(json);

//fitbitCredentials
final fitbitCredentials =
json['study_fitbit_credentials'] as Map<String, dynamic>?;
if (fitbitCredentials != null && fitbitCredentials.isNotEmpty) {
study.fitbitCredentials = StudyFitbitCredentials.fromJson(
(json['study_fitbit_credentials'] as Map<String, dynamic>));
}

final List? repo = json['repo'] as List?;
if (repo != null && repo.isNotEmpty) {
study.repo =
Expand Down Expand Up @@ -322,7 +334,9 @@ class Study extends SupabaseObjectFunctions<Study>
// - Status

bool get isDraft => status == StudyStatus.draft;

bool get isRunning => status == StudyStatus.running;

bool get isClosed => status == StudyStatus.closed;

bool isReadonly(User user) {
Expand Down
6 changes: 0 additions & 6 deletions core/lib/src/models/tables/study.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions core/lib/src/models/tables/study_fitbit_credentials.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:studyu_core/core.dart';

part 'study_fitbit_credentials.g.dart';

@JsonSerializable()
class StudyFitbitCredentials
extends SupabaseObjectFunctions<StudyFitbitCredentials> {
static const String tableName = 'study_fitbit_credentials';

@override
Map<String, Object> get primaryKeys => {'code': studyId};

@JsonKey(name: 'study_id')
String studyId;
@JsonKey(name: 'fitbit_credentials')
FitbitCredentials fitbitCredentials;

StudyFitbitCredentials(this.studyId, this.fitbitCredentials);

factory StudyFitbitCredentials.fromJson(Map<String, dynamic> json) =>
_$StudyFitbitCredentialsFromJson(json);

@override
Map<String, dynamic> toJson() => _$StudyFitbitCredentialsToJson(this);
}
22 changes: 22 additions & 0 deletions core/lib/src/models/tables/study_fitbit_credentials.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions core/lib/src/util/supabase_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ String tableName(Type cls) => switch (cls) {
== Repo => Repo.tableName,
== StudyInvite => StudyInvite.tableName,
== StudyUUser => StudyUUser.tableName,
== StudyFitbitCredentials => StudyFitbitCredentials.tableName,
_ => throw ArgumentError('$cls is not a supported Supabase type'),
};

Expand All @@ -32,6 +33,7 @@ abstract class SupabaseObjectFunctions<T extends SupabaseObject>
== Repo => Repo.fromJson(json) as T,
== StudyInvite => StudyInvite.fromJson(json) as T,
== StudyUUser => StudyUUser.fromJson(json) as T,
== StudyFitbitCredentials => StudyFitbitCredentials.fromJson(json) as T,
_ => throw ArgumentError('$T is not a supported Supabase type'),
};

Expand Down
51 changes: 49 additions & 2 deletions database/studyu-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ CREATE TABLE public.study (
icon_name text NOT NULL,
-- published is deprecated, use status instead
published boolean DEFAULT false NOT NULL,
fitbit_credentials jsonb,
status public.study_status DEFAULT 'draft'::public.study_status NOT NULL,
registry_published boolean DEFAULT false NOT NULL,
questionnaire jsonb NOT NULL,
Expand Down Expand Up @@ -634,6 +633,50 @@ ALTER TABLE public.study_invite OWNER TO postgres;

COMMENT ON TABLE public.study_invite IS 'Study invite codes';

--
-- Name: study_fitbit_credentials; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public.study_fitbit_credentials (
study_id uuid NOT NULL PRIMARY KEY,
fitbit_credentials jsonb NOT NULL
);

ALTER TABLE public.study_fitbit_credentials OWNER TO postgres;


-- SELECT policy: Allow access if the user is either the study owner (or collaborator) or a study participant.
CREATE POLICY "Fitbit: study owner or participant can select"
ON public.study_fitbit_credentials
FOR SELECT
USING (
(
SELECT public.can_edit(auth.uid(), study)
FROM public.study
WHERE study.id = study_fitbit_credentials.study_id
)
OR public.is_study_subject_of(auth.uid(), study_fitbit_credentials.study_id)
);

-- Modification policy: Allow INSERT, UPDATE, DELETE only if the user is the study owner (or collaborator).
CREATE POLICY "Fitbit: study owner can modify credentials"
ON public.study_fitbit_credentials
FOR ALL
USING (
(
SELECT public.can_edit(auth.uid(), study)
FROM public.study
WHERE study.id = study_fitbit_credentials.study_id
)
)
WITH CHECK (
(
SELECT public.can_edit(auth.uid(), study)
FROM public.study
WHERE study.id = study_fitbit_credentials.study_id
)
);


--
-- Name: COLUMN study_invite.preselected_intervention_ids; Type: COMMENT; Schema: public; Owner: postgres
Expand Down Expand Up @@ -731,7 +774,6 @@ ALTER TABLE ONLY public.repo
ALTER TABLE ONLY public.study_invite
ADD CONSTRAINT study_invite_pkey PRIMARY KEY (code);


--
-- Name: study study_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
Expand Down Expand Up @@ -807,6 +849,9 @@ ALTER TABLE ONLY public.study_invite
ADD CONSTRAINT "study_invite_studyId_fkey" FOREIGN KEY (study_id) REFERENCES public.study(id) ON DELETE CASCADE;


ALTER TABLE ONLY public.study_fitbit_credentials
ADD CONSTRAINT "study_fitbit_credentials_studyId_fkey" FOREIGN KEY (study_id) REFERENCES public.study(id) ON DELETE CASCADE;

--
-- Name: study_subject study_subject_loginCode_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
Expand Down Expand Up @@ -1091,6 +1136,8 @@ ALTER TABLE public.study_subject ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.subject_progress ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.study_fitbit_credentials ENABLE ROW LEVEL SECURITY;

--
-- Name: user; Type: ROW SECURITY; Schema: public; Owner: postgres
--
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
import 'package:reactive_forms/reactive_forms.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_designer_v2/features/design/fitbit/fitbit_credentials_form_data.dart';
import 'package:studyu_designer_v2/features/design/study_form_validation.dart';
import 'package:studyu_designer_v2/features/forms/form_validation.dart';
import 'package:studyu_designer_v2/features/forms/form_view_model.dart';
import 'package:studyu_designer_v2/repositories/fitbit_credentials_repository.dart';

//TODO: right now FitbitCredentials is part of Study form controller, this is not an issue it still works but I think I need to refactor it.
class FitbitCredentialsFormViewModel
extends FormViewModel<FitbitCredentialsFormData> {
extends FormViewModel<StudyFitbitCredentials> {
FitbitCredentialsFormViewModel({
required this.study,
required this.fitbitCredentialsRepository,
super.delegate,
super.formData,
super.autosave = true,
super.validationSet,
});

final Study study;
final IFitbitCredentialsRepository fitbitCredentialsRepository;

// - Form fields

final FormControl<String> clientIdControl = FormControl();
final FormControl<String> clientSecretControl = FormControl();

@override
void initControls() {
clientIdControl.value =
study.fitbitCredentials?.fitbitCredentials.clientId ?? '';
clientSecretControl.value =
study.fitbitCredentials?.fitbitCredentials.clientSecret ?? '';
}

@override
late final FormGroup form = FormGroup({
'client_id': clientIdControl,
'client_secret': clientSecretControl,
});

@override
void setControlsFrom(FitbitCredentialsFormData data) {
clientIdControl.value = data.clientId;
clientSecretControl.value = data.clientSecret;
void setControlsFrom(StudyFitbitCredentials data) {
clientIdControl.value = data.fitbitCredentials.clientId;
clientSecretControl.value = data.fitbitCredentials.clientSecret;
}

@override
FitbitCredentialsFormData buildFormData() {
return FitbitCredentialsFormData(
clientId: clientIdControl.value ?? '',
clientSecret: clientSecretControl.value ?? '',
StudyFitbitCredentials buildFormData() {
return StudyFitbitCredentials(
study.id,
FitbitCredentials(
clientId: clientIdControl.value ?? '',
clientSecret: clientSecretControl.value ?? '',
),
);
}

Expand All @@ -62,7 +76,8 @@ class FitbitCredentialsFormViewModel

//TODO: translations
Map<String, dynamic>? _validateFitbitCredentials(
AbstractControl<dynamic> control,) {
AbstractControl<dynamic> control,
) {
final hasFitbitQuestion = study.observations.any((observation) {
if (observation.type != 'questionnaire') return false;

Expand All @@ -89,6 +104,13 @@ class FitbitCredentialsFormViewModel
return null;
}

@override
Future<StudyFitbitCredentials> save({bool updateState = true}) {
return fitbitCredentialsRepository
.save(buildFormData())
.then((wrapped) => wrapped!.model);
}

@override
// TODO: implement titles
Map<FormMode, String> get titles => throw UnimplementedError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class FitbitCredentialsFormData implements IStudyFormData {
factory FitbitCredentialsFormData.fromStudy(Study study) {
final fitbitCredentials = study.fitbitCredentials;
return FitbitCredentialsFormData(
clientId: fitbitCredentials?.clientId ?? '',
clientSecret: fitbitCredentials?.clientSecret ?? '',
clientId: fitbitCredentials?.fitbitCredentials.clientId ?? '',
clientSecret: fitbitCredentials?.fitbitCredentials.clientSecret ?? '',
);
}

Expand All @@ -24,7 +24,7 @@ class FitbitCredentialsFormData implements IStudyFormData {
final credentials =
FitbitCredentials(clientId: clientId, clientSecret: clientSecret);

study.fitbitCredentials = credentials;
study.fitbitCredentials = StudyFitbitCredentials(study.id, credentials);

return study;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ class _SurveyQuestionFormViewState

final fitbitCredentials = state.study.value?.fitbitCredentials;
return fitbitCredentials == null ||
fitbitCredentials.clientId.isEmpty ||
fitbitCredentials.clientSecret.isEmpty;
fitbitCredentials.fitbitCredentials.clientId.isEmpty ||
fitbitCredentials.fitbitCredentials.clientSecret.isEmpty;
}

WidgetBuilder get questionTypeBodyBuilder {
Expand Down
Loading

0 comments on commit 25c45c1

Please sign in to comment.