Skip to content

Commit

Permalink
Merge pull request #1431 from eciis/feature-toggles
Browse files Browse the repository at this point in the history
Create feature toggles micro service
  • Loading branch information
JuliePessoa authored Feb 11, 2019
2 parents 0690c66 + 6c09e7f commit c64f458
Show file tree
Hide file tree
Showing 62 changed files with 17,205 additions and 105 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,12 @@ paket-files/
frontend/config.js
support/config.js
landing/config.js
feature-toggles/config.js

frontend/firebase-config.js
support/firebase-config.js
backend/firebase_config.py
feature-toggles/firebase-config.js

backend/app_version.py

Expand All @@ -547,3 +549,4 @@ backend/Pipfile
#Frontend tests
frontend/test/package-lock.json
frontend/test/yarn.lock
feature-toggles/test/yarn.lock
13 changes: 11 additions & 2 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ pipeline {

}
stages {
stage('build') {
steps {
sh './setup_env_test clean'
sh './setup_frontend_tests'
}
}
stage('Tests') {
steps {
parallel(
"Backend": {
sh './ecis test server --clean'
sh './ecis test server'
},
"Frontend": {
sh './ecis test client --clean'
sh './ecis test client'
},
"Feature": {
sh './ecis test feature'
}
)
}
Expand Down
26 changes: 26 additions & 0 deletions backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from models import Comment
from models import Invite
from models import Event
from models import Feature
from utils import NotAuthorizedException
from google.appengine.ext import ndb
from google.appengine.api import search
Expand All @@ -37,6 +38,22 @@
delectus, ut aut reiciendis voluptatibus maiores alias consequatur \
aut perferendis doloribus asperiores repellat.'

features = [
{
"name": 'manage-inst-edit',
"enable_mobile": "DISABLED",
"enable_desktop": "ALL",
"translation_dict": {
"pt-br": "Editar informações da instituição"
}
}
]

def create_features():
for feature in features:
if not Feature.get_by_id(feature['name']):
Feature.create(**feature)


def add_comments_to_post(user, user_reply, post, institution, comments_qnt=3):
"""Add comments to post."""
Expand Down Expand Up @@ -520,11 +537,20 @@ def get(self):
splab.posts = []
splab.put()

create_features()

jsonList.append({"msg": "database initialized with a few posts"})

self.response.write(json.dumps(jsonList))

class CreateFeaturesHandler(BaseHandler):
def get(self):
create_features()
self.response.write({"msg": "database initialized with a few features"})

app = webapp2.WSGIApplication([
('/admin/reset', ResetHandler),
('/admin/create-features', CreateFeaturesHandler)
], debug=True)


Expand Down
4 changes: 3 additions & 1 deletion backend/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .invite_user_handler import *
from .institution_parent_handler import *
from .institution_children_handler import *
from .feature_toggle_handler import *

handlers = [
base_handler, erro_handler, event_collection_handler, event_handler,
Expand All @@ -61,6 +62,7 @@
user_request_collection_handler, user_timeline_handler, vote_handler,
invite_hierarchy_collection_handler, invite_user_collection_handler,
invite_institution_handler, invite_user_handler, institution_parent_handler,
institution_children_handler]
institution_children_handler, feature_toggle_handler
]

__all__ = [prop for handler in handlers for prop in handler.__all__]
62 changes: 62 additions & 0 deletions backend/handlers/feature_toggle_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""Feature Toggle handler."""

import json
from . import BaseHandler
from utils import json_response, Utils
from util import login_required
from models import Feature, get_deciis
from custom_exceptions import NotAuthorizedException


__all__ = ['FeatureToggleHandler']

def to_json(feature_list, language="pt-br"):
"""
Method to generate list of feature models in json format object.
Params:
feature_list -- List of features objects
"""

features = [feature.make(language) for feature in feature_list]
return json.dumps(features)

class FeatureToggleHandler(BaseHandler):
"""Feature toggle hanler."""

@json_response
@login_required
def get(self, user):
"""
Method to get all features or filter by name using query parameter.
"""

feature_name = self.request.get('name')
language = self.request.get('lang')

if feature_name:
features = [Feature.get_feature(feature_name)]
else:
features = Feature.get_all_features()

self.response.write(to_json(features, language))

@login_required
@json_response
def put(self, user):
"""
Method for modifying the properties of one or more features.
"""

"""
The super user is the admin of
'Departamento do Complexo Industrial e Inovação em Saúde".
"""
super_user = get_deciis().admin

Utils._assert(not (super_user == user.key), "User not allowed to modify features!", NotAuthorizedException)

feature_body = json.loads(self.request.body)
feature = Feature.set_visibility(feature_body)
self.response.write(json.dumps(feature.make()))
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from handlers import InviteHandler
from handlers import InstitutionParentHandler
from handlers import InstitutionChildrenHandler
from handlers import FeatureToggleHandler

methods = set(webapp2.WSGIApplication.allowed_methods)
methods.add('PATCH')
Expand Down Expand Up @@ -96,6 +97,7 @@
("/api/user/institutions/(.*)", UserHandler),
("/api/user/timeline.*", UserTimelineHandler),
("/api/search/institution", SearchHandler),
("/api/feature-toggle.*", FeatureToggleHandler),
("/login", LoginHandler),
("/logout", LogoutHandler),
("/api/.*", ErroHandler)
Expand Down
3 changes: 2 additions & 1 deletion backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
from .post import *
from .survey_post import *
from .factory_post import *
from .feature import *


models = [
user, address, institution, event, invite, invite_institution,
invite_institution_children, invite_institution_parent, invite_user,
request, request_user, request_institution_parent, request_institution_children,
invite_user_adm, request_institution, factory_invites, post,
survey_post, factory_post
survey_post, factory_post, feature
]

__all__ = [prop for model in models for prop in model.__all__]
87 changes: 87 additions & 0 deletions backend/models/feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Feature model."""
import json
from google.appengine.ext import ndb

__all__ = ['Feature']

class Feature(ndb.Model):
"""
Model of Feature.
"""

name = ndb.StringProperty()
enable_mobile = ndb.StringProperty(
choices=set(["SUPER_USER", "ADMIN", "ALL", "DISABLED"]))
enable_desktop = ndb.StringProperty(
choices=set(["SUPER_USER", "ADMIN", "ALL", "DISABLED"]))
translation = ndb.JsonProperty(default="{}")

@staticmethod
@ndb.transactional(xg=True)
def create(name, translation_dict, enable_mobile="ALL", enable_desktop="ALL"):
"""
Method to create new feature.
Params:
name -- Name of the new feature
enable_mobile -- (Optional) User group to which the feature is enabled in the mobile version. If not received will be enabled for everyone.
enable_desktop -- (Optional) User group to which the feature is enabled in the desktop version. If not received will be enabled for everyone.
"""

feature = Feature(id=name, name=name, enable_desktop=enable_desktop, enable_mobile=enable_mobile)
feature.translation = json.dumps(translation_dict)
feature.put()
return feature

@staticmethod
def set_visibility(feature_dict):
"""
Method to enable or disable feature.
Params:
features_dict -- dictionary containing the properties of the feature model to be modified.
"""

feature = Feature.get_feature(feature_dict.get('name'))
feature.enable_desktop = feature_dict['enable_desktop']
feature.enable_mobile = feature_dict['enable_mobile']
feature.put()
return feature

@staticmethod
def get_all_features():
"""
Method to get all stored features.
"""

features = Feature.query().fetch()
return features

@staticmethod
def get_feature(feature_name):
"""
Method to get feature by name.
Params:
feature_name -- name of the requested feature
"""

feature = Feature.get_by_id(feature_name)

if feature:
return feature
else:
raise Exception("Feature not found!")

def make(self, language="pt-br"):
"""
Method to make feature.
"""
make_obj = {
'name': self.name,
'enable_mobile': self.enable_mobile,
'enable_desktop': self.enable_desktop,
'translation': json.loads(self.translation).get(language)
}

return make_obj
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Feature toggle handler test"""

import json
from ..test_base_handler import TestBaseHandler
from handlers import FeatureToggleHandler
from models import Feature
from mock import patch
from .. import mocks


USER = {'email': '[email protected]'}

class FeatureToggleHandlerTest(TestBaseHandler):
"""
Feature Toggle Handler Test.
"""

@classmethod
def setUp(cls):
"""Provide the base for the tests."""
super(FeatureToggleHandlerTest, cls).setUp()
app = cls.webapp2.WSGIApplication(
[("/api/feature-toggles.*",
FeatureToggleHandler)
], debug=True)
cls.testapp = cls.webtest.TestApp(app)

cls.feature = Feature.create('feature-test', {'pt-br': 'Feature Teste'})
cls.other_feature = Feature.create('feature-test-other', {'pt-br': 'Feature Teste'})


@patch('util.login_service.verify_token', return_value=USER)
def test_get_all(self, verify_token):
"""Test get all features."""

features = self.testapp.get('/api/feature-toggles?lang=pt-br').json
features_make = [self.feature.make(), self.other_feature.make()]

self.assertEquals(len(features), 2)
self.assertItemsEqual(features, features_make)

@patch('util.login_service.verify_token', return_value=USER)
def test_get_by_query(self, verify_token):
"""Test get feature with query parameter."""
feature = self.testapp.get('/api/feature-toggles?name=feature-test&lang=pt-br').json
feature_make = [self.feature.make()]

self.assertListEqual(feature, feature_make)

feature = self.testapp.get('/api/feature-toggles?name=feature-test-other&lang=pt-br').json
feature_make = [self.other_feature.make()]

self.assertListEqual(feature, feature_make)

with self.assertRaises(Exception) as raises_context:
self.testapp.get('/api/feature-toggles?name=sfjkldh')

exception_message = self.get_message_exception(str(raises_context.exception))

self.assertEquals(exception_message, "Error! Feature not found!")

@patch('util.login_service.verify_token', return_value=USER)
def test_put(self, verify_token):
"""Test put features."""

user_admin = mocks.create_user('[email protected]')
user = mocks.create_user()
deciis = mocks.create_institution('DECIIS')
deciis.trusted = True
deciis.add_member(user_admin)
deciis.set_admin(user_admin.key)
user_admin.add_institution(deciis.key)
user_admin.add_institution_admin(deciis.key)

feature = self.feature.make()
other_feature = self.other_feature.make()

feature['enable_mobile'] = 'DISABLED'
other_feature['enable_desktop'] = 'DISABLED'

self.testapp.put_json('/api/feature-toggles', feature)
self.testapp.put_json('/api/feature-toggles', other_feature)

self.feature = self.feature.key.get()
self.other_feature = self.other_feature.key.get()

self.assertEquals(self.feature.enable_desktop, 'ALL')
self.assertEquals(self.feature.enable_mobile, 'DISABLED')
self.assertEquals(self.other_feature.enable_desktop, 'DISABLED')
self.assertEquals(self.other_feature.enable_mobile, 'ALL')

verify_token._mock_return_value = {'email': user.email[0]}

with self.assertRaises(Exception) as raises_context:
self.testapp.put_json('/api/feature-toggles', feature)

exception_message = self.get_message_exception(str(raises_context.exception))

self.assertEquals(exception_message, 'Error! User not allowed to modify features!')
Loading

0 comments on commit c64f458

Please sign in to comment.