From 82b5015be60ce9374856933eb35e37a817958de5 Mon Sep 17 00:00:00 2001 From: dallasjc <39286778+dallasjc@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:51:43 -0500 Subject: [PATCH 01/16] Allow all users to import external arguments --- models/import.js | 2 +- views/import-vote-page.js | 4 ++-- views/measure-comments.js | 19 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/models/import.js b/models/import.js index 0b8dacf..950035a 100644 --- a/models/import.js +++ b/models/import.js @@ -8,7 +8,7 @@ module.exports = (event, state) => { case '/legislation/:shortId/import': case '/nominations/:shortId/import': case '/:username/:shortId/import': - if (!state.user || !state.user.is_admin) return [{ ...state, loading: { page: true } }, redirect('/')] + if (!state.user) return [state, redirect('/join')] return [state] default: return [state] diff --git a/views/import-vote-page.js b/views/import-vote-page.js index cc1a3ab..4d87aa5 100644 --- a/views/import-vote-page.js +++ b/views/import-vote-page.js @@ -6,9 +6,9 @@ module.exports = ({ error, location, user }, dispatch) => {

Import Argument to ${location.params.shortId}

- ${!user || !user.is_admin ? html`
You do not have permission to import votes.
` : ''} + ${!user ? html`
Login to import votes.
` : ''} -
+ ${error ? html`
${error.message}
` : ''} diff --git a/views/measure-comments.js b/views/measure-comments.js index 3e3c8e1..49b7017 100644 --- a/views/measure-comments.js +++ b/views/measure-comments.js @@ -28,7 +28,7 @@ const voteOrSignatureView = (state, dispatch) => (vote) => { const noCommentsView = () => html`

No comments yet.

` const filtersView = (state, dispatch) => { - const { loading, location, measures, user } = state + const { loading, location, measures } = state const measure = measures[location.params.shortId] const pagination = measure.commentsPagination || { offset: 0, limit: 25 } const { path, query } = location @@ -107,16 +107,15 @@ const filtersView = (state, dispatch) => {
- ${user && user.is_admin ? html` -
-
- - ${icon(faPlus)} - Import external argument - -
+ +
+ - ` : ''} +
From eecf0e2b788e16d444d08cac8d7a274416cfdc22 Mon Sep 17 00:00:00 2001 From: Dallas Cole Date: Thu, 26 Sep 2019 16:21:28 -0500 Subject: [PATCH 02/16] Convert proxy-search form to author-search --- effects/import.js | 17 +++ models/import.js | 17 +++ views/import-author.js | 221 ++++++++++++++++++++++++++++++++++++++ views/import-vote-page.js | 15 ++- 4 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 effects/import.js create mode 100644 views/import-author.js diff --git a/effects/import.js b/effects/import.js new file mode 100644 index 0000000..53bcd76 --- /dev/null +++ b/effects/import.js @@ -0,0 +1,17 @@ +const { api } = require('../helpers') + +exports.searchAuthor = ({ event, ...formData }, user) => (dispatch) => { + const terms = formData.add_author && formData.add_author.search && formData.add_author.search + + if (!terms) return dispatch({ type: 'authorSearchResultsUpdated', results: [], terms }) + + return api(dispatch, `/search_results_detailed?terms=fts(english).${encodeURIComponent(terms.replace(/ /g, ':* & ').replace(/$/, ':*'))}&resource_type=eq.user&limit=5`, { user }) + .then((results) => { + return dispatch({ + type: 'import:authorSearchResultsUpdated', + results: results.filter(({ resource }) => resource.twitter_username !== 'dsernst').map(({ resource }) => resource), + terms, + }) + }) + .catch(error => dispatch({ type: 'error', error })) +} diff --git a/models/import.js b/models/import.js index 950035a..2d89d5b 100644 --- a/models/import.js +++ b/models/import.js @@ -13,6 +13,18 @@ module.exports = (event, state) => { default: return [state] } + case 'import:authorSearched': + return [{ + ...state, + loading: { ...state.loading, proxySearch: true }, + }, combineEffects([preventDefault(event.event), importEffect('searchAuthor', event, state.user)])] + case 'import:authorSearchResultsUpdated': + return [{ + ...state, + loading: { ...state.loading, authorSearch: false }, + authorSearchResults: event.results, + authorSearchTerms: event.terms, + }] case 'import:voteImportFormSubmitted': return [{ ...state, @@ -64,3 +76,8 @@ const importVote = ({ type, event, ...form }, url, user) => (dispatch) => { .then(() => dispatch({ type: 'redirected', url: url.replace('/import', '') })) .catch((error) => dispatch({ type: 'error', error })) } +const importEffect = (name, ...args) => (dispatch) => { + return import('../effects/import').then((effects) => { + return (effects.default || effects)[name].apply(null, args)(dispatch) + }) +} diff --git a/views/import-author.js b/views/import-author.js new file mode 100644 index 0000000..58bffb0 --- /dev/null +++ b/views/import-author.js @@ -0,0 +1,221 @@ +const { APP_NAME } = process.env +const { avatarURL, handleForm, html } = require('../helpers') +const { icon } = require('@fortawesome/fontawesome-svg-core') +const { faUser } = require('@fortawesome/free-solid-svg-icons/faUser') +const { faTwitter } = require('@fortawesome/free-brands-svg-icons/faTwitter') +const { faExclamationTriangle } = require('@fortawesome/free-solid-svg-icons/faExclamationTriangle') +const { faEnvelope } = require('@fortawesome/free-solid-svg-icons/faEnvelope') +const { faHandshake } = require('@fortawesome/free-solid-svg-icons/faHandshake') +const { faPlus } = require('@fortawesome/free-solid-svg-icons/faPlus') + +module.exports = (state, dispatch) => { + const { location } = state + const tab = location.query.tab || 'search' + const path = location.path + + return html` +
+ + ${tab === 'email' ? addAuthorByEmailForm(state, dispatch) : []} + ${tab === 'twitter' ? addAuthorByTwitterForm(state, dispatch) : []} + ${tab === 'search' ? addAuthorBySearchForm(state, dispatch) : []} +
+ ` +} + +const addAuthorByEmailForm = (state, dispatch) => { + const { error } = state + + return html` +
+ +
+
+
+
+ + ${error && error.name + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faUser)}` + } + ${error && error.name ? html`

${error.message}

` : ''} +
+
+
+
+ + ${error && error.name + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faUser)}` + } + ${error && error.name ? html`

${error.message}

` : ''} +
+
+
+
+ + ${error && error.email + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faEnvelope)}` + } + ${error && error.email ? html`

${error.message}

` : ''} +
+
+
+ + ${error && error.name + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faUser)}` + } + ${error && error.name ? html`

${error.message}

` : ''} +
+
+
+
+
+ +
+
+
+
+

They'll be sent a notification email.

+
+ ` +} + +const addAuthorByTwitterForm = (state, dispatch) => { + const { error } = state + return html` +
+ +
+
+
+
+ + ${error && error.message + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faTwitter)}` + } + ${error && error.message ? html`

${error.message}

` : ''} +
+
+ +
+
+
+
+

They'll be sent an invitation tweet.

+
+ ` +} + +const addAuthorBySearchForm = (state, dispatch) => { + const { error, loading, proxies, authorSearchResults = [], authorSearchTerms } = state + + return html` + +
+ +
+
+ + ${error && error.message + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faUser)}` + } + ${error && error.message ? html`

${error.message}

` : ''} +
+
+ +
+
+
+
+
+ ${authorSearchTerms && !authorSearchResults.length ? html`

No results for "${authorSearchTerms}"

` : ''} + ${authorSearchResults.map(result => searchResult(proxies, result, dispatch))} + ${authorSearchTerms ? html`

Can't find who you're looking for?
Add them by email or Twitter username.

` : ''} +
+ ` +} + +const searchResult = (proxies, result, dispatch) => { + const { first_name, id, last_name, username, twitter_username } = result + + return html` +
+
+
+ ${username || twitter_username + ? html` + + + + ` : html` + + `} +
+
+ +
+ ${proxies.some(({ to_id }) => to_id === id) + ? searchResultAdded({ id, proxies }, dispatch) : searchResultAdd(id, dispatch)} +
+
+ ` +} + +const searchResultAdd = (id, dispatch) => { + return html` +
+ + +
+ ` +} + +const searchResultAdded = ({ id }) => { + return html` +
+ + +
+ ` +} diff --git a/views/import-vote-page.js b/views/import-vote-page.js index 4d87aa5..0f8845f 100644 --- a/views/import-vote-page.js +++ b/views/import-vote-page.js @@ -1,6 +1,8 @@ const { handleForm, html } = require('../helpers') +const authorForm = require('./import-author') -module.exports = ({ error, location, user }, dispatch) => { +module.exports = (state, dispatch) => { + const { error, location, user } = state return html`
@@ -11,7 +13,10 @@ module.exports = ({ error, location, user }, dispatch) => {
${error ? html`
${error.message}
` : ''} - +
+ + ${authorForm(state, dispatch)} +
@@ -29,12 +34,6 @@ module.exports = ({ error, location, user }, dispatch) => {
-
- -
- -
-
From 18f7b413bd0e0b3a3205877d42787a331c54cf1f Mon Sep 17 00:00:00 2001 From: dallasjc <39286778+dallasjc@users.noreply.github.com> Date: Thu, 26 Sep 2019 18:20:29 -0500 Subject: [PATCH 03/16] Make searchAuthor form functional --- effects/import.js | 101 ++++++++++++++++++++++++++++++++++++++ models/import.js | 6 +++ views/import-author.js | 47 +++++++++--------- views/import-vote-page.js | 3 +- 4 files changed, 133 insertions(+), 24 deletions(-) diff --git a/effects/import.js b/effects/import.js index 53bcd76..5ce79a2 100644 --- a/effects/import.js +++ b/effects/import.js @@ -1,4 +1,5 @@ const { api } = require('../helpers') +const fetch = require('isomorphic-fetch') exports.searchAuthor = ({ event, ...formData }, user) => (dispatch) => { const terms = formData.add_author && formData.add_author.search && formData.add_author.search @@ -15,3 +16,103 @@ exports.searchAuthor = ({ event, ...formData }, user) => (dispatch) => { }) .catch(error => dispatch({ type: 'error', error })) } +exports.addAuthorViaEmail = ({ event, ...formData }, proxies, user) => (dispatch) => { + if (!formData.add_author) return + + const name = formData.add_author.name.trim().split(' ') + + if (name.length < 2) { + return dispatch({ type: 'error', error: Object.assign(new Error('Please enter a first and last name'), { name: true }) }) + } else if (name.length > 5) { + return dispatch({ type: 'error', error: Object.assign(new Error('Please enter only a first and last name'), { name: true }) }) + } + + const first_name = formData.add_author.name.trim().split(' ')[0] + const last_name = formData.add_author.name.trim().split(' ').slice(1).join(' ') + + return api(dispatch, '/delegations', { + method: 'POST', + headers: { Prefer: 'return=representation' }, // returns created delegation in response + body: JSON.stringify({ + from_id: user.id, + first_name, + last_name, + email: formData.add_author.email ? formData.add_author.email.toLowerCase().trim() : null, + }), + user, + }) + .then((delegations) => { + if (event) event.target.reset() + return api(dispatch, `/delegations_detailed?id=eq.${delegations[0].id}`, { user }).then(profiles => { + return dispatch({ + type: 'proxy:proxiesUpdated', + proxies: (proxies || []).concat(profiles[0] || delegations[0]), + }) + }) + }) + .catch(errorHandler(dispatch)) +} + +exports.addAuthorViaTwitter = ({ event, ...formData }, proxies, user) => (dispatch) => { + if (!formData.add_author) return + + let twitter_username = null + if (formData.add_author.twitter_username) { + twitter_username = formData.add_author.twitter_username.trim().replace(/@/g, '') + } + + return fetch('/rpc/twitter_username_search', { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ twitter_username }), + }) + .then(res => { + if (res.status === 404) { + return res.json().then(json => { + return Promise.reject(new Error(json.message)) + }) + } + return res.json() + }) + .then((twitter_user) => { + return api(dispatch, '/delegations', { + method: 'POST', + headers: { Prefer: 'return=representation' }, // returns created delegation in response + body: JSON.stringify({ + from_id: user.id, + twitter_username: twitter_user.twitter_username.replace(/@/g, ''), + twitter_displayname: twitter_user.name, + twitter_avatar: twitter_user.avatar, + bio: twitter_user.description, + delegate_rank: 0, + }), + user, + }) + .then((newProxies) => { + if (event) event.target.reset() + return api(dispatch, `/delegations_detailed?id=eq.${newProxies[0].id}`, { user }).then(profiles => { + return dispatch({ + type: 'proxy:proxiesUpdated', + proxies: (proxies || []).concat(profiles[0] || newProxies[0]), + }) + }) + }) + }) + .catch(errorHandler(dispatch)) +} + +exports.addAuthorViaSearch = ({ event, ...formData }) => (dispatch) => { + dispatch({ type: 'cookieSet', key: 'author_id', value: formData.author_id }) + dispatch({ type: 'cookieSet', key: 'author_username', value: formData.author_username }) +} +const errorHandler = (error) => { + if (error.code === 23514) { + if (~error.message.indexOf('email')) { + error.email = true + error.message = 'Invalid email address' + } + } +} diff --git a/models/import.js b/models/import.js index 2d89d5b..ae0165a 100644 --- a/models/import.js +++ b/models/import.js @@ -18,6 +18,12 @@ module.exports = (event, state) => { ...state, loading: { ...state.loading, proxySearch: true }, }, combineEffects([preventDefault(event.event), importEffect('searchAuthor', event, state.user)])] + case 'import:addedAuthorViaEmail': + return [state, combineEffects([preventDefault(event.event), importEffect('addAuthorViaEmail', event, state.user)])] + case 'import:addedAuthorViaTwitter': + return [state, combineEffects([preventDefault(event.event), importEffect('addAuthorViaTwitter', event, state.user)])] + case 'import:addedAuthorViaSearch': + return [state, combineEffects([preventDefault(event.event), importEffect('addAuthorViaSearch', event, state.user)])] case 'import:authorSearchResultsUpdated': return [{ ...state, diff --git a/views/import-author.js b/views/import-author.js index 58bffb0..3bd36c4 100644 --- a/views/import-author.js +++ b/views/import-author.js @@ -4,8 +4,8 @@ const { icon } = require('@fortawesome/fontawesome-svg-core') const { faUser } = require('@fortawesome/free-solid-svg-icons/faUser') const { faTwitter } = require('@fortawesome/free-brands-svg-icons/faTwitter') const { faExclamationTriangle } = require('@fortawesome/free-solid-svg-icons/faExclamationTriangle') +const { faEdit } = require('@fortawesome/free-solid-svg-icons/faEdit') const { faEnvelope } = require('@fortawesome/free-solid-svg-icons/faEnvelope') -const { faHandshake } = require('@fortawesome/free-solid-svg-icons/faHandshake') const { faPlus } = require('@fortawesome/free-solid-svg-icons/faPlus') module.exports = (state, dispatch) => { @@ -34,7 +34,7 @@ const addAuthorByEmailForm = (state, dispatch) => { return html` - +
@@ -57,6 +57,16 @@ const addAuthorByEmailForm = (state, dispatch) => { ${error && error.name ? html`

${error.message}

` : ''}
+
+
+ + ${error && error.name + ? html`${icon(faExclamationTriangle)}` + : html`${icon(faUser)}` + } + ${error && error.name ? html`

${error.message}

` : ''} +
+
@@ -66,21 +76,11 @@ const addAuthorByEmailForm = (state, dispatch) => { } ${error && error.email ? html`

${error.message}

` : ''}
-
-
- - ${error && error.name - ? html`${icon(faExclamationTriangle)}` - : html`${icon(faUser)}` - } - ${error && error.name ? html`

${error.message}

` : ''} -
-
@@ -110,7 +110,7 @@ const addAuthorByTwitterForm = (state, dispatch) => {
@@ -123,7 +123,7 @@ const addAuthorByTwitterForm = (state, dispatch) => { } const addAuthorBySearchForm = (state, dispatch) => { - const { error, loading, proxies, authorSearchResults = [], authorSearchTerms } = state + const { error, loading, cookies, authorSearchResults = [], authorSearchTerms } = state return html`