From 51f5cf77fb1a25e99baf8ac074b492cbf74e338c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 8 Aug 2013 12:42:08 -0400 Subject: [PATCH] Convert `Discourse.User` to use `Discourse.Singleton` --- .../controllers/admin_dashboard_controller.js | 4 +-- .../javascripts/admin/models/flagged_post.js | 2 +- app/assets/javascripts/discourse.js | 4 +-- .../discourse/components/click_track.js | 4 +-- .../discourse/components/utilities.js | 2 +- .../controllers/composer_controller.js | 2 +- .../discourse/controllers/flag_controller.js | 6 ++-- .../controllers/list_categories_controller.js | 2 +- .../controllers/preferences_controller.js | 4 +-- .../discourse/controllers/user_controller.js | 4 +-- .../javascripts/discourse/mixins/singleton.js | 23 ++++++++++++-- .../discourse/models/action_summary.js | 2 +- .../javascripts/discourse/models/composer.js | 4 +-- .../discourse/models/topic_details.js | 2 +- .../javascripts/discourse/models/user.js | 31 +++++++------------ .../discourse/models/user_action.js | 4 +-- .../discourse/views/quote_button_view.js | 4 +-- .../discourse/views/user_selector_view.js | 2 +- .../components/click_track_test.js | 6 ++-- test/javascripts/components/utilities_test.js | 4 +-- .../controllers/flag_controller_test.js | 6 ++-- test/javascripts/jshint_all.js.erb | 1 + test/javascripts/mixins/singleton_test.js | 24 ++++++++++++++ 23 files changed, 90 insertions(+), 57 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js index 5dc03a709ba..fd3d2cfdf40 100644 --- a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js @@ -12,11 +12,11 @@ Discourse.AdminDashboardController = Ember.Controller.extend({ problemsCheckMinutes: 1, foundProblems: function() { - return(Discourse.User.current('admin') && this.get('problems') && this.get('problems').length > 0); + return(Discourse.User.currentProp('admin') && this.get('problems') && this.get('problems').length > 0); }.property('problems'), thereWereProblems: function() { - if(!Discourse.User.current('admin')) { return false } + if(!Discourse.User.currentProp('admin')) { return false } if( this.get('foundProblems') ) { this.set('hadProblems', true); return true; diff --git a/app/assets/javascripts/admin/models/flagged_post.js b/app/assets/javascripts/admin/models/flagged_post.js index 05d26e1ff37..59f2d4dd98e 100644 --- a/app/assets/javascripts/admin/models/flagged_post.js +++ b/app/assets/javascripts/admin/models/flagged_post.js @@ -62,7 +62,7 @@ Discourse.FlaggedPost = Discourse.Post.extend({ }.property('post_actions.@each.name_key'), canDeleteAsSpammer: function() { - return (Discourse.User.current('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted')); + return (Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted')); }.property('flaggedForSpam'), deletePost: function() { diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 4856710c1ed..b95625eb429 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -38,7 +38,7 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { $('title').text(title); var notifyCount = this.get('notifyCount'); - if (notifyCount > 0 && !Discourse.User.current('dynamic_favicon')) { + if (notifyCount > 0 && !Discourse.User.currentProp('dynamic_favicon')) { title = "(" + notifyCount + ") " + title; } // chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome @@ -49,7 +49,7 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { }.observes('title', 'hasFocus', 'notifyCount'), faviconChanged: function() { - if(Discourse.User.current('dynamic_favicon')) { + if(Discourse.User.currentProp('dynamic_favicon')) { $.faviconNotify( Discourse.SiteSettings.favicon_url, this.get('notifyCount') ); diff --git a/app/assets/javascripts/discourse/components/click_track.js b/app/assets/javascripts/discourse/components/click_track.js index 399d5a8bafb..0628b080e06 100644 --- a/app/assets/javascripts/discourse/components/click_track.js +++ b/app/assets/javascripts/discourse/components/click_track.js @@ -38,7 +38,7 @@ Discourse.ClickTrack = { userId = $link.data('user-id'); if (!userId) userId = $article.data('user-id'); - var ownLink = userId && (userId === Discourse.User.current('id')); + var ownLink = userId && (userId === Discourse.User.currentProp('id')); // Build a Redirect URL var trackingUrl = Discourse.getURL("/clicks/track?url=" + encodeURIComponent(href)); @@ -99,7 +99,7 @@ Discourse.ClickTrack = { } // Otherwise, use a custom URL with a redirect - if (Discourse.User.current('external_links_in_new_tab')) { + if (Discourse.User.currentProp('external_links_in_new_tab')) { var win = window.open(trackingUrl, '_blank'); win.focus(); } else { diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js index e3d4c276b99..454e0c61834 100644 --- a/app/assets/javascripts/discourse/components/utilities.js +++ b/app/assets/javascripts/discourse/components/utilities.js @@ -202,7 +202,7 @@ Discourse.Utilities = { } // ensures that new users can upload a file - if (Discourse.User.current('trust_level') === 0 && Discourse.SiteSettings['newuser_max_' + type + 's'] === 0) { + if (Discourse.User.currentProp('trust_level') === 0 && Discourse.SiteSettings['newuser_max_' + type + 's'] === 0) { bootbox.alert(I18n.t('post.errors.' + type + '_upload_not_allowed_for_new_user')); return false; } diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js index b1799fd4ee7..a5efa0ea8e4 100644 --- a/app/assets/javascripts/discourse/controllers/composer_controller.js +++ b/app/assets/javascripts/discourse/controllers/composer_controller.js @@ -140,7 +140,7 @@ Discourse.ComposerController = Discourse.Controller.extend({ if (this.get('model.editingPost')) return; // If creating a topic, use topic_count, otherwise post_count - var count = this.get('model.creatingTopic') ? Discourse.User.current('topic_count') : Discourse.User.current('reply_count'); + var count = this.get('model.creatingTopic') ? Discourse.User.currentProp('topic_count') : Discourse.User.currentProp('reply_count'); if (count >= Discourse.SiteSettings.educate_until_posts) { this.set('educationClosed', true); this.set('educationContents', ''); diff --git a/app/assets/javascripts/discourse/controllers/flag_controller.js b/app/assets/javascripts/discourse/controllers/flag_controller.js index 4f99dae2175..1284e847918 100644 --- a/app/assets/javascripts/discourse/controllers/flag_controller.js +++ b/app/assets/javascripts/discourse/controllers/flag_controller.js @@ -35,7 +35,7 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc canTakeAction: function() { // We can only take actions on non-custom flags if (this.get('selected.is_custom_flag')) return false; - return Discourse.User.current('staff'); + return Discourse.User.currentProp('staff'); }.property('selected.is_custom_flag'), submitText: function(){ @@ -66,7 +66,7 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc }, canDeleteSpammer: function() { - if (Discourse.User.current('staff') && this.get('selected.name_key') === 'spam') { + if (Discourse.User.currentProp('staff') && this.get('selected.name_key') === 'spam') { return this.get('userDetails.can_be_deleted') && this.get('userDetails.can_delete_all_posts'); } else { return false; @@ -84,7 +84,7 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc }.observes('username'), fetchUserDetails: function() { - if( Discourse.User.current('staff') && this.get('username') ) { + if( Discourse.User.currentProp('staff') && this.get('username') ) { var flagController = this; Discourse.AdminUser.find(this.get('username').toLowerCase()).then(function(user){ flagController.set('userDetails', user); diff --git a/app/assets/javascripts/discourse/controllers/list_categories_controller.js b/app/assets/javascripts/discourse/controllers/list_categories_controller.js index 6595af3c7b7..96722a0a4ea 100644 --- a/app/assets/javascripts/discourse/controllers/list_categories_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_categories_controller.js @@ -25,7 +25,7 @@ Discourse.ListCategoriesController = Discourse.ObjectController.extend({ }.property('categories.@each'), canEdit: function() { - return Discourse.User.current('staff'); + return Discourse.User.currentProp('staff'); }.property(), // clear a pinned topic diff --git a/app/assets/javascripts/discourse/controllers/preferences_controller.js b/app/assets/javascripts/discourse/controllers/preferences_controller.js index 69c4618d791..278c232e338 100644 --- a/app/assets/javascripts/discourse/controllers/preferences_controller.js +++ b/app/assets/javascripts/discourse/controllers/preferences_controller.js @@ -47,8 +47,8 @@ Discourse.PreferencesController = Discourse.ObjectController.extend({ return model.save().then(function() { // model was saved preferencesController.set('saving', false); - if (Discourse.User.current('id') === model.get('id')) { - Discourse.User.current().set('name', model.get('name')); + if (Discourse.User.currentProp('id') === model.get('id')) { + Discourse.User.currentProp('name', model.get('name')); } preferencesController.set('bio_cooked', diff --git a/app/assets/javascripts/discourse/controllers/user_controller.js b/app/assets/javascripts/discourse/controllers/user_controller.js index f99c0b462cb..9151922315a 100644 --- a/app/assets/javascripts/discourse/controllers/user_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_controller.js @@ -9,11 +9,11 @@ Discourse.UserController = Discourse.ObjectController.extend({ viewingSelf: function() { - return this.get('content.username') === Discourse.User.current('username'); + return this.get('content.username') === Discourse.User.currentProp('username'); }.property('content.username'), canSeePrivateMessages: function() { - return this.get('viewingSelf') || Discourse.User.current('staff'); + return this.get('viewingSelf') || Discourse.User.currentProp('staff'); }.property('viewingSelf') }); diff --git a/app/assets/javascripts/discourse/mixins/singleton.js b/app/assets/javascripts/discourse/mixins/singleton.js index 0a69684a3c7..85f6fdd487f 100644 --- a/app/assets/javascripts/discourse/mixins/singleton.js +++ b/app/assets/javascripts/discourse/mixins/singleton.js @@ -17,12 +17,26 @@ Discourse.Singleton = Em.Mixin.create({ **/ current: function() { if (!this._current) { - this._current = this.create({}); + this._current = this.createCurrent(); } return this._current; }, + + /** + How the singleton instance is created. This can be overridden + with logic for creating (or even returning null) your instance. + + By default it just calls `create` with an empty object. + + @method createCurrent + @returns {Ember.Object} the instance that will be your singleton + **/ + createCurrent: function() { + return this.create({}); + }, + /** Returns or sets a property on the singleton instance. @@ -32,11 +46,14 @@ Discourse.Singleton = Em.Mixin.create({ @returns the value of the property **/ currentProp: function(property, value) { + var instance = this.current(); + if (!instance) { return; } + if (typeof(value) !== "undefined") { - this.current().set(property, value); + instance.set(property, value); return value; } else { - return this.current().get(property); + return instance.get(property); } } diff --git a/app/assets/javascripts/discourse/models/action_summary.js b/app/assets/javascripts/discourse/models/action_summary.js index 80517ae147f..926f1cb2c98 100644 --- a/app/assets/javascripts/discourse/models/action_summary.js +++ b/app/assets/javascripts/discourse/models/action_summary.js @@ -120,7 +120,7 @@ Discourse.ActionSummary = Discourse.Model.extend({ var users = Em.A(); actionSummary.set('users', users); _.each(result,function(user) { - if (user.id === Discourse.User.current('id')) { + if (user.id === Discourse.User.currentProp('id')) { users.pushObject(Discourse.User.current()); } else { users.pushObject(Discourse.User.create(user)); diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js index 87bc40c1235..c85bcbb00e0 100644 --- a/app/assets/javascripts/discourse/models/composer.js +++ b/app/assets/javascripts/discourse/models/composer.js @@ -48,7 +48,7 @@ Discourse.Composer = Discourse.Model.extend({ canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'), showAdminOptions: function() { - if (this.get('creatingTopic') && Discourse.User.current('staff')) return true; + if (this.get('creatingTopic') && Discourse.User.currentProp('staff')) return true; return false; }.property('canEditTitle'), @@ -551,7 +551,7 @@ Discourse.Composer = Discourse.Model.extend({ flashDraftStatusForNewUser: function() { var $draftStatus = $('#draft-status'); - if (Discourse.User.current('trust_level') === 0) { + if (Discourse.User.currentProp('trust_level') === 0) { $draftStatus.toggleClass('flash', true); setTimeout(function() { $draftStatus.removeClass('flash'); }, 250); } diff --git a/app/assets/javascripts/discourse/models/topic_details.js b/app/assets/javascripts/discourse/models/topic_details.js index a5aa17e2286..a4f4858e913 100644 --- a/app/assets/javascripts/discourse/models/topic_details.js +++ b/app/assets/javascripts/discourse/models/topic_details.js @@ -38,7 +38,7 @@ Discourse.TopicDetails = Discourse.Model.extend({ if (typeof this.get('notifications_reason_id') === 'number') { locale_string += "_" + this.get('notifications_reason_id'); } - return I18n.t(locale_string, { username: Discourse.User.current('username_lower') }); + return I18n.t(locale_string, { username: Discourse.User.currentProp('username_lower') }); }.property('notification_level', 'notifications_reason_id'), diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js index 0d679f99ecc..9225966ab66 100644 --- a/app/assets/javascripts/discourse/models/user.js +++ b/app/assets/javascripts/discourse/models/user.js @@ -283,29 +283,20 @@ Discourse.User = Discourse.Model.extend({ }); -Discourse.User.reopenClass({ +Discourse.User.reopenClass(Discourse.Singleton, { + /** - Returns the currently logged in user + The current singleton will retrieve its attributes from the `PreloadStore` + if it exists. Otherwise, no instance is created. - @method current - @param {String} optional property to return from the user if the user exists - @returns {Discourse.User} the logged in user + @method createCurrent + @returns {Discourse.User} the user, if logged in. **/ - current: function(property) { - if (!this.currentUser) { - var userJson = PreloadStore.get('currentUser'); - if (userJson) { - this.currentUser = Discourse.User.create(userJson); - } - } - - // If we found the current user - if (this.currentUser && (typeof property !== "undefined")) { - return this.currentUser.get(property); - } - - return this.currentUser; + createCurrent: function() { + var userJson = PreloadStore.get('currentUser'); + if (userJson) { return Discourse.User.create(userJson); } + return null; }, /** @@ -316,7 +307,7 @@ Discourse.User.reopenClass({ **/ logout: function() { var discourseUserClass = this; - return Discourse.ajax("/session/" + Discourse.User.current('username'), { + return Discourse.ajax("/session/" + Discourse.User.currentProp('username'), { type: 'DELETE' }).then(function () { discourseUserClass.currentUser = null; diff --git a/app/assets/javascripts/discourse/models/user_action.js b/app/assets/javascripts/discourse/models/user_action.js index 975e6b92a40..c5c084e7163 100644 --- a/app/assets/javascripts/discourse/models/user_action.js +++ b/app/assets/javascripts/discourse/models/user_action.js @@ -91,11 +91,11 @@ Discourse.UserAction = Discourse.Model.extend({ }.property('descriptionKey'), sameUser: function() { - return this.get('username') === Discourse.User.current('username'); + return this.get('username') === Discourse.User.currentProp('username'); }.property('username'), targetUser: function() { - return this.get('target_username') === Discourse.User.current('username'); + return this.get('target_username') === Discourse.User.currentProp('username'); }.property('target_username'), targetUserUrl: Discourse.computed.url('target_username', '/users/%@'), diff --git a/app/assets/javascripts/discourse/views/quote_button_view.js b/app/assets/javascripts/discourse/views/quote_button_view.js index f7c4378b5ad..9f2b1a4b01c 100644 --- a/app/assets/javascripts/discourse/views/quote_button_view.js +++ b/app/assets/javascripts/discourse/views/quote_button_view.js @@ -48,7 +48,7 @@ Discourse.QuoteButtonView = Discourse.View.extend({ view.set('isMouseDown', true); if ($(e.target).hasClass('quote-button') || $(e.target).hasClass('create')) return; // do *not* deselect when quoting has been disabled by the user - if (!Discourse.User.current('enable_quoting')) return; + if (!Discourse.User.currentProp('enable_quoting')) return; // deselects only when the user left click // (allows anyone to `extend` their selection using shift+click) if (e.which === 1 && !e.shiftKey) controller.deselectText(); @@ -80,7 +80,7 @@ Discourse.QuoteButtonView = Discourse.View.extend({ selectText: function(target, controller) { var $target = $(target); // breaks if quoting has been disabled by the user - if (!Discourse.User.current('enable_quoting')) return; + if (!Discourse.User.currentProp('enable_quoting')) return; // retrieve the post id from the DOM var postId = $target.closest('.boxed').data('post-id'); // select the text diff --git a/app/assets/javascripts/discourse/views/user_selector_view.js b/app/assets/javascripts/discourse/views/user_selector_view.js index 96b6a4cce4e..6517c8e2922 100644 --- a/app/assets/javascripts/discourse/views/user_selector_view.js +++ b/app/assets/javascripts/discourse/views/user_selector_view.js @@ -15,7 +15,7 @@ Discourse.UserSelector = Discourse.TextField.extend({ dataSource: function(term) { var exclude = selected; if (userSelectorView.get('excludeCurrentUser')){ - exclude = exclude.concat([Discourse.User.current('username')]); + exclude = exclude.concat([Discourse.User.currentProp('username')]); } return Discourse.UserSearch.search({ term: term, diff --git a/test/javascripts/components/click_track_test.js b/test/javascripts/components/click_track_test.js index 72eb1c59398..d3c3ba28b8e 100644 --- a/test/javascripts/components/click_track_test.js +++ b/test/javascripts/components/click_track_test.js @@ -85,12 +85,12 @@ var badgeClickCount = function(id, expected) { }; test("does not update badge clicks on my own link", function() { - this.stub(Discourse.User, 'current').returns(314); + this.stub(Discourse.User, 'currentProp').withArgs('id').returns(314); badgeClickCount('with-badge', 1); }); test("does not update badge clicks in my own post", function() { - this.stub(Discourse.User, 'current').returns(3141); + this.stub(Discourse.User, 'currentProp').withArgs('id').returns(3141); badgeClickCount('with-badge-but-not-mine', 1); }); @@ -167,7 +167,7 @@ test("does not track via AJAX for attachments", function() { test("tracks custom urls when opening in another window", function() { var clickEvent = generateClickEventOn('a'); - this.stub(Discourse.User, "current").returns(true); + this.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true); ok(!track(clickEvent)); ok(this.windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank')); }); diff --git a/test/javascripts/components/utilities_test.js b/test/javascripts/components/utilities_test.js index 36a40be7a3f..ec4ac8cfbf2 100644 --- a/test/javascripts/components/utilities_test.js +++ b/test/javascripts/components/utilities_test.js @@ -24,7 +24,7 @@ test("uploading one file", function() { test("new user cannot upload images", function() { Discourse.SiteSettings.newuser_max_images = 0; - this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0); + this.stub(Discourse.User, 'currentProp').withArgs("trust_level").returns(0); this.stub(bootbox, "alert"); ok(!validUpload([{name: "image.png"}])); @@ -33,7 +33,7 @@ test("new user cannot upload images", function() { test("new user cannot upload attachments", function() { Discourse.SiteSettings.newuser_max_attachments = 0; - this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0); + this.stub(Discourse.User, 'currentProp').withArgs("trust_level").returns(0); this.stub(bootbox, "alert"); ok(!validUpload([{name: "roman.txt"}])); diff --git a/test/javascripts/controllers/flag_controller_test.js b/test/javascripts/controllers/flag_controller_test.js index 7b226bcf4b5..a8bbc23dd72 100644 --- a/test/javascripts/controllers/flag_controller_test.js +++ b/test/javascripts/controllers/flag_controller_test.js @@ -17,7 +17,7 @@ module("Discourse.FlagController canDeleteSpammer"); test("canDeleteSpammer not staff", function(){ var flagController = controllerFor('flag', buildPost()); - this.stub(Discourse.User, 'current').returns(false); // Discourse.User.current('staff') returns false + this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false); flagController.set('selected', Discourse.PostActionType.create({name_key: 'spam'})); equal(flagController.get('canDeleteSpammer'), false, 'false if current user is not staff'); }); @@ -28,7 +28,7 @@ var canDeleteSpammer = function(test, postActionType, expected, testName) { }; test("canDeleteSpammer spam not selected", function(){ - this.stub(Discourse.User, 'current').returns(true); // Discourse.User.current('staff') returns true + this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true); this.flagController = controllerFor('flag', buildPost()); this.flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true})); canDeleteSpammer(this, 'off_topic', false, 'false if current user is staff, but selected is off_topic'); @@ -38,7 +38,7 @@ test("canDeleteSpammer spam not selected", function(){ }); test("canDeleteSpammer spam selected", function(){ - this.stub(Discourse.User, 'current').returns(true); + this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true); this.flagController = controllerFor('flag', buildPost()); this.flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true})); diff --git a/test/javascripts/jshint_all.js.erb b/test/javascripts/jshint_all.js.erb index 3854f883ef0..0515fa670ce 100644 --- a/test/javascripts/jshint_all.js.erb +++ b/test/javascripts/jshint_all.js.erb @@ -124,6 +124,7 @@ var jsHintOpts = { "controllerFor", "containsInstance", "deepEqual", + "notEqual", "Blob", "File"], "node" : false, diff --git a/test/javascripts/mixins/singleton_test.js b/test/javascripts/mixins/singleton_test.js index 95216983227..08f1b53d3ea 100644 --- a/test/javascripts/mixins/singleton_test.js +++ b/test/javascripts/mixins/singleton_test.js @@ -34,4 +34,28 @@ test("currentProp writing", function() { DummyModel.currentProp('adventure', null); equal(DummyModel.currentProp('adventure'), null, 'we can set the value to null'); +}); + +test("createCurrent", function() { + var Shoe = Ember.Object.extend({}); + Shoe.reopenClass(Discourse.Singleton, { + createCurrent: function() { + return Shoe.create({toes: 5}); + } + }); + + equal(Shoe.currentProp('toes'), 5, 'it created the class using `createCurrent`'); +}); + + +test("createCurrent that returns null", function() { + var Missing = Ember.Object.extend({}); + Missing.reopenClass(Discourse.Singleton, { + createCurrent: function() { + return null; + } + }); + + blank(Missing.current(), "it doesn't return an instance"); + blank(Missing.currentProp('madeup'), "it won't raise an error asking for a property. Will just return null."); }); \ No newline at end of file