Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interview import dataset #1188

Draft
wants to merge 25 commits into
base: interview-copy-dataset
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a96d85c
Add import function to create set modal on pages
jochenklar Nov 7, 2024
271d1d9
Add separate search endpoint for values and use AsyncSelect in PageHe…
jochenklar Nov 8, 2024
9eda9d9
Fix errors after rebase
jochenklar Nov 8, 2024
30194d2
Add error handling to copy-set action and fix/add tests
jochenklar Nov 8, 2024
80e5ef9
Improve PageHeadFormModal
jochenklar Nov 8, 2024
e88a844
Add import function into existing set
jochenklar Nov 8, 2024
4dff50f
Update/fix tests
jochenklar Nov 8, 2024
8f9f3a6
Rename import/copy answers to reuse answers
jochenklar Nov 9, 2024
b9a547a
Fix copy_set action and update tests
jochenklar Nov 9, 2024
ef84164
Add reuse value modal for questions
jochenklar Nov 9, 2024
6ed31f2
Add reuse value model to checkboxes
jochenklar Nov 10, 2024
c8de022
Add project filter to reuse value modal
jochenklar Nov 10, 2024
30e3742
Add useLsState hook and use it to store reuse modal information
jochenklar Nov 10, 2024
40208cd
Fix page component
jochenklar Nov 10, 2024
6c5e2f3
Fix react select error state
jochenklar Nov 10, 2024
e4d35a3
Fix reuse modal
jochenklar Nov 10, 2024
9d25784
Add tests for value search endpoint
jochenklar Nov 10, 2024
0a6abff
Fix QuestionSetCopySet component
jochenklar Nov 10, 2024
29dbaa5
Fix Page component
jochenklar Nov 10, 2024
8055cb6
Refactor interview modals
jochenklar Nov 10, 2024
71d4b84
Fix Value model
jochenklar Nov 10, 2024
7984080
Fix typo
jochenklar Nov 11, 2024
d88ad3e
Update .gitignore
jochenklar Nov 12, 2024
24f6831
Fix radio input
jochenklar Nov 12, 2024
3966d34
Fix search component
jochenklar Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
__pycache__
*.pyc
*~
*.swp
.DS_Store

testing/config/settings/local.py
testing/log/
.pytest*/
Expand All @@ -6,19 +12,12 @@ static_root
media_root
components_root

docs/_build*

__pycache__
*.pyc
*~
*.swp
.DS_Store
.ruff_cache

env
env2
env3

Makefile

.coverage
.coverage.*
htmlcov
Expand All @@ -32,16 +31,18 @@ dist
.imdone/

.pytest_cache
.ruff_cache
.on-save.json

rdmo/management/static

rdmo/core/static/core/js/base.js
rdmo/core/static/core/js/base.js*
rdmo/core/static/core/fonts
rdmo/core/static/core/css/base.css

rdmo/projects/static/projects/js/*.js
rdmo/projects/static/projects/js/interview.js*
rdmo/projects/static/projects/js/projects.js*
rdmo/projects/static/projects/fonts
rdmo/projects/static/projects/css/*.css
rdmo/projects/static/projects/css

screenshots
41 changes: 41 additions & 0 deletions rdmo/core/assets/js/hooks/useLsState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react'
import { isEmpty, isPlainObject, omit as lodashOmit } from 'lodash'

import { checkStoreId } from '../utils/store'

const useLsState = (path, initialValue, omit = []) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this localStorage useLsState hook just a re-implementation of what was already stored in localStorage before or is it a new state altogether??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we had configActions which are synced with the local storage. Here I bypass the redux store and just use a useState hook which is synced to the local storage.

checkStoreId()

const getLs = (path) => {
const data = JSON.parse(localStorage.getItem(path))
return isPlainObject(data) ? lodashOmit(data, omit) : data
}

const setLs = (path, value) => {
const data = isPlainObject(value) ? lodashOmit(value, omit) : value
localStorage.setItem(path, JSON.stringify(data))
}

// get the value from the local storage
const lsValue = getLs(path)

// setup the state with the value from the local storage or the provided initialValue
const [value, setValue] = useState(isEmpty(lsValue) ? initialValue : lsValue)

return [
value,
(value) => {
setLs(path, value)
setValue(value)
},
() => {
if (isPlainObject(initialValue)) {
setValue({ ...initialValue, ...getLs(path) })
} else {
setValue(initialValue)
}
}
]
}

export default useLsState
5 changes: 5 additions & 0 deletions rdmo/core/assets/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ form .yesno label {
/* modals */

.modal-body {
> div:last-child,
> p:last-child,
formgroup:last-child .form-group {
margin-bottom: 0;
Expand Down Expand Up @@ -495,3 +496,7 @@ li.has-warning > a.control-label > i {
color: $link-color;
cursor: pointer;
}

.has-error .react-select__control {
border-color: #a94442;
}
2 changes: 2 additions & 0 deletions rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@

OPTIONSET_PROVIDERS = []

PROJECT_VALUES_SEARCH_LIMIT = 10

PROJECT_VALUES_VALIDATION = False

PROJECT_VALUES_VALIDATION_URL = True
Expand Down
21 changes: 13 additions & 8 deletions rdmo/projects/assets/js/interview/actions/interviewActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,18 +563,20 @@ export function deleteSetError(errors) {
return {type: DELETE_SET_ERROR, errors}
}

export function copySet(currentSet, currentSetValue, attrs) {
const pendingId = `copySet/${currentSet.set_prefix}/${currentSet.set_index}`
export function copySet(currentSet, copySetValue, attrs) {
const pendingId = isNil(currentSet) ? 'copySet' : `copySet/${currentSet.set_prefix}/${currentSet.set_index}`

return (dispatch, getState) => {
dispatch(addToPending(pendingId))
dispatch(copySetInit())

// create a new set
const set = SetFactory.create(attrs)
// create a new set (create) or use the current one (import)
const set = isNil(attrs.id) ? SetFactory.create(attrs) : currentSet

// create a value for the text if the page has an attribute
const value = isNil(attrs.attribute) ? null : ValueFactory.create(attrs)
// create a value for the text if the page has an attribute (create) or use the current one (import)
const value = isNil(attrs.attribute) ? null : (
isNil(attrs.id) ? ValueFactory.create(attrs) : attrs
)

// create a callback function to be called immediately or after saving the value
const copySetCallback = (setValues) => {
Expand All @@ -583,7 +585,10 @@ export function copySet(currentSet, currentSetValue, attrs) {
const state = getState().interview

const page = state.page
const values = [...state.values, ...setValues]
const values = [
...state.values.filter(v => !setValues.some(sv => compareValues(v, sv))), // remove updated values
...setValues
]
const sets = gatherSets(values)

initSets(sets, page)
Expand Down Expand Up @@ -616,7 +621,7 @@ export function copySet(currentSet, currentSetValue, attrs) {
})
)
} else {
promise = ValueApi.copySet(projectId, currentSetValue, value)
promise = ValueApi.copySet(projectId, value, copySetValue)
}

return promise.then((values) => {
Expand Down
4 changes: 4 additions & 0 deletions rdmo/projects/assets/js/interview/api/ProjectApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import BaseApi from 'rdmo/core/assets/js/api/BaseApi'

class ProjectsApi extends BaseApi {

static fetchProjects(params) {
return this.get(`/api/v1/projects/projects/?${encodeParams(params)}`)
}

static fetchOverview(projectId) {
return this.get(`/api/v1/projects/projects/${projectId}/overview/`)
}
Expand Down
12 changes: 9 additions & 3 deletions rdmo/projects/assets/js/interview/api/ValueApi.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import BaseApi from 'rdmo/core/assets/js/api/BaseApi'
import { encodeParams } from 'rdmo/core/assets/js/utils/api'
import isUndefined from 'lodash/isUndefined'
import { isUndefined } from 'lodash'

class ValueApi extends BaseApi {

static fetchValues(projectId, params) {
return this.get(`/api/v1/projects/projects/${projectId}/values/?${encodeParams(params)}`)
}

static searchValues(params) {
return this.get(`/api/v1/projects/values/search/?${encodeParams(params)}`)
}

static storeValue(projectId, value) {
if (isUndefined(value.id)) {
return this.post(`/api/v1/projects/projects/${projectId}/values/`, value)
Expand All @@ -29,8 +33,10 @@ class ValueApi extends BaseApi {
}
}

static copySet(projectId, currentSetValue, setValue) {
return this.post(`/api/v1/projects/projects/${projectId}/values/${currentSetValue.id}/set/`, setValue)
static copySet(projectId, setValue, copySetValue) {
return this.post(`/api/v1/projects/projects/${projectId}/values/set/`, {
...setValue, copy_set_value: copySetValue.id
})
}

static deleteSet(projectId, setValue) {
Expand Down
135 changes: 135 additions & 0 deletions rdmo/projects/assets/js/interview/components/main/Search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react'
import PropTypes from 'prop-types'
import AsyncSelect from 'react-select/async'
import { useDebouncedCallback } from 'use-debounce'
import { isEmpty, isNil, pick } from 'lodash'

import ProjectApi from '../../api/ProjectApi'
import ValueApi from '../../api/ValueApi'

const Search = ({ attribute, values, setValues, collection = false }) => {
// create a key for the first AsyncSelect, to reset the loaded values when project or snapshot changes
const key = (values.project ? values.project.id : '') + (values.snapshot ? '-all' : '')

const handleLoadValues = useDebouncedCallback((search, callback) => {
ValueApi.searchValues({
attribute,
search,
project: values.project ? values.project.id : '',
snapshot: values.snapshot ? 'all' : '',
collection
}).then(response => {
if (collection) {
// if the search component is used from QuestionReuseValues/CheckboxWidget
// the list of values from the server needs to be reduced to show only one entry
// for each set_prefix/set_index combination
callback(response.reduce((collections, value) => {
// look if a value for the same project/snapshot/set_prefix/set_index already exists in values_list
const collection = isNil(collections) ? null : collections.find(v => (
(v.project == value.project) &&
(v.snapshot == value.snapshot) &&
(v.set_prefix == value.set_prefix) &&
(v.set_index == value.set_index)
))
if (isNil(collection)) {
// append the value
return [...collections, { ...value, values: [value] }]
} else {
// update the value_title and the values array of the existing value
collection.value_label += '; ' + value.value_label
collection.values.push(value)
return collections
}
}, []))
} else {
callback(response)
}
})
}, 500)

const handleLoadProjects = useDebouncedCallback((search, callback) => {
ProjectApi.fetchProjects({ search })
.then(response => callback(response.results.map(project => pick(project, 'id', 'title'))))
}, 500)

return <>
<AsyncSelect
key={key}
classNamePrefix="react-select"
className='react-select'
placeholder={gettext('Search for project or snapshot title, or answer text ...')}
noOptionsMessage={() => gettext(
'No answers match your search.'
)}
loadingMessage={() => gettext('Loading ...')}
options={[]}
value={values.value}
onChange={(value) => setValues({ ...values, value })}
getOptionValue={(value) => value}
getOptionLabel={(value) => value.value_label}
formatOptionLabel={(value) => (
<div>
{gettext('Project')} <strong>{value.project_label}</strong>
{
value.snapshot && <>
<span className="mr-5 ml-5">&rarr;</span>
{gettext('Snapshot')} <strong>{value.snapshot_label}</strong>
</>
}
<span className="mr-5 ml-5">&rarr;</span>
{value.value_label}
</div>
)}
loadOptions={handleLoadValues}
defaultOptions
isClearable
backspaceRemovesValue={true}
/>

<AsyncSelect
classNamePrefix='react-select'
className='react-select mt-10'
placeholder={gettext('Restrict the search to a particular project ...')}
noOptionsMessage={() => gettext(
'No projects matching your search.'
)}
loadingMessage={() => gettext('Loading ...')}
options={[]}
value={values.project}
onChange={(project) => setValues({
...values,
value: (isEmpty(project) || project == values.project) ? values.value : '', // reset value
project: project
})}
getOptionValue={(project) => project}
getOptionLabel={(project) => project.title}
loadOptions={handleLoadProjects}
defaultOptions
isClearable
backspaceRemovesValue={true}
/>

<div className="checkbox">
<label>
<input
type="checkbox"
checked={values.snapshot}
onChange={() => setValues({
...values,
value: values.snapshot ? '' : values.value, // reset value
snapshot: !values.snapshot })}
/>
<span>{gettext('Include snapshots in the search')}</span>
</label>
</div>
</>
}

Search.propTypes = {
attribute: PropTypes.number.isRequired,
values: PropTypes.object.isRequired,
setValues: PropTypes.func.isRequired,
collection: PropTypes.bool
}

export default Search
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import get from 'lodash/get'
import { isNil } from 'lodash'
import { isNil, minBy } from 'lodash'

import Html from 'rdmo/core/assets/js/components/Html'

Expand All @@ -21,8 +21,8 @@ const Page = ({ config, templates, overview, page, sets, values, fetchPage,

// sanity check
if (isNil(currentSet)) {
currentSetIndex = 0
currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == 0))
currentSetIndex = get(minBy(sets, 'set_index'), 'set_index', 0)
currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == currentSetIndex))
}

const isManager = (overview.is_superuser || overview.is_editor || overview.is_reviewer)
Expand Down
Loading
Loading