Skip to content

Commit

Permalink
Add parent_fields to ThroughSerializer and add create button to Order…
Browse files Browse the repository at this point in the history
…edMultiSelectItem
  • Loading branch information
jochenklar committed Mar 27, 2023
1 parent 69332e0 commit ce7d6d7
Show file tree
Hide file tree
Showing 39 changed files with 656 additions and 139 deletions.
87 changes: 64 additions & 23 deletions rdmo/core/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.auth.models import Group
from django.contrib.sites.models import Site
from django.db.models import Max
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.utils import model_meta
Expand Down Expand Up @@ -61,49 +62,51 @@ def __init__(self, *args, **kwargs):
class ThroughModelSerializerMixin(object):

def create(self, validated_data):
parent_fields = self.get_parent_fields(validated_data)
through_fields = self.get_through_fields(validated_data)
instance = super().create(validated_data)
instance = self.set_through_fields(instance, through_fields)
instance = self.set_parent_fields(instance, parent_fields)
return instance

def update(self, instance, validated_data):
self.get_parent_fields(validated_data)
through_fields = self.get_through_fields(validated_data)
instance = super().update(instance, validated_data)
instance = self.set_through_fields(instance, through_fields)
return instance

def get_through_fields(self, validated_data):
try:
self.Meta.through_fields
except AttributeError:
return None

model_info = model_meta.get_field_info(self.Meta.model)

through_fields = {}
for field_name in self.Meta.through_fields:
field = self.get_fields().get(field_name)

through_model = model_info.reverse_relations[field.source].related_model

target_field_name = field.child.Meta.fields[0]
for fn, f in model_meta.get_field_info(through_model).forward_relations.items():
if fn != target_field_name:
source_field_name = fn
through_fields[field.source] = {
'source_field_name': source_field_name,
'target_field_name': target_field_name,
'through_model': through_model,
'validated_data': validated_data.pop(field.source, None)
}
for field_name, source_name, target_name, through_name in self.Meta.through_fields:
through_model = model_info.reverse_relations[through_name].related_model
through_fields[field_name] = (through_model, validated_data.pop(through_name, []))

return through_fields

def set_through_fields(self, instance, through_fields):
for field_name, field_config in through_fields.items():
if field_config['validated_data'] is not None:
items = list(getattr(instance, field_name).all())
try:
self.Meta.through_fields
except AttributeError:
return instance

for field_name, source_name, target_name, through_name in self.Meta.through_fields:
through_model, validated_data = through_fields[field_name]
if validated_data:
items = list(getattr(instance, through_name).all())

for data in field_config['validated_data']:
for data in validated_data:
try:
# look for the item in items
item = next(filter(lambda item: getattr(item, field_config['target_field_name']) ==
data.get(field_config['target_field_name']), items))
item = next(filter(lambda item: getattr(item, target_name) ==
data.get(target_name), items))
# update order if the item if it changed
if item.order != data.get('order'):
item.order = data.get('order')
Expand All @@ -114,9 +117,9 @@ def set_through_fields(self, instance, through_fields):
except StopIteration:
# create a new item
new_data = dict({
field_config['source_field_name']: instance
source_name: instance
}, **data)
new_item = field_config['through_model'](**new_data)
new_item = through_model(**new_data)
new_item.save()

# remove the remainders of the items list
Expand All @@ -125,6 +128,44 @@ def set_through_fields(self, instance, through_fields):

return instance

def get_parent_fields(self, validated_data):
try:
self.Meta.parent_fields
except AttributeError:
return None

model_info = model_meta.get_field_info(self.Meta.model)

parent_fields = {}
for field_name, source_name, target_name, through_name in self.Meta.parent_fields:
parent_model = model_info.reverse_relations[field_name].related_model
parent_model_info = model_meta.get_field_info(parent_model)

through_model = parent_model_info.reverse_relations[through_name].related_model

parent_fields[field_name] = (through_model, validated_data.pop(field_name, []))

return parent_fields

def set_parent_fields(self, instance, parent_fields):
try:
self.Meta.parent_fields
except AttributeError:
return instance

for field_name, source_name, target_name, through_name in self.Meta.parent_fields:
through_model, validated_data = parent_fields[field_name]

for parent in validated_data:
order = (getattr(parent, through_name).aggregate(order=Max('order')).get('order') or 0) + 1
through_model(**{
source_name: parent,
target_name: instance,
'order': order
}).save()

return instance


class ElementExportSerializerMixin(serializers.ModelSerializer):

Expand Down
38 changes: 18 additions & 20 deletions rdmo/management/assets/js/actions/elementActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,8 @@ export function fetchElementsError(error) {

// fetch element

export function fetchElement(elementType, elementId, elementAction) {
export function fetchElement(elementType, elementId, elementAction=null) {
return function(dispatch, getState) {
if (isNil(elementAction)) elementAction = null

updateLocation(getState().config.baseUrl, elementType, elementId, elementAction)

dispatch(fetchElementInit(elementType, elementId, elementAction))
Expand Down Expand Up @@ -386,7 +384,7 @@ export function storeElementError(element, error) {

// createElement

export function createElement(elementType) {
export function createElement(elementType, parent={}) {
return function(dispatch, getState) {
updateLocation(getState().config.baseUrl, elementType, null, 'create')

Expand All @@ -396,7 +394,7 @@ export function createElement(elementType) {
switch (elementType) {
case 'catalogs':
action = (dispatch) => Promise.all([
QuestionsFactory.createCatalog(),
QuestionsFactory.createCatalog(getState().config),
QuestionsApi.fetchSections('index'),
CoreApi.fetchGroups(),
CoreApi.fetchSites(),
Expand All @@ -407,42 +405,42 @@ export function createElement(elementType) {

case 'sections':
action = (dispatch) => Promise.all([
QuestionsFactory.createSection(),
QuestionsFactory.createSection(getState().config, parent),
QuestionsApi.fetchPages('index'),
]).then(([element, pages]) => dispatch(createElementSuccess({
element, pages
element, parent, pages
})))
break

case 'pages':
action = (dispatch) => Promise.all([
QuestionsFactory.createPage(),
QuestionsFactory.createPage(getState().config, parent),
DomainApi.fetchAttributes('index'),
ConditionsApi.fetchConditions('index'),
QuestionsApi.fetchQuestionSets('index'),
QuestionsApi.fetchQuestions('index')
]).then(([element, attributes, conditions,
questionsets, questions]) => dispatch(createElementSuccess({
element, attributes, conditions, questionsets, questions
element, parent, attributes, conditions, questionsets, questions
})))
break

case 'questionsets':
action = (dispatch) => Promise.all([
QuestionsFactory.createQuestionSet(),
QuestionsFactory.createQuestionSet(getState().config, parent),
DomainApi.fetchAttributes('index'),
ConditionsApi.fetchConditions('index'),
QuestionsApi.fetchQuestionSets('index'),
QuestionsApi.fetchQuestions('index')
]).then(([element, attributes, conditions,
questionsets, questions]) => dispatch(createElementSuccess({
element, attributes, conditions, questionsets, questions
element, parent, attributes, conditions, questionsets, questions
})))
break

case 'questions':
action = (dispatch) => Promise.all([
QuestionsFactory.createQuestion(),
QuestionsFactory.createQuestion(getState().config, parent),
DomainApi.fetchAttributes('index'),
OptionsApi.fetchOptionSets('index'),
OptionsApi.fetchOptions('index'),
Expand All @@ -451,13 +449,13 @@ export function createElement(elementType) {
QuestionsApi.fetchValueTypes()
]).then(([element, attributes, optionsets, options, conditions,
widgetTypes, valueTypes]) => dispatch(createElementSuccess({
element, attributes, optionsets, options, conditions, widgetTypes, valueTypes
element, parent, attributes, optionsets, options, conditions, widgetTypes, valueTypes
})))
break

case 'attributes':
action = (dispatch) => Promise.all([
DomainFactory.createAttribute(),
DomainFactory.createAttribute(getState().config),
DomainApi.fetchAttributes('index'),
]).then(([element, attributes]) => dispatch(createElementSuccess({
element, attributes
Expand All @@ -466,7 +464,7 @@ export function createElement(elementType) {

case 'optionsets':
action = (dispatch) => Promise.all([
OptionsFactory.createOptionSet(),
OptionsFactory.createOptionSet(getState().config),
OptionsApi.fetchOptions('index'),
OptionsApi.fetchProviders(),
]).then(([element, options, providers]) => dispatch(createElementSuccess({
Expand All @@ -476,16 +474,16 @@ export function createElement(elementType) {

case 'options':
action = (dispatch) => Promise.all([
OptionsFactory.createOption(),
OptionsFactory.createOption(getState().config, parent),
OptionsApi.fetchOptionSets('index'),
]).then(([element, optionsets]) => dispatch(createElementSuccess({
element, optionsets
element, parent, optionsets
})))
break

case 'conditions':
action = (dispatch) => Promise.all([
ConditionsFactory.createCondition(),
ConditionsFactory.createCondition(getState().config),
ConditionsApi.fetchRelations(),
DomainApi.fetchAttributes('index'),
OptionsApi.fetchOptions('index'),
Expand All @@ -496,7 +494,7 @@ export function createElement(elementType) {

case 'tasks':
action = (dispatch) => Promise.all([
TasksFactory.createTask(),
TasksFactory.createTask(getState().config),
DomainApi.fetchAttributes('index'),
ConditionsApi.fetchConditions('index'),
QuestionsApi.fetchCatalogs('index'),
Expand All @@ -510,7 +508,7 @@ export function createElement(elementType) {

case 'views':
action = (dispatch) => Promise.all([
ViewsFactory.createView(),
ViewsFactory.createView(getState().config),
QuestionsApi.fetchCatalogs('index'),
CoreApi.fetchSites(),
CoreApi.fetchGroups()
Expand Down
11 changes: 11 additions & 0 deletions rdmo/management/assets/js/components/common/Buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,15 @@ DeleteButton.propTypes = {
onClick: PropTypes.func.isRequired
}

const DefaultButton = ({ text, onClick }) => (
<button className="element-button btn btn-default" onClick={event => onClick()}>
{text}
</button>
)

DeleteButton.propTypes = {
onClick: PropTypes.func.isRequired
}


export { BackButton, SaveButton, CreateButton, NewButton, DeleteButton }
46 changes: 44 additions & 2 deletions rdmo/management/assets/js/components/common/Links.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,49 @@ EditLink.propTypes = {
onClick: PropTypes.func.isRequired
}

const AddLink = ({ element, verboseName, onClick }) => {
const handleClick = (event) => {
event.preventDefault()
onClick()
}

const title = interpolate(gettext('Add %s'), [verboseName])

return (
<a href="" className="element-link fa fa-plus"
title={title}
onClick={event => handleClick(event)}>
</a>
)
}

AddLink.propTypes = {
element: PropTypes.object.isRequired,
verboseName: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
}

const AddSquareLink = ({ element, verboseName, onClick }) => {
const handleClick = (event) => {
event.preventDefault()
onClick()
}

const title = interpolate(gettext('Add %s'), [verboseName])

return (
<a href="" className="element-link fa fa-plus-square"
title={title}
onClick={event => handleClick(event)}>
</a>
)
}

AddSquareLink.propTypes = {
element: PropTypes.object.isRequired,
verboseName: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
}

const AvailableLink = ({ element, verboseName, onClick }) => {
const handleClick = (event) => {
Expand Down Expand Up @@ -153,5 +196,4 @@ ExtendLink.propTypes = {
onClick: PropTypes.func.isRequired
}


export { EditLink, AvailableLink, LockedLink, NestedLink, ExportLink, ExtendLink }
export { EditLink, AddLink, AddSquareLink, AvailableLink, LockedLink, NestedLink, ExportLink, ExtendLink }
8 changes: 5 additions & 3 deletions rdmo/management/assets/js/components/edit/EditAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ const EditAttribute = ({ config, attribute, elements, elementActions }) => {
}
</div>

<div className="panel-body panel-border">
{ info }
</div>
{
attribute.id && <div className="panel-body panel-border">
{ info }
</div>
}

<div className="panel-body">
<div className="row">
Expand Down
11 changes: 7 additions & 4 deletions rdmo/management/assets/js/components/edit/EditCatalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const EditCatalog = ({ config, catalog, elements, elementActions }) => {
const updateCatalog = (key, value) => elementActions.updateElement(catalog, {[key]: value})
const storeCatalog = () => elementActions.storeElement('catalogs', catalog)
const deleteCatalog = () => elementActions.deleteElement('catalogs', catalog)
const createSection = () => elementActions.createElement('sections', catalog)

const [showDeleteModal, openDeleteModal, closeDeleteModal] = useDeleteModal()

Expand All @@ -46,9 +47,11 @@ const EditCatalog = ({ config, catalog, elements, elementActions }) => {
}
</div>

<div className="panel-body panel-border">
{ info }
</div>
{
catalog.id && <div className="panel-body panel-border">
{ info }
</div>
}

<div className="panel-body">
<div className="row">
Expand All @@ -70,7 +73,7 @@ const EditCatalog = ({ config, catalog, elements, elementActions }) => {
<div className="col-sm-12">
<OrderedMultiSelect config={config} element={catalog} field="sections"
options={sections} verboseName="section"
onChange={updateCatalog} />
onChange={updateCatalog} onCreate={createSection} />
</div>
<div className="col-sm-12">
<Tabs id="#catalog-tabs" defaultActiveKey={0} animation={false}>
Expand Down

0 comments on commit ce7d6d7

Please sign in to comment.