diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index c8e6c41b1d5c79129c119f0670e628e5d479722b..f4db03d66c6fee37ca1e3bfd3c177d6e092b7c4d 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -404,7 +404,7 @@ class SlackAdapter(AdapterBase): reviewers_to_notify.append(reviewer) reviewers = ', '.join( - self.slack_id(reviewer) or str(reviewer) for reviewer in reviewers_to_notify + str(reviewer) for reviewer in reviewers_to_notify ) return ( @@ -430,42 +430,46 @@ class SlackAdapter(AdapterBase): def slack_channel(self, submission): try: - target_room = submission.get_from_parent('slack_channel') + target_rooms = submission.get_from_parent('slack_channel').split(',') except AttributeError: # If not a submission object, set room to default. - target_room = self.target_room + target_rooms = self.target_room else: - if not target_room: + if not target_rooms: # If no custom room, set to default. - target_room = self.target_room + target_rooms = self.target_room + else: + # Always send a copy to the default channel as well. + target_rooms.append(self.target_room) - # Make sure the channel name starts with a "#". - if target_room and not target_room.startswith('#'): - target_room = f"#{target_room}" + # Make sure each channel name starts with a "#". + for i, target_room in enumerate(target_rooms): + if target_room and not target_room.startswith('#'): + target_rooms[i] = f"#{target_room}" - return target_room + return target_rooms def send_message(self, message, recipient, **kwargs): try: submission = kwargs['submission'] except Exception: # If no submission, set room to default. - target_room = self.target_room + target_rooms = self.target_room else: - target_room = self.slack_channel(submission) + target_rooms = self.slack_channel(submission) - if not self.destination or not target_room: + if not self.destination or not target_rooms: errors = list() if not self.destination: errors.append('Destination URL') - if not target_room: + if not target_rooms: errors.append('Room ID') return 'Missing configuration: {}'.format(', '.join(errors)) message = ' '.join([recipient, message]).strip() data = { - "room": target_room, + "room": target_rooms, "message": message, } response = requests.post(self.destination, json=data) @@ -548,7 +552,7 @@ class EmailAdapter(AdapterBase): return [ reviewer.email for reviewer in submission.missing_reviewers.all() - if submission.phase.permissions.can_review(reviewer) + if submission.phase.permissions.can_review(reviewer) and not reviewer.is_apply_staff ] def render_message(self, template, **kwargs): diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py index 39457c66646f70add9cc3f312d1121eece265dda..cecd6396f2b825e8962db33cfc16410f961f831f 100644 --- a/opentech/apply/funds/tables.py +++ b/opentech/apply/funds/tables.py @@ -154,7 +154,7 @@ def get_used_funds(request): def get_round_leads(request): User = get_user_model() - return User.objects.filter(roundbase_lead__isnull=False).distinct() + return User.objects.filter(submission_lead__isnull=False).distinct() def get_reviewers(request): diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index 01659e1e832604ade8551660dc9610befa41bb46..18f28c20b82880d988121a79862c5bdbb3a80d31 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -607,14 +607,17 @@ class ApplicantSubmissionEditView(BaseSubmissionEditView): ) action = set(self.request.POST.keys()) & set(self.transitions.keys()) - transition = self.transitions[action.pop()] - - self.object.perform_transition( - transition.target, - self.request.user, - request=self.request, - notify=not (revision or submitting_proposal), # Use the other notification - ) + try: + transition = self.transitions[action.pop()] + except KeyError: + pass + else: + self.object.perform_transition( + transition.target, + self.request.user, + request=self.request, + notify=not (revision or submitting_proposal), # Use the other notification + ) return HttpResponseRedirect(self.get_success_url()) diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py index 58b017eae90a70b4c7ae2535b16fa3dbdab62a8a..983dc966de90f05c6ec59e23d72edfcc65b376ba 100644 --- a/opentech/apply/funds/workflow.py +++ b/opentech/apply/funds/workflow.py @@ -161,6 +161,10 @@ reviewer_review_permissions = make_permissions(edit=[staff_can], review=[staff_c applicant_edit_permissions = make_permissions(edit=[applicant_can], review=[staff_can]) +staff_applicant_edit_permissions = make_permissions(edit=[staff_can, applicant_can]) + +staff_edit_permissions = make_permissions(edit=[staff_can]) + Request = Stage('Request', False) @@ -259,7 +263,7 @@ SingleStageDefinition = [ 'display': 'Accepted', 'future': 'Application Outcome', 'stage': Request, - 'permissions': no_permissions, + 'permissions': staff_applicant_edit_permissions, }, 'rejected': { 'display': 'Dismissed', @@ -383,7 +387,7 @@ SingleStageExternalDefinition = [ 'display': 'Accepted', 'future': 'Application Outcome', 'stage': RequestExt, - 'permissions': no_permissions, + 'permissions': staff_applicant_edit_permissions, }, 'ext_rejected': { 'display': 'Dismissed', @@ -625,7 +629,7 @@ DoubleStageDefinition = [ 'display': 'Accepted', 'future': 'Final Determination', 'stage': Proposal, - 'permissions': no_permissions, + 'permissions': staff_applicant_edit_permissions, }, 'proposal_rejected': { 'display': 'Dismissed', diff --git a/opentech/apply/stream_forms/blocks.py b/opentech/apply/stream_forms/blocks.py index 67730468d785ec6b7f043d4459caca42bafb6f5c..fd7dcdfaf08550809dc5a5451691061ac584fcba 100644 --- a/opentech/apply/stream_forms/blocks.py +++ b/opentech/apply/stream_forms/blocks.py @@ -37,9 +37,11 @@ class FormFieldBlock(StructBlock): return self.widget def get_field_kwargs(self, struct_value): - kwargs = {'label': struct_value['field_label'], - 'help_text': struct_value['help_text'], - 'required': struct_value.get('required', False)} + kwargs = { + 'label': struct_value['field_label'], + 'help_text': struct_value['help_text'], + 'required': struct_value.get('required', False) + } if 'default_value' in struct_value: kwargs['initial'] = struct_value['default_value'] form_widget = self.get_widget(struct_value) @@ -52,8 +54,9 @@ class FormFieldBlock(StructBlock): return self.get_field_class(struct_value)(**field_kwargs) def serialize(self, value, context): + field_kwargs = self.get_field_kwargs(value) return { - 'question': value['field_label'], + 'question': field_kwargs['label'], 'answer': context.get('data'), 'type': self.name, } @@ -172,10 +175,11 @@ class RadioButtonsFieldBlock(OptionalFormFieldBlock): icon = 'radio-empty' def get_field_kwargs(self, struct_value): - kwargs = super(RadioButtonsFieldBlock, - self).get_field_kwargs(struct_value) - kwargs['choices'] = [(choice, choice) - for choice in struct_value['choices']] + kwargs = super().get_field_kwargs(struct_value) + kwargs['choices'] = [ + (choice, choice) + for choice in struct_value['choices'] + ] return kwargs @@ -187,8 +191,7 @@ class DropdownFieldBlock(RadioButtonsFieldBlock): icon = 'arrow-down-big' def get_field_kwargs(self, struct_value): - kwargs = super(DropdownFieldBlock, - self).get_field_kwargs(struct_value) + kwargs = super().get_field_kwargs(struct_value) kwargs['choices'].insert(0, BLANK_CHOICE_DASH[0]) return kwargs @@ -205,12 +208,17 @@ class CheckboxesFieldBlock(OptionalFormFieldBlock): template = 'stream_forms/render_list_field.html' def get_field_kwargs(self, struct_value): - kwargs = super(CheckboxesFieldBlock, - self).get_field_kwargs(struct_value) - kwargs['choices'] = [(choice, choice) - for choice in struct_value['checkboxes']] + kwargs = super().get_field_kwargs(struct_value) + kwargs['choices'] = [ + (choice, choice) + for choice in struct_value['checkboxes'] + ] return kwargs + def prepare_data(self, value, data, serialize=False): + base_prepare = super().prepare_data + return [base_prepare(value, item, serialize) for item in data] + def get_searchable_content(self, value, data): return data diff --git a/opentech/public/forms/migrations/0002_add_document_choice.py b/opentech/public/forms/migrations/0002_add_document_choice.py new file mode 100644 index 0000000000000000000000000000000000000000..fc99f0ea03d6218f0a25a3ca7b3f826f2f82aa28 --- /dev/null +++ b/opentech/public/forms/migrations/0002_add_document_choice.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.9 on 2019-02-16 08:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('public_forms', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='formfield', + name='field_type', + field=models.CharField(choices=[('singleline', 'Single line text'), ('multiline', 'Multi-line text'), ('email', 'Email'), ('number', 'Number'), ('url', 'URL'), ('checkbox', 'Checkbox'), ('checkboxes', 'Checkboxes'), ('dropdown', 'Drop down'), ('multiselect', 'Multiple select'), ('radio', 'Radio buttons'), ('date', 'Date'), ('datetime', 'Date/time'), ('hidden', 'Hidden field'), ('document', 'Upload Document')], max_length=16, verbose_name='field type'), + ), + ] diff --git a/opentech/public/forms/models.py b/opentech/public/forms/models.py index b627a83a2b51fc5cc93b6d815606fe8bf22516f7..0e9824c684d14dd7d622f46a380c2b05f42d5fac 100644 --- a/opentech/public/forms/models.py +++ b/opentech/public/forms/models.py @@ -1,4 +1,12 @@ +import os +import json + +from django.core.files.storage import get_storage_class +from django.core.serializers.json import DjangoJSONEncoder +from django.conf import settings from django.db import models +from django.forms import FileField +from django.utils.translation import ugettext_lazy as _ from modelcluster.fields import ParentalKey @@ -6,17 +14,34 @@ from wagtail.core.fields import RichTextField from wagtail.admin.edit_handlers import ( FieldPanel, FieldRowPanel, MultiFieldPanel, InlinePanel ) -from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField +from wagtail.contrib.forms.forms import FormBuilder +from wagtail.contrib.forms.models import ( + AbstractEmailForm, AbstractFormField, FORM_FIELD_CHOICES +) from wagtail.search import index from opentech.public.utils.models import BasePage +webform_storage = get_storage_class(getattr(settings, 'PRIVATE_FILE_STORAGE', None))() + class FormField(AbstractFormField): + FORM_FIELD_CHOICES = FORM_FIELD_CHOICES + (('document', 'Upload Document'),) + field_type = models.CharField( + verbose_name=_('field type'), + max_length=16, + choices=FORM_FIELD_CHOICES + ) page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields') +class ExtendedFormBuilder(FormBuilder): + def create_document_field(self, field, options): + return FileField(**options) + + class FormPage(AbstractEmailForm, BasePage): + form_builder = ExtendedFormBuilder subpage_types = [] intro = RichTextField(blank=True) @@ -38,3 +63,25 @@ class FormPage(AbstractEmailForm, BasePage): FieldPanel('subject'), ], "Email"), ] + + def process_form_submission(self, form): + cleaned_data = form.cleaned_data + + for name, field in form.fields.items(): + if isinstance(field, FileField): + file_data = cleaned_data[name] + if file_data: + file_name = file_data.name + file_name = webform_storage.generate_filename(file_name) + upload_to = os.path.join('webform', str(self.id), file_name) + saved_file_name = webform_storage.save(upload_to, file_data) + file_details_dict = {name: webform_storage.url(saved_file_name)} + cleaned_data.update(file_details_dict) + else: + del cleaned_data[name] + + form_data = json.dumps(cleaned_data, cls=DjangoJSONEncoder) + return self.get_submission_class().objects.create( + form_data=form_data, + page=self, + ) diff --git a/opentech/public/funds/migrations/0010_correct_related_page_required.py b/opentech/public/funds/migrations/0010_correct_related_page_required.py new file mode 100644 index 0000000000000000000000000000000000000000..ec36b53a129aa6f10d25ee98bd6d5f207909501c --- /dev/null +++ b/opentech/public/funds/migrations/0010_correct_related_page_required.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.9 on 2019-02-07 04:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('public_funds', '0009_allow_mailto_in_linkfield'), + ] + + operations = [ + migrations.AlterField( + model_name='baseapplicationrelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + migrations.AlterField( + model_name='labpagerelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + ] diff --git a/opentech/public/home/migrations/0011_correct_related_page_behaviour.py b/opentech/public/home/migrations/0011_correct_related_page_behaviour.py new file mode 100644 index 0000000000000000000000000000000000000000..d9ae0e233a007e87a0bac29e752089e9e2f7af7b --- /dev/null +++ b/opentech/public/home/migrations/0011_correct_related_page_behaviour.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.9 on 2019-02-07 04:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0010_add_rfp_to_homepage'), + ] + + operations = [ + migrations.AlterField( + model_name='promotedfunds', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + migrations.AlterField( + model_name='promotedlabs', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + migrations.AlterField( + model_name='promotedrfps', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + ] diff --git a/opentech/public/news/migrations/0008_correct_related_page_behaviour.py b/opentech/public/news/migrations/0008_correct_related_page_behaviour.py new file mode 100644 index 0000000000000000000000000000000000000000..cc73bad761d0af282de744b1f72a8f307df17c3c --- /dev/null +++ b/opentech/public/news/migrations/0008_correct_related_page_behaviour.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.9 on 2019-02-07 04:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0007_newsindex_introduction'), + ] + + operations = [ + migrations.AlterField( + model_name='newspagerelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + migrations.AlterField( + model_name='newsprojectrelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='news_mentions', to='wagtailcore.Page'), + ), + ] diff --git a/opentech/public/news/models.py b/opentech/public/news/models.py index 4cd4d5a915264bc040f4beeb88bc46761bdfac3a..05f029338bd829128aee9fc1cc7e3c5655a3e35a 100644 --- a/opentech/public/news/models.py +++ b/opentech/public/news/models.py @@ -55,9 +55,7 @@ class NewsPageRelatedPage(RelatedPage): class NewsProjectRelatedPage(RelatedPage): page = models.ForeignKey( 'wagtailcore.Page', - null=True, - blank=True, - on_delete=models.SET_NULL, + on_delete=models.CASCADE, related_name='news_mentions', ) source_page = ParentalKey( diff --git a/opentech/public/projects/migrations/0007_fix_related_page_required_behaviour.py b/opentech/public/projects/migrations/0007_fix_related_page_required_behaviour.py new file mode 100644 index 0000000000000000000000000000000000000000..8c5f73d615ef7d160358aeff3b3d8ff60360a0fd --- /dev/null +++ b/opentech/public/projects/migrations/0007_fix_related_page_required_behaviour.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.9 on 2019-02-07 04:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0006_allow_blank_source'), + ] + + operations = [ + migrations.AlterField( + model_name='projectpagerelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + ] diff --git a/opentech/public/standardpages/migrations/0003_correct_related_page_behaviour.py b/opentech/public/standardpages/migrations/0003_correct_related_page_behaviour.py new file mode 100644 index 0000000000000000000000000000000000000000..32d5281a150deaa4735c3c2870263c25b2fb6d7f --- /dev/null +++ b/opentech/public/standardpages/migrations/0003_correct_related_page_behaviour.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.9 on 2019-02-07 04:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('standardpages', '0002_add_header_image'), + ] + + operations = [ + migrations.AlterField( + model_name='informationpagerelatedpage', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page'), + ), + ] diff --git a/opentech/public/utils/models.py b/opentech/public/utils/models.py index 559a4e7dde1061cfd10b60da1be88f3dc12503ea..d9f398daec148a3041c267406d3b0dd65ab240f0 100644 --- a/opentech/public/utils/models.py +++ b/opentech/public/utils/models.py @@ -80,7 +80,11 @@ class LinkFields(models.Model): # Related pages class RelatedPage(Orderable, models.Model): - page = models.ForeignKey('wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') + page = models.ForeignKey( + 'wagtailcore.Page', + on_delete=models.CASCADE, + related_name='+', + ) class Meta: abstract = True diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js index d362877c05f4a68ff759eba4ec779fefcebd31e6..a3a79823d4ccd89b1021c3b8d7044d361249422e 100644 --- a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js @@ -76,6 +76,7 @@ const answerTypes = { 'radios': BasicAnswer, // SPECIAL + 'checkboxes': BasicListAnswer, 'rich_text': RichTextAnswer, 'address': AddressAnswer, 'category': BasicListAnswer, diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.scss b/opentech/static_src/src/app/src/containers/AddNoteForm.scss index 82c8ea9731d11f4acafb1b4b6e1aeb724543b6b6..bf20e2f0912b5d8fd4f46be12ff77b4dfeab7f7d 100644 --- a/opentech/static_src/src/app/src/containers/AddNoteForm.scss +++ b/opentech/static_src/src/app/src/containers/AddNoteForm.scss @@ -31,7 +31,6 @@ $submit-button-height: 60px; @include media-query(tablet-landscape) { height: 100%; - padding-bottom: 95px; } } @@ -47,6 +46,7 @@ $submit-button-height: 60px; font-size: 18px; height: $submit-button-height; opacity: 1; + z-index: 20; } textarea, diff --git a/opentech/static_src/src/sass/apply/components/_messages.scss b/opentech/static_src/src/sass/apply/components/_messages.scss index 69ae151b8e57fe22635e7cd42e6df7389033a3cf..83e4c8cbaac268032fdacf29b1aebe111135d332 100644 --- a/opentech/static_src/src/sass/apply/components/_messages.scss +++ b/opentech/static_src/src/sass/apply/components/_messages.scss @@ -53,6 +53,7 @@ padding-right: 20px; margin: 0; flex: 1; + word-break: break-word; } &__button { diff --git a/opentech/static_src/src/sass/public/components/_messages.scss b/opentech/static_src/src/sass/public/components/_messages.scss index 69ae151b8e57fe22635e7cd42e6df7389033a3cf..83e4c8cbaac268032fdacf29b1aebe111135d332 100644 --- a/opentech/static_src/src/sass/public/components/_messages.scss +++ b/opentech/static_src/src/sass/public/components/_messages.scss @@ -53,6 +53,7 @@ padding-right: 20px; margin: 0; flex: 1; + word-break: break-word; } &__button { diff --git a/requirements.txt b/requirements.txt index 447eedd7188a382b02e7f547bef194191e9b62e0..ec0a4b7e3ba68864947a9d5c7b4d22361c891182 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,47 +1,47 @@ -django~=2.0.0 -djangorestframework==3.9.0 -django-fsm==2.6.0 -wagtail~=2.2.0 -psycopg2==2.7.3.1 -Pillow==4.3.0 -django-bleach==0.3.0 -django-extensions==2.0.0 -django-countries==5.1 -Werkzeug==0.11.11 -stellar==0.4.3 -django-tinymce4-lite==1.7.0 -uwsgidecorators==1.1.0 -django-hijack==2.1.9 -django-anymail==3.0 -celery==4.2.1 -django-webpack-loader==0.6.0 +# Development dependencies, install manually if needed. +# stellar==0.4.5 +# Werkzeug==0.14.1 +# Test dependencies +flake8 factory_boy==2.9.2 # wagtail_factories - waiting on merge and release form master branch git+git://github.com/mvantellingen/wagtail-factories.git#egg=wagtail_factories -responses==0.9.0 - -flake8 +responses==0.10.4 -social_auth_app_django==3.1.0 -django-tables2==1.21.1 -django-filter==1.1.0 -django_select2==6.0.1 +# Monitor dependencies +scout-apm==2.0.1 +raven==6.9.0 # Production dependencies +boto3==1.7.75 +celery==4.2.1 dj-database-url==0.5.0 +django-anymail==3.0 django-basic-auth-ip-whitelist==0.2.1 +django-bleach==0.3.0 +django-countries==5.1 +django-extensions==2.0.0 +django-filter==1.1.0 +django-fsm==2.6.0 django-heroku==0.3.1 +django-hijack==2.1.9 +django-pagedown==1.0.6 django-pwned-passwords==2.0.0 django-redis==4.10.0 django-referrer-policy==1.0 -whitenoise==4.0 -gunicorn==19.9.0 -ConcurrentLogHandler==0.9.1 -raven==6.9.0 django-storages==1.6.6 -boto3==1.7.75 +django-tables2==1.21.1 +django-tinymce4-lite==1.7.0 +django-webpack-loader==0.6.0 +django_select2==6.0.1 +djangorestframework==3.9.0 +django~=2.0.0 +gunicorn==19.9.0 mailchimp3==3.0.4 -scout-apm==1.3.4 mistune==0.8.4 -django-pagedown==1.0.6 +Pillow==4.3.0 +psycopg2==2.7.3.1 +social_auth_app_django==3.1.0 +wagtail~=2.2.0 +whitenoise==4.0