Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
5cc32a22
Commit
5cc32a22
authored
Jul 09, 2015
by
Randall Leeds
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2354 from hypothesis/proper-feature-flags
Proper feature flags
parents
2ae0d7ec
cc2ac9a3
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
187 additions
and
3 deletions
+187
-3
app-controller.coffee
h/static/scripts/app-controller.coffee
+7
-2
app.coffee
h/static/scripts/app.coffee
+4
-0
features.js
h/static/scripts/features.js
+65
-0
app-controller-test.coffee
h/static/scripts/test/app-controller-test.coffee
+7
-0
features-test.js
h/static/scripts/test/features-test.js
+103
-0
notifications.html
h/templates/client/settings/notifications.html
+1
-1
No files found.
h/static/scripts/app-controller.coffee
View file @
5cc32a22
...
@@ -5,19 +5,24 @@ module.exports = class AppController
...
@@ -5,19 +5,24 @@ module.exports = class AppController
this
.
$inject
=
[
this
.
$inject
=
[
'$controller'
,
'$document'
,
'$location'
,
'$rootScope'
,
'$route'
,
'$scope'
,
'$controller'
,
'$document'
,
'$location'
,
'$rootScope'
,
'$route'
,
'$scope'
,
'$window'
,
'$window'
,
'auth'
,
'drafts'
,
'identity'
,
'auth'
,
'drafts'
,
'
features'
,
'
identity'
,
'permissions'
,
'streamer'
,
'annotationUI'
,
'permissions'
,
'streamer'
,
'annotationUI'
,
'annotationMapper'
,
'threading'
'annotationMapper'
,
'threading'
]
]
constructor
:
(
constructor
:
(
$controller
,
$document
,
$location
,
$rootScope
,
$route
,
$scope
,
$controller
,
$document
,
$location
,
$rootScope
,
$route
,
$scope
,
$window
,
$window
,
auth
,
drafts
,
identity
,
auth
,
drafts
,
features
,
identity
,
permissions
,
streamer
,
annotationUI
,
permissions
,
streamer
,
annotationUI
,
annotationMapper
,
threading
annotationMapper
,
threading
)
->
)
->
$controller
(
'AnnotationUIController'
,
{
$scope
})
$controller
(
'AnnotationUIController'
,
{
$scope
})
# Allow all child scopes to look up feature flags as:
#
# if ($scope.feature('foo')) { ... }
$scope
.
feature
=
features
.
flagEnabled
$scope
.
auth
=
auth
$scope
.
auth
=
auth
isFirstRun
=
$location
.
search
().
hasOwnProperty
(
'firstrun'
)
isFirstRun
=
$location
.
search
().
hasOwnProperty
(
'firstrun'
)
...
...
h/static/scripts/app.coffee
View file @
5cc32a22
...
@@ -78,6 +78,8 @@ setupStreamer = [
...
@@ -78,6 +78,8 @@ setupStreamer = [
$http
.
defaults
.
headers
.
common
[
'X-Client-Id'
]
=
clientId
$http
.
defaults
.
headers
.
common
[
'X-Client-Id'
]
=
clientId
]
]
setupFeatures
=
[
'features'
,
(
features
)
->
features
.
fetch
()]
module
.
exports
=
angular
.
module
(
'h'
,
[
module
.
exports
=
angular
.
module
(
'h'
,
[
'angulartics'
'angulartics'
'angulartics.google.analytics'
'angulartics.google.analytics'
...
@@ -128,6 +130,7 @@ module.exports = angular.module('h', [
...
@@ -128,6 +130,7 @@ module.exports = angular.module('h', [
.
service
(
'bridge'
,
require
(
'./bridge'
))
.
service
(
'bridge'
,
require
(
'./bridge'
))
.
service
(
'crossframe'
,
require
(
'./cross-frame'
))
.
service
(
'crossframe'
,
require
(
'./cross-frame'
))
.
service
(
'drafts'
,
require
(
'./drafts'
))
.
service
(
'drafts'
,
require
(
'./drafts'
))
.
service
(
'features'
,
require
(
'./features'
))
.
service
(
'flash'
,
require
(
'./flash'
))
.
service
(
'flash'
,
require
(
'./flash'
))
.
service
(
'formRespond'
,
require
(
'./form-respond'
))
.
service
(
'formRespond'
,
require
(
'./form-respond'
))
.
service
(
'host'
,
require
(
'./host'
))
.
service
(
'host'
,
require
(
'./host'
))
...
@@ -155,6 +158,7 @@ module.exports = angular.module('h', [
...
@@ -155,6 +158,7 @@ module.exports = angular.module('h', [
.
config
(
configureRoutes
)
.
config
(
configureRoutes
)
.
config
(
configureTemplates
)
.
config
(
configureTemplates
)
.
run
(
setupFeatures
)
.
run
(
setupCrossFrame
)
.
run
(
setupCrossFrame
)
.
run
(
setupStreamer
)
.
run
(
setupStreamer
)
.
run
(
setupHost
)
.
run
(
setupHost
)
h/static/scripts/features.js
0 → 100644
View file @
5cc32a22
/**
* Feature flag client.
*
* This is a small utility which will periodically retrieve the application
* feature flags from a JSON endpoint in order to expose these to the
* client-side application.
*
* All feature flags implicitly start toggled off. When `flagEnabled` is first
* called (or alternatively when `fetch` is called explicitly) an XMLHTTPRequest
* will be made to retrieve the current feature flag values from the server.
* Once these are retrieved, `flagEnabled` will return current values.
*
* If `flagEnabled` is called and the cache is more than `CACHE_TTL`
* milliseconds old, then it will trigger a new fetch of the feature flag
* values. Note that this is again done asynchronously, so it is only later
* calls to `flagEnabled` that will return the updated values.
*
* Users of this service should assume that the value of any given flag can
* change at any time and should write code accordingly. Feature flags should
* not be cached, and should not be interrogated only at setup time.
*/
"use strict"
;
var
CACHE_TTL
=
5
*
60
*
1000
;
// 5 minutes
function
features
(
$document
,
$http
,
$log
)
{
var
cache
=
null
;
var
featuresURL
=
new
URL
(
'/app/features'
,
$document
.
prop
(
'baseURI'
));
function
fetch
()
{
$http
.
get
(
featuresURL
)
.
success
(
function
(
data
)
{
cache
=
[
Date
.
now
(),
data
];
})
.
error
(
function
()
{
$log
.
warn
(
'features service: failed to load features data'
);
});
}
function
flagEnabled
(
name
)
{
// Trigger a fetch if the cache is more than CACHE_TTL milliseconds old.
// We don't wait for the fetch to complete, so it's not this call that
// will see new data.
if
(
cache
===
null
||
(
Date
.
now
()
-
cache
[
0
])
>
CACHE_TTL
)
{
fetch
();
}
if
(
cache
===
null
)
{
return
false
;
}
var
flags
=
cache
[
1
];
if
(
!
flags
.
hasOwnProperty
(
name
))
{
$log
.
warn
(
'features service: looked up unknown feature:'
,
name
);
return
false
;
}
return
flags
[
name
];
}
return
{
fetch
:
fetch
,
flagEnabled
:
flagEnabled
};
}
module
.
exports
=
[
'$document'
,
'$http'
,
'$log'
,
features
];
h/static/scripts/test/app-controller-test.coffee
View file @
5cc32a22
...
@@ -11,6 +11,7 @@ describe 'AppController', ->
...
@@ -11,6 +11,7 @@ describe 'AppController', ->
fakeAnnotationUI
=
null
fakeAnnotationUI
=
null
fakeAuth
=
null
fakeAuth
=
null
fakeDrafts
=
null
fakeDrafts
=
null
fakeFeatures
=
null
fakeIdentity
=
null
fakeIdentity
=
null
fakeLocation
=
null
fakeLocation
=
null
fakeParams
=
null
fakeParams
=
null
...
@@ -54,6 +55,11 @@ describe 'AppController', ->
...
@@ -54,6 +55,11 @@ describe 'AppController', ->
discard
:
sandbox
.
spy
()
discard
:
sandbox
.
spy
()
}
}
fakeFeatures
=
{
fetch
:
sandbox
.
spy
()
flagEnabled
:
sandbox
.
stub
().
returns
(
false
)
}
fakeIdentity
=
{
fakeIdentity
=
{
watch
:
sandbox
.
spy
()
watch
:
sandbox
.
spy
()
request
:
sandbox
.
spy
()
request
:
sandbox
.
spy
()
...
@@ -97,6 +103,7 @@ describe 'AppController', ->
...
@@ -97,6 +103,7 @@ describe 'AppController', ->
$provide
.
value
'annotationUI'
,
fakeAnnotationUI
$provide
.
value
'annotationUI'
,
fakeAnnotationUI
$provide
.
value
'auth'
,
fakeAuth
$provide
.
value
'auth'
,
fakeAuth
$provide
.
value
'drafts'
,
fakeDrafts
$provide
.
value
'drafts'
,
fakeDrafts
$provide
.
value
'features'
,
fakeFeatures
$provide
.
value
'identity'
,
fakeIdentity
$provide
.
value
'identity'
,
fakeIdentity
$provide
.
value
'$location'
,
fakeLocation
$provide
.
value
'$location'
,
fakeLocation
$provide
.
value
'$routeParams'
,
fakeParams
$provide
.
value
'$routeParams'
,
fakeParams
...
...
h/static/scripts/test/features-test.js
0 → 100644
View file @
5cc32a22
"use strict"
;
var
mock
=
require
(
'angular-mock'
);
var
assert
=
chai
.
assert
;
sinon
.
assert
.
expose
(
assert
,
{
prefix
:
null
});
describe
(
'h:features'
,
function
()
{
var
$httpBackend
;
var
httpHandler
;
var
features
;
var
sandbox
;
before
(
function
()
{
angular
.
module
(
'h'
,
[])
.
service
(
'features'
,
require
(
'../features'
));
});
beforeEach
(
mock
.
module
(
'h'
));
beforeEach
(
mock
.
module
(
function
(
$provide
)
{
sandbox
=
sinon
.
sandbox
.
create
();
var
fakeDocument
=
{
prop
:
sandbox
.
stub
()
};
fakeDocument
.
prop
.
withArgs
(
'baseURI'
).
returns
(
'http://foo.com/'
);
$provide
.
value
(
'$document'
,
fakeDocument
);
}));
beforeEach
(
mock
.
inject
(
function
(
$injector
)
{
$httpBackend
=
$injector
.
get
(
'$httpBackend'
);
features
=
$injector
.
get
(
'features'
);
httpHandler
=
$httpBackend
.
when
(
'GET'
,
'http://foo.com/app/features'
);
httpHandler
.
respond
(
200
,
{
foo
:
true
,
bar
:
false
});
}));
afterEach
(
function
()
{
$httpBackend
.
verifyNoOutstandingExpectation
();
$httpBackend
.
verifyNoOutstandingRequest
();
sandbox
.
restore
();
});
it
(
'fetch should retrieve features data'
,
function
()
{
$httpBackend
.
expect
(
'GET'
,
'http://foo.com/app/features'
);
features
.
fetch
();
$httpBackend
.
flush
();
});
it
(
'fetch should not explode for errors fetching features data'
,
function
()
{
httpHandler
.
respond
(
500
,
"ASPLODE!"
);
features
.
fetch
();
$httpBackend
.
flush
();
});
it
(
'flagEnabled should retrieve features data'
,
function
()
{
$httpBackend
.
expect
(
'GET'
,
'http://foo.com/app/features'
);
features
.
flagEnabled
(
'foo'
);
$httpBackend
.
flush
();
});
it
(
'flagEnabled should return false initially'
,
function
()
{
var
result
=
features
.
flagEnabled
(
'foo'
);
$httpBackend
.
flush
();
assert
.
isFalse
(
result
);
});
it
(
'flagEnabled should return flag values when data is loaded'
,
function
()
{
features
.
fetch
();
$httpBackend
.
flush
();
var
foo
=
features
.
flagEnabled
(
'foo'
);
assert
.
isTrue
(
foo
);
var
bar
=
features
.
flagEnabled
(
'bar'
);
assert
.
isFalse
(
bar
);
});
it
(
'flagEnabled should return false for unknown flags'
,
function
()
{
features
.
fetch
();
$httpBackend
.
flush
();
var
baz
=
features
.
flagEnabled
(
'baz'
);
assert
.
isFalse
(
baz
);
});
it
(
'flagEnabled should trigger a new fetch after cache expiry'
,
function
()
{
var
clock
=
sandbox
.
useFakeTimers
();
$httpBackend
.
expect
(
'GET'
,
'http://foo.com/app/features'
);
features
.
flagEnabled
(
'foo'
);
$httpBackend
.
flush
();
clock
.
tick
(
301
*
1000
);
$httpBackend
.
expect
(
'GET'
,
'http://foo.com/app/features'
);
features
.
flagEnabled
(
'foo'
);
$httpBackend
.
flush
();
});
});
h/templates/client/settings/notifications.html
View file @
5cc32a22
<div
class=
"tab-pane"
title=
"Notifications"
>
<div
ng-if=
"feature('notification')"
class=
"tab-pane"
title=
"Notifications"
>
<form
class=
"account-form form"
name=
"notificationsForm"
>
<form
class=
"account-form form"
name=
"notificationsForm"
>
<p
class=
"form-description"
>
Receive notification emails when:
</p>
<p
class=
"form-description"
>
Receive notification emails when:
</p>
<div
class=
"form-field form-checkbox-list"
>
<div
class=
"form-field form-checkbox-list"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment