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

Fix #761: Attributes that depend on party type don't show up on party creation #1076

Merged
merged 1 commit into from
Feb 7, 2017

Conversation

bjohare
Copy link
Contributor

@bjohare bjohare commented Jan 24, 2017

Proposed changes in this pull request

Fixes #761

  • Adds a SchemaSelectorMixin to provide a queryable interface to all project attributes namespaced by ContentType.
  • Adds an AttributeFormMixin to provide common attribute form processing.
  • Adds an AttributeForm subclass of the plain Django Form, which enables creation of multiple domain entities with attributes.
  • Adds an AttributeModelForm subclass of Django's ModelForm which enables creation of a single domain entity with attributes.
  • AttributeModelForm overrides the AttributeModelForm provided by django-jsonattrs.
  • Refactored TenureRelationshipForm to use the new AttributeForm.
  • Refactored LocationForm, PartyForm, TenureRelationshipEditForm to use the new AttributeModelForm.
  • Refactored LocationsAdd and TenureRelationshipAdd views to remove hardcoded schema selectors form keyword arguments.
  • Adds a custom set of filters to annotate django-jsonattr form fields with parsley validation attributes.
  • Adds javascript to enable/disable attribute fields according to Party type when creating or editing Parties.
  • Updates pytest-cov library to version 2.4.0
  • Adds ipython version 5.1.0 to provide a more functional python shell in the dev vm.

When should this PR be merged

Anytime

Risks

None

Follow up actions

None

Checklist (for reviewing)

General

  • Is this PR explained thoroughly? All code changes must be accounted for in the PR description.
  • Is the PR labeled correctly? It should have the migration label if a new migration is added.
    Is the risk level assessment sufficient? The risks section should contain all risks that might be introduced with the PR and which actions we need to take to mitigate these risks. Possible risks are database migrations, new libraries that need to be installed or changes to deployment scripts.

Functionality

  • Are all requirements met? Compare implemented functionality with the requirements specification.
  • Does the UI work as expected? There should be no Javascript errors in the console; all resources should load. There should be no unexpected errors. Deliberately try to break the feature to find out if there are corner cases that are not handled.

Code

  • Do you fully understand the introduced changes to the code? If not ask for clarification, it might uncover ways to solve a problem in a more elegant and efficient way.
  • Does the PR introduce any inefficient database requests? Use the debug server to check for duplicate requests.
  • Are all necessary strings marked for translation? All strings that are exposed to users via the UI must be marked for translation.

Tests

  • Are there sufficient test cases? Ensure that all components are tested individually; models, forms, and serializers should be tested in isolation even if a test for a view covers these components.
  • If this is a bug fix, are tests for the issue in place There must be a test case for the bug to ensure the issue won’t regress. Make sure that the tests break without the new code to fix the issue.

Documentation

  • Are changes to the UI documented in the platform docs? If this PR introduces new platform site functionality or changes existing ones, the changes must be documented in the Cadasta Platform Documentation.
  • Are changes to the API documented in the API docs? If this PR introduces new API functionality or changes existing ones, the changes must be documented in the API docs.
  • Are reusable components documented? If this PR introduces components that are relevant to other developers (for instance a mixin for a view or a generic form) they should be documented in the Wiki.

@CLAassistant
Copy link

CLAassistant commented Jan 24, 2017

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@oliverroick oliverroick left a comment

Choose a reason for hiding this comment

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

Looks good, just two things. I might be wrong about the set_parsley_required filter; let me know.

attr.choice_labels != []):
chs = list(zip(attr.choices, attr.choice_labels))
else:
# chs = list(map(lambda c: (c, c), attr.choices))
Copy link
Member

Choose a reason for hiding this comment

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

Can you remove the comment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will do..

<div class="clearfix form-group{% if field.errors %} has-error{% endif %}">
<label class="control-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{% if field|field_type == "datefield" %}
{% render_field field class+="form-control datepicker" %}
{{ field|add_class:"form-control datepicker"|set_parsley_required }}
Copy link
Member

Choose a reason for hiding this comment

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

The set_parsley_required filter is not necessary, I think. You're creating the field instances anyway in AttributeFormMixin, so you could add the attribute to the field's widget's attributes:. Something along the lines of:

field.widget.attrs['parsley_required'] = field.required

That way you can get rid of the filter and you can go back to {% render_field field class+="form-control datepicker" %} for this...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like the Widget is not instantiated at the point I need it to be in the AttributeFormMixin so I'd need code like this to make it work:

if attr.required:
    widget_attrs = {'data-parsley-required': 'true'}
    widget = field.widget(attrs=widget_attrs) # instatiate the widget and add attrs
    args.update({'widget': widget}) # update the initial args on the field with the customized widget

This works but complicates the create_model_fields method on the mixin.. at least the filter is reusable?

Copy link
Member

Choose a reason for hiding this comment

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

Argh, ok if the widget is not yet initialised then we shouldn't mess around with that. There might be weird side effects of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, have left this as is..

Copy link
Contributor

@linzjax linzjax left a comment

Choose a reason for hiding this comment

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

This is pretty slick. Aside from one show stopper error and some nit picky DRYing stuff, it looks good.

else:
if attr.required and new_item:
args['required'] = True
if len(attr.default) > 0 and len(
Copy link
Contributor

@linzjax linzjax Jan 25, 2017

Choose a reason for hiding this comment

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

This is throwing an error when an integer field is required: object of type 'int' has no len()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Crapola! well spotted, thanks..

$('.party-gr .form-control').prop('disabled', 'disabled');
$('.party-in .form-control').prop('disabled', 'disabled');
}

Copy link
Contributor

Choose a reason for hiding this comment

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

These could be refactored down to a single function:

function enableConditions(val) {
  var types = ['co','gr','in']
  types.splice(types.indexOf(val), 1)
  $('.party-' + val).removeClass('hidden');
  $('.party-' + val + ' .form-control').prop('disabled', '');
  for (i in types) {
    $('.party-' + types[i]).addClass('hidden')
    $('.party-' + types[i] +  '.form-control').prop('disabled', 'disabled');
  }
}

which could shorten toggelStates to:

if (val === '') {
    disableConditionals();
} else {
    enableConditions(val);
    toggleParsleyRequired(val);
}

Which is maybe less clear, but if we add more party types in the future you'd only have to change one value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice one, thanks.. wasn't happy with that stuff..

assert party.name == 'The Beatles'
assert party.type == 'GR'
print('++++ BLAH 3 ++++')
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you still need these print statements?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These have been removed already..

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh right, red means delete. Whoops!


{% for field in form %}
{% if "party::in" in field.name %}
<div class="form-group{% if field.errors %} has-error{% endif %} party-in hidden">
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the rest of the form is the same, is there a way to wrap the if statements around just the div with the party-{type} class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, this is better now I think..

)


class AttributeModelForm(AttributeFormMixin, ModelForm):
Copy link
Contributor

Choose a reason for hiding this comment

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

This is just to satisfy my own curiosity: Why do you need both the Form and ModelForm?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need attribute forms that don't inherit from ModelForm. If you look at the existing TenureRelationshipForm in spatial/forms.py you'll see that it can't inherit from django-jsonattrs AttributeModelForm because its creating instances of two models, Party and TenureRelationship, so I've added an AttributeForm that subclasses a plain django Form. There might be a way to merge them into a singe AttributeForm but I think its cleaner to have two to choose from.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oooh interesting. That makes sense.

Copy link
Contributor

@linzjax linzjax left a comment

Choose a reason for hiding this comment

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

Looks great! 👍

field = model._meta.get_field(selector_field.partition('.')[0])
if field.choices:
choices = [
(field.name, choice[0]) for choice in field.choices]
Copy link
Contributor

Choose a reason for hiding this comment

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

You're creating an array of 2-tuples here where the first tuple item is field.name but later, you don't actually use the field name since you are only referencing the choice[1] item. Maybe you can simplify choices to just a simple array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right.. will fix this.

content_type=content_type,
selectors=conditionals)
if schemas:
key = '{value}'.format(value=choice[1])
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't choice[1] already a string? Why do you need to do this string formatting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix.



class AttributeForm(AttributeFormMixin, Form):
def add_attribute_fields(self, content_type):
Copy link
Contributor

Choose a reason for hiding this comment

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

This method is exactly the same as AttributeModelForm's corresponding method except the latter already knows its content_type based on the form's model field. Why not DRY these two methods into one method in AttributeFormMixin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making content_type an optional parameter and checking for meta.Model would probably allow this. However, I prefer having two separate form subclasses. I think its clearer to subclass from AttributeForm or AttributeModelForm rather than from AttributeFormMixin or AttributeModelForm but that's just a personal preference.. I don't feel we have to apply the DRY principle in every case..


class SchemaSelectorMixin():

def get_attributes(self, project):
Copy link
Contributor

Choose a reason for hiding this comment

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

This method provides an easy way to get the attributes for any model that have attributes by creating a big dictionary. But for forms that only handle 1 or 2 models, you are doing a lot of data manipulation including possibly unneeded database calls via Schema.objects.lookup. Can't we have a more lightweight solution for cases where the view doesn't need to know the attributes of the whole project?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right about this, I think this mixin could be improved. There should be a way to just query by content_type without having to query for all project attributes. Also there should be some simple caching mechanism to avoid attribute lookups altogether if its already been done. I don't want to make these changes at this point as this mixin is used across forms, exports and imports which are all in progress. I'll open an issue once all the pending PR's are merged.



@register.filter(name='display_choice_verbose')
def display_choice_verbose(field):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure, but does this filter work well with translations?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind. I checked for myself and found out that this code is OK. However, the party type labels are themselves not coded properly for lazy translation in the platform and so I have submitted PR #1093 to fix that.

{% csrf_token %}

{% for field in form %}
{% if field.name|slice:":7" == "party::" %}
Copy link
Contributor

Choose a reason for hiding this comment

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

What about attributes that apply to any type of party? It seems these attributes won't get displayed in this template.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind. I checked for myself and confirmed that this works.

return kwargs

def get_context_data(self, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

This method seems unnecessary.

k = k[length::]
attributes[k] = v
return attributes
def clean(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to DRY this method with the similar one in PartyForm, maybe using a mixin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm.. possibly. I think there are significant differences between the two though.. TenureRelationshipForm.clean needs to remove all Party related ValidationErrors if saving a new entity, in addition to errors related to party type.. while the PartyForm.clean method only cleans up errors related to party type. I'm not in favour of creating mixins that are not genuinely reusable.. Where else would that proposed mixin be used? I think we have a lot of mixins in the code already and I find them quite confusing at times..

# remove validation errors for required fields
# which are not related to the current party type
new_entity = self.cleaned_data.get('new_entity', False)
if not new_entity:
Copy link
Contributor

@seav seav Feb 3, 2017

Choose a reason for hiding this comment

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

If new_entity is false, this means that the new tenure relationship only needs the ID of an existing party. Therefore the checks here are unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because all attribute fields for all party types have to be added to the form (otherwise we can't switch between attribute form fields based on party type selections in the UI), if any of these unused fields are required, when the form is submitted they generate ValidationErrors. This code removes these validation errors. There are two cases handled here:

  1. The user is saving an existing Party so no validation is required.. we remove all ValidationErrors related to the party fields.

  2. If the user is saving a new Party of type IN, the form may still have required GR or CO attributes that we want to ignore.. we need to remove any validation errors not related to the current party type at this point otherwise the form won't save..

Copy link
Contributor

@seav seav left a comment

Choose a reason for hiding this comment

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

General comment: It seems this PR has some code specific to parties simply because it is the only model that has a schema selector based on type (based on the JSONATTRS_SCHEMA_SELECTORS Django setting). But in the future it is conceivable that we may have conditional attributes based on the type of spatial unit or tenure relationship. So it would be nice if we are able to make things more generic.

@seav
Copy link
Contributor

seav commented Feb 3, 2017

While the add party page template is currently unused (see #768 and #973), this template should also be updated to include party_attrs.html similar to the party edit page and the tenure relationship add page.

@bjohare
Copy link
Contributor Author

bjohare commented Feb 3, 2017

@seav I think there are places where the code is specific to Party but in those cases the code is dealing explicitly with Party modification or creation. I don't think there's anything in there that would prevent this from working with more conditional attribute type models. I guess it would become clearer if we have to implement this..

@bjohare
Copy link
Contributor Author

bjohare commented Feb 3, 2017

@seav the party_add.html template includes party_form.html which in turn includes party_attrs.html

@seav
Copy link
Contributor

seav commented Feb 3, 2017

the party_add.html template includes party_form.html which in turn includes party_attrs.html

Oh, yeah. Ah. So the problem is that form.new_item is missing and therefore I couldn't select the party type in this page.

@wonderchook
Copy link
Contributor

I think there will be a point where we will need conditional attributes for locations as well. Some partners do want to collect things like points of interest and parcel boundaries in one project. So this piece being reusable would be good, but if that is quite a bit of work I would say we could wait until implementing conditional attributes for locations and relationships.

@oliverroick
Copy link
Member

I think, we should wait with implementing conditional attributes and make this a separate task. Brian has been working on this for a bit and I think it will be good for him to tick this off.

@amplifi amplifi merged commit a32d351 into master Feb 7, 2017
@amplifi amplifi deleted the bugfix/#761 branch February 7, 2017 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Attributes that depend on party type don't show up on party creation
7 participants