diff --git a/surveys/forms.py b/surveys/forms.py index 2bf8f079..894c8903 100644 --- a/surveys/forms.py +++ b/surveys/forms.py @@ -24,7 +24,10 @@ def create_default_helper(self): self.helper.label_class = 'col-lg-3' self.helper.field_class = 'col-lg-9' self.helper.form_method = 'post' - self.helper.add_input(Submit('submit', 'Submit', css_class='float-right')) + self.add_inputs(self.helper) + + def add_inputs(self, helper): + helper.add_input(Submit('submit', 'Submit', css_class='float-right')) class AgencyCreateForm(JustSpacesForm): @@ -216,22 +219,61 @@ class Meta: } +class CensusAreaRegionSelectForm(JustSpacesForm): + class Meta: + model = survey_models.CensusArea + fields = ['name', 'region'] + + def add_inputs(self, helper): + # Override base method to remove Submit button from this form. + return None + + class CensusAreaCreateForm(JustSpacesForm): use_required_attribute = False + restrict_by_agency = forms.BooleanField( + label='Restrict to my agency', + initial=False, + help_text=( + 'This will make this CensusArea viewable only by you and members ' + 'of your agency. If this box is unchecked, all users will be able ' + 'to find and use your CensusArea for their own analyses.' + ), + required=False + ) class Meta: model = survey_models.CensusArea - fields = ['name', 'agency', 'fips_codes'] + fields = ['name', 'region', 'fips_codes', 'restrict_by_agency'] widgets = { 'fips_codes': widgets.MultiSelectGeometryWidget(), } - def __init__(self, *args, **kwargs): + def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) + self.user = user self.fields['fips_codes'].widget.choices = [ (choice.fips_code, choice) for choice in survey_models.CensusBlockGroup.objects.all() ] + if self.instance.agency: + self.fields['restrict_by_agency'].initial = True + + def save(self, commit=True): + if self.cleaned_data['restrict_by_agency'] is True: + self.instance.agency = self.user.agency + else: + self.instance.agency = None + return super().save(commit=commit) + + +class CensusAreaEditForm(CensusAreaCreateForm): + class Meta: + model = survey_models.CensusArea + fields = ['name', 'fips_codes', 'restrict_by_agency'] + widgets = { + 'fips_codes': widgets.MultiSelectGeometryWidget(), + } class SurveyChartForm(forms.ModelForm): diff --git a/surveys/migrations/0017_census_region.py b/surveys/migrations/0017_census_region.py new file mode 100644 index 00000000..21aefeb5 --- /dev/null +++ b/surveys/migrations/0017_census_region.py @@ -0,0 +1,43 @@ +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +def create_philadelphia(apps, schema_editor): + CensusRegion = apps.get_model('surveys', 'CensusRegion') + CensusRegion.objects.get_or_create( + name='Philadelphia', + slug='philadelphia', + fips_codes=['42101'] + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('surveys', '0016_census_area_agency'), + ] + + operations = [ + migrations.CreateModel( + name='CensusRegion', + fields=[ + ('fips_codes', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=12), size=None)), + ('slug', models.SlugField(max_length=255, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.RunPython(create_philadelphia, reverse_code=migrations.RunPython.noop), + migrations.AddField( + model_name='censusblockgroup', + name='region', + field=models.ForeignKey(default='philadelphia', on_delete=django.db.models.deletion.PROTECT, to='surveys.CensusRegion'), + preserve_default=False, + ), + migrations.AddField( + model_name='censusarea', + name='region', + field=models.ForeignKey(default='philadelphia', on_delete=django.db.models.deletion.PROTECT, to='surveys.CensusRegion'), + preserve_default=False, + ), + ] diff --git a/surveys/models.py b/surveys/models.py index 315927db..cfa32055 100644 --- a/surveys/models.py +++ b/surveys/models.py @@ -53,7 +53,18 @@ def __str__(self): return self.name +class CensusRegion(models.Model): + # FIPS Codes that define this region, typically a set of Counties + fips_codes = pg_fields.ArrayField(models.CharField(max_length=12)) + slug = models.SlugField(max_length=255, primary_key=True) + name = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.name + + class CensusBlockGroup(models.Model): + region = models.ForeignKey('CensusRegion', on_delete=models.PROTECT) fips_code = models.CharField(max_length=12, primary_key=True) geom = geo_models.MultiPolygonField(srid=4269) @@ -90,6 +101,7 @@ class CensusArea(models.Model): 'all agencies.' ) ) + region = models.ForeignKey('CensusRegion', on_delete=models.PROTECT) is_active = models.BooleanField(default=True) is_preset = models.BooleanField(default=False) diff --git a/surveys/static/js/spin.min.js b/surveys/static/js/spin.min.js new file mode 100644 index 00000000..6028cda6 --- /dev/null +++ b/surveys/static/js/spin.min.js @@ -0,0 +1,2 @@ +//fgnass.github.com/spin.js#v1.3.3 +!function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=k.substring(0,k.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(n.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",n.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}n.addRule(".spin-vml","behavior:url(#default#VML)"),i.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function g(a,g,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~g}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:h(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)g(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)g(i);return b(a,m)},i.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1):parseInt(h.left,10)+j)+"px",top:("auto"==h.top?d.y-c.y+(b.offsetHeight>>1):parseInt(h.top,10)+j)+"px"})),i.setAttribute("role","progressbar"),f.lines(i,f.opts),!k){var l,m=0,n=(h.lines-1)*(1-h.direction)/2,o=h.fps,p=o/h.speed,q=(1-h.opacity)/(p*h.trail/100),r=p/h.lines;!function s(){m++;for(var a=0;a>1)+"px"})}for(var i,j=0,l=(f.lines-1)*(1-f.direction)/2;j - {% crispy form %} - +
+ {% crispy form %} +
+ {% endblock %} {% block footer_js %} + + {% endblock %} diff --git a/surveys/templates/census_area_list.html b/surveys/templates/census_area_list.html index e84531bb..0cc8f8be 100644 --- a/surveys/templates/census_area_list.html +++ b/surveys/templates/census_area_list.html @@ -13,7 +13,7 @@ {% block head %}Census Areas{% endblock %} {% block top_button %} - Create new census area + Create new census area {% endblock %} {% block subhead %} diff --git a/surveys/templates/partials/chart_form.html b/surveys/templates/partials/chart_form.html index fa8741d3..72b847b3 100644 --- a/surveys/templates/partials/chart_form.html +++ b/surveys/templates/partials/chart_form.html @@ -67,7 +67,7 @@ {{form.census_areas | attr:"class:form-control basic-multiple"}} Select one or multiple Census geographies to compare to the data collected for this question. - Don't see the census area you need? Create a new one here. + Don't see the census area you need? Create a new one here. diff --git a/surveys/urls.py b/surveys/urls.py index 5b674263..e1530473 100644 --- a/surveys/urls.py +++ b/surveys/urls.py @@ -192,6 +192,10 @@ # API endpoint for retreiving CensusObservation data path('acs/', view=survey_views.census_area_to_observation, name="acs"), + url(r'census-areas/region/$', + staff_required_custom_login(survey_views.CensusAreaRegionSelect.as_view()), + name='census-areas-region-select'), + url(r'census-areas/create/$', staff_required_custom_login(survey_views.CensusAreaCreate.as_view()), name='census-areas-create'), diff --git a/surveys/views.py b/surveys/views.py index 579a9531..1f167de0 100644 --- a/surveys/views.py +++ b/surveys/views.py @@ -1,7 +1,7 @@ import json import uuid -from django.views.generic import TemplateView, ListView, UpdateView, DetailView +from django.views.generic import TemplateView, ListView, UpdateView, DetailView, FormView from django.views.generic.edit import CreateView from django.db.models import Q @@ -83,7 +83,7 @@ class AgencyRestrictQuerysetMixin(object): Provide common methods allowing views to restrict their querysets based on the user's Agency. """ - def get_queryset_for_agency(self, agency_filter='agency'): + def get_queryset_for_agency(self, queryset, agency_filter='agency'): """ Filter the queryset based on the user's agency. The 'agency_filter' string will be used as the filter kwarg for the Agency lookup; e.g. if the Agency @@ -94,9 +94,9 @@ def get_queryset_for_agency(self, agency_filter='agency'): agency_null_kwargs = {agency_filter + '__isnull': True} if self.request.user.agency is not None: - return self.queryset.filter(Q(**agency_kwargs) | Q(**agency_null_kwargs)) + return queryset.filter(Q(**agency_kwargs) | Q(**agency_null_kwargs)) else: - return self.queryset + return queryset class AgencyInitialMixin(object): @@ -184,7 +184,7 @@ class LocationList(AgencyRestrictQuerysetMixin, ListView): queryset = pldp_models.Location.objects.all().exclude(is_active=False) def get_queryset(self): - return self.get_queryset_for_agency() + return self.get_queryset_for_agency(super().get_queryset()) class LocationDetail(DetailView): @@ -277,7 +277,7 @@ class StudyList(AgencyRestrictQuerysetMixin, ListView): queryset = pldp_models.Study.objects.all().exclude(is_active=False) def get_queryset(self): - return self.get_queryset_for_agency() + return self.get_queryset_for_agency(super().get_queryset()) class StudyDeactivate(TemplateView): @@ -453,7 +453,7 @@ class SurveyListEdit(AgencyRestrictQuerysetMixin, ListView): queryset = survey_models.SurveyFormEntry.objects.filter(active=True, is_cloneable=False, published=False) def get_queryset(self): - return self.get_queryset_for_agency('study__agency') + return self.get_queryset_for_agency(super().get_queryset(), 'study__agency') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -470,7 +470,7 @@ class SurveyListRun(AgencyRestrictQuerysetMixin, ListView): queryset = survey_models.SurveyFormEntry.objects.filter(active=True, is_cloneable=False, published=True) def get_queryset(self): - return self.get_queryset_for_agency('study__agency') + return self.get_queryset_for_agency(super().get_queryset(), 'study__agency') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -497,7 +497,7 @@ class SurveySubmittedList(AgencyRestrictQuerysetMixin, ListView): queryset = pldp_models.Survey.objects.all() def get_queryset(self): - return self.get_queryset_for_agency('study__agency') + return self.get_queryset_for_agency(super().get_queryset(), 'study__agency') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -518,12 +518,40 @@ def get_context_data(self, **kwargs): return context +class CensusAreaRegionSelect(AgencyInitialMixin, FormView): + form_class = survey_forms.CensusAreaRegionSelectForm + template_name = 'census_area_create.html' + + def get_initial(self): + initial = {} + + if self.request.user.agency: + initial['agency'] = self.request.user.agency + + class CensusAreaCreate(AgencyInitialMixin, CreateView): form_class = survey_forms.CensusAreaCreateForm model = survey_models.CensusArea template_name = "census_area_create.html" success_url = reverse_lazy('census-areas-list') + def get_initial(self): + initial = {} + + for var in ['region', 'name']: + if self.request.GET.get(var): + initial[var] = self.request.GET[var] + + if 'region' not in initial.keys(): + initial['region'] = 'philadelphia' + + return initial + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + class CensusAreaList(AgencyRestrictQuerysetMixin, ListView): model = survey_models.CensusArea @@ -532,16 +560,21 @@ class CensusAreaList(AgencyRestrictQuerysetMixin, ListView): queryset = survey_models.CensusArea.objects.all().exclude(is_active=False) def get_queryset(self): - return self.get_queryset_for_agency() + return self.get_queryset_for_agency(super().get_queryset()) class CensusAreaEdit(UpdateView): model = survey_models.CensusArea template_name = "census_area_edit.html" - form_class = survey_forms.CensusAreaCreateForm + form_class = survey_forms.CensusAreaEditForm context_object_name = 'form_object' success_url = reverse_lazy('census-areas-list') + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + class CensusAreaDeactivate(TemplateView): template_name = "census_area_deactivate.html" diff --git a/tests/conftest.py b/tests/conftest.py index 6de9cd96..fa54902a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ from fobi.models import FormElementEntry from users.models import JustSpacesUser -from surveys.models import SurveyFormEntry, CensusArea +from surveys.models import SurveyFormEntry, CensusArea, CensusRegion from pldp.models import Location, Agency, Study, StudyArea, Survey, \ SurveyRow, SurveyComponent @@ -386,29 +386,42 @@ def survey_component(db, survey_row): @pytest.fixture @pytest.mark.django_db -def census_area(db): +def census_region(db): + return CensusRegion.objects.create( + fips_codes=['1'], + slug='test-region', + name='test region' + ) + + +@pytest.fixture +@pytest.mark.django_db +def census_area(db, census_region): return CensusArea.objects.create( name='test area', fips_codes=['1'], + region=census_region, agency=None ) @pytest.fixture @pytest.mark.django_db -def census_area_agency_1(db, agency): +def census_area_agency_1(db, agency, census_region): return CensusArea.objects.create( name='test area (Agency 1)', fips_codes=['42'], + region=census_region, agency=agency ) @pytest.fixture @pytest.mark.django_db -def census_area_agency_2(db, agency_2): +def census_area_agency_2(db, agency_2, census_region): return CensusArea.objects.create( name='test area (Agency 2)', fips_codes=['42'], + region=census_region, agency=agency_2 ) diff --git a/tests/test_views.py b/tests/test_views.py index b9e8a196..54f9afc1 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,11 +1,12 @@ import uuid +from urllib.parse import urlencode import pytest from django.urls import reverse from pldp.forms import AGE_COMPLEX_CHOICES from pldp.models import SurveyComponent -from surveys.models import CensusObservation +from surveys.models import CensusObservation, CensusArea from fobi_custom.plugins.form_elements.fields import types as fobi_types @@ -275,13 +276,60 @@ def test_survey_submitted_detail(client, user_staff, survey_form_entry, survey, @pytest.mark.django_db -def test_census_area_create(client, user_staff): +def test_census_area_region_select(client, user_staff): + client.force_login(user_staff) + url = reverse('census-areas-region-select') + response = client.get(url) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_census_area_create(client, user_staff, census_region): client.force_login(user_staff) url = reverse('census-areas-create') response = client.get(url) assert response.status_code == 200 - assert response.context['form'].initial.get('agency') == user_staff.agency + # With no 'region' param, the region should default to Philadelphia. + assert response.context['form'].initial.get('region') == 'philadelphia' + + data = { + 'name': 'foobar', + 'region': census_region.slug, + 'fips_codes': ['42'], + 'restrict_by_agency': True + } + post_res = client.post(url, data) + assert post_res.status_code == 302 + + new_census_area = CensusArea.objects.last() + assert new_census_area.name == 'foobar' + assert new_census_area.region == census_region + assert new_census_area.fips_codes == ['42'] + assert new_census_area.agency == user_staff.agency + + # Test creating a CensusArea without an agency restriction + del data['restrict_by_agency'] + no_agency_post_res = client.post(url, data) + assert no_agency_post_res.status_code == 302 + + no_agency_census_area = CensusArea.objects.last() + assert no_agency_census_area.agency is None + + +@pytest.mark.django_db +def test_census_area_create_params(client, user_staff): + client.force_login(user_staff) + params = { + 'name': 'Foo bar', + 'region': 'testregion', + } + url = reverse('census-areas-create') + '?' + urlencode(params) + response = client.get(url) + + assert response.status_code == 200 + for var, val in params.items(): + assert response.context['form'].initial.get(var) == val @pytest.mark.django_db