Skip to content

Commit fc9bd24

Browse files
authored
Merge pull request #276 from developmentseed/feature/org-privacy-policy
Org Privacy Policy
2 parents ac9f4c0 + fadeefe commit fc9bd24

File tree

13 files changed

+257
-62
lines changed

13 files changed

+257
-62
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
exports.up = async function (knex) {
3+
await knex.schema.alterTable('organization', table => {
4+
table.json('privacy_policy')
5+
})
6+
}
7+
8+
exports.down = async function (knex) {
9+
await knex.schema.alterTable('organization', table => {
10+
table.dropColumn('privacy_policy')
11+
})
12+
}

app/lib/organization.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const db = require('../db')
22
const team = require('./team')
3-
const { map, prop, includes } = require('ramda')
3+
const { map, prop, includes, has, isNil } = require('ramda')
44
const { unpack, PropertyRequiredError } = require('./utils')
55

66
// Organization attributes (without profile)
@@ -10,6 +10,7 @@ const orgAttributes = [
1010
'description',
1111
'privacy',
1212
'teams_can_be_public',
13+
'privacy_policy',
1314
'created_at',
1415
'updated_at'
1516
]
@@ -113,9 +114,10 @@ async function destroy (id) {
113114
* @return {promise}
114115
*/
115116
async function update (id, data) {
116-
if (!data.name) throw new Error('data.name property is required')
117-
118117
const conn = await db()
118+
if (has('name', data) && isNil(prop('name', data))) {
119+
throw new Error('data.name property is required')
120+
}
119121
return unpack(conn('organization').where('id', id).update(data).returning(orgAttributes))
120122
}
121123

app/manage/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,15 @@ function manageRouter (nextApp) {
299299
return nextApp.render(req, res, '/org-edit-profile', { id: req.params.id })
300300
})
301301

302+
router.get('/organizations/:id/edit-privacy-policy', can('organization:edit'), (req, res) => {
303+
return nextApp.render(req, res, '/org-edit-privacy-policy', { id: req.params.id })
304+
})
305+
302306
router.get('/organizations/:id/profile', can('organization:member'), (req, res) => {
303307
return nextApp.render(req, res, '/profile-form', { id: req.params.id, formType: 'org' })
304308
})
305309

306-
router.get('/organizations/:id/edit-team-profiles', can('organization:member'), (req, res) => {
310+
router.get('/organizations/:id/edit-team-profiles', can('organization:edit'), (req, res) => {
307311
return nextApp.render(req, res, '/org-edit-team-profile', { id: req.params.id })
308312
})
309313

components/button.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const style = css`
5757
backgroundColor: #777777;
5858
border: 2px solid #555;
5959
color: ${theme.colors.baseColor};
60+
transition: none;
61+
opacity: 0.68;
62+
box-shadow: 0 0;
63+
cursor: not-allowed;
6064
}
6165
6266
.button.danger {
@@ -72,13 +76,16 @@ const style = css`
7276
`
7377

7478
export default function Button ({ name, id, value, variant, type, disabled, href, onClick, children, size }) {
79+
let classes = [`button`, variant, size]
80+
if (disabled) classes.push('disabled')
81+
let classNames = classes.join(' ')
7582
if (type === 'submit') {
76-
return <button type='submit' className={[`button`, variant, size].join(' ')} disabled={disabled} name={name} id={id} onClick={onClick} value='value'>{children || value}<style jsx>{style}</style></button>
83+
return <button type='submit' className={classNames} disabled={disabled} name={name} id={id} onClick={onClick} value='value'>{children || value}<style jsx>{style}</style></button>
7784
}
7885
if (href) {
7986
let fullUrl
8087
(href.startsWith('http')) ? (fullUrl = href) : (fullUrl = join(publicRuntimeConfig.APP_URL, href))
81-
return <a href={fullUrl} className={[`button`, variant, size].join(' ')} disabled={disabled} name={name} id={id}>{children || value}<style jsx>{style}</style></a>
88+
return <a href={fullUrl} className={classNames} disabled={disabled} name={name} id={id}>{children || value}<style jsx>{style}</style></a>
8289
}
83-
return <div onClick={onClick} className={[`button`, variant, size].join(' ')} disabled={disabled}>{children}<style jsx>{style}</style></div>
90+
return <div onClick={onClick} className={classNames} disabled={disabled}>{children}<style jsx>{style}</style></div>
8491
}

components/privacy-policy-form.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react'
2+
import { Formik, Field, Form } from 'formik'
3+
import Button from '../components/button'
4+
5+
function validateBody (value) {
6+
if (!value) return 'Body of privacy policy is required'
7+
}
8+
9+
function validateConsentText (value) {
10+
if (!value) return 'Consent Text of privacy policy is required'
11+
}
12+
13+
function renderError (text) {
14+
return <div className='form--error'>{text}</div>
15+
}
16+
17+
function renderErrors (errors) {
18+
const keys = Object.keys(errors)
19+
return keys.map((key) => {
20+
return renderError(errors[key])
21+
})
22+
}
23+
24+
export default function PrivacyPolicyForm ({ initialValues, onSubmit }) {
25+
return (
26+
<Formik
27+
initialValues={initialValues}
28+
onSubmit={onSubmit}
29+
render={({ status, isSubmitting, submitForm, values, errors, setFieldValue, setErrors, setStatus }) => {
30+
return (
31+
<Form>
32+
<div className='form-control form-control__vertical'>
33+
<label htmlFor='body'>Body<span className='form--required'>*</span></label>
34+
<Field cols={40} rows={6} component='textarea' name='body' value={values.body} required className={errors.body ? 'form-error' : ''} validate={validateBody} />
35+
</div>
36+
<div className='form-control form-control__vertical'>
37+
<label htmlFor='consentText'>Consent Text<span className='form--required'>*</span></label>
38+
<Field cols={40} rows={6} component='textarea' name='consentText' value={values.consentText} required className={errors.consentText ? 'form-error' : ''} validate={validateConsentText} />
39+
</div>
40+
<div className='form-control form-control__vertical'>
41+
{(status && status.errors) && (renderErrors(status.errors))}
42+
<Button
43+
disabled={isSubmitting}
44+
variant='primary'
45+
onClick={() => {
46+
if (Object.keys(errors).length) {
47+
setErrors(errors)
48+
return setStatus({
49+
errors
50+
})
51+
}
52+
return submitForm()
53+
}}
54+
type='submit'
55+
value='submit'
56+
/>
57+
</div>
58+
</Form>
59+
)
60+
}}
61+
/>
62+
)
63+
}

components/profile-form.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import Router from 'next/router'
66
import descriptionPopup from './description-popup'
77
import { Formik, Field, useField, Form, ErrorMessage } from 'formik'
88
import { getOrgMemberAttributes, getTeamMemberAttributes, getMyProfile, setMyProfile } from '../lib/profiles-api'
9+
import { getOrg } from '../lib/org-api'
910
import { getTeam } from '../lib/teams-api'
1011
import Button from '../components/button'
11-
import { propOr } from 'ramda'
12+
import { propOr, prop } from 'ramda'
1213

1314
function GenderSelectField (props) {
1415
const [field, meta, { setValue, setTouched }] = useField(props.name)
@@ -88,31 +89,47 @@ export default class ProfileForm extends Component {
8889
memberAttributes: [],
8990
orgAttributes: [],
9091
profileValues: {},
92+
consentChecked: true,
9193
loading: true,
9294
error: undefined
9395
}
96+
97+
this.setConsentChecked = this.setConsentChecked.bind(this)
9498
}
9599

96100
async componentDidMount () {
97101
this.getProfileForm()
98102
}
99103

104+
setConsentChecked (checked) {
105+
this.setState({
106+
consentChecked: checked
107+
})
108+
}
109+
100110
async getProfileForm () {
101111
const { id } = this.props
102112
try {
103113
let memberAttributes = []
104114
let orgAttributes = []
115+
let org = {}
116+
let consentChecked = true
105117
const returnUrl = `/teams/${this.props.id}`
106118
const team = await getTeam(id)
107119
if (team.org) {
120+
org = await getOrg(team.org.organization_id)
108121
orgAttributes = await getOrgMemberAttributes(team.org.organization_id)
122+
consentChecked = !(org && org.privacy_policy)
109123
}
110124
memberAttributes = await getTeamMemberAttributes(id)
111125
let profileValues = (await getMyProfile()).tags
112126
this.setState({
113127
id,
114128
returnUrl,
129+
team,
115130
memberAttributes,
131+
consentChecked,
132+
org,
116133
orgAttributes,
117134
profileValues,
118135
loading: false
@@ -127,7 +144,8 @@ export default class ProfileForm extends Component {
127144
}
128145

129146
render () {
130-
let { memberAttributes, orgAttributes, profileValues, returnUrl, loading } = this.state
147+
let { memberAttributes, orgAttributes, org, team, profileValues, returnUrl, consentChecked, loading } = this.state
148+
profileValues = profileValues || {}
131149

132150
if (loading) {
133151
return (
@@ -185,9 +203,12 @@ export default class ProfileForm extends Component {
185203
})
186204
const yupSchema = Yup.object().shape(schema)
187205

206+
const teamName = prop('name', team) || 'team'
207+
const orgName = prop('name', org) || 'org'
208+
188209
return (
189210
<article className='inner page'>
190-
<h1>Add Your Profile</h1>
211+
<h1>Edit your profile details</h1>
191212
<Formik
192213
enableReinitialize
193214
validateOnBlur
@@ -215,7 +236,7 @@ export default class ProfileForm extends Component {
215236
<Form>
216237
{orgAttributes.length > 0
217238
? <>
218-
<h2>Org Profile</h2>
239+
<h2>Details for <b>{orgName}</b></h2>
219240
{orgAttributes.map(attribute => {
220241
return <div className='form-control form-control__vertical'>
221242
<label>{attribute.name}
@@ -244,7 +265,7 @@ export default class ProfileForm extends Component {
244265
</>
245266
: ''
246267
}
247-
<h2>Team Profile</h2>
268+
<h2>Details for <b>{teamName}</b></h2>
248269
{ memberAttributes.length > 0 ? memberAttributes.map(attribute => {
249270
return <div className='form-control form-control__vertical'>
250271
<label>{attribute.name}
@@ -272,9 +293,22 @@ export default class ProfileForm extends Component {
272293
})
273294
: 'No profile form to fill yet'
274295
}
296+
{ org && org.privacy_policy
297+
? <div>
298+
<h2>Privacy Policy</h2>
299+
<div style={{ maxHeight: '100px', width: '80%', overflow: 'scroll', marginBottom: '1rem' }}>
300+
{org.privacy_policy.body}
301+
</div>
302+
<div style={{ maxHeight: '100px', width: '80%', overflow: 'scroll' }}>
303+
<input type='checkbox' checked={consentChecked} onChange={e => this.setConsentChecked(e.target.checked)} />
304+
{org.privacy_policy.consentText}
305+
</div>
306+
</div>
307+
: <div />
308+
}
275309
{status && status.msg && <div>{status.msg}</div>}
276-
<div style={{ marginTop: '1rem' }}className='form-control form-control__vertical'>
277-
<Button type='submit' variant='submit' disabled={isSubmitting}>
310+
<div style={{ marginTop: '1rem' }} className='form-control form-control__vertical'>
311+
<Button type='submit' variant='submit' disabled={!consentChecked || isSubmitting}>
278312
{addProfileText}
279313
</Button>
280314
</div>

compose.dev.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,4 @@ services:
5757
- POSTGRES_DB=osm-teams-test
5858
- PGDATA=/opt/postgres/data
5959
volumes:
60-
- ./docker-data/test-db:/opt/postgres/data
61-
60+
- ./docker-data/test-db:/opt/postgres/data

lib/org-api.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function createOrg (data) {
3232
*
3333
*/
3434
export async function getMyOrgs () {
35-
const res = await fetch('/api/my/organizations')
35+
const res = await fetch(join(publicRuntimeConfig.APP_URL, '/api/my/organizations'))
3636

3737
if (res.status === 200) {
3838
return res.json()
@@ -111,6 +111,21 @@ export async function updateOrg (id, values) {
111111
})
112112
}
113113

114+
/**
115+
* updateOrgPrivacyPolicy
116+
* @param {integer} id id of organization
117+
* @param {data} values data to update
118+
*/
119+
export async function updateOrgPrivacyPolicy (id, privacyPolicy) {
120+
return fetch(join(URL, `${id}`), {
121+
method: 'PUT',
122+
body: JSON.stringify({ 'privacy_policy': privacyPolicy }),
123+
headers: {
124+
'Content-Type': 'application/json; charset=utf-8'
125+
}
126+
})
127+
}
128+
114129
/**
115130
* destroyOrg
116131
* delete an org

0 commit comments

Comments
 (0)