It is recommended that you go through our workshop first, to familiarize yourself with the technologies and the contribution process.
- Check out this tutorial if you don't know how to make a PR.
- Increase the version number in the
pubspec.yaml
file with the following guidelines in mind:- Build number (0.2.1+4) is for very small changes and bug fixes (usually not visible to the end user).
- Patch version (0.2.1+4) is for minor improvements that may be visible to an attentive end user.
- Minor version (0.2.1+4) is for added functionality (i.e. merging a branch that introduces a new feature).
- Major version (0.2.1+4) marks important project milestones.
- Document any non-obvious parts of the code and make sure the commit description is clear on why the change is necessary.
- If it's a new feature, write at least one test for it.
- Make sure you have the Project view open in the Project tab on the left in Android Studio (not Android).
- Flutter comes with Hot Reload (the lightning icon, or Ctrl+\ / ⌘\), which allows you to load changes in the code quickly into an already running app, without you needing to reinstall it. It's a very handy feature, but it doesn't work all the time - if you change the code, use Hot Reload but don't see the expected changes, or see some weird behaviour, you may need to close and restart the app (or even reinstall).
- If running on web doesn't give the expected results after changing some code, you may need to clear the cache (in Chrome: Ctrl+Shift+C / ⌘+Shift+C to open the Inspect menu, then right-click the Refresh button, and select Empty cache and Hard reload.)
This project uses the official Dart style guide with the following mentions:
- Android Studio (IntelliJ) with the
dartfmt
tool is used to automatically format the code, including the order of imports. - For consistency, the
new
keyword (which is optional in Dart) should not be used. - Where necessary, comments should use Markdown formatting (e.g. backticks - ` - for code snippets
and
[brackets]
for code references). - Use only single apostrophes - ' - for strings (e.g.
'hello'
instead of"hello"
)
This application uses Firebase to manage remote storage and authentication.
This application uses flutterfire plugins in order to access Firebase services. They are already enabled in the pubspec file and ready to import and use in the code.
Cloud Firestore is a noSQL database that organises its data in collections and documents.
Collections are simply a list of documents, where each document has an ID within the collection.
Documents are similar to a JSON file (or a C struct
, if you prefer), in that they contain
different fields which have three important components: a name - what we use to refer to the
field, similar to a dictionary key -, a type (which can be one of string
, number
,
boolean
, map
, array
, null
- yeah null is its own type -, timestamp
, geopoint
,
reference
- sort of like a pointer to another document), and the actual value, the data
contained in the field.
In addition to fields, documents can contain collections... which contain other documents... which
can contain collections, and so on and so forth, allowing us to create a hierarchical structure
within the database.
More information about the Firestore data model can be found here.
Firestore allows for defining specific security rules for each collection. Rules can be applied for
each different type of transaction - reads
(where single-document reads - get
- and queries -
list
- can have different rules) and writes
(where create
, delete
and update
can be
treated separately).
More information on Firestore security rules can be found here.
The project database contains the following collections:
users
This collection stores per-user data. The document key is the user's `uid` (from FirebaseAuth).All the documents in the collection share the same structure:
Field | Type | Required? | Additional info |
---|---|---|---|
group | string |
🗹 | e.g. “314CB” |
name | map<string, string> |
🗹 | keys are “first” and “last” |
permissionLevel | number |
☐ | a numeric value that defines what the user is allowed to do; if missing, it is treated as being equal to zero |
- websites
A user can define their own websites, that only they have access to. These will reside in the websites sub-collection, and have the following field structure, similar to the one in the root-level websites collection:
Field | Type | Required? | Additional info |
---|---|---|---|
category | string |
🗹 | one of: “learning”, “association”, “administrative”, “resource”, “other” |
icon | string |
☐ | path in Firebase Storage; if missing, it defaults to "icons/websites/globe.png" |
label | string |
🗹 | unless specified, the app sets this to be the link without the protocol |
link | string |
🗹 | it needs to include the protocol |
Anyone can create a new user (a new document in this collection) if the permissionLevel
of
the created user is 0, null or not set at all.
Authenticated users can only read, delete and update their own document (including its
subcollections) and no one else's. However, they cannot modify the permissionLevel
field.
websites
This collection stores useful websites, shown in the app under the *Portal* page. Who they are relevant for depends on the `degree` and `relevance` fields (for more information, see the filters collection).All the documents in the collection share the same structure:
Field | Type | Required? | Additional info |
---|---|---|---|
category | string |
🗹 | one of: “learning”, “association”, “administrative”, “resource”, “other” |
degree | string |
⍰ | “BSc” or “MSc”, must be specified if relevance is not *null* |
editedBy | array<string> |
☐ | list of user IDs |
icon | string |
☐ | path in Firebase Storage; if missing, it defaults to "icons/websites/globe.png" |
label | string |
🗹 | unless specified, the app sets this to be the link without the protocol |
link | string |
🗹 | it needs to include the protocol |
relevance | null / list<string> |
🗹 | *null* if relevant for everyone, otherwise a string of filter node names |
Since websites in this collection are public information (anyone can read), altering and adding data here is a privilege and needs to be monitored, therefore anyone who wants to modify this data needs to be authenticated in the first place.
Users can create a new public website only if their permissionLevel
is equal to or greater
than three and they sign the data by putting their uid
in the addedBy
field.
Users can update a website if they do not modify the addedBy
field and they sign the
modification by adding their uid
at the end of the editedBy
list.
Users can only delete a website if they are the ones who created it (their uid
is equal to
the addedBy
field) or if their permissionLevel
is equal to or greater than four.
filters
This collection storesFilter
objects.
These are basically trees with named nodes and levels. In the case of the relevance filter, they are
meant to represent the way the University organises students:
All
_______________|_______________
/ \
BSc MSc // Degree
________|________ ________|__ ...
/ \ / |
IS CTI IA SPRC ... // Specialization
...|... ______|______ ⋮ ⋮
/ | | \
CTI-1 CTI-2 CTI-3 CTI-4 // Year
⋮ ⋮ __|... ⋮
/ |
3-CA 3-CB ... // Series
__|...
/ |
331CA 332CA ... // Group
All the documents in the collection share the same structure:
Field | Type | Required? | Additional info |
---|---|---|---|
levelNames | array<map<string, string>> |
🗹 | localized names for each tree level (e.g. "Year"); the map keys are the locale strings ("en", "ro") |
root | map<string, map<string, map<...>>> |
🗹 | nested map representing the tree structure, where the key is the name of the node and the value is a map of its children; the leaf nodes have an empty map as a value, **not** *null* or something else |
Filter structure is public information and should never (or very rarely) need to be modified, therefore for this collection, anyone can read but no one can write.
import_moodle
This collection contains class data imported directly from the University's Moodle instance. The data is exported as a spreadsheet from Moodle, and imported to our app's Firestore using a Node.js script. Additional information about classes is stored in the classes collection.The structure of the documents in the collection is the same as the columns in the export file:
All of the fields are strings
. In the app, shortname
is used to extract the class' acronym,
fullname
is the class' name, and category_path
defines the category under which the class is
listed on the ClassesPage.
This is public information already available on Moodle, and will never be editable directly through the app. Therefore for this collection, anyone can read but no one can write.
classes
This collection stores information about classes defined in the import_moodle collection. The ID of a document in this collection corresponds to an ID of a document in import_moodle.All the documents in the collection share the same structure:
Field | Type | Required? | Additional info |
---|---|---|---|
grading | map<string, number> |
☐ | map where the key is the name of the evaluation (e.g. “Exam”) and the value is the number of points that specific evaluation weighs (generally out of 10 total) |
lecturer | string |
☐ | the ID of a person in the people collection |
shortcuts | array<map<string, string>> |
☐ | array of maps representing relevant links for a class, similar to websites; map keys are "addedBy", "link", "name", and "type", with "type" being one of "main", "classbook", "resource" and "other" |
Since classes in this collection are public information (anyone can read), altering and adding data here is a privilege and needs to be monitored, therefore anyone who wants to modify this data needs to be authenticated in the first place.
Users can update an existing class document if their permissionLevel
is equal to or greater
than three. Additionally, they can only create a new class document if a document with that
specific ID exists in the import_moodle collection.
Documents in this collection cannot be deleted.
people
This collection currently contains information about faculty staff, extracted from the official website using a Python scraper.All the documents in the collection share the same structure:
Field | Type | Required? | Additional info |
---|---|---|---|
string |
🗹 | ||
name | string |
🗹 | |
office | string |
🗹 | |
phone | string |
🗹 | |
photo | string |
🗹 | a link to the person's photo |
position | string |
🗹 | the person's position within the faculty, e.g. "Professor, Dr." |
This is public information already available on the official website, and currently cannot be edited through the app due to privacy concerns. Therefore for this collection, anyone can read but no one can write.
All strings that are visible to the user should be internationalised and set in the corresponding
.arb
files within the l10n
folder. The
Flutter Intl Android Studio plugin does
all the hard work for you by generating the code when you save an .arb
file. Strings can then be
accessed using S.of(context).stringID
.
In the database, internationalized strings are saved as a dictionary where the locale is the key:
{
'ro': 'Îmi place Flutter!',
'en': 'I like Flutter!'
}
These will have a corresponding Map
variable in the Dart code (e.g. Map<String, String> infoByLocale
). See WebsiteProvider
for a
serialization/deserialization example.
Changing the app's language is done via the settings page.
The LocaleProvider
class offers utility methods for
fetching the current locale string. See PortalPage
for a usage example.
If you need to use icons other than the ones provided by the Material library, the process is as follows:
- Convert the
.ttf
custom font in the project to an.svg
font (using a tool such as this one). - Go to FlutterIcon and upload (drag & drop) the file you obtained earlier in order to import the icons.
- Check that the imported icons are the ones defined in the
CustomIcons
class to make sure nothing went wrong with the conversion, and select all of them. - (Upload and) select any additional icons that you want to use in the project, then click Download.
- Rename the font file in the archive downloaded earlier to
CustomIcons.ttf
and replace the custom font in the project. - Copy the IconData definitions from the
.dart
file in the archive and replace the corresponding definitions in theCustomIcons
class; - Check that everything still works correctly :)
Note: FontAwesome icons are recommended, where
possible, because they are consistent with the overall style. For additional action icons check out
FontAwesomeActions - the repo provides an .svg
font
you can upload directly into FlutterIcon.