Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data refresh #110

Open
wants to merge 258 commits into
base: master
Choose a base branch
from
Open

Data refresh #110

wants to merge 258 commits into from

Conversation

todesj
Copy link
Collaborator

@todesj todesj commented Nov 13, 2021

No description provided.

@todesj todesj requested a review from BraydenKO as a code owner November 13, 2021 22:30
Copy link
Member

@Levi-Lesches Levi-Lesches left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, you clearly understand the structure of a service and how to create one. Problem is, you don't really need to here. The root of the issue is that you're not accessing these timestamps for the sake of showing them in the app; you need them to manipulate other data (by refreshing them). Which means it shouldn't be its own service that your models call directly, but rather something that should be handled implicitly by the services layer, maybe in Database.init.

If you split your code into two parts, 1) deciding whether to update data, and 2) updating the data, you should only make part 1 implicit in Database.init. Remember that the user can always manually refresh the data in the UI, so you should make part 2 a separate function, perhaps a .update() on each HybridX.

All my comments in the code are either formatting or just an in-context explanation of the above.

@@ -75,6 +75,8 @@ class ScheduleModel extends Model {
];
setToday();
notifyListeners();
await Services.instance.database.dataRefresh.setRefreshData("schedule");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how you're using HybridCalendar here. That means you're not guaranteed to get data from the cloud. Which is a good thing: the on-device database is just as valid. The only place where you'd know where you got the data from would be either:

  • an explicit call to calendarDatabase.cloud.getSomething()
  • in the HybridCalendar itself, where you can see which database is chosen for each function

Try to figure out a better place to put this.

@@ -42,5 +42,7 @@ class UserModel extends Model {
for (final String scope in scopeStrings)
parseAdminScope(scope)
];
await Services.instance.database.dataRefresh.setRefreshData("user");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

@@ -50,7 +50,8 @@ class DashboardModel with ChangeNotifier {
try {
await Services.instance.database.calendar.signIn();
await Services.instance.database.sports.signIn();
await schedule.initCalendar();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you not need to initialize the calendar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I call schedule.init() in .getRefreshData(). schedule.init() calls init calendar in it so it was initializing twice

@@ -50,7 +50,8 @@ class DashboardModel with ChangeNotifier {
try {
await Services.instance.database.calendar.signIn();
await Services.instance.database.sports.signIn();
await schedule.initCalendar();
await Services.instance.database.dataRefresh.getRefreshData();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without even seeing what this does, you can probably tell it's not really named that well. The above functions do things, like signIn or initCalendar, but this function just gets something (the refresh data) and ignores the result. I'll leave more comments in that function, just keep this in mind. You always want to name your functions in the form of a question, like isX or hasX, or a command, like getX, setX, and initX.

Comment on lines 9 to 18
@override
Future<void> signIn() async { }
@override
final CloudDataRefresh cloud = CloudDataRefresh();
@override
final LocalDataRefresh local = LocalDataRefresh();
@override
Future<void> getRefreshData() => cloud.getRefreshData();
@override
Future<void> setRefreshData(String data) => local.setRefreshData(data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@override
Future<void> signIn() async { }
@override
final CloudDataRefresh cloud = CloudDataRefresh();
@override
final LocalDataRefresh local = LocalDataRefresh();
@override
Future<void> getRefreshData() => cloud.getRefreshData();
@override
Future<void> setRefreshData(String data) => local.setRefreshData(data);
@override
Future<void> signIn() async { }
@override
final CloudDataRefresh cloud = CloudDataRefresh();
@override
final LocalDataRefresh local = LocalDataRefresh();
@override
Future<void> getRefreshData() => cloud.getRefreshData();
@override
Future<void> setRefreshData(String data) => local.setRefreshData(data);

First of all, spacing.

Secondly, notice that you separate the cloud and local databases by splitting them into getRefreshData and setRefreshData. That's good design within HybridDataRefresh, since you care about having a difference between the two. However, the function that uses the HybridDataRefresh (like Database.init or Services.init) won't care, and will just want the data to come from the right places automatically.

That's the whole point of the HybridX interface. It's to compile the cloud and local databases into a single class that knows when to use which so that every other function can call HybridX.doSomething() without worrying. All this to say that you should have a function, maybe Future<void> update(String data), that 1) downloads the refresh timestamp from the cloud, 2) compares it with the local timestamp, 3) updates the data if needed, and 4) save the current timestamp to the local database.

You'll notice a few problems, namely:

  • if you download each refresh timestamp separately, you're essentially reading the same document many times for no reason
  • how would HybridDataRefresh know how to update, say, HybridCalendar if it determines it needs to update the calendar? Each HybridX should be separate from the others (modular), so it wouldn't make sense to give it direct access to every other database.

I think those two problems highlight the need for a better spot for this to go. Maybe you can give each HybridX a .update function, and in Database.init, you can download all the refresh timestamps at once and call any .update functions that are needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait .getRefreshData() and .setRefreshData() do two different things not something for just one local and the other on the cloud. The thing is the way I currently designed it, I don't need the ability to set the dates in the cloud from the app (at least for now). Also when Database.init() is called isn't the user not signed in yet?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait .getRefreshData() and .setRefreshData() do two different things not something for just one local and the other on the cloud.

Of course, but the point is they're targeted at the local vs the cloud databases, and whoever uses HybridDataRefresh has to know that, which defeats the point of a HybridX class that can make the local vs cloud choice by itself.

The thing is the way I currently designed it, I don't need the ability to set the dates in the cloud from the app

So then you don't need both .get and .set in your interface, because the interface dictates what the local, cloud, and hybrid databases should share in common.

Also when Database.init() is called isn't the user not signed in yet?

Right, there are two cases:

  1. the user is signed in, and Database.init can access Firestore
  2. the user isn't signed in, Database.init cannot access Firestore and Database.signIn has to instead.

This whole feature only applies to case 1. If the user isn't signed in yet, you don't have to "refresh" anything, all the data will be downloaded in signIn. So you need an if statement in Database.init to check if the user is signed in (see the Auth class) and if so, see if the data needs to be updated.

Comment on lines 15 to 26
if(DateTime.parse(map["user"]!).isAfter(Services.instance.prefs
.getRefreshData("user")!)||
Services.instance.prefs.getRefreshData("user")==null){
await Services.instance.database.user.signIn();
await Models.instance.user.init();
print(map["user"]);
}
if(DateTime.parse(map["schedule"]!).isAfter(Services.instance.prefs
.getRefreshData("schedule")!)){
await Models.instance.schedule.init();
print(map["schedule"]);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I noticed the condition isn't exactly the same between these two -- is that important?
  2. You made SharedPreferences.getRefreshData return DateTime?, but you immediately use !. Maybe you can make it non-nullable with a helpful error message if the right key can't be found.
  3. The keys passed to SharedPreferences are simple nouns like schedule and user. You can imagine some future confusion because it sounds like it actually holds the user/schedule data. Maybe clarify by making it last_update_schedule or user_last_update.
  4. You don't want to sign in any services here, because you should already be fully signed in. You can't access Firestore unless you're already authenticated, so you can't possibly have gotten the data above and not be signed in yet.
  5. You don't want to be touching any models here. Services are fully initialized before the models are even touched, and that's by design. Each XModel should get its data from a HybridX. If you want to change the data that the model will get, make sure to change it in the database, and the model will pick it up. That's also why the cloud and local databases are separated but collected together in the HybridDatabase -- so that the model can use the local data when appropriate, and assume that it's in-sync with the cloud data.
  6. The repetitive structure and the deep coupling (ie, that CloudDataRefresh knows about HybridUser and HybridCalendar and depends on them to exist) show that you need to put this all somewhere else. See my above suggestions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but for the User Model isnt .signIn where it actually downloads the data?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're right, I was confusing that with the idea of actually authenticating (maybe that means we need to rename the function, maybe like onSignIn to show it only happens after sign-in, or saveData to show that's all we're doing).

So yes, signIn would be the right function to call, but again, not from here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the condition isn't exactly the same between these two -- is that important?

I just forgot to add it to the other one oops

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yes, signIn would be the right function to call, but again, not from here.

Why not from here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to be touching any models here. Services are fully initialized before the models are even touched, and that's by design. Each XModel should get its data from a HybridX. If you want to change the data that the model will get, make sure to change it in the database, and the model will pick it up. That's also why the cloud and local databases are separated but collected together in the HybridDatabase -- so that the model can use the local data when appropriate, and assume that it's in-sync with the cloud data.

Also don't I need to reinitialize the model the properly store the data in the right places to be presented in the UI??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not from here?

Because you should be keeping the services separate, and this service is doing way too much work in other services. You're better off building this functionality directly into the other services so they can stay independent.

Also don't I need to reinitialize the model the properly store the data in the right places to be presented in the UI??

You don't re-initialize, because they haven't been initialized. The models assume that HybridX.getY() will always give the right data, and HybridX.getY assumes that the local database is up-to-date. Since the last assumption is wrong, all you need to do is just fill the local database with the updated cloud data and let RamLife handle the rest.

@@ -15,6 +15,9 @@ class Preferences extends Service {

late SharedPreferences _prefs;

///
static String refreshData(String data) => "refresh_$data";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say to remove this function. It doesn't say getRefreshData, but it implies that it actually does so, when really all it does it return a string. You can probably just hard-code this string template in the actual function.

Suggested change
static String refreshData(String data) => "refresh_$data";

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if we eventually add another form of data that needs to be updated isn't it just easier to just leave this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how I have it in my suggestion below. The "refresh_$data" part is good, because it lets you use a variable to create a unique string. I'm just saying it doesn't need to be its own function, and you can stick it into the below function.

@@ -30,6 +33,19 @@ class Preferences extends Service {
return result;
}

/// Returns the last date the data was retrieved from the
/// database as [DateTime].
DateTime? getRefreshData(String data){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say you return a DateTime?, but you never return null. As you saw above, that caused some confusion with the !. Instead of using ! below, handle the case where you get null and spit out an error message, and that way you can return a plain DateTime.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it will be null the first time the user updates the data no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! So the ! will result in an error. That's why you should explicitly check for null and handle it properly. The ! is really a last resort, similar to dynamic in that you're telling Dart "I know this will lead to an error if it happens, but it will never happen". You shouldn't be using it to silence the errors, because those errors usually mean you need to handle the null case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I do handle it within the .getRefreshData() function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well firstly, it's not a good idea to say you return something you don't actually ever return -- in this case, null. It's impossible for this function to ever return null. Second, you have a ! in this function, which means you don't actually handle the null case in the HybridDataRefresh service because it hits this ! before it can be returned.

Comment on lines 39 to 40
final String? lastUpdate = _prefs.getString(refreshData(data));
return DateTime.parse(lastUpdate!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final String? lastUpdate = _prefs.getString(refreshData(data));
return DateTime.parse(lastUpdate!);
final String? lastUpdate = _prefs.getString(refreshData(data));
return DateTime.parse(lastUpdate!);

Indentation

Comment on lines 46 to 52
Future<void> setRefreshData(String data, DateTime value) async{
await _prefs.setString(refreshData(data), value.toString());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Something I figured out quite late is that you actually don't need async/await -- you can return the Future<void> given to you by _prefs.setString directly.
  2. If you know you're always going to use DateTime.now, you might as well use it here.
  3. You shouldn't name it data because it doesn't actually hold any data, just the name of the data. You shouldn't name it key either, since the actual key is refresh_X. `name seems to be better.
  4. You might as well get rid of the above refreshData function and make the string directly.
Suggested change
Future<void> setRefreshData(String data, DateTime value) async{
await _prefs.setString(refreshData(data), value.toString());
}
Future<void> setRefreshData(String name) =>
_prefs.setString("refresh_$name", DateTime.now().toString());

@todesj
Copy link
Collaborator Author

todesj commented Nov 14, 2021

First of all, you clearly understand the structure of a service and how to create one. Problem is, you don't really need to here. The root of the issue is that you're not accessing these timestamps for the sake of showing them in the app; you need them to manipulate other data (by refreshing them). Which means it shouldn't be its own service that your models call directly, but rather something that should be handled implicitly by the services layer, maybe in Database.init.

If you split your code into two parts, 1) deciding whether to update data, and 2) updating the data, you should only make part 1 implicit in Database.init. Remember that the user can always manually refresh the data in the UI, so you should make part 2 a separate function, perhaps a .update() on each HybridX.

All my comments in the code are either formatting or just an in-context explanation of the above.

OK I know that a service might be overkill here but it is really that bad if I use. a service instead so I don't have to change it?

@Levi-Lesches
Copy link
Member

It's not really about overkill -- overkill is kinda the name of the game around organization. It's about separation. The reason we stopped getting these super-weird bugs is because the database is completely separate from the rest of the backend, which is completely separate from the fronted. Services are meant to help with that: each service gets a chance to init, and a Database is a special service that can also signIn. Each HybridDatabase is a special type of Database that can also manage two other Databases, a local and a cloud. But still, everything's separate. Just like services shouldn't interact with each other, you don't want databases interacting with each other either.

The way it's set up now, the refresh process is its own database that has control over every other database, which you don't really want. It's simpler to modify the definition of a DatabaseService to also include a function for updating, just like it already has functions for initialization and signing in. That way, each HybridDatabase can "keep its hands to itself" and only handle its own changes, while the master Database class can call the updates on each one. In that way it's like how each service has an init function but the master Services class goes to each service and calls them one-by-one.

@CLAassistant
Copy link

CLAassistant commented Nov 24, 2021

CLA assistant check
All committers have signed the CLA.

todesj and others added 8 commits January 8, 2022 18:12
* Added schedule_search.dart

* Added schedule search model

* Update lib/src/models/view/schedule_search.dart

Co-authored-by: Levi Lesches <[email protected]>

* Update lib/src/models/view/schedule_search.dart

Co-authored-by: Levi Lesches <[email protected]>

* Fixes some formatting

* Added docs and exported schedule_search.dart in lib/models.dart

* Cleaned up schedule_search.dart

* Cleaned up models.dart

* Added Subject.id, PeriodData.name, PerioData.dayName

* Update lib/src/data/schedule/subject.dart

Co-authored-by: Levi Lesches <[email protected]>

* Add Period.dayName, and Period.name (Student-Lyf#81)

* Converted firestore/lib/src/helpers to firestore-py/lib/utils

* Ported firestore/lib/src/services to firestore-py/lib/services

* Removed (and revoked) Firebase credentials

* Cleanup

* Cleaned up logging/Firebase issues

* Added some __init__.py

* Ported firebase/firestore/node/calendar.dart to firebase/firestore-py/bin/calendar.py

* Ported firebase/firestore/node/admins.dart to firebase/firestore-py/bin/admins.py

* Ported firebase/firestore/node/feedback.dart to firebase/firestore-py/bin/feedback.py

* Added some data python files and sections python files

* Continued translating more files (Note: some are incomplete!)
lib/data/schedule
lib/sections/reader
edited lib/utils/dir to use constants from constants.yaml

* Added bin/faculty, bin/secions, facultylogic and facultyreader python files
Also added constants.py to get data from constants.yaml

* Ported firebase/firestore/node/students.dart --> firebase/firestore-py/bin/students.py

* Cleanup

* Delete student.py

* Fix students.py and uploading

* Added some more python files. many variable should be converted to
snake_case

* Changed camel case variables to snake_case

* small edits, opted for a different constants.py file too

* Fixed some typos

* Fixed bug where testers had no schedule (instead of a blank one)

* ---

* Finished scripts to upload data

* Changed dayNames to a list rather than set

* Make Fridays a Friday schedule by default

* Modified feedback.py to actually output feedback

* Added students reader

* Cleanup

* Fixed a bug in faculty/logic.py where a (Student-Lyf#101)

teacher's periods weren't being added to theit schedule, instead the
teacher's schedule was being set to contain only one period.

Co-authored-by: todesj <[email protected]>
Co-authored-by: todesj <[email protected]>
Co-authored-by: Brayden Kohler <[email protected]>
Co-authored-by: BraydenKO <[email protected]>
* Enabled Emulator use

* Fixed emulator error

* Update lib/src/services/firebase_core.dart

Added doc comment for `FirebaseCore.shouldUseEmulator`

Co-authored-by: Levi Lesches <[email protected]>
Makes the schedule icon appear in the dashboard when the side sheet is not persistently open
Adds Podfile.lock to the repo and updates XCode.
@Levi-Lesches
Copy link
Member

I cleaned up the commits by force-pushing. Some reviews are gone now but now it's good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants