From 4eb969ea273029e7dbc183d594785996ae8765ba Mon Sep 17 00:00:00 2001 From: Ned Zimmerman Date: Tue, 30 Jun 2020 08:49:01 -0600 Subject: [PATCH] feat: add a tool flow (resolves #37) (#119) * feat: add a tool, first pass (#37) * feat: add a tool, second pass (#37) * fix: enable another rule * fix: add another ignore path * feat: move labels to forms * fix: restore labels to model * fix: restore labels to model * feat: niche handling, first pass * feat: niche selection * feat: finish tool flow * fix: localize some tool choice strings, set defaults to empty * feat: set Tool.submitted_by_email on creation --- maps/forms.py | 84 ++++++++++++++++++- maps/static/maps/css/app.scss | 19 +++++ maps/static/maps/js/app.js | 49 +++++++++++ .../maps/profiles/tool/basic_info.html | 66 +++++++++++++++ .../maps/profiles/tool/detailed_info.html | 33 ++++++++ maps/templatetags/maps_extras.py | 2 + maps/urls.py | 9 +- maps/views.py | 64 +++++++++++++- mdi/admin.py | 2 + mdi/migrations/0084_alter_tool_choices.py | 23 +++++ .../0085_add_tool_submitted_by_email.py | 18 ++++ mdi/models.py | 25 +++++- mdi/views.py | 2 + setup.cfg | 4 +- 14 files changed, 387 insertions(+), 13 deletions(-) create mode 100644 maps/templates/maps/profiles/tool/basic_info.html create mode 100644 maps/templates/maps/profiles/tool/detailed_info.html create mode 100644 mdi/migrations/0084_alter_tool_choices.py create mode 100644 mdi/migrations/0085_add_tool_submitted_by_email.py diff --git a/maps/forms.py b/maps/forms.py index cc6b5e7..fa9e005 100644 --- a/maps/forms.py +++ b/maps/forms.py @@ -7,7 +7,8 @@ from django.template.defaultfilters import safe from django_countries.fields import CountryField from accounts.models import Role, SocialNetwork, UserSocialNetwork -from mdi.models import Organization, Category, Language, OrganizationSocialNetwork, Stage, Type +from mdi.models import Organization, Category, Language, OrganizationSocialNetwork, Stage, Tool, Type, Pricing, License + class BaseForm(forms.Form): error_css_class = 'error' @@ -28,12 +29,14 @@ def __init__(self, *args, **kwargs): super(BaseModelForm, self).__init__(*args, **kwargs) + class IndividualProfileDeleteForm(BaseModelForm): class Meta: model = get_user_model() fields = ['has_profile'] widgets = {'has_profile': HiddenInput} + class IndividualBasicInfoForm(BaseModelForm): first_name = CharField( required=True, @@ -64,6 +67,7 @@ class Meta: 'url': _('Website address') } + class IndividualContactInfoForm(BaseModelForm): city = CharField( required=True, @@ -103,6 +107,7 @@ def __init__(self, *args, **kwargs): self.fields['lat'].value = self.lat self.fields['lng'].value = self.lng + class IndividualRolesForm(BaseForm): roles = forms.ModelMultipleChoiceField( queryset=Role.objects.all(), @@ -158,6 +163,8 @@ class Meta: help_texts = { 'community_skills': _('Provide a short description.') } + + class IndividualDetailedInfoForm(BaseModelForm): class Meta: model = get_user_model() @@ -174,6 +181,7 @@ class Meta: 'projects': _('List any current or past projects you would like to share with others.') } + class IndividualSocialNetworkForm(BaseModelForm): class Meta: model = UserSocialNetwork @@ -188,6 +196,7 @@ class Meta: IndividualSocialNetworkFormSet = formset_factory(IndividualSocialNetworkForm, extra=0) + class OrganizationTypeForm(BaseForm): type = forms.ModelChoiceField( Type.objects.filter(name__in=[ @@ -202,6 +211,8 @@ class OrganizationTypeForm(BaseForm): initial=0, widget=RadioSelect(attrs={'class': 'input-group radio'}) ) + + class OrganizationBasicInfoForm(BaseModelForm): languages = forms.ModelMultipleChoiceField( queryset=Language.objects.all(), @@ -307,6 +318,7 @@ def __init__(self, *args, **kwargs): else: self.fields['year_founded'].required = False + class OrganizationContactInfoForm(BaseModelForm): city = CharField( required=True, @@ -336,6 +348,7 @@ class Meta: 'postal_code': _('ZIP or postal code') } + class OrganizationDetailedInfoForm(BaseModelForm): categories = forms.ModelMultipleChoiceField( queryset=Category.objects.all(), @@ -402,6 +415,7 @@ class Meta: 'sectors': _('Hold down the ctrl (Windows) or command (macOS) key to select multiple options.'), } + class OrganizationScopeAndImpactForm(BaseModelForm): geo_scope = forms.ChoiceField( choices=[ @@ -432,6 +446,7 @@ class Meta: 'impacted_exact_number': _('Include clients and users as well as their family members or others indirectly impacted by the work of your co-operative.') } + class OrganizationSocialNetworkForm(BaseModelForm): class Meta: model = OrganizationSocialNetwork @@ -445,3 +460,70 @@ class Meta: OrganizationSocialNetworkFormSet = formset_factory(OrganizationSocialNetworkForm, extra=0) + + +class ToolBasicInfoForm(BaseModelForm): + class Meta: + model = Tool + fields = [ + 'name', + 'niches', + 'description', + 'url' + ] + labels = { + 'name': _('Name of tool'), + 'niches': _('What is this tool used for?'), + 'description': _('Description of tool'), + 'url': _('URL of tool website') + } + help_texts = { + 'description': _('Max 270 characters.') + } + + +class ToolDetailedInfoForm(BaseModelForm): + pricing = forms.ModelChoiceField( + queryset=Pricing.objects.all(), + empty_label=_('Not sure'), + required=False, + label=_('How much does this tool cost?'), + widget=RadioSelect(attrs={'class': 'input-group radio'}) + ) + + license = forms.ModelChoiceField( + queryset=License.objects.all(), + empty_label=_('Not sure'), + required=False, + label=_('Please choose a specific free / libre / open source license') + ) + + sector = forms.ChoiceField( + choices=[('no', _('No')), ('yes', _('Yes'))], + required=True, + initial='no', + label=_('Is this tool for a specific sector or sectors?'), + widget=RadioSelect(attrs={'class': 'input-group radio'}) + ) + + class Meta: + model = Tool + fields = [ + 'pricing', + 'license_type', + 'license', + 'sector', + 'sectors', + 'languages_supported', + 'coop_made' + ] + labels = { + 'license_type': _('How is this tool licensed?'), + 'sectors': _('Please choose a sector or sectors:'), + 'languages_supported': _('What languages does this tool support?'), + 'coop_made': _('Was this tool created by a co-op?') + } + widgets = { + 'license_type': RadioSelect(attrs={'class': 'input-group radio'}), + 'coop_made': RadioSelect(attrs={'class': 'input-group radio'}) + } diff --git a/maps/static/maps/css/app.scss b/maps/static/maps/css/app.scss index 08daaac..f1dae79 100644 --- a/maps/static/maps/css/app.scss +++ b/maps/static/maps/css/app.scss @@ -136,6 +136,7 @@ select.multiple { textarea + label, select + label, .helptext + label, + .input-group + label, ul.checkbox + label, ul.radio + label { margin-top: rem(45); @@ -519,3 +520,21 @@ h1 + .profile__meta { } } } + +.niches > li { + padding: rem(8) 0 rem(6); + position: relative; +} + +.niches button { + position: absolute; + right: 0; + top: 0; +} + +[for="id_detailed_info-license"], +#id_detailed_info-license, +[for="id_detailed_info-sectors"], +#id_detailed_info-sectors { + display: none; +} diff --git a/maps/static/maps/js/app.js b/maps/static/maps/js/app.js index f51f14e..e83f03e 100644 --- a/maps/static/maps/js/app.js +++ b/maps/static/maps/js/app.js @@ -208,6 +208,55 @@ if (basicInfo) { new Pinecone.Card( card ); }); +[...document.querySelectorAll( '.input-group__parent > li' )].forEach( (container) => { + const input = container.querySelector( '.input--parent' ); + const subInputs = container.querySelectorAll( '.input-group__descendant input' ); + if ( 0 < subInputs.length ) { + new Pinecone.NestedCheckbox( container, input, subInputs ); + } +} ); + +[...document.querySelectorAll( '.filter-disclosure-label' )].forEach( (label) => { + new Pinecone.DisclosureButton( label, { buttonVariant: 'button--disc', visuallyHiddenLabel: true } ); +} ); + +[...document.querySelectorAll('[name="detailed_info-license_type"')].forEach(element => { + element.addEventListener('change', () => { + const license = document.getElementById('id_detailed_info-license'); + const licenseLabel = document.querySelector('[for="id_detailed_info-license"]'); + if (element.value === 'floss' || element.value === 'proprietary-with-floss-integration-tools') { + license.style.display = 'block'; + licenseLabel.style.display = 'block'; + } else { + license.style.display = 'none'; + licenseLabel.style.display = 'none'; + } + }); +}); + +[...document.querySelectorAll('[name="detailed_info-sector"')].forEach(element => { + element.addEventListener('change', () => { + const sectors = document.getElementById('id_detailed_info-sectors'); + const sectorsLabel = document.querySelector('[for="id_detailed_info-sectors"]'); + if (element.value === 'yes') { + sectors.style.display = 'block'; + sectorsLabel.style.display = 'block'; + } else { + sectors.style.display = 'none'; + sectorsLabel.style.display = 'none'; + } + }); +}); + +[...document.querySelectorAll('[role="checkbox"]')].forEach(checkbox => { + checkbox.addEventListener('click', () => { + if (checkbox.getAttribute('aria-checked') !== 'true') { + const disclosureButton = checkbox.parentNode.querySelector('button'); + disclosureButton.setAttribute('aria-expanded', 'true'); + } + }); +}); + [...document.querySelectorAll('.delete-organization')].forEach((form) => { form.addEventListener('submit', (event) => { event.preventDefault(); diff --git a/maps/templates/maps/profiles/tool/basic_info.html b/maps/templates/maps/profiles/tool/basic_info.html new file mode 100644 index 0000000..ccb7c3e --- /dev/null +++ b/maps/templates/maps/profiles/tool/basic_info.html @@ -0,0 +1,66 @@ +{% extends "maps/base.html" %} +{% load i18n %} +{% load maps_extras %} +{% block bodyclass %}form-wizard{% endblock %} +{% block title %}{{ 'Basic information'|titlify }}{% endblock %}} + +{% block content %} +
+ +
+ {% if form.errors %} + {% include 'maps/profiles/errors.html' %} + {% endif %} +

{% trans 'Basic information' %}

+ {{ wizard.management_form }} + {% csrf_token %} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} + {% else %} + {{ wizard.form.name.label_tag }} + {{ wizard.form.name }} + {{ wizard.form.niches.label_tag }} +
    + {% for parent, value in niche_dict.items %} +
  • + {% if value.children|length > 0%} + + + +
      + {% for child in value.children %} +
    • + +
    • + {% endfor %} +
    + {% else %} + + {% endif %} +
  • + {% endfor %} +
+ {{ wizard.form.description.label_tag }} + {{ wizard.form.description }} + {{ wizard.form.description.help_text | safe }} + {{ wizard.form.url.label_tag }} + {{ wizard.form.url }} + {% endif %} + {% include 'maps/profiles/footer.html' %} +
+
+ {% include 'maps/profiles/cancel.html' %} +{% endblock %} diff --git a/maps/templates/maps/profiles/tool/detailed_info.html b/maps/templates/maps/profiles/tool/detailed_info.html new file mode 100644 index 0000000..de9c647 --- /dev/null +++ b/maps/templates/maps/profiles/tool/detailed_info.html @@ -0,0 +1,33 @@ +{% extends "maps/base.html" %} +{% load i18n %} +{% load maps_extras %} +{% block bodyclass %}form-wizard{% endblock %} +{% block title %}{{ 'Detailed information'|titlify }}{% endblock %}} + +{% block content %} +
+ +
+ {% if form.errors %} + {% include 'maps/profiles/errors.html' %} + {% endif %} +

{% trans 'Detailed information' %}

+ {{ wizard.management_form }} + {% csrf_token %} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} + {% else %} + {{ wizard.form }} + {% endif %} + {% include 'maps/profiles/footer.html' %} +
+
+ {% include 'maps/profiles/cancel.html' %} +{% endblock %} diff --git a/maps/templatetags/maps_extras.py b/maps/templatetags/maps_extras.py index 6362ae1..cfe70b9 100644 --- a/maps/templatetags/maps_extras.py +++ b/maps/templatetags/maps_extras.py @@ -2,12 +2,14 @@ register = template.Library() + @register.filter(name='titlify') def titlify(value): """Prepends value and emdash to base title""" title_base = 'Platform Co-op Directory' return value + ' – ' + title_base + @register.inclusion_tag('maps/partials/icon.html') def icon(name, **kwargs): modifier = kwargs.get('modifier', name) diff --git a/maps/urls.py b/maps/urls.py index 42488ec..6217a1d 100644 --- a/maps/urls.py +++ b/maps/urls.py @@ -1,15 +1,16 @@ from django.urls import path from django.conf.urls import url from . import views -from .views import INDIVIDUAL_FORMS, ORGANIZATION_FORMS, show_more_about_you_condition, show_scope_and_impact_condition,\ - IndividualProfileWizard, OrganizationProfileWizard, OrganizationDelete, PrivacyPolicyView, TermsOfServiceView, AboutPageView +from .views import INDIVIDUAL_FORMS, ORGANIZATION_FORMS, TOOL_FORMS, show_more_about_you_condition, show_scope_and_impact_condition,\ + IndividualProfileWizard, OrganizationProfileWizard, ToolWizard, OrganizationDelete, PrivacyPolicyView, TermsOfServiceView, AboutPageView from accounts.models import UserSocialNetwork from mdi.models import SocialNetwork urlpatterns = [ path('', views.index, name='index'), - path('profiles/individual', IndividualProfileWizard.as_view(INDIVIDUAL_FORMS, condition_dict={'more_about_you': show_more_about_you_condition}, instance_dict={'social_networks': UserSocialNetwork}), name='individual-profile'), - path('profiles/organization', OrganizationProfileWizard.as_view(ORGANIZATION_FORMS, condition_dict={'scope_and_impact': show_scope_and_impact_condition}, instance_dict={'social_networks': UserSocialNetwork}), name='organization-profile'), + path('add/individual', IndividualProfileWizard.as_view(INDIVIDUAL_FORMS, condition_dict={'more_about_you': show_more_about_you_condition}, instance_dict={'social_networks': UserSocialNetwork}), name='individual-profile'), + path('add/organization', OrganizationProfileWizard.as_view(ORGANIZATION_FORMS, condition_dict={'scope_and_impact': show_scope_and_impact_condition}, instance_dict={'social_networks': UserSocialNetwork}), name='organization-profile'), + path('add/tool', ToolWizard.as_view(TOOL_FORMS), name='add-tool'), path('organizations/', views.organization_detail, name='organization-detail'), path('organizations//delete', OrganizationDelete.as_view(), name='organization-delete'), path('individuals/', views.individual_detail, name='individual-detail'), diff --git a/maps/views.py b/maps/views.py index 7c6f93e..e8a500d 100644 --- a/maps/views.py +++ b/maps/views.py @@ -11,12 +11,13 @@ from django.shortcuts import get_object_or_404, render, redirect from django.forms import inlineformset_factory from accounts.models import UserSocialNetwork -from mdi.models import Organization, SocialNetwork, OrganizationSocialNetwork, Relationship, EntitiesEntities +from mdi.models import Organization, SocialNetwork, OrganizationSocialNetwork, Relationship, EntitiesEntities, \ + Tool, Niche from formtools.wizard.views import SessionWizardView from .forms import GeolocationForm, IndividualProfileDeleteForm, IndividualRolesForm, IndividualBasicInfoForm, \ IndividualMoreAboutYouForm, IndividualDetailedInfoForm, IndividualContactInfoForm, IndividualSocialNetworkFormSet, \ OrganizationTypeForm, OrganizationBasicInfoForm, OrganizationContactInfoForm, OrganizationDetailedInfoForm, \ - OrganizationScopeAndImpactForm, OrganizationSocialNetworkFormSet + OrganizationScopeAndImpactForm, OrganizationSocialNetworkFormSet, ToolBasicInfoForm, ToolDetailedInfoForm from django_countries import countries from django.contrib.gis.geos import Point import os @@ -103,6 +104,17 @@ def manage_socialnetworks(request, user_id): 'social_networks': 'maps/profiles/organization/social_networks.html' } +TOOL_FORMS = [ + ('basic_info', ToolBasicInfoForm), + ('detailed_info', ToolDetailedInfoForm) +] + +TOOL_TEMPLATES = { + 'basic_info': 'maps/profiles/tool/basic_info.html', + 'detailed_info': 'maps/profiles/tool/detailed_info.html' +} + + def show_more_about_you_condition(wizard): cleaned_data = wizard.get_cleaned_data_for_step('roles') or {'roles': []} if (len(cleaned_data['roles']) == 1 and cleaned_data['roles'][0].name == 'Other'): @@ -110,6 +122,7 @@ def show_more_about_you_condition(wizard): return True + def show_scope_and_impact_condition(wizard): cleaned_data = wizard.get_cleaned_data_for_step('org_type') or {'type': False} if (cleaned_data['type'] and cleaned_data['type'].name == 'Cooperative'): @@ -117,6 +130,7 @@ def show_scope_and_impact_condition(wizard): return False + class IndividualProfileWizard(LoginRequiredMixin, SessionWizardView): def get_template_names(self): return [INDIVIDUAL_TEMPLATES[self.steps.current]] @@ -198,6 +212,7 @@ def done(self, form_list, form_dict, **kwargs): return redirect('individual-detail', user_id=user.id) + class OrganizationProfileWizard(LoginRequiredMixin, SessionWizardView): def get_template_names(self): return [ORGANIZATION_TEMPLATES[self.steps.current]] @@ -256,6 +271,42 @@ def done(self, form_list, form_dict, **kwargs): return redirect('organization-detail', organization_id=org.id) + +class ToolWizard(LoginRequiredMixin, SessionWizardView): + def get_template_names(self): + return [TOOL_TEMPLATES[self.steps.current]] + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form=form, **kwargs) + if self.steps.current == 'basic_info': + niche_dict = {} + niches = Niche.objects.all() + for niche in niches: + parent = niche.parent() + if parent not in niche_dict: + niche_dict[parent] = {'children': []} + if niche.child(): + niche_dict[parent]['children'].append({'id': niche.id, 'name': niche.child()}) + else: + niche_dict[parent]['id'] = niche.id + context.update({'niche_dict': niche_dict}) + return context + + def done(self, form_list, form_dict, **kwargs): + user = self.request.user + form_dict = self.get_all_cleaned_data() + tool = Tool(submitted_by_email=user.email) + for k, v in form_dict.items(): + if k not in ['niches', 'sectors', 'languages_supported']: + setattr(tool, k, v) + tool.save() + tool.niches.set(form_dict['niches']) + tool.sectors.set(form_dict['sectors']) + tool.languages_supported.set(form_dict['languages_supported']) + messages.success(self.request, 'Thank you for submitting this tool.') + return HttpResponseRedirect('/my-profiles/') + + def index(request): template = loader.get_template('maps/index.html') context = { @@ -311,6 +362,7 @@ def individual_detail(request, user_id): } return render(request, 'maps/individual_detail.html', context) + class OrganizationDelete(DeleteView): model = Organization success_url = reverse_lazy('my-profiles') @@ -322,6 +374,8 @@ def delete(self, request, *args, **kwargs): return super(OrganizationDelete, self).delete(request, *args, **kwargs) # My Profiles + + @login_required def my_profiles(request): user = request.user @@ -344,16 +398,22 @@ def my_profiles(request): return render(request, 'maps/my_profiles.html', context) # Account Seetings + + @login_required def account_settings(request): return render(request, 'maps/account_settings.html') # Static pages + + class PrivacyPolicyView(TemplateView): template_name = "maps/privacy_policy.html" + class TermsOfServiceView(TemplateView): template_name = "maps/terms_of_service.html" + class AboutPageView(TemplateView): template_name = "maps/about.html" diff --git a/mdi/admin.py b/mdi/admin.py index bca1d71..7b8e380 100644 --- a/mdi/admin.py +++ b/mdi/admin.py @@ -33,10 +33,12 @@ class LegalStatusAdmin(ModelAdmin): class ServiceAdmin(ModelAdmin): list_display = ('name', 'order', 'description', ) + @admin.register(Type) class TypeAdmin(ModelAdmin): list_display = ('name', 'description') + class OrganizationSocialNetworkInline(TabularInline): model = OrganizationSocialNetwork extra = 3 diff --git a/mdi/migrations/0084_alter_tool_choices.py b/mdi/migrations/0084_alter_tool_choices.py new file mode 100644 index 0000000..1b02afd --- /dev/null +++ b/mdi/migrations/0084_alter_tool_choices.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-06-26 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mdi', '0083_add_type_icon'), + ] + + operations = [ + migrations.AlterField( + model_name='tool', + name='coop_made', + field=models.CharField(blank=True, choices=[('', 'Not sure'), ('yes', 'Yes'), ('no', 'No')], default='', max_length=16, verbose_name='Made by a cooperative'), + ), + migrations.AlterField( + model_name='tool', + name='license_type', + field=models.CharField(blank=True, choices=[('', 'Not sure'), ('proprietary', 'Proprietary'), ('proprietary-with-floss-integration-tools', 'Proprietary with free / libre / open source integration tools'), ('floss', 'Free / libre / open source')], default='', max_length=64, verbose_name='License type'), + ), + ] diff --git a/mdi/migrations/0085_add_tool_submitted_by_email.py b/mdi/migrations/0085_add_tool_submitted_by_email.py new file mode 100644 index 0000000..345b1f6 --- /dev/null +++ b/mdi/migrations/0085_add_tool_submitted_by_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-06-26 17:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mdi', '0084_alter_tool_choices'), + ] + + operations = [ + migrations.AddField( + model_name='tool', + name='submitted_by_email', + field=models.EmailField(default='', max_length=255), + ), + ] diff --git a/mdi/models.py b/mdi/models.py index b3b602f..db6b10f 100644 --- a/mdi/models.py +++ b/mdi/models.py @@ -25,6 +25,7 @@ class Meta: def __str__(self): return self.name + class Category(models.Model): name = models.CharField(blank=False, max_length=255, unique=True) type = models.ForeignKey(Type, blank=True, null=True, on_delete=models.CASCADE) @@ -111,6 +112,7 @@ class Meta: def __str__(self): return self.name + class Sector(models.Model): name = models.CharField(blank=False, max_length=255, unique=True) description = models.CharField(blank=True, default='', max_length=255) @@ -152,6 +154,15 @@ class Meta: def __str__(self): return self.name + def parent(self): + return self.name.split(' - ')[0] + + def child(self): + if len(self.name.split(' - ')) > 1: + return self.name.split(' - ')[1] + else: + return False + class Pricing(models.Model): name = models.CharField(blank=False, max_length=255) @@ -196,17 +207,23 @@ class Tool(models.Model): description = models.TextField(blank=True, default='') url = models.URLField(blank=False, max_length=255) license_type = models.CharField(blank=True, default='', max_length=64, - choices=[('proprietary', 'Proprietary'), ('proprietary-with-floss-integration-tools', 'Proprietary with free / libre / open source integration tools'), ('floss', 'Free / libre / open source')], verbose_name='License type') - license = models.ForeignKey(License, blank=True, null=True, on_delete=models.CASCADE, verbose_name='Free / libre / open source license') + choices=[ + ('', _('Not sure')), + ('proprietary', _('Proprietary')), + ('proprietary-with-floss-integration-tools', _('Proprietary with free / libre / open source integration tools')), + ('floss', _('Free / libre / open source')) + ], + verbose_name=_('License type')) + license = models.ForeignKey(License, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Free / libre / open source license')) pricing = models.ForeignKey(Pricing, blank=True, null=True, on_delete=models.CASCADE) niches = models.ManyToManyField(Niche) languages_supported = models.ManyToManyField(Language, blank=True) sectors = models.ManyToManyField(Sector, blank=True) - coop_made = models.CharField(blank=False, default=0, max_length=16, - choices=[('unknown', 'Not sure'), ('yes', 'Yes'), ('no', 'No')], verbose_name='Made by a cooperative') + coop_made = models.CharField(blank=True, default='', max_length=16, choices=[('', _('Not sure')), ('yes', _('Yes')), ('no', _('No'))], verbose_name=_('Made by a cooperative')) notes = models.TextField(blank=True, default='') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + submitted_by_email = models.EmailField(default='', max_length=255) def use_count(self): return self.organization_set.count() diff --git a/mdi/views.py b/mdi/views.py index 19c61dd..2ae14c1 100644 --- a/mdi/views.py +++ b/mdi/views.py @@ -8,9 +8,11 @@ from .models import Organization, Sector, Tool, License from rest_framework.response import Response + def map(request): return HttpResponse("Where's ma maps?") + @api_view(['GET']) def api_root(request, format=None): return Response({ diff --git a/setup.cfg b/setup.cfg index 29c6241..4a3a861 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [pycodestyle] max_line_length = 120 -ignore = E302,E501,E711,E741 -exclude = .cmdi,**/migrations +ignore = E501,E711,E741 +exclude = .cmdi,accounts/migrations,mdi/migrations