diff --git a/app/controllers/surveillance_programs_controller.rb b/app/controllers/surveillance_programs_controller.rb index c0306c49..9743dcc0 100644 --- a/app/controllers/surveillance_programs_controller.rb +++ b/app/controllers/surveillance_programs_controller.rb @@ -1,5 +1,24 @@ class SurveillanceProgramsController < ApplicationController + authorize_resource only: [:create] + def index render json: SurveillanceProgram.all end + + def create + @surveillance_program = SurveillanceProgram.new(surveillance_program_params) + if SurveillanceProgram.find_by(name: @surveillance_program.name) + render json: { msg: "A surveillance program named #{@surveillance_program.name} already exists" }, status: :unprocessable_entity + elsif @surveillance_program.save + render json: SurveillanceProgram.all + else + render json: { msg: 'Error saving program - check format, name cannot be blank' }, status: :unprocessable_entity + end + end + + private + + def surveillance_program_params + params.permit(:name, :description, :acronym) + end end diff --git a/app/controllers/surveillance_systems_controller.rb b/app/controllers/surveillance_systems_controller.rb index da0bd6a2..2746fb79 100644 --- a/app/controllers/surveillance_systems_controller.rb +++ b/app/controllers/surveillance_systems_controller.rb @@ -1,5 +1,24 @@ class SurveillanceSystemsController < ApplicationController + authorize_resource only: [:create] + def index render json: SurveillanceSystem.all end + + def create + @surveillance_system = SurveillanceSystem.new(surveillance_system_params) + if SurveillanceSystem.find_by(name: @surveillance_system.name) + render json: { msg: "A surveillance system named #{@surveillance_system.name} already exists" }, status: :unprocessable_entity + elsif @surveillance_system.save + render json: SurveillanceSystem.all + else + render json: { msg: 'Error saving system - check format, name cannot be blank' }, status: :unprocessable_entity + end + end + + private + + def surveillance_system_params + params.permit(:name, :description, :acronym) + end end diff --git a/app/models/surveillance_program.rb b/app/models/surveillance_program.rb index df48d4c6..e4543d7b 100644 --- a/app/models/surveillance_program.rb +++ b/app/models/surveillance_program.rb @@ -1,3 +1,4 @@ class SurveillanceProgram < ApplicationRecord has_many :surveys + validates :name, presence: true end diff --git a/app/models/surveillance_system.rb b/app/models/surveillance_system.rb index 84845a73..a20f4863 100644 --- a/app/models/surveillance_system.rb +++ b/app/models/surveillance_system.rb @@ -1,3 +1,4 @@ class SurveillanceSystem < ApplicationRecord has_many :surveys + validates :name, presence: true end diff --git a/config/routes.rb b/config/routes.rb index f13348c9..63b872ff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ Rails.application.routes.draw do - resources :surveillance_systems, only: [:index, :show] - resources :surveillance_programs, only: [:index, :show] + resources :surveillance_systems, only: [:index, :create] + resources :surveillance_programs, only: [:index, :create] get 'response_types', to: 'response_types#index', as: :response_types get 'question_types', to: 'question_types#index', as: :question_types get 'concepts', to: 'concepts#index', as: :concepts diff --git a/features/admin_panel.feature b/features/admin_panel.feature index a9814a10..fed0c2b1 100644 --- a/features/admin_panel.feature +++ b/features/admin_panel.feature @@ -30,3 +30,38 @@ Feature: Admin Panel And I should see "admin@gmail.com" When I click on the "remove_admin@gmail.com" button Then I should not see "admin@gmail.com" + + Scenario: Add program + Given I am the admin test_author@gmail.com + And there is an admin with the email admin@gmail.com + When I go to the dashboard + And I click on the "account-dropdown" link + And I click on the "Admin Panel" link + And I click on the "Program List" link + And I fill in the "program-name" field with "New Program" + And I click on the "submit-prog-sys" button + And I fill in the "program-name" field with "Just clearing the text" + Then I should see "New Program" + + Scenario: Add System + Given I am the admin test_author@gmail.com + And there is an admin with the email admin@gmail.com + When I go to the dashboard + And I click on the "account-dropdown" link + And I click on the "Admin Panel" link + And I click on the "System List" link + And I fill in the "system-name" field with "New System" + And I click on the "submit-prog-sys" button + And I fill in the "system-name" field with "Just clearing the text" + Then I should see "New System" + + Scenario: Add System with name error + Given I am the admin test_author@gmail.com + And there is an admin with the email admin@gmail.com + When I go to the dashboard + And I click on the "account-dropdown" link + And I click on the "Admin Panel" link + And I click on the "System List" link + And I fill in the "system-description" field with "Trying to add system with no name" + And I click on the "submit-prog-sys" button + Then I should see "Error saving system - check format, name cannot be blank" diff --git a/test/controllers/surveillance_programs_controller_test.rb b/test/controllers/surveillance_programs_controller_test.rb index 23148f7e..e843dea0 100644 --- a/test/controllers/surveillance_programs_controller_test.rb +++ b/test/controllers/surveillance_programs_controller_test.rb @@ -1,8 +1,24 @@ require 'test_helper' class SurveillanceProgramsControllerTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + test 'should get index' do get surveillance_programs_url assert_response :success end + + test 'should not be allowed to create program' do + post surveillance_programs_url, params: { name: 'test prog' } + assert_response :forbidden + end + + test 'should be allowed to create program' do + @admin = users(:admin) + @admin.add_role :admin + @admin.save! + sign_in @admin + post surveillance_programs_url, params: { name: 'test prog' } + assert_response :success + end end diff --git a/test/controllers/surveillance_systems_controller_test.rb b/test/controllers/surveillance_systems_controller_test.rb index 653ca559..89eca01c 100644 --- a/test/controllers/surveillance_systems_controller_test.rb +++ b/test/controllers/surveillance_systems_controller_test.rb @@ -1,8 +1,24 @@ require 'test_helper' class SurveillanceSystemsControllerTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + test 'should get index' do get surveillance_systems_url assert_response :success end + + test 'should not be allowed to create system' do + post surveillance_systems_url, params: { name: 'test sys' } + assert_response :forbidden + end + + test 'should be allowed to create system' do + @admin = users(:admin) + @admin.add_role :admin + @admin.save! + sign_in @admin + post surveillance_systems_url, params: { name: 'test sys' } + assert_response :success + end end diff --git a/test/frontend/components/comment_test.js b/test/frontend/components/comment_test.js index 3ad65962..f9c0b330 100644 --- a/test/frontend/components/comment_test.js +++ b/test/frontend/components/comment_test.js @@ -23,6 +23,7 @@ describe('Comment', () => { }; component = renderComponent(Comment, { comment: comment, + loggedIn: true, addComment: function() {} }); }); diff --git a/webpack/_routes.js b/webpack/_routes.js index d760a0f3..f019d3a7 100644 --- a/webpack/_routes.js +++ b/webpack/_routes.js @@ -680,15 +680,9 @@ Based on Rails routes of Vocabulary::Application // root => / // function(options) root_path: Utils.route([], {}, [7,"/",false]), -// surveillance_program => /surveillance_programs/:id(.:format) - // function(id, options) - surveillance_program_path: Utils.route([["id",true],["format",false]], {}, [2,[7,"/",false],[2,[6,"surveillance_programs",false],[2,[7,"/",false],[2,[3,"id",false],[1,[2,[8,".",false],[3,"format",false]],false]]]]]), // surveillance_programs => /surveillance_programs(.:format) // function(options) surveillance_programs_path: Utils.route([["format",false]], {}, [2,[7,"/",false],[2,[6,"surveillance_programs",false],[1,[2,[8,".",false],[3,"format",false]],false]]]), -// surveillance_system => /surveillance_systems/:id(.:format) - // function(id, options) - surveillance_system_path: Utils.route([["id",true],["format",false]], {}, [2,[7,"/",false],[2,[6,"surveillance_systems",false],[2,[7,"/",false],[2,[3,"id",false],[1,[2,[8,".",false],[3,"format",false]],false]]]]]), // surveillance_systems => /surveillance_systems(.:format) // function(options) surveillance_systems_path: Utils.route([["format",false]], {}, [2,[7,"/",false],[2,[6,"surveillance_systems",false],[1,[2,[8,".",false],[3,"format",false]],false]]]), diff --git a/webpack/actions/surveillance_program_actions.js b/webpack/actions/surveillance_program_actions.js index 21802395..d61d4143 100644 --- a/webpack/actions/surveillance_program_actions.js +++ b/webpack/actions/surveillance_program_actions.js @@ -1,7 +1,9 @@ import axios from 'axios'; import routes from '../routes'; +import { getCSRFToken } from './index'; import { - FETCH_SURVEILLANCE_PROGRAMS + FETCH_SURVEILLANCE_PROGRAMS, + ADD_PROGRAM } from './types'; export function fetchSurveillancePrograms() { @@ -15,3 +17,24 @@ export function fetchSurveillancePrograms() { }) }; } + +export function addProgram(name, description=null, acronym=null, callback=null, failureHandler=null) { + const postPromise = axios.post(routes.surveillanceProgramsPath(), { + headers: { + 'X-Key-Inflection': 'camel', + 'Accept': 'application/json' + }, + authenticityToken: getCSRFToken(), + name, description, acronym + }); + if (callback) { + postPromise.then(callback); + } + if (failureHandler) { + postPromise.catch(failureHandler); + } + return { + type: ADD_PROGRAM, + payload: postPromise + }; +} diff --git a/webpack/actions/surveillance_system_actions.js b/webpack/actions/surveillance_system_actions.js index 10666a23..183183a6 100644 --- a/webpack/actions/surveillance_system_actions.js +++ b/webpack/actions/surveillance_system_actions.js @@ -1,7 +1,9 @@ import axios from 'axios'; import routes from '../routes'; +import { getCSRFToken } from './index'; import { - FETCH_SURVEILLANCE_SYSTEMS + FETCH_SURVEILLANCE_SYSTEMS, + ADD_SYSTEM } from './types'; export function fetchSurveillanceSystems() { @@ -15,3 +17,24 @@ export function fetchSurveillanceSystems() { }) }; } + +export function addSystem(name, description=null, acronym=null, callback=null, failureHandler=null) { + const postPromise = axios.post(routes.surveillanceSystemsPath(), { + headers: { + 'X-Key-Inflection': 'camel', + 'Accept': 'application/json' + }, + authenticityToken: getCSRFToken(), + name, description, acronym + }); + if (callback) { + postPromise.then(callback); + } + if (failureHandler) { + postPromise.catch(failureHandler); + } + return { + type: ADD_SYSTEM, + payload: postPromise + }; +} diff --git a/webpack/actions/types.js b/webpack/actions/types.js index 9dec0b0c..bccd82d4 100644 --- a/webpack/actions/types.js +++ b/webpack/actions/types.js @@ -103,10 +103,14 @@ export const FETCH_TAGS_FULFILLED = 'FETCH_TAGS_FULFILLED'; // Surveillance System Types export const FETCH_SURVEILLANCE_SYSTEMS = 'FETCH_SURVEILLANCE_SYSTEMS'; export const FETCH_SURVEILLANCE_SYSTEMS_FULFILLED = 'FETCH_SURVEILLANCE_SYSTEMS_FULFILLED'; +export const ADD_SYSTEM = 'ADD_SYSTEM'; +export const ADD_SYSTEM_FULFILLED = 'ADD_SYSTEM_FULFILLED'; // Surveillance Program Types export const FETCH_SURVEILLANCE_PROGRAMS = 'FETCH_SURVEILLANCE_PROGRAMS'; export const FETCH_SURVEILLANCE_PROGRAMS_FULFILLED = 'FETCH_SURVEILLANCE_PROGRAMS_FULFILLED'; +export const ADD_PROGRAM = 'ADD_PROGRAM'; +export const ADD_PROGRAM_FULFILLED = 'ADD_PROGRAM_FULFILLED'; // Survey types export const PUBLISH_SURVEY = 'PUBLISH_SURVEY'; diff --git a/webpack/components/Comment.js b/webpack/components/Comment.js index 8dc0db3c..1231f265 100644 --- a/webpack/components/Comment.js +++ b/webpack/components/Comment.js @@ -13,8 +13,7 @@ class Comment extends Component { - {this.props.comment.id} - {distanceInWordsToNow(parse(this.props.comment.createdAt,''), {addSuffix: true})} by {this.props.comment.userName} + {distanceInWordsToNow(parse(this.props.comment.createdAt,''), {addSuffix: true})} by {this.props.comment.userName}
@@ -28,7 +27,7 @@ class Comment extends Component {

{this.props.comment.comment}

-
+ {this.props.loggedIn &&
this.collapse = input} role="button" data-toggle="collapse" href={"#replyComment_"+this.props.comment.id} aria-expanded="false" aria-controls={"replyComment_"+this.props.comment.id}>reply @@ -40,9 +39,8 @@ class Comment extends Component { comments={this.props.comments} addComment={this.props.addComment} />
-
+
} {this.renderChildren()} - @@ -56,12 +54,13 @@ class Comment extends Component { .map((comment) => { return ; }); } } - } +} @@ -82,6 +81,7 @@ commentType.children = PropTypes.arrayOf(commentType); Comment.propTypes = { comment: commentType, + loggedIn: PropTypes.bool, addComment: PropTypes.func.isRequired, comments: PropTypes.array.isRequired }; diff --git a/webpack/containers/AdminPanel.js b/webpack/containers/AdminPanel.js index 24d3ed26..6802fb5b 100644 --- a/webpack/containers/AdminPanel.js +++ b/webpack/containers/AdminPanel.js @@ -5,6 +5,8 @@ import { bindActionCreators } from 'redux'; import values from 'lodash/values'; import { setSteps } from '../actions/tutorial_actions'; import { revokeAdmin, grantAdmin } from '../actions/admin_actions'; +import { addProgram } from '../actions/surveillance_program_actions'; +import { addSystem } from '../actions/surveillance_system_actions'; import { revokePublisher, grantPublisher } from '../actions/publisher_actions'; import currentUserProps from '../prop-types/current_user_props'; @@ -12,11 +14,14 @@ class AdminPanel extends Component { constructor(props){ super(props); this.selectTab = this.selectTab.bind(this); - this.onInputChange = this.onInputChange.bind(this); + this.handleChange = this.handleChange.bind(this); this.onFormSubmit = this.onFormSubmit.bind(this); this.state = { selectedTab: 'admin-list', - searchEmail: '' + searchEmail: '', + name: '', + description: '', + acronym: '' }; } @@ -43,20 +48,38 @@ class AdminPanel extends Component { }]); } - onInputChange(event){ - this.setState({searchEmail: event.target.value, error: {}}); + handleChange(field) { + return (event) => { + let newState = {}; + newState[field] = event.target.value; + newState['error'] = {}; + this.setState(newState); + }; } - onFormSubmit(event){ + onFormSubmit(event) { event.preventDefault(); - if (this.state.selectedTab === 'admin-list') { - this.props.grantAdmin(this.state.searchEmail, null, (failureResponse) => { - this.setState({error: failureResponse.response.data}); - }); - } else { - this.props.grantPublisher(this.state.searchEmail, null, (failureResponse) => { - this.setState({error: failureResponse.response.data}); - }); + switch (this.state.selectedTab) { + case 'admin-list': + this.props.grantAdmin(this.state.searchEmail, null, (failureResponse) => { + this.setState({error: failureResponse.response.data}); + }); + break; + case 'publisher-list': + this.props.grantPublisher(this.state.searchEmail, null, (failureResponse) => { + this.setState({error: failureResponse.response.data}); + }); + break; + case 'program-list': + this.props.addProgram(this.state.name, this.state.description, this.state.acronym, null, (failureResponse) => { + this.setState({error: failureResponse.response.data}); + }); + break; + default: + this.props.addSystem(this.state.name, this.state.description, this.state.acronym, null, (failureResponse) => { + this.setState({error: failureResponse.response.data}); + }); + break; } } @@ -66,13 +89,12 @@ class AdminPanel extends Component {
{this.state.error && this.state.error.msg && -
- +
{this.state.error.msg}
}
- + @@ -83,6 +105,37 @@ class AdminPanel extends Component { ); } + progSysForm(type) { + return( +
+
+
+ {this.state.error && this.state.error.msg && +
+ {this.state.error.msg} +
+ } +
+
+ + +
+
+ + +
+
+ + +
+
+ +

+
+
+ ); + } + adminTab() { var adminList = values(this.props.adminList); return( @@ -120,6 +173,32 @@ class AdminPanel extends Component { ); } + programTab() { + var programList = values(this.props.programList); + return( +
+

Program List

+ {this.progSysForm('program')} + {programList.map((prog) => { + return (

{prog.name}
{prog.description}

); + })} +
+ ); + } + + systemTab() { + var systemList = values(this.props.systemList); + return( +
+

System List

+ {this.progSysForm('system')} + {systemList.map((sys) => { + return (

{sys.name}
{sys.description}

); + })} +
+ ); + } + render() { return (
@@ -141,10 +220,18 @@ class AdminPanel extends Component { + +
{this.adminTab()} {this.publisherTab()} + {this.programTab()} + {this.systemTab()}
@@ -160,19 +247,25 @@ function mapStateToProps(state) { const props = {}; props.adminList = state.admins; props.publisherList = state.publishers; + props.programList = state.surveillancePrograms; + props.systemList = state.surveillanceSystems; props.currentUser = state.currentUser; return props; } function mapDispatchToProps(dispatch) { - return bindActionCreators({setSteps, revokeAdmin, revokePublisher, grantPublisher, grantAdmin}, dispatch); + return bindActionCreators({setSteps, addProgram, addSystem, revokeAdmin, revokePublisher, grantPublisher, grantAdmin}, dispatch); } AdminPanel.propTypes = { adminList: PropTypes.object, publisherList: PropTypes.object, + programList: PropTypes.object, + systemList: PropTypes.object, currentUser: currentUserProps, setSteps: PropTypes.func, + addProgram: PropTypes.func, + addSystem: PropTypes.func, revokeAdmin: PropTypes.func, revokePublisher: PropTypes.func, grantAdmin: PropTypes.func, diff --git a/webpack/containers/CommentList.js b/webpack/containers/CommentList.js index 0232cb24..5c007d9e 100644 --- a/webpack/containers/CommentList.js +++ b/webpack/containers/CommentList.js @@ -2,9 +2,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import isEmpty from 'lodash/isEmpty'; + import Comment from '../components/Comment'; import CommentForm from '../components/CommentForm'; import { addComment, fetchComments} from '../actions/comment'; +import currentUserProps from '../prop-types/current_user_props'; class CommentList extends Component { componentWillMount() { @@ -18,19 +21,20 @@ class CommentList extends Component { } render() { + let loggedIn = !isEmpty(this.props.currentUser); return (
- + addComment={this.props.addComment}/>}
- {this.renderChildren()} + {isEmpty(this.props.comments) && !loggedIn ? (

No Comments Yet

) : (this.renderChildren(loggedIn))}
); } - renderChildren (){ + renderChildren(loggedIn) { if(this.props.comments){ return this.props.comments.filter((c) => c.parentId==null).map((comment) => { // Each List Item Component needs a key attribute for uniqueness: @@ -39,6 +43,7 @@ class CommentList extends Component { // the item value return ; }); @@ -52,6 +57,7 @@ function mapDispatchToProps(dispatch) { function mapStateToProps(state, ownProps) { return { + currentUser: state.currentUser, comments: state.comments.filter((comment) => comment.commentableId === ownProps.commentableId) }; } @@ -62,8 +68,8 @@ CommentList.propTypes = { commentableId: PropTypes.number.isRequired, commentableType: PropTypes.string.isRequired, replyToComment: PropTypes.func, + currentUser: currentUserProps, fetchComments: PropTypes.func.isRequired - }; export default connect(mapStateToProps, mapDispatchToProps)(CommentList); diff --git a/webpack/containers/Help.js b/webpack/containers/Help.js index 98570871..93a6b7a2 100644 --- a/webpack/containers/Help.js +++ b/webpack/containers/Help.js @@ -255,6 +255,17 @@ class Help extends Component { ); } + taggingInstructions() { + return( +
+

Tagging Content

+

Surveys, Forms, and Response Sets may all be tagged to facilitate content discovery and reuse. Currently, a tag consists of a name, a value or code, and a code system (which is optional depending on if the tag is coded or not).

+

When editing content and a user starts typing in the tag column of the table a dropdown list will appear of all previously used tags. A user can use the arrow keys to navigate the list and select a tag that was previously used, or continue typing to enter a completely new tag.

+

If the user wants to look for tags by the code or the code system typing these values into the tag field will filter the list by the code or code system as well.

+
+ ); + } + commentInstructions() { return(
@@ -270,6 +281,29 @@ class Help extends Component { ); } + adminInstructions() { + return( +
+

Admin Panel

+

Getting to the panel (requires administrator role):

+
    +
  • When logged in to an account with administrator privileges, navigate to the account dropdown (click the email and gear-icon link in the top right of the application)
  • +
  • Click the Admin Panel menu item to navigate to the main administration page
  • +
  • The page has a number of tabs with different utilities described in detail below
  • +
+

Tabs:

+ +

Admin List

+

This list will populate with all of the users who have administrative privileges. The admin role allows access to all content and all functionality in the application. To the right of each user name and email is a remove button that will revoke the admin role from that user. The admin role can be granted by typing in the email of a user and clicking the plus button. The user will then appear on the admin list or an error will be displayed explaining any issues with the addition.

+

Publisher List

+

For usage instructions please see the information about the Admin List above. Adding members to this list allows them to see draft content they did not author and publish that content to make it public.

+
+ ); + } + instructionsTab() { return(
@@ -287,7 +321,9 @@ class Help extends Component { + +
@@ -296,7 +332,9 @@ class Help extends Component { {this.accountInstructions()} {this.viewInstructions()} {this.editInstructions()} + {this.taggingInstructions()} {this.commentInstructions()} + {this.adminInstructions()}
); diff --git a/webpack/reducers/index.js b/webpack/reducers/index.js index 9b30ecf5..e836df18 100644 --- a/webpack/reducers/index.js +++ b/webpack/reducers/index.js @@ -14,7 +14,9 @@ import { REVOKE_PUBLISHER_FULFILLED, FETCH_ADMINS_FULFILLED, GRANT_ADMIN_FULFILLED, - REVOKE_ADMIN_FULFILLED + REVOKE_ADMIN_FULFILLED, + ADD_PROGRAM_FULFILLED, + ADD_SYSTEM_FULFILLED } from '../actions/types'; @@ -39,8 +41,8 @@ const questionTypes = byIdWithIndividualReducer(FETCH_QUESTION_TYPES_FULFILLED, FETCH_QUESTION_TYPE_FULFILLED, 'questionTypes'); const responseTypes = byIdWithIndividualReducer(FETCH_RESPONSE_TYPES_FULFILLED, FETCH_RESPONSE_TYPE_FULFILLED, 'responseTypes'); -const surveillanceSystems = byIdReducer(FETCH_SURVEILLANCE_SYSTEMS_FULFILLED); -const surveillancePrograms = byIdReducer(FETCH_SURVEILLANCE_PROGRAMS_FULFILLED); +const surveillanceSystems = byIdReducer(FETCH_SURVEILLANCE_SYSTEMS_FULFILLED, ADD_SYSTEM_FULFILLED); +const surveillancePrograms = byIdReducer(FETCH_SURVEILLANCE_PROGRAMS_FULFILLED, ADD_PROGRAM_FULFILLED); const publishers = byIdReducer(FETCH_PUBLISHERS_FULFILLED, GRANT_PUBLISHER_FULFILLED, REVOKE_PUBLISHER_FULFILLED); const admins = byIdReducer(FETCH_ADMINS_FULFILLED, GRANT_ADMIN_FULFILLED, REVOKE_ADMIN_FULFILLED);