diff --git a/gulpfile.js b/gulpfile.js index 85ed64030c0677b9e398a21198814269fb194643..c25abcae7119a05d57cf7b17697f43ccd392ed72 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -266,10 +266,10 @@ gulp.task('watch:app', function watch (callback) { gulp.task('watch', gulp.parallel('watch:css', 'watch:lint:sass', 'watch:js', 'watch:lint:js', 'watch:images', 'watch:fonts', 'watch:static')); // Build everything. -gulp.task('build', gulp.series(gulp.parallel('styles:production', 'scripts:production', 'app:production', 'images', 'fonts', 'lint'), 'collectstatic')); +gulp.task('build', gulp.series(gulp.parallel(gulp.series('styles:production', 'scripts:production', 'app:production'), 'images', 'fonts', 'lint'), 'collectstatic')); // Deploy everything. -gulp.task('deploy', gulp.parallel('styles:production', 'scripts:production', 'app:production', 'images', 'fonts')); +gulp.task('deploy', gulp.parallel(gulp.series('styles:production', 'scripts:production', 'app:production'), 'images', 'fonts')); // The default task. gulp.task('default', gulp.series('build')); diff --git a/opentech/api/__init__.py b/opentech/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/api/pagination.py b/opentech/api/pagination.py new file mode 100644 index 0000000000000000000000000000000000000000..1382e50358d993cb6629244709a19261b0616df7 --- /dev/null +++ b/opentech/api/pagination.py @@ -0,0 +1,6 @@ +from rest_framework import pagination + + +class StandardResultsSetPagination(pagination.PageNumberPagination): + page_size_query_param = 'page_size' + max_page_size = 1000 diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index 29c0ad9d865174320b45a13d5f8f5c2f5f73aa4a..a7cb75e0afddeb0094c57b55fb515547cb29ac91 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -1,3 +1,4 @@ +import json import requests from django.db import models @@ -5,6 +6,7 @@ from django.conf import settings from django.contrib import messages from django.template.loader import render_to_string +from .models import INTERNAL, PUBLIC from .options import MESSAGES from .tasks import send_mail @@ -21,6 +23,7 @@ neat_related = { MESSAGES.APPLICANT_EDIT: 'revision', MESSAGES.EDIT: 'revision', MESSAGES.COMMENT: 'comment', + MESSAGES.SCREENING: 'old_status', } @@ -117,7 +120,7 @@ class ActivityAdapter(AdapterBase): adapter_type = "Activity Feed" always_send = True messages = { - MESSAGES.TRANSITION: 'Progressed from {old_phase.display_name} to {submission.phase}', + MESSAGES.TRANSITION: 'handle_transition', MESSAGES.NEW_SUBMISSION: 'Submitted {submission.title} for {submission.page.title}', MESSAGES.EDIT: 'Edited', MESSAGES.APPLICANT_EDIT: 'Edited', @@ -127,14 +130,18 @@ class ActivityAdapter(AdapterBase): MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated', MESSAGES.NEW_REVIEW: 'Submitted a review', MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed', + MESSAGES.SCREENING: 'Screening status from {old_status} to {submission.screening_status}' } def recipients(self, message_type, **kwargs): return [None] - def extra_kwargs(self, message_type, **kwargs): - if message_type in [MESSAGES.OPENED_SEALED, MESSAGES.REVIEWERS_UPDATED]: - from .models import INTERNAL + def extra_kwargs(self, message_type, submission, **kwargs): + from .models import INTERNAL + if message_type in [MESSAGES.OPENED_SEALED, MESSAGES.REVIEWERS_UPDATED, MESSAGES.SCREENING]: + return {'visibility': INTERNAL} + if message_type == MESSAGES.TRANSITION and not submission.phase.permissions.can_view(submission.user): + # User's shouldn't see status activity changes for stages that aren't visible to the them return {'visibility': INTERNAL} return {} @@ -150,12 +157,40 @@ class ActivityAdapter(AdapterBase): return ' '.join(message) + def handle_transition(self, old_phase, submission, **kwargs): + base_message = 'Progressed from {old_display} to {new_display}' + + new_phase = submission.phase + + staff_message = base_message.format( + old_display=old_phase.display_name, + new_display=new_phase.display_name, + ) + + if new_phase.permissions.can_view(submission.user): + # we need to provide a different message to the applicant + if not old_phase.permissions.can_view(submission.user): + old_phase = submission.workflow.previous_visible(old_phase, submission.user) + + applicant_message = base_message.format( + old_display=old_phase.public_name, + new_display=new_phase.public_name, + ) + + return json.dumps({ + INTERNAL: staff_message, + PUBLIC: applicant_message, + }) + + return staff_message + def send_message(self, message, user, submission, **kwargs): from .models import Activity, PUBLIC visibility = kwargs.get('visibility', PUBLIC) related = kwargs['related'] - if isinstance(related, models.Model): + has_correct_fields = all(hasattr(related, attr) for attr in ['author', 'submission', 'get_absolute_url']) + if has_correct_fields and isinstance(related, models.Model): related_object = related else: related_object = None @@ -300,6 +335,11 @@ class EmailAdapter(AdapterBase): def recipients(self, message_type, submission, **kwargs): if message_type == MESSAGES.READY_FOR_REVIEW: return self.reviewers(submission) + + if message_type == MESSAGES.TRANSITION: + # Only notify the applicant if the new phase can be seen within the workflow + if not submission.phase.permissions.can_view(submission.user): + return [] return [submission.user.email] def reviewers(self, submission): diff --git a/opentech/apply/activity/migrations/0013_add_new_event_type_screening.py b/opentech/apply/activity/migrations/0013_add_new_event_type_screening.py new file mode 100644 index 0000000000000000000000000000000000000000..065b7afa51b2253c7411086c11f82d114c81c1e4 --- /dev/null +++ b/opentech/apply/activity/migrations/0013_add_new_event_type_screening.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.10 on 2019-01-09 16:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0012_add_generic_relation_to_activity'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='type', + field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('READY_FOR_REVIEW', 'Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission')], max_length=50), + ), + ] diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py index ca9a78cb0baafbf87323780ab7f3b20e1971317f..46d744e035fa0af24a76b6c63996edf6e3523e6e 100644 --- a/opentech/apply/activity/options.py +++ b/opentech/apply/activity/options.py @@ -6,6 +6,7 @@ class MESSAGES(Enum): EDIT = 'Edit' APPLICANT_EDIT = "Applicant Edit" NEW_SUBMISSION = 'New Submission' + SCREENING = 'Screening' TRANSITION = 'Transition' DETERMINATION_OUTCOME = 'Determination Outcome' INVITED_TO_PROPOSAL = 'Invited To Proposal' diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html index 23056f2fcd17b5d104676122e098172af5611f71..f999f30d48506338aea55ecea8cafb49b56a566a 100644 --- a/opentech/apply/activity/templates/activity/include/listing_base.html +++ b/opentech/apply/activity/templates/activity/include/listing_base.html @@ -19,7 +19,7 @@ updated <a href="{{ activity.submission.get_absolute_url }}">{{ activity.submission.title }}</a> {% endif %} - {{ activity.message|submission_links|markdown|bleach }} + {{ activity|display_for:request.user|submission_links|markdown|bleach }} {% if not submission_title and activity|user_can_see_related:request.user %} {% with url=activity.related_object.get_absolute_url %} diff --git a/opentech/apply/activity/templatetags/activity_tags.py b/opentech/apply/activity/templatetags/activity_tags.py index 6f8c87ba50afb7cdca21112a8517701ebf75636f..1f43a872e263ae8585d4b1a0c4cf39cb1626868e 100644 --- a/opentech/apply/activity/templatetags/activity_tags.py +++ b/opentech/apply/activity/templatetags/activity_tags.py @@ -1,8 +1,12 @@ +import json + from django import template from opentech.apply.determinations.models import Determination from opentech.apply.review.models import Review +from ..models import INTERNAL, PUBLIC, REVIEWER + register = template.Library() @@ -25,3 +29,18 @@ def user_can_see_related(activity, user): return True return False + + +@register.filter +def display_for(activity, user): + try: + message_data = json.loads(activity.message) + except json.JSONDecodeError: + return activity.message + + visibile_for_user = activity.visibility_for(user) + + if set(visibile_for_user) & set([INTERNAL, REVIEWER]): + return message_data[INTERNAL] + + return message_data[PUBLIC] diff --git a/opentech/apply/activity/tests/test_messaging.py b/opentech/apply/activity/tests/test_messaging.py index ec782ecf9cbf2f567b2e96db42c0b513dc17271d..d804f3c4f52f815e9b8376e62ea926cd269b3574 100644 --- a/opentech/apply/activity/tests/test_messaging.py +++ b/opentech/apply/activity/tests/test_messaging.py @@ -11,9 +11,10 @@ from django.contrib.messages import get_messages from opentech.apply.utils.testing import make_request from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory -from opentech.apply.users.tests.factories import UserFactory, ReviewerFactory +from opentech.apply.review.tests.factories import ReviewFactory +from opentech.apply.users.tests.factories import ReviewerFactory, UserFactory -from ..models import Activity, Event, Message +from ..models import Activity, Event, Message, INTERNAL, PUBLIC from ..messaging import ( AdapterBase, ActivityAdapter, @@ -222,6 +223,66 @@ class TestActivityAdapter(TestCase): self.assertTrue('1' in message) self.assertTrue('2' in message) + def test_internal_transition_kwarg_for_invisible_transition(self): + submission = ApplicationSubmissionFactory(status='post_review_discussion') + kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, submission=submission) + + self.assertEqual(kwargs['visibility'], INTERNAL) + + def test_public_transition_kwargs(self): + submission = ApplicationSubmissionFactory() + kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, submission=submission) + + self.assertNotIn('visibility', kwargs) + + def test_handle_transition_public_to_public(self): + submission = ApplicationSubmissionFactory(status='more_info') + old_phase = submission.workflow.phases_for()[0] + + message = self.adapter.handle_transition(old_phase, submission) + message = json.loads(message) + + self.assertIn(submission.phase.display_name, message[INTERNAL]) + self.assertIn(old_phase.display_name, message[INTERNAL]) + self.assertIn(submission.phase.public_name, message[PUBLIC]) + self.assertIn(old_phase.public_name, message[PUBLIC]) + + def test_handle_transition_to_private_to_public(self): + submission = ApplicationSubmissionFactory(status='more_info') + old_phase = submission.workflow.phases_for()[1] + + message = self.adapter.handle_transition(old_phase, submission) + message = json.loads(message) + + self.assertIn(submission.phase.display_name, message[INTERNAL]) + self.assertIn(old_phase.display_name, message[INTERNAL]) + self.assertIn(submission.phase.public_name, message[PUBLIC]) + self.assertIn(old_phase.public_name, message[PUBLIC]) + + def test_handle_transition_to_public_to_private(self): + submission = ApplicationSubmissionFactory(status='internal_review') + old_phase = submission.workflow.phases_for()[0] + + message = self.adapter.handle_transition(old_phase, submission) + + self.assertIn(submission.phase.display_name, message) + self.assertIn(old_phase.display_name, message) + + def test_lead_not_saved_on_activity(self): + submission = ApplicationSubmissionFactory() + user = UserFactory() + self.adapter.send_message('a message', user=user, submission=submission, related=user) + activity = Activity.objects.first() + self.assertEqual(activity.related_object, None) + + def test_review_saved_on_activtiy(self): + submission = ApplicationSubmissionFactory() + user = UserFactory() + review = ReviewFactory(submission=submission) + self.adapter.send_message('a message', user=user, submission=submission, related=review) + activity = Activity.objects.first() + self.assertEqual(activity.related_object, review) + class TestSlackAdapter(AdapterMixin, TestCase): target_url = 'https://my-slack-backend.com/incoming/my-very-secret-key' diff --git a/opentech/apply/activity/views.py b/opentech/apply/activity/views.py index 07ba2d1dcb64df46393c08315b44aaf410260ad0..12600a8bd8033929a4513bcc34f4511fd1a92289 100644 --- a/opentech/apply/activity/views.py +++ b/opentech/apply/activity/views.py @@ -35,12 +35,14 @@ class ActivityContextMixin: 'actions': Activity.actions.filter(submission=self.object).select_related( 'user', ).prefetch_related( - 'related_object', + 'related_object__author', + 'related_object__submission', ).visible_to(self.request.user), 'comments': Activity.comments.filter(submission=self.object).select_related( 'user', ).prefetch_related( - 'related_object', + 'related_object__author', + 'related_object__submission', ).visible_to(self.request.user), } diff --git a/opentech/apply/categories/blocks.py b/opentech/apply/categories/blocks.py index 02359dc54c454236183c6407d962f59fc8e4c687..b8bf9a67c1180cd53a62490fae49aa46930fe6e9 100644 --- a/opentech/apply/categories/blocks.py +++ b/opentech/apply/categories/blocks.py @@ -74,12 +74,11 @@ class CategoryQuestionBlock(OptionalFormFieldBlock): else: return forms.RadioSelect - def render(self, value, context): - data = context['data'] + def prepare_data(self, value, data, serialize): category = value['category'] if data: - context['data'] = category.options.filter(id__in=data).values_list('value', flat=True) - return super().render(value, context) + data = category.options.filter(id__in=data).values_list('value', flat=True) + return data def get_searchable_content(self, value, data): return None diff --git a/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html index fa7d3b774881004160cebd2b9dcb3ea31da5aa4d..122386a8bcd37d53a6cae7b7de0609c86dbdc0ee 100644 --- a/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html +++ b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html @@ -1,6 +1,6 @@ {% extends "base-apply.html" %} {% load render_table from django_tables2 %} -{% load static wagtailcore_tags workflow_tags %} +{% load static wagtailcore_tags workflow_tags statusbar_tags %} {% block title %}Submission Dashboard{% endblock %} @@ -27,7 +27,7 @@ <h5 class="heading heading--no-margin"><a class="link link--underlined" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h5> <h6 class="heading heading--no-margin heading--submission-meta"><span>Submitted:</span> {{ submission.submit_time.date }} by {{ submission.user.get_full_name }}</h6> </div> - {% include "funds/includes/status_bar.html" with phases=submission.workflow current_phase=submission.phase class="status-bar--small" %} + {% status_bar submission.workflow submission.phase request.user css_class="status-bar--small" %} </div> {% if request.user|has_edit_perm:submission %} <a class="button button--primary" href="{% url 'funds:submissions:edit' submission.id %}"> diff --git a/opentech/apply/dashboard/templates/dashboard/dashboard.html b/opentech/apply/dashboard/templates/dashboard/dashboard.html index 1f355a82e7656ae982adc85d3280d10c078ae040..de14c3caef48d98e96c5c102aa8cdbd8362a1118 100644 --- a/opentech/apply/dashboard/templates/dashboard/dashboard.html +++ b/opentech/apply/dashboard/templates/dashboard/dashboard.html @@ -26,17 +26,6 @@ {% endif %} </div> </div> - -<div class="wrapper wrapper--large wrapper--inner-space-medium"> - <div class="wrapper wrapper--large wrapper--inner-space-medium"> - <h3>Applications awaiting determination</h3> - {% if awaiting_determination.data %} - {% render_table awaiting_determination %} - {% else %} - No applications awaiting determination - {% endif %} - </div> -</div> {% endblock %} {% block extra_js %} diff --git a/opentech/apply/dashboard/tests/test_views.py b/opentech/apply/dashboard/tests/test_views.py index df5a41eaa3227dd9a9b27ffe49ab4323717cd8fd..795fcf11f404596c3f6e6eeb885cfef8a8fc3a87 100644 --- a/opentech/apply/dashboard/tests/test_views.py +++ b/opentech/apply/dashboard/tests/test_views.py @@ -57,12 +57,6 @@ class TestStaffDashboard(BaseViewTestCase): url_name = 'dashboard:{}' base_view_name = 'dashboard' - def test_can_see_need_determinations(self): - ApplicationSubmissionFactory(status='concept_review_discussion', workflow_stages=2, lead=self.user, - form_data__title='Internet of things') - response = self.get_page() - self.assertContains(response, 'Internet of things') - def test_cannot_see_submission_in_determination_when_not_lead(self): ApplicationSubmissionFactory(status='concept_review_discussion', workflow_stages=2, form_data__title='Reviewr') response = self.get_page() diff --git a/opentech/apply/dashboard/views.py b/opentech/apply/dashboard/views.py index 0592593f845e14cfb446e985017e690067def5e2..b8703e77d2457cd107d877c81bd101f44ad90d3f 100644 --- a/opentech/apply/dashboard/views.py +++ b/opentech/apply/dashboard/views.py @@ -16,15 +16,8 @@ class AdminDashboardView(TemplateView): in_review = SubmissionsTable(qs.in_review_for(request.user), prefix='in-review-') RequestConfig(request, paginate={'per_page': 10}).configure(in_review) - awaiting_determination = AdminSubmissionsTable( - qs.awaiting_determination_for(request.user), - prefix='pending-determination-' - ) - RequestConfig(request, paginate={'per_page': 10}).configure(awaiting_determination) - return render(request, 'dashboard/dashboard.html', { 'in_review': in_review, - 'awaiting_determination': awaiting_determination, }) diff --git a/opentech/apply/funds/admin.py b/opentech/apply/funds/admin.py index 559a76bf512d52c8f94da44f33c9761a8374f645..689f73027216344b55a4b82eed731de5b7b55dfb 100644 --- a/opentech/apply/funds/admin.py +++ b/opentech/apply/funds/admin.py @@ -1,6 +1,7 @@ from wagtail.contrib.modeladmin.helpers import PermissionHelper from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup +from opentech.apply.funds.models import ScreeningStatus from opentech.apply.review.admin import ReviewFormAdmin from opentech.apply.utils.admin import ListRelatedMixin from .admin_helpers import ( @@ -27,6 +28,28 @@ class RoundAdmin(BaseRoundAdmin): menu_icon = 'repeat' +class ScreeningStatusPermissionHelper(PermissionHelper): + def user_can_edit_obj(self, user, obj): + """ + Return a boolean to indicate whether `user` is permitted to 'change' + a specific `self.model` instance. + """ + return user.is_superuser + + def user_can_delete_obj(self, user, obj): + """ + Return a boolean to indicate whether `user` is permitted to 'delete' + a specific `self.model` instance. + """ + return user.is_superuser + + +class ScreeningStatusAdmin(ModelAdmin): + model = ScreeningStatus + menu_icon = 'tag' + permission_helper_class = ScreeningStatusPermissionHelper + + class SealedRoundAdmin(BaseRoundAdmin): model = SealedRound menu_icon = 'locked' @@ -82,4 +105,5 @@ class ApplyAdminGroup(ModelAdminGroup): ApplicationFormAdmin, ReviewFormAdmin, CategoryAdmin, + ScreeningStatusAdmin, ) diff --git a/opentech/apply/funds/api_views.py b/opentech/apply/funds/api_views.py new file mode 100644 index 0000000000000000000000000000000000000000..4e294f673bb74547106e2521eff91bb93be5fc8a --- /dev/null +++ b/opentech/apply/funds/api_views.py @@ -0,0 +1,47 @@ +from django.db.models import Q +from rest_framework import generics +from rest_framework import permissions +from django_filters import rest_framework as filters + +from wagtail.core.models import Page + +from opentech.api.pagination import StandardResultsSetPagination +from .models import ApplicationSubmission +from .serializers import SubmissionListSerializer, SubmissionDetailSerializer +from .permissions import IsApplyStaffUser + + +class RoundLabFilter(filters.ModelChoiceFilter): + def filter(self, qs, value): + if not value: + return qs + + return qs.filter(Q(round=value) | Q(page=value)) + + +class SubmissionsFilter(filters.FilterSet): + # TODO replace with better call to Round and Lab base class + round = RoundLabFilter(queryset=Page.objects.all()) + + class Meta: + model = ApplicationSubmission + fields = ('status', 'round') + + +class SubmissionList(generics.ListAPIView): + queryset = ApplicationSubmission.objects.current() + serializer_class = SubmissionListSerializer + permission_classes = ( + permissions.IsAuthenticated, IsApplyStaffUser, + ) + filter_backends = (filters.DjangoFilterBackend,) + filter_class = SubmissionsFilter + pagination_class = StandardResultsSetPagination + + +class SubmissionDetail(generics.RetrieveAPIView): + queryset = ApplicationSubmission.objects.all() + serializer_class = SubmissionDetailSerializer + permission_classes = ( + permissions.IsAuthenticated, IsApplyStaffUser, + ) diff --git a/opentech/apply/funds/blocks.py b/opentech/apply/funds/blocks.py index d6a87a5f919876520865192f3e1dde137a13a782..7ddff78f7ef5869c3f92078415d452ab913d88d3 100644 --- a/opentech/apply/funds/blocks.py +++ b/opentech/apply/funds/blocks.py @@ -39,6 +39,9 @@ class ValueBlock(ApplicationSingleIncludeFieldBlock): class Meta: label = _('Requested amount') + def prepare_data(self, value, data, serialize): + return '$' + str(data) + class EmailBlock(ApplicationMustIncludeFieldBlock): name = 'email' @@ -62,14 +65,29 @@ class AddressFieldBlock(ApplicationSingleIncludeFieldBlock): def format_data(self, data): # Based on the fields listed in addressfields/widgets.py + return ', '.join( + data[field] + for field in order_fields + if data[field] + ) + + def prepare_data(self, value, data, serialize): order_fields = [ 'thoroughfare', 'premise', 'localityname', 'administrativearea', 'postalcode', 'country' ] - address = json.loads(data) - return ', '.join( - address[field] + data = json.loads(data) + data = { + field: data[field] for field in order_fields - if address[field] + } + + if serialize: + return data + + return ', '.join( + value + for value in data.values() + if value ) @@ -108,7 +126,7 @@ class DurationBlock(ApplicationMustIncludeFieldBlock): field_kwargs['choices'] = self.DURATION_OPTIONS.items() return field_kwargs - def format_data(self, data): + def prepare_data(self, value, data, serialize): return self.DURATION_OPTIONS[int(data)] class Meta: diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py index dd435d559c7c1ad8fe05463bbf774398e294af66..0b1df3d3b4a79c45b485b0cff5600b80a4a3961a 100644 --- a/opentech/apply/funds/forms.py +++ b/opentech/apply/funds/forms.py @@ -22,6 +22,20 @@ class ProgressSubmissionForm(forms.ModelForm): self.should_show = bool(choices) +class ScreeningSubmissionForm(forms.ModelForm): + + class Meta: + model = ApplicationSubmission + fields = ('screening_status',) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super().__init__(*args, **kwargs) + self.should_show = False + if (self.instance.active and self.user.is_apply_staff) or self.user.is_superuser: + self.should_show = True + + class UpdateSubmissionLeadForm(forms.ModelForm): class Meta: model = ApplicationSubmission diff --git a/opentech/apply/funds/migrations/0049_screening_status.py b/opentech/apply/funds/migrations/0049_screening_status.py new file mode 100644 index 0000000000000000000000000000000000000000..ad1fb85306d9ac71a70cf6a79071ba401f60a1a3 --- /dev/null +++ b/opentech/apply/funds/migrations/0049_screening_status.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.10 on 2019-01-10 15:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0048_add_field_slack_channel'), + ] + + operations = [ + migrations.CreateModel( + name='ScreeningStatus', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=128)), + ], + options={ + 'verbose_name_plural': 'screening statuses', + }, + ), + migrations.AddField( + model_name='applicationsubmission', + name='screening_status', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='funds.ScreeningStatus', verbose_name='screening status'), + ), + ] diff --git a/opentech/apply/funds/migrations/0050_roundsandlabs.py b/opentech/apply/funds/migrations/0050_roundsandlabs.py new file mode 100644 index 0000000000000000000000000000000000000000..c1f65610caac3d58ea047bf60072ee920f5d98c0 --- /dev/null +++ b/opentech/apply/funds/migrations/0050_roundsandlabs.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.9 on 2019-01-16 16:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0040_page_draft_title'), + ('funds', '0049_screening_status'), + ] + + operations = [ + migrations.CreateModel( + name='RoundsAndLabs', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/opentech/apply/funds/models/__init__.py b/opentech/apply/funds/models/__init__.py index 9cfc41a3b66c9995a9b7aca916bceec94f8b0928..101a4b48011e5d342fcf8164a75a073020d4de32 100644 --- a/opentech/apply/funds/models/__init__.py +++ b/opentech/apply/funds/models/__init__.py @@ -1,11 +1,12 @@ from django.utils.translation import ugettext_lazy as _ -from .applications import ApplicationBase, RoundBase, LabBase +from .applications import ApplicationBase, RoundBase, LabBase, RoundsAndLabs # NOQA from .forms import ApplicationForm +from .screening import ScreeningStatus from .submissions import ApplicationSubmission, ApplicationRevision -__all__ = ['ApplicationSubmission', 'ApplicationRevision', 'ApplicationForm'] +__all__ = ['ApplicationSubmission', 'ApplicationRevision', 'ApplicationForm', 'ScreeningStatus'] class FundType(ApplicationBase): diff --git a/opentech/apply/funds/models/applications.py b/opentech/apply/funds/models/applications.py index 0119e8c6503b93503336affddad338f2fd37ba3c..30097f1d3833a2c6f29beef9d29353bddf1c3c80 100644 --- a/opentech/apply/funds/models/applications.py +++ b/opentech/apply/funds/models/applications.py @@ -3,7 +3,21 @@ from datetime import date from django.conf import settings from django.core.exceptions import ValidationError from django.db import models -from django.db.models import OuterRef, Q, Subquery +from django.db.models import ( + Case, + CharField, + Count, + F, + FloatField, + Func, + IntegerField, + OuterRef, + Q, + Subquery, + When, +) +from django.db.models.functions import Coalesce, Length + from django.http import Http404 from django.utils.functional import cached_property from django.utils.text import mark_safe @@ -18,7 +32,7 @@ from wagtail.admin.edit_handlers import ( ObjectList, TabbedInterface, ) -from wagtail.core.models import PageManager, PageQuerySet +from wagtail.core.models import Page, PageManager, PageQuerySet from ..admin_forms import WorkflowFormAdminForm from ..edit_handlers import ReadOnlyPanel, ReadOnlyInlinePanel @@ -104,6 +118,11 @@ class RoundBaseManager(PageQuerySet): ) return rounds + def closed(self): + rounds = self.live().public().specific() + rounds = rounds.filter(end_date__lt=date.today()) + return rounds + class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore is_creatable = False @@ -314,3 +333,125 @@ class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm): # type: ig def open_round(self): return self.live + + +class RoundsAndLabsQueryset(PageQuerySet): + def new(self): + return self.filter(start_date__gt=date.today()) + + def open(self): + return self.filter(Q(end_date__gte=date.today(), start_date__lte=date.today()) | Q(end_date__isnull=True)) + + def closed(self): + return self.filter(end_date__lt=date.today()) + + +class RoundsAndLabsProgressQueryset(RoundsAndLabsQueryset): + def active(self): + return self.filter(progress__lt=100) + + def inactive(self): + return self.filter(progress=100) + + +class RoundsAndLabsManager(PageManager): + def get_queryset(self, base_queryset=RoundsAndLabsQueryset): + funds = ApplicationBase.objects.filter(path=OuterRef('parent_path')) + + return base_queryset(self.model, using=self._db).type(SubmittableStreamForm).annotate( + lead=Coalesce( + F('roundbase__lead__full_name'), + F('labbase__lead__full_name'), + ), + start_date=F('roundbase__start_date'), + end_date=F('roundbase__end_date'), + parent_path=Left(F('path'), Length('path') - ApplicationBase.steplen, output_field=CharField()), + fund=Subquery(funds.values('title')[:1]), + ) + + def with_progress(self): + submissions = ApplicationSubmission.objects.filter(Q(round=OuterRef('pk')) | Q(page=OuterRef('pk'))).current() + closed_submissions = submissions.inactive() + + return self.get_queryset(RoundsAndLabsProgressQueryset).annotate( + total_submissions=Coalesce( + Subquery( + submissions.values('round').annotate(count=Count('pk')).values('count'), + output_field=IntegerField(), + ), + 0, + ), + closed_submissions=Coalesce( + Subquery( + closed_submissions.values('round').annotate(count=Count('pk')).values('count'), + output_field=IntegerField(), + ), + 0, + ), + ).annotate( + progress=Case( + When(total_submissions=0, then=None), + default=(F('closed_submissions') * 100) / F('total_submissions'), + output_fields=FloatField(), + ) + + ) + + def open(self): + return self.get_queryset().open() + + def closed(self): + return self.get_queryset().closed() + + def new(self): + return self.get_queryset().new() + + +class RoundsAndLabs(Page): + """ + This behaves as a useful way to get all the rounds and labs that are defined + in the project regardless of how they are implemented (lab/round/sealed_round) + """ + class Meta: + proxy = True + + def __eq__(self, other): + # This is one way equality RoundAndLab == Round/Lab + # Round/Lab == RoundAndLab returns False due to different + # Concrete class + if not isinstance(other, models.Model): + return False + if not isinstance(other, SubmittableStreamForm): + return False + my_pk = self.pk + if my_pk is None: + return self is other + return my_pk == other.pk + + objects = RoundsAndLabsManager() + + def save(self, *args, **kwargs): + raise NotImplementedError('Do not save through this model') + + +# TODO remove in django 2.1 where this is fixed +F.relabeled_clone = lambda self, relabels: self + + +# TODO remove in django 2.1 where this is added +class Left(Func): + function = 'LEFT' + arity = 2 + + def __init__(self, expression, length, **extra): + """ + expression: the name of a field, or an expression returning a string + length: the number of characters to return from the start of the string + """ + if not hasattr(length, 'resolve_expression'): + if length < 1: + raise ValueError("'length' must be greater than 0.") + super().__init__(expression, length, **extra) + + def get_substr(self): + return Substr(self.source_expressions[0], Value(1), self.source_expressions[1]) diff --git a/opentech/apply/funds/models/mixins.py b/opentech/apply/funds/models/mixins.py index 87a1dd39ea31e63666f76fac46e9071fa3dd6d88..31a5b899affdd6817f197a886a099939f855dd7a 100644 --- a/opentech/apply/funds/models/mixins.py +++ b/opentech/apply/funds/models/mixins.py @@ -137,6 +137,22 @@ class AccessFormData: if isinstance(field.block, SingleIncludeMixin) } + @property + def normal_blocks(self): + return [ + field_id + for field_id in self.question_field_ids + if field_id not in self.named_blocks + ] + + def serialize(self, field_id): + field = self.field(field_id) + data = self.data(field_id) + return field.render(context={ + 'serialize': True, + 'data': data, + }) + def render_answer(self, field_id, include_question=False): try: field = self.field(field_id) @@ -149,8 +165,7 @@ class AccessFormData: # Returns a list of the rendered answers return [ self.render_answer(field_id, include_question=True) - for field_id in self.question_field_ids - if field_id not in self.named_blocks + for field_id in self.normal_blocks ] def output_answers(self): diff --git a/opentech/apply/funds/models/screening.py b/opentech/apply/funds/models/screening.py new file mode 100644 index 0000000000000000000000000000000000000000..c590704b145651314664e60798c8a61ff563dc46 --- /dev/null +++ b/opentech/apply/funds/models/screening.py @@ -0,0 +1,11 @@ +from django.db import models + + +class ScreeningStatus(models.Model): + title = models.CharField(max_length=128) + + class Meta: + verbose_name_plural = "screening statuses" + + def __str__(self): + return self.title diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 3e5237407d5813713e2c34b6343ae84cede99b9c..0adeb684360ff6ddfa38b4406b7b2d01e752ea1f 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -61,7 +61,8 @@ class JSONOrderable(models.QuerySet): field = field[1:] else: descending = False - return OrderBy(RawSQL(f'LOWER({self.json_field}->>%s)', (field,)), descending=descending, nulls_last=True) + db_table = self.model._meta.db_table + return OrderBy(RawSQL(f'LOWER({db_table}.{self.json_field}->>%s)', (field,)), descending=descending, nulls_last=True) field_ordering = [build_json_order_by(field) for field in field_names] return super().order_by(*field_ordering) @@ -309,6 +310,14 @@ class ApplicationSubmission( # Workflow inherited from WorkflowHelpers status = FSMField(default=INITIAL_STATE, protected=True) + screening_status = models.ForeignKey( + 'funds.ScreeningStatus', + related_name='+', + on_delete=models.SET_NULL, + verbose_name='screening status', + null=True, + ) + is_draft = False live_revision = models.OneToOneField( @@ -640,6 +649,9 @@ class ApplicationRevision(AccessFormData, models.Model): class Meta: ordering = ['-timestamp'] + def __str__(self): + return f'Revision for {self.submission.title} by {self.author} ' + @property def form_fields(self): return self.submission.form_fields diff --git a/opentech/apply/funds/permissions.py b/opentech/apply/funds/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..ec6f22f83b78b3476cf267333a68149c7c32e0df --- /dev/null +++ b/opentech/apply/funds/permissions.py @@ -0,0 +1,13 @@ +from rest_framework import permissions + + +class IsApplyStaffUser(permissions.BasePermission): + """ + Custom permission to only allow OTF Staff or higher + """ + + def has_permission(self, request, view): + return request.user.is_apply_staff + + def has_object_permission(self, request, view, obj): + return request.user.is_apply_staff diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/funds/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..3203a7ecca0cefc53594d594c9b39964655dbf3e --- /dev/null +++ b/opentech/apply/funds/serializers.py @@ -0,0 +1,47 @@ +from rest_framework import serializers + +from .models import ApplicationSubmission + + +class SubmissionListSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='funds:submissions-api:detail') + + class Meta: + model = ApplicationSubmission + fields = ('id', 'title', 'status', 'url') + + +class SubmissionDetailSerializer(serializers.ModelSerializer): + questions = serializers.SerializerMethodField() + meta_questions = serializers.SerializerMethodField() + stage = serializers.CharField(source='stage.name') + + class Meta: + model = ApplicationSubmission + fields = ('id', 'title', 'stage', 'meta_questions', 'questions') + + def serialize_questions(self, obj, fields): + for field_id in fields: + yield obj.serialize(field_id) + + def get_meta_questions(self, obj): + meta_questions = { + 'title': 'Project Name', + 'full_name': 'Legal Name', + 'email': 'Email', + 'value': 'Requested Funding', + 'duration': 'Project Duration', + 'address': 'Address' + } + data = self.serialize_questions(obj, obj.named_blocks.values()) + data = [ + { + **response, + 'question': meta_questions.get(response['type'], response['question']) + } + for response in data + ] + return data + + def get_questions(self, obj): + return self.serialize_questions(obj, obj.normal_blocks) diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py index b9d5626c11bfcfcb34d7da0b083ea5d3c48b231d..4456510530e47f6676caf9973e04d0eed9e8d320 100644 --- a/opentech/apply/funds/tables.py +++ b/opentech/apply/funds/tables.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.db.models import F, Q from django.utils.html import format_html from django.utils.text import mark_safe, slugify +from django.utils.translation import ugettext_lazy as _ import django_filters as filters import django_tables2 as tables @@ -12,14 +13,14 @@ from django_tables2.utils import A from wagtail.core.models import Page -from opentech.apply.funds.models import ApplicationSubmission, Round +from opentech.apply.funds.models import ApplicationSubmission, Round, ScreeningStatus from opentech.apply.funds.workflow import STATUSES from opentech.apply.users.groups import STAFF_GROUP_NAME from .widgets import Select2MultiCheckboxesWidget def make_row_class(record): - css_class = 'submission-meta__row' if record.next else 'all-submissions__parent' + css_class = 'submission-meta__row' if record.next else 'all-submissions-table__parent' css_class += '' if record.active else ' is-inactive' return css_class @@ -34,21 +35,22 @@ class SubmissionsTable(tables.Table): submit_time = tables.DateColumn(verbose_name="Submitted") phase = tables.Column(verbose_name="Status", order_by=('status',)) stage = tables.Column(verbose_name="Type", order_by=('status',)) - page = tables.Column(verbose_name="Fund") + fund = tables.Column(verbose_name="Fund", accessor='page') comments = tables.Column(accessor='comment_count', verbose_name="Comments") last_update = tables.DateColumn(accessor="last_update", verbose_name="Last updated") class Meta: model = ApplicationSubmission order_by = ('-last_update',) - fields = ('title', 'phase', 'stage', 'page', 'round', 'submit_time', 'last_update') + fields = ('title', 'phase', 'stage', 'fund', 'round', 'submit_time', 'last_update') sequence = fields + ('comments',) template_name = 'funds/tables/table.html' row_attrs = { 'class': make_row_class, 'data-record-id': lambda record: record.id, } - attrs = {'class': 'all-submissions'} + attrs = {'class': 'all-submissions-table'} + empty_text = _('No submissions available') def render_user(self, value): return value.get_full_name() @@ -67,15 +69,21 @@ class AdminSubmissionsTable(SubmissionsTable): """Adds admin only columns to the submissions table""" lead = tables.Column(order_by=('lead.full_name',)) reviews_stats = tables.TemplateColumn(template_name='funds/tables/column_reviews.html', verbose_name=mark_safe("Reviews\n<span>Assgn.\tComp.</span>"), orderable=False) + screening_status = tables.Column(verbose_name="Screening") class Meta(SubmissionsTable.Meta): - fields = ('title', 'phase', 'stage', 'page', 'round', 'lead', 'submit_time', 'last_update', 'reviews_stats') # type: ignore + fields = ('title', 'phase', 'stage', 'fund', 'round', 'lead', 'submit_time', 'last_update', 'screening_status', 'reviews_stats') # type: ignore sequence = fields + ('comments',) def render_lead(self, value): return format_html('<span>{}</span>', value) +class SummarySubmissionsTable(AdminSubmissionsTable): + class Meta(AdminSubmissionsTable.Meta): + orderable = False + + def get_used_rounds(request): return Round.objects.filter(submissions__isnull=False).distinct() @@ -96,6 +104,11 @@ def get_reviewers(request): return User.objects.filter(Q(submissions_reviewer__isnull=False) | Q(groups__name=STAFF_GROUP_NAME) | Q(is_superuser=True)).distinct() +def get_screening_statuses(request): + return ScreeningStatus.objects.filter( + id__in=ApplicationSubmission.objects.all().values('screening_status__id').distinct('screening_status__id')) + + class Select2CheckboxWidgetMixin(filters.Filter): def __init__(self, *args, **kwargs): label = kwargs.get('label') @@ -129,15 +142,98 @@ class StatusMultipleChoiceFilter(Select2MultipleChoiceFilter): class SubmissionFilter(filters.FilterSet): round = Select2ModelMultipleChoiceFilter(queryset=get_used_rounds, label='Rounds') - funds = Select2ModelMultipleChoiceFilter(name='page', queryset=get_used_funds, label='Funds') + fund = Select2ModelMultipleChoiceFilter(name='page', queryset=get_used_funds, label='Funds') status = StatusMultipleChoiceFilter() lead = Select2ModelMultipleChoiceFilter(queryset=get_round_leads, label='Leads') reviewers = Select2ModelMultipleChoiceFilter(queryset=get_reviewers, label='Reviewers') + screening_status = Select2ModelMultipleChoiceFilter(queryset=get_screening_statuses, label='Screening') class Meta: model = ApplicationSubmission - fields = ('funds', 'round', 'status') + fields = ('fund', 'round', 'status') + + def __init__(self, *args, exclude=list(), **kwargs): + super().__init__(*args, **kwargs) + + self.filters = { + field: filter + for field, filter in self.filters.items() + if field not in exclude + } class SubmissionFilterAndSearch(SubmissionFilter): query = filters.CharFilter(field_name='search_data', lookup_expr="icontains", widget=forms.HiddenInput) + + +class RoundsTable(tables.Table): + title = tables.LinkColumn('funds:rounds:detail', args=[A('pk')], orderable=True, text=lambda record: record.title) + fund = tables.Column(accessor=A('specific.fund')) + lead = tables.Column() + start_date = tables.Column() + end_date = tables.Column() + progress = tables.Column(verbose_name="Determined") + + class Meta: + fields = ('title', 'fund', 'lead', 'start_date', 'end_date', 'progress') + attrs = {'class': 'all-rounds-table'} + + def render_lead(self, value): + return format_html('<span>{}</span>', value) + + def render_progress(self, record): + return f'{record.progress}%' + + def _field_order(self, field, desc): + return getattr(F(f'{field}'), 'desc' if desc else 'asc')(nulls_last=True) + + def order_start_date(self, qs, desc): + return qs.order_by(self._field_order('start_date', desc)), True + + def order_end_date(self, qs, desc): + return qs.order_by(self._field_order('end_date', desc)), True + + def order_fund(self, qs, desc): + return qs.order_by(self._field_order('fund', desc)), True + + def order_progress(self, qs, desc): + return qs.order_by(self._field_order('progress', desc)), True + + +class ActiveRoundFilter(Select2MultipleChoiceFilter): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, choices=[('active', 'Active'), ('inactive', 'Inactive')], **kwargs) + + def filter(self, qs, value): + if value is None or len(value) != 1: + return qs + + value = value[0] + if value == 'active': + return qs.active() + else: + return qs.inactive() + + +class OpenRoundFilter(Select2MultipleChoiceFilter): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, choices=[('open', 'Open'), ('closed', 'Closed'), ('new', 'Not Started')], **kwargs) + + def filter(self, qs, value): + if value is None or len(value) != 1: + return qs + + value = value[0] + if value == 'closed': + return qs.closed() + if value == 'new': + return qs.new() + + return qs.open() + + +class RoundsFilter(filters.FilterSet): + fund = Select2ModelMultipleChoiceFilter(queryset=get_used_funds, label='Funds') + lead = Select2ModelMultipleChoiceFilter(queryset=get_round_leads, label='Leads') + active = ActiveRoundFilter(label='Active') + round_state = OpenRoundFilter(label='Open') diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html index e0a41610a6575d80050c321abbd4416b41a59bb6..d39cf28ada9ae8697491c16f859ae05c0674b285 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html @@ -12,6 +12,7 @@ {% block sidebar_forms %} {% include "funds/includes/actions.html" %} + {% include "funds/includes/screening_form.html" %} {% include "funds/includes/progress_form.html" %} {% include "funds/includes/update_lead_form.html" %} {% include "funds/includes/update_reviewer_form.html" %} @@ -32,6 +33,10 @@ </div> {% endblock %} +{% block screening_status %} + {% include 'funds/includes/screening_status_block.html' %} +{% endblock %} + {% block determination %} {% include 'determinations/includes/determination_block.html' with submission=object %} {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html index cb2a5197abb9eb45947716b98d599828db483004..c0b2799b270626282dc4d8f387e5a12f8f37f4d0 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html @@ -1,5 +1,5 @@ {% extends "base-apply.html" %} -{% load static workflow_tags wagtailcore_tags %} +{% load static workflow_tags wagtailcore_tags statusbar_tags %} {% block title %}{{ object.title }}{% endblock %} {% block body_class %}{% endblock %} {% block content %} @@ -17,7 +17,7 @@ <span>{{ object.round }}</span> <span>Lead: {{ object.lead }}</span> </h5> - {% include "funds/includes/status_bar.html" with phases=object.workflow current_phase=object.phase same_stage=True%} + {% status_bar object.workflow object.phase request.user same_stage=True%} <div class="tabs js-tabs"> <div class="tabs__container"> @@ -42,7 +42,7 @@ </div> </div> -<div class="wrapper wrapper--large wrapper--tabs"> +<div class="wrapper wrapper--large wrapper--tabs js-tabs-content"> {# Tab 1 #} <div class="tabs__content" id="tab-1"> {% block admin_actions %} @@ -86,6 +86,11 @@ {% include 'determinations/includes/applicant_determination_block.html' with submission=object %} {% endblock %} + {% if request.user.is_apply_staff %} + {% block screening_status %} + {% endblock %} + {% endif %} + {% block reviews %} {% endblock %} @@ -138,4 +143,3 @@ <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> {% endblock %} - diff --git a/opentech/apply/funds/templates/funds/base_submissions_table.html b/opentech/apply/funds/templates/funds/base_submissions_table.html new file mode 100644 index 0000000000000000000000000000000000000000..d9214f80da196c86ca5211367455a9e292fd1bac --- /dev/null +++ b/opentech/apply/funds/templates/funds/base_submissions_table.html @@ -0,0 +1,25 @@ +{% extends "base-apply.html" %} +{% load static %} +{% load render_table from django_tables2 %} + +{% block extra_css %} +{{ filter.form.media.css }} +{% endblock %} + +{% block content %} + {% block table %} + {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action %} + + {% render_table table %} + {% endblock %} +{% endblock %} + +{% block extra_js %} + {{ filter.form.media.js }} + <script src="{% static 'js/apply/all-submissions-table.js' %}"></script> + <script src="https://cdn.jsdelivr.net/npm/symbol-es6@0.1.2/symbol-es6.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/url-search-params/1.1.0/url-search-params.js"></script> + <script src="{% static 'js/apply/submission-filters.js' %}"></script> + <script src="{% static 'js/apply/submission-tooltips.js' %}"></script> + <script src="{% static 'js/apply/tabs.js' %}"></script> +{% endblock %} diff --git a/opentech/apply/funds/templates/funds/includes/actions.html b/opentech/apply/funds/templates/funds/includes/actions.html index b66583a84a0bb7c13435a6d61033d60c8ddc17ef..63e1ac00bf41195d1d0fcbee753b3310ed722e09 100644 --- a/opentech/apply/funds/templates/funds/includes/actions.html +++ b/opentech/apply/funds/templates/funds/includes/actions.html @@ -4,7 +4,12 @@ <div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions {% if mobile %}sidebar__inner--mobile{% endif %}"> <h5>Actions to take</h5> + + <a data-fancybox data-src="#screen-application" class="button button--bottom-space button--primary button--full-width {% if screening_form.should_show %}is-not-disabled{% else %}is-disabled{% endif %}" href="#">Screen application</a> + <a data-fancybox data-src="#update-status" class="button button--primary button--full-width {% if progress_form.should_show %}is-not-disabled{% else %}is-disabled{% endif %}" href="#">Update status</a> + + <p class="sidebar__separator">Assign</p> <div class="wrapper wrapper--sidebar-buttons"> diff --git a/opentech/apply/funds/templates/funds/includes/activity-feed.html b/opentech/apply/funds/templates/funds/includes/activity-feed.html index e51c07d70dd324dea8ce5539f33b116a9ff08e9a..0a3595a6d1765fd7ab028c1bc83200a646f45054 100644 --- a/opentech/apply/funds/templates/funds/includes/activity-feed.html +++ b/opentech/apply/funds/templates/funds/includes/activity-feed.html @@ -13,7 +13,7 @@ </div> </div> - <div class="wrapper wrapper--medium wrapper--activity-feed"> + <div class="wrapper wrapper--medium wrapper--activity-feed js-tabs-content"> <div class="tabs__content tabs__content--current" id="tab-1"> {% include "activity/include/comment_list.html" with submission_title=True %} </div> diff --git a/opentech/apply/funds/templates/funds/includes/round-block-listing.html b/opentech/apply/funds/templates/funds/includes/round-block-listing.html new file mode 100644 index 0000000000000000000000000000000000000000..0c4fc9625b50ffbb9da14c63b2e28656d4c484fb --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/round-block-listing.html @@ -0,0 +1,33 @@ +<ul class="round-block"> + {% for round in rounds %} + {% if forloop.counter0 < 5 %} + <li class="round-block__item"> + <h5 class="round-block__title">{{ round }}</h5> + <p> {{ round.fund|default_if_none:"-" }} </p> + <p class="round-block__date"> + {% if round.end_date %} + {{ display_text }} {{ round.end_date|date:"Y-m-d" }} + {% else %} + Open + {% endif %} + </p> + <p class="round-block__determination"> + {% if round.progress is None%} + - + {% else %} + {{ round.progress }}% Determined + {% endif %} + </p> + <a class="round-block__view" href="{% url 'apply:rounds:detail' pk=round.pk %}">View</a> + </li> + {% else %} + <li class="round-block__item round-block__item--more"> + <a href="{% url 'apply:rounds:list' %}{{ query }}">Show all</a> + </li> + {% endif %} + {% empty %} + <p class="round-block__not-found"> + There are no {% if round.end_date %} {{ display_text|lower }} {% else %} open {% endif %} rounds + </p> + {% endfor %} +</ul> diff --git a/opentech/apply/funds/templates/funds/includes/round-block.html b/opentech/apply/funds/templates/funds/includes/round-block.html new file mode 100644 index 0000000000000000000000000000000000000000..8fdca206107d2438cef6a3115d49dfe101260797 --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/round-block.html @@ -0,0 +1,22 @@ +<div class="wrapper wrapper--bottom-space"> + <section class="section section--with-options"> + <h4 class="heading heading--normal heading--no-margin">All Rounds and Labs</h4> + <div class="js-tabs"> + <a class="tab__item tab__item--alt" href="#closed-rounds" data-tab="tab-1">Closed</a> + <a class="tab__item tab__item--alt" href="#open-rounds" data-tab="tab-2">Open</a> + </div> + </section> + + {# Closed rounds/labs tab #} + <div class="js-tabs-content"> + <div class="tabs__content" id="tab-1"> + {% include "funds/includes/round-block-listing.html" with rounds=closed_rounds display_text="Closed" query=closed_query %} + </div> + + {# Open rounds/labs tab #} + <div class="tabs__content" id="tab-2"> + {% include "funds/includes/round-block-listing.html" with rounds=open_rounds display_text="Open until" query=open_query %} + </div> + </div> + +</div> diff --git a/opentech/apply/funds/templates/funds/includes/screening_form.html b/opentech/apply/funds/templates/funds/includes/screening_form.html new file mode 100644 index 0000000000000000000000000000000000000000..87b3f9cd4a6cfdb2341f270704c9d793e1c7bec5 --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/screening_form.html @@ -0,0 +1,7 @@ +{% if screening_form.should_show %} +<div class="modal" id="screen-application"> + <h4>Update status</h4> + <p>Current status: {{ object.screening_status }}</p> + {% include 'funds/includes/delegated_form_base.html' with form=screening_form value='Screen'%} +</div> +{% endif %} diff --git a/opentech/apply/funds/templates/funds/includes/screening_status_block.html b/opentech/apply/funds/templates/funds/includes/screening_status_block.html new file mode 100644 index 0000000000000000000000000000000000000000..c278709042f9a5f194de16f7dfc536606e92d497 --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/screening_status_block.html @@ -0,0 +1,6 @@ +<div class="sidebar__inner"> + <h5>Screening Status</h5> + <p> + {{ object.screening_status|default:"Awaiting Screen status" }} + </p> +</div> \ No newline at end of file diff --git a/opentech/apply/funds/templates/funds/includes/search.html b/opentech/apply/funds/templates/funds/includes/search.html deleted file mode 100644 index 5e3afc9d9c48017d2c59cf5baa5acc832010b672..0000000000000000000000000000000000000000 --- a/opentech/apply/funds/templates/funds/includes/search.html +++ /dev/null @@ -1,6 +0,0 @@ -<form action="{% url 'funds:search' %}" method="get" role="search" class="form form--header-search-desktop"> - <button class="button button--search" type="submit" aria-label="Search"> - <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg> - </button> - <input class="input input--search" type="text" placeholder="Search submissions" name="query"{% if search_query %} value="{{ search_query }}{% endif %}" aria-label="Search input"> -</form> diff --git a/opentech/apply/funds/templates/funds/includes/status_bar.html b/opentech/apply/funds/templates/funds/includes/status_bar.html index bc4fce3f3e4b0bcae5b618911e39ea7010cf7b24..eb5235a2ec9213c99817cb3f12941e53e70e6e78 100644 --- a/opentech/apply/funds/templates/funds/includes/status_bar.html +++ b/opentech/apply/funds/templates/funds/includes/status_bar.html @@ -1,26 +1,28 @@ +{% load statusbar_tags %} <div class="status-bar {{ class }}"> - {% for phase_name, phase in phases.items %} - {% if not same_stage or current_phase.stage == phase.stage %} - {% ifchanged phase.step %} - <div class="status-bar__item - {% if phase.step == current_phase.step %} - status-bar__item--is-current - {% elif current_phase.step > phase.step %} - status-bar__item--is-complete - {% endif %}"> - <span class="status-bar__tooltip" - {% if phase.step != current_phase.step %} - data-title="{{ phase }}" aria-label="{{ phase }}" - {% else %} - data-title="{{ current_phase }}" aria-label="{{ current_phase }}" - {% endif %} - ></span> - <svg class="status-bar__icon"><use xlink:href="#tick-alt"></use></svg> - </div> - {% endifchanged %} - {% endif %} + {% for phase in phases %} + {% ifchanged phase.step %} + <div class="status-bar__item + {% if phase.step == current_phase.step %} + status-bar__item--is-current + {% elif current_phase.step > phase.step %} + status-bar__item--is-complete + {% endif %}"> + <span class="status-bar__tooltip" + {% status_display current_phase phase public as display_text %} + data-title="{{ display_text }}" aria-label="{{ display_text }}" + ></span> + <svg class="status-bar__icon"><use xlink:href="#tick-alt"></use></svg> + </div> + {% endifchanged %} {% endfor %} </div> <div class="status-bar--mobile"> - <h6 class="status-bar__subheading">{{ current_phase }}</h6> + <h6 class="status-bar__subheading"> + {% if public %} + {{ current_phase.public_name }} + {% else %} + {{ current_phase }} + {% endif %} + </h6> </div> diff --git a/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html new file mode 100644 index 0000000000000000000000000000000000000000..e4cff0f2d2f2147b94009bcebc63f27aefbe9f7f --- /dev/null +++ b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html @@ -0,0 +1,29 @@ +<div class="wrapper wrapper--table-actions"> + <button class="button button--filters button--contains-icons js-toggle-filters">Filters</button> + + {% if use_search|default:False %} + <form method="get" role="search" class="form form--search js-search-form"> + <button class="button button--search" type="submit" aria-label="Search"> + <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg> + </button> + <input class="input input--search input--secondary js-search-input" type="text" placeholder="Search submissions" name="query"{% if search_term %} value="{{ search_term }}"{% endif %} aria-label="Search input"> + </form> + {% endif %} +</div> + +<div class="filters"> + <div class="filters__header"> + <button class="filters__button js-clear-filters">Clear</button> + <div>Filter by</div> + <button class="filters__button js-close-filters">Close</button> + </div> + + <form action="{{ filter_action }}" method="get" class="form form--filters js-filter-form"> + <ul class="form__filters select2"> + {{ filter.form.as_ul }} + <li> + <button class="button button--primary" type="submit" value="Filter">Filter</button> + </li> + </ul> + </form> +</div> diff --git a/opentech/apply/funds/templates/funds/rounds.html b/opentech/apply/funds/templates/funds/rounds.html new file mode 100644 index 0000000000000000000000000000000000000000..3f030e2baf8155a2a9d8809578d55a44517ddc8f --- /dev/null +++ b/opentech/apply/funds/templates/funds/rounds.html @@ -0,0 +1,34 @@ +{% extends "base-apply.html" %} +{% load static %} +{% load render_table from django_tables2 %} + +{% block title %}Rounds{% endblock %} + +{% block extra_css %} +{{ filter.form.media.css }} +{% endblock %} + + +{% block content %} +<div class="admin-bar"> + <div class="admin-bar__inner"> + <div> + <h1 class="gamma heading heading--no-margin heading--bold">Rounds</h1> + <h5>Explore current and past rounds</h5> + </div> + </div> +</div> + +<div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term %} + {% render_table table %} +</div> + +{% endblock %} + +{% block extra_js %} +{{ filter.form.media.js }} +<script src="https://cdnjs.cloudflare.com/ajax/libs/url-search-params/1.1.0/url-search-params.js"></script> +<script src="{% static 'js/apply/submission-filters.js' %}"></script> +<script src="{% static 'js/apply/submission-tooltips.js' %}"></script> +{% endblock %} diff --git a/opentech/apply/funds/templates/funds/submission_sealed.html b/opentech/apply/funds/templates/funds/submission_sealed.html index ffd8069eafd28583caab930593e2447ca2b98f4d..15976eb85070f40ec11f9d1a09f8100c41767216 100644 --- a/opentech/apply/funds/templates/funds/submission_sealed.html +++ b/opentech/apply/funds/templates/funds/submission_sealed.html @@ -18,7 +18,7 @@ <div class="wrapper wrapper--medium"> <h2 class="heading">This application is sealed until the round is closed</h2> <h3>The round ends on: {{ object.round.specific.end_date }}</h3> - <a class="button button--primary" href="{% url 'apply:submissions:list' %}">Go back</a> + <a class="button button--primary" href="{% url 'apply:submissions:overview' %}">Go back</a> {% if can_view_sealed %} <p>As an admin you are allowed to access the application. However, this action will be recorded.</p> <form method="post"> diff --git a/opentech/apply/funds/templates/funds/submissions.html b/opentech/apply/funds/templates/funds/submissions.html index adf89194bf39019ee4c80427aeab312757190a06..353450fa78d785a9339a12023d7eb3f69c5f3736 100644 --- a/opentech/apply/funds/templates/funds/submissions.html +++ b/opentech/apply/funds/templates/funds/submissions.html @@ -1,64 +1,32 @@ -{% extends "base-apply.html" %} -{% load render_table from django_tables2 %} +{% extends "funds/base_submissions_table.html" %} {% load static %} {% block title %}Submissions{% endblock %} -{% block extra_css %} - {{ filter.form.media.css }} -{% endblock %} - {% block content %} <div class="admin-bar"> <div class="admin-bar__inner wrapper--search"> {% block page_header %} <div> - <h1 class="gamma heading heading--no-margin heading--bold">Received Submissions</h1> - <h5>Track and explore recent submissions</h5> + <h1 class="gamma heading heading--no-margin heading--bold">All Submissions</h1> </div> {% endblock %} - {% include "funds/includes/search.html" %} </div> </div> <div class="wrapper wrapper--large wrapper--inner-space-medium"> - - {% if table.data or active_filters %} - <div class="button button--filters button--contains-icons js-open-filters">Filter By</div> - - <div class="filters js-filter-wrapper"> - <div class="filters__header"> - <div class="js-clear-filters">Clear</div> - <div>Filter by</div> - <div class="js-close-filters">Close</div> - </div> - - <form action="" method="get" class="form form--filters"> - <ul class="form__filters select2 js-filter-list"> - {{ filter.form.as_ul }} - <li> - <button class="button button--primary" type="submit" value="Filter">Filter</button> - </li> - </ul> - </form> - </div> - {% endif %} - - {% render_table table %} + {% block table %} + {{ block.super }} + {% endblock %} </div> <a href="#" class="js-open-feed link link--open-feed"> <h4 class="heading heading--no-margin heading--activity-feed">Activity Feed</h4> </a> - {% include "funds/includes/activity-feed.html" %} {% endblock %} {% block extra_js %} - {{ filter.form.media.js }} - <script src="{% static 'js/apply/tabs.js' %}"></script> - <script src="{% static 'js/apply/all-submissions-table.js' %}"></script> - <script src="{% static 'js/apply/submission-filters.js' %}"></script> - <script src="{% static 'js/apply/submission-tooltips.js' %}"></script> + {{ block.super }} <script src="{% static 'js/apply/activity-feed.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/submissions_by_round.html b/opentech/apply/funds/templates/funds/submissions_by_round.html index 30324a16596f2200fd6a3c983fe4d8bbf9f845dd..2aee68595093c4fb1ecc7df571dbef701a500b53 100644 --- a/opentech/apply/funds/templates/funds/submissions_by_round.html +++ b/opentech/apply/funds/templates/funds/submissions_by_round.html @@ -1,25 +1,26 @@ -{% extends "base-apply.html" %} +{% extends "funds/base_submissions_table.html" %} {% load render_bundle from webpack_loader %} {% block title %}{{ object }}{% endblock %} {% block content %} -<div class="admin-bar"> - <div class="admin-bar__inner"> - <div> - <h5><a href="{% url "apply:submissions:list" %}">< Submissions</a></h5> - <h1 class="gamma heading heading--no-margin heading--bold">{{ object }}</h1> - <h5>{% if object.fund %}{{ object.fund }} | {% endif %}Lead: {{ object.lead }}</h5> + <div class="admin-bar"> + <div class="admin-bar__inner admin-bar__inner--with-button"> + <div> + <h1 class="gamma heading heading--no-margin heading--bold">{{ object }}</h1> + <p class="admin-bar__meta">{% if object.fund %}{{ object.fund }} <span>|</span> {% endif %}Lead: {{ object.lead }}</p> + </div> + <div id="submissions-by-round-app-react-switcher"></div> </div> </div> -</div> -<div class="wrapper wrapper--large"> - <div id="react-app"> - <h2>THERE WILL BE A TABLE HERE</h2> + <div id="submissions-by-round-react-app" data-round-id="{{ object.id }}"> + <div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% block table %} + {{ block.super }} + {% endblock %} + </div> </div> -</div> - {% render_bundle 'main' %} {% endblock %} diff --git a/opentech/apply/funds/templates/funds/submissions_overview.html b/opentech/apply/funds/templates/funds/submissions_overview.html new file mode 100644 index 0000000000000000000000000000000000000000..3963db804992e273f05a4202a2846aede8554c0a --- /dev/null +++ b/opentech/apply/funds/templates/funds/submissions_overview.html @@ -0,0 +1,42 @@ +{% extends "funds/base_submissions_table.html" %} +{% load static %} +{% block title %}Submissions{% endblock %} + +{% block content %} +<div class="admin-bar"> + <div class="admin-bar__inner wrapper--search"> + {% block page_header %} + <div> + <h1 class="gamma heading heading--no-margin heading--bold">Submissions</h1> + <h5>Track and explore recent submissions</h5> + </div> + {% endblock %} + </div> +</div> + +<div class="wrapper wrapper--large wrapper--inner-space-medium"> + + {% if closed_rounds or open_rounds %} + {% include "funds/includes/round-block.html" with closed_rounds=closed_rounds open_rounds=open_rounds %} + {% endif %} + + {% block table %} + <h4 class="heading heading--normal heading--no-margin">All Submissions</h4> + {{ block.super }} + <div class="all-submissions-table__more"> + <a href="{% url 'apply:submissions:list' %}">Show all</a> + </div> + {% endblock %} +</div> + +<a href="#" class="js-open-feed link link--open-feed"> + <h4 class="heading heading--no-margin heading--activity-feed">Activity Feed</h4> +</a> +{% include "funds/includes/activity-feed.html" %} + +{% endblock %} + +{% block extra_js %} + {{ block.super }} + <script src="{% static 'js/apply/activity-feed.js' %}"></script> +{% endblock %} diff --git a/opentech/apply/funds/templates/funds/tables/table.html b/opentech/apply/funds/templates/funds/tables/table.html index 9a7848ff770c3d0dd7df0cfb57a6be91f56c75d5..cb3095fc7f961f67e78ed0eadc73308fbf4e2f24 100644 --- a/opentech/apply/funds/templates/funds/tables/table.html +++ b/opentech/apply/funds/templates/funds/tables/table.html @@ -12,7 +12,7 @@ </tr> {% with submission=row.record %} - <tr class="all-submissions__child" data-parent-id="{{ submission.id }}"> + <tr class="all-submissions-table__child" data-parent-id="{{ submission.id }}"> <td colspan="{{ table.columns|length }}"> <table class="submission-meta"> <tr class="submission-meta__row"> @@ -48,7 +48,7 @@ {% if row.record.previous %} {# we have a linked application, re-render the header row #} - <tr class="all-submissions__child" data-parent-id="{{ row.record.id }}"> + <tr class="all-submissions-table__child" data-parent-id="{{ row.record.id }}"> <td colspan="{{ table.columns|length }}"> <table class="submission-meta"> <tr class="submission-meta__row"> @@ -71,3 +71,7 @@ {% endif %} {% endblock %} + +{% block table.tbody.empty_text %} +<tr class="all-submissions-table__empty"><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr> +{% endblock table.tbody.empty_text %} diff --git a/opentech/apply/funds/templatetags/statusbar_tags.py b/opentech/apply/funds/templatetags/statusbar_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..438adb2180ffe5c9c604c5f432f5e3e8a618779e --- /dev/null +++ b/opentech/apply/funds/templatetags/statusbar_tags.py @@ -0,0 +1,56 @@ +from django import template + +register = template.Library() + + +@register.inclusion_tag('funds/includes/status_bar.html') +def status_bar(workflow, current_phase, user, css_class='', same_stage=False): + + phases = workflow.phases_for(user) + + if same_stage and not user.is_applicant: + phases = [ + phase for phase in phases + if phase.stage == current_phase.stage + ] + + if not current_phase.permissions.can_view(user): + current_phase = workflow.previous_visible(current_phase, user) + + # Current step not shown for user, move current phase to last good location + elif not workflow.stepped_phases[current_phase.step][0].permissions.can_view(user): + new_phase_list = [] + for phase in reversed(phases): + if phase.step <= current_phase.step and current_phase not in new_phase_list: + next_phase = current_phase + else: + next_phase = phase + new_phase_list = [next_phase, *new_phase_list] + phases = new_phase_list + + return { + 'phases': phases, + 'current_phase': current_phase, + 'class': css_class, + 'public': user.is_applicant, + } + + +@register.simple_tag() +def status_display(current_phase, phase, public): + if phase.step == current_phase.step: + if public: + return current_phase.public_name + else: + return current_phase.display_name + + if phase.step > current_phase.step: + if public: + return phase.future_name_public + else: + return phase.future_name_staff + + if public: + return phase.public_name + else: + return phase.display_name diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py index fc9722cc7ccae76c1a29ed81903a0c8240b59f5e..6cc1e8165379c40dc229242e941bcb90c123e21d 100644 --- a/opentech/apply/funds/tests/factories/models.py +++ b/opentech/apply/funds/tests/factories/models.py @@ -10,6 +10,7 @@ from opentech.apply.funds.models import ( LabType, RequestForPartners, Round, + ScreeningStatus, SealedRound, ) from opentech.apply.funds.models.forms import ( @@ -43,6 +44,7 @@ __all__ = [ 'LabBaseFormFactory', 'LabSubmissionFactory', 'RequestForPartnersFactory', + 'ScreeningStatusFactory', 'SealedRoundFactory', 'SealedSubmissionFactory', 'TodayRoundFactory', @@ -134,6 +136,10 @@ class RoundFactory(wagtail_factories.PageFactory): start_date=factory.LazyFunction(datetime.date.today), end_date=factory.LazyFunction(lambda: datetime.date.today() + datetime.timedelta(days=7)), ) + closed = factory.Trait( + start_date=factory.LazyFunction(lambda: datetime.date.today() - datetime.timedelta(days=7)), + end_date=factory.LazyFunction(lambda: datetime.date.today() - datetime.timedelta(days=1)), + ) title = factory.Sequence('Round {}'.format) start_date = factory.Sequence(lambda n: datetime.date.today() + datetime.timedelta(days=7 * n + 1)) @@ -311,3 +317,10 @@ class LabBaseReviewFormFactory(AbstractReviewFormFactory): model = LabBaseReviewForm lab = factory.SubFactory(LabFactory) + + +class ScreeningStatusFactory(factory.DjangoModelFactory): + class Meta: + model = ScreeningStatus + + title = factory.Iterator(["Bad", "Good"]) diff --git a/opentech/apply/funds/tests/models/__init__.py b/opentech/apply/funds/tests/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/funds/tests/models/test_roundsandlabs.py b/opentech/apply/funds/tests/models/test_roundsandlabs.py new file mode 100644 index 0000000000000000000000000000000000000000..5eb8709d7b39e407a568deb51dd14ed9ff0ee27c --- /dev/null +++ b/opentech/apply/funds/tests/models/test_roundsandlabs.py @@ -0,0 +1,138 @@ +from django.test import TestCase + +from opentech.apply.funds.models import RoundsAndLabs + +from opentech.apply.funds.tests.factories import ( + ApplicationSubmissionFactory, + FundTypeFactory, + LabFactory, + LabSubmissionFactory, + RoundFactory, +) + + +class BaseRoundsAndLabTestCase: + def test_can_get(self): + obj = self.base_factory() + qs = RoundsAndLabs.objects.all() + self.assertEqual(qs.first(), obj) + + def test_with_progress(self): + obj = self.base_factory() + self.submission_factory(**{self.relation_to_app: obj}) + qs = RoundsAndLabs.objects.with_progress() + fetched_obj = qs.first() + self.assertEqual(fetched_obj.total_submissions, 1) + self.assertEqual(fetched_obj.closed_submissions, 0) + self.assertEqual(fetched_obj.progress, 0) + + def test_with_determined(self): + obj = self.base_factory() + self.submission_factory(**{self.relation_to_app: obj}, rejected=True) + qs = RoundsAndLabs.objects.with_progress() + fetched_obj = qs.first() + self.assertEqual(fetched_obj.total_submissions, 1) + self.assertEqual(fetched_obj.closed_submissions, 1) + self.assertEqual(fetched_obj.progress, 100) + + def test_annotated(self): + obj = self.base_factory() + qs = RoundsAndLabs.objects.with_progress() + fetched_obj = qs.first() + self.assertEqual(fetched_obj.lead, obj.lead.full_name) + self.assertEqual(fetched_obj.start_date, getattr(obj, 'start_date', None)) + self.assertEqual(fetched_obj.end_date, getattr(obj, 'end_date', None)) + self.assertEqual(fetched_obj.parent_path, obj.get_parent().path) + self.assertEqual(fetched_obj.fund, getattr(getattr(obj, 'fund', None), 'title', None)) + + def test_active(self): + obj = self.base_factory() + self.submission_factory(**{self.relation_to_app: obj}) + base_qs = RoundsAndLabs.objects.with_progress() + fetched_obj = base_qs.active().first() + self.assertEqual(fetched_obj, obj) + self.assertFalse(base_qs.inactive().exists()) + + def test_no_submissions_not_either(self): + self.base_factory() + base_qs = RoundsAndLabs.objects.with_progress() + self.assertFalse(base_qs.inactive().exists()) + self.assertFalse(base_qs.active().exists()) + + def test_inactive(self): + obj = self.base_factory() + self.submission_factory(**{self.relation_to_app: obj}, rejected=True) + base_qs = RoundsAndLabs.objects.with_progress() + fetched_obj = base_qs.inactive().first() + self.assertEqual(fetched_obj, obj) + self.assertFalse(base_qs.active().exists()) + + +class TestForLab(BaseRoundsAndLabTestCase, TestCase): + base_factory = LabFactory + submission_factory = LabSubmissionFactory + relation_to_app = 'page' + + # Specific tests as labs and round have very different behaviour here + def test_new(self): + self.base_factory() + fetched_obj = RoundsAndLabs.objects.new().first() + self.assertIsNone(fetched_obj) + + def test_closed(self): + self.base_factory() + fetched_obj = RoundsAndLabs.objects.closed().first() + self.assertIsNone(fetched_obj) + + def test_open(self): + obj = self.base_factory() + fetched_obj = RoundsAndLabs.objects.open().first() + self.assertEqual(fetched_obj, obj) + + +class TestForRound(BaseRoundsAndLabTestCase, TestCase): + base_factory = RoundFactory + submission_factory = ApplicationSubmissionFactory + relation_to_app = 'round' + + # Specific tests as labs and round have very different behaviour here + def test_new(self): + round = self.base_factory() + fetched_obj = RoundsAndLabs.objects.new().first() + self.assertEqual(fetched_obj, round) + + def test_closed(self): + round = self.base_factory(closed=True) + fetched_obj = RoundsAndLabs.objects.closed().first() + self.assertEqual(fetched_obj, round) + + def test_open(self): + obj = self.base_factory(now=True) + fetched_obj = RoundsAndLabs.objects.open().first() + self.assertEqual(fetched_obj, obj) + + +class TestRoundsAndLabsManager(TestCase): + def test_cant_get_fund(self): + FundTypeFactory() + qs = RoundsAndLabs.objects.all() + self.assertEqual(qs.count(), 0) + + def test_doesnt_confuse_lab_and_round(self): + round = RoundFactory() + lab = LabFactory() + + # Lab 50% progress + LabSubmissionFactory(page=lab) + LabSubmissionFactory(page=lab, rejected=True) + + # Round 0% progress + ApplicationSubmissionFactory.create_batch(2, round=round) + + fetched_lab = RoundsAndLabs.objects.with_progress().last() + fetched_round = RoundsAndLabs.objects.with_progress().first() + + self.assertEqual([fetched_round, fetched_lab], [round, lab]) + + self.assertEqual(fetched_round.progress, 0) + self.assertEqual(fetched_lab.progress, 50) diff --git a/opentech/apply/funds/tests/test_views.py b/opentech/apply/funds/tests/test_views.py index 2b1cc1c3bb748f1be73dc47e1dda271fb2b0f6ed..6b8943cd74e0dd8fb7784adef43d447496efed78 100644 --- a/opentech/apply/funds/tests/test_views.py +++ b/opentech/apply/funds/tests/test_views.py @@ -1,15 +1,14 @@ from datetime import datetime, timedelta import json -from opentech.apply.activity.models import Activity +from opentech.apply.activity.models import Activity, INTERNAL from opentech.apply.determinations.tests.factories import DeterminationFactory from opentech.apply.funds.tests.factories import ( ApplicationSubmissionFactory, ApplicationRevisionFactory, InvitedToProposalFactory, - LabFactory, LabSubmissionFactory, - RoundFactory, + ScreeningStatusFactory, SealedRoundFactory, SealedSubmissionFactory, ) @@ -182,6 +181,25 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase): response = self.get_page(submission) self.assertNotContains(response, 'Value') + def test_can_screen_submission(self): + screening_outcome = ScreeningStatusFactory() + self.post_page(self.submission, {'form-submitted-screening_form': '', 'screening_status': screening_outcome.id}) + submission = self.refresh(self.submission) + self.assertEqual(submission.screening_status, screening_outcome) + + def test_cant_screen_submission(self): + """ + Now that the submission has been rejected, we cannot screen it as staff + """ + submission = ApplicationSubmissionFactory(rejected=True) + screening_outcome = ScreeningStatusFactory() + response = self.post_page(submission, {'form-submitted-screening_form': '', 'screening_status': screening_outcome.id}) + self.assertEqual(response.context_data['screening_form'].should_show, False) + + def test_can_view_submission_screening_block(self): + response = self.get_page(self.submission) + self.assertContains(response, 'Screening Status') + class TestReviewersUpdateView(BaseSubmissionViewTestCase): user_factory = StaffFactory @@ -361,6 +379,21 @@ class TestApplicantSubmissionView(BaseSubmissionViewTestCase): response = self.get_page(submission, 'edit') self.assertEqual(response.status_code, 403) + def test_cant_screen_submission(self): + """ + Test that an applicant cannot set the screening status + and that they don't see the screening status form. + """ + screening_outcome = ScreeningStatusFactory() + response = self.post_page(self.submission, {'form-submitted-screening_form': '', 'screening_status': screening_outcome.id}) + self.assertNotIn('screening_form', response.context_data) + submission = self.refresh(self.submission) + self.assertNotEqual(submission.screening_status, screening_outcome) + + def test_cant_see_screening_status_block(self): + response = self.get_page(self.submission) + self.assertNotContains(response, 'Screening Status') + class TestRevisionsView(BaseSubmissionViewTestCase): user_factory = UserFactory @@ -551,49 +584,35 @@ class TestSuperUserSealedView(BaseSubmissionViewTestCase): self.assertTrue(str(second.id) in self.client.session['peeked']) -class ByRoundTestCase(BaseViewTestCase): - url_name = 'apply:submissions:{}' - base_view_name = 'by_round' - - def get_kwargs(self, instance): - return {'pk': instance.id} - - -class TestStaffSubmissionByRound(ByRoundTestCase): - user_factory = StaffFactory - - def test_can_access_round_page(self): - new_round = RoundFactory() - response = self.get_page(new_round) - self.assertContains(response, new_round.title) - - def test_can_access_lab_page(self): - new_lab = LabFactory() - response = self.get_page(new_lab) - self.assertContains(response, new_lab.title) - - def test_cant_access_normal_page(self): - new_round = RoundFactory() - page = new_round.get_site().root_page - response = self.get_page(page) - self.assertEqual(response.status_code, 404) - +class TestSuperUserSubmissionView(BaseSubmissionViewTestCase): + user_factory = SuperUserFactory -class TestApplicantSubmissionByRound(ByRoundTestCase): - user_factory = UserFactory + @classmethod + def setUpTestData(cls): + cls.submission = ApplicationSubmissionFactory() + super().setUpTestData() - def test_cant_access_round_page(self): - new_round = RoundFactory() - response = self.get_page(new_round) - self.assertEqual(response.status_code, 403) + def __setUp__(self): + self.refresh(self.submission) - def test_cant_access_lab_page(self): - new_lab = LabFactory() - response = self.get_page(new_lab) - self.assertEqual(response.status_code, 403) + def test_can_screen_submission(self): + screening_outcome = ScreeningStatusFactory() + self.post_page(self.submission, {'form-submitted-screening_form': '', 'screening_status': screening_outcome.id}) + submission = self.refresh(self.submission) + self.assertEqual(submission.screening_status, screening_outcome) + + def test_can_screen_applications_in_final_status(self): + """ + Now that the submission has been rejected (final determination), + we can still screen it because we are super user + """ + submission = ApplicationSubmissionFactory(rejected=True) + screening_outcome = ScreeningStatusFactory() + response = self.post_page(submission, {'form-submitted-screening_form': '', 'screening_status': screening_outcome.id}) + submission = self.refresh(submission) + self.assertEqual(response.context_data['screening_form'].should_show, True) + self.assertEqual(submission.screening_status, screening_outcome) - def test_cant_access_normal_page(self): - new_round = RoundFactory() - page = new_round.get_site().root_page - response = self.get_page(page) - self.assertEqual(response.status_code, 403) + # Check that an activity was created that should only be viewable internally + activity = Activity.objects.filter(message__contains='Screening status').first() + self.assertEqual(activity.visibility, INTERNAL) diff --git a/opentech/apply/funds/tests/views/test_rounds.py b/opentech/apply/funds/tests/views/test_rounds.py new file mode 100644 index 0000000000000000000000000000000000000000..93b3bd7107be9653543ac74446b761de78d9bd14 --- /dev/null +++ b/opentech/apply/funds/tests/views/test_rounds.py @@ -0,0 +1,99 @@ +from opentech.apply.funds.tests.factories import ( + LabFactory, + RoundFactory, +) + +from opentech.apply.users.tests.factories import ( + ReviewerFactory, + StaffFactory, + UserFactory, +) +from opentech.apply.utils.testing.tests import BaseViewTestCase + + +class BaseAllRoundsViewTestCase(BaseViewTestCase): + url_name = 'funds:rounds:{}' + base_view_name = 'list' + + +class TestStaffRoundPage(BaseAllRoundsViewTestCase): + user_factory = StaffFactory + + def test_can_access_page(self): + response = self.get_page() + self.assertEqual(response.status_code, 200) + + +class TestReviewerAllRoundPage(BaseAllRoundsViewTestCase): + user_factory = ReviewerFactory + + def test_cant_access_page(self): + response = self.get_page() + self.assertEqual(response.status_code, 403) + + +class TestApplicantRoundPage(BaseAllRoundsViewTestCase): + user_factory = UserFactory + + def test_cant_access_page(self): + response = self.get_page() + self.assertEqual(response.status_code, 403) + + +class ByRoundTestCase(BaseViewTestCase): + url_name = 'apply:rounds:{}' + base_view_name = 'detail' + + def get_kwargs(self, instance): + try: + return {'pk': instance.id} + except AttributeError: + return {'pk': instance['id']} + + +class TestStaffSubmissionByRound(ByRoundTestCase): + user_factory = StaffFactory + + def test_can_access_round_page(self): + new_round = RoundFactory() + response = self.get_page(new_round) + self.assertContains(response, new_round.title) + + def test_can_access_lab_page(self): + new_lab = LabFactory() + response = self.get_page(new_lab) + self.assertContains(response, new_lab.title) + + def test_cant_access_normal_page(self): + new_round = RoundFactory() + page = new_round.get_site().root_page + response = self.get_page(page) + self.assertEqual(response.status_code, 404) + + def test_cant_access_non_existing_page(self): + response = self.get_page({'id': 555}) + self.assertEqual(response.status_code, 404) + + +class TestApplicantSubmissionByRound(ByRoundTestCase): + user_factory = UserFactory + + def test_cant_access_round_page(self): + new_round = RoundFactory() + response = self.get_page(new_round) + self.assertEqual(response.status_code, 403) + + def test_cant_access_lab_page(self): + new_lab = LabFactory() + response = self.get_page(new_lab) + self.assertEqual(response.status_code, 403) + + def test_cant_access_normal_page(self): + new_round = RoundFactory() + page = new_round.get_site().root_page + response = self.get_page(page) + self.assertEqual(response.status_code, 403) + + def test_cant_access_non_existing_page(self): + response = self.get_page({'id': 555}) + self.assertEqual(response.status_code, 403) diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py index d9f29072400c042075367adee6bcb78654ff56b0..7897ce287fc42c0919b4bbd171a1f4c9096a8ca3 100644 --- a/opentech/apply/funds/urls.py +++ b/opentech/apply/funds/urls.py @@ -3,13 +3,15 @@ from django.urls import include, path from .views import ( RevisionCompareView, RevisionListView, + RoundListView, SubmissionsByRound, SubmissionDetailView, SubmissionEditView, SubmissionListView, + SubmissionOverviewView, SubmissionSealedView, - SubmissionSearchView, ) +from .api_views import SubmissionList, SubmissionDetail revision_urls = ([ @@ -21,7 +23,8 @@ revision_urls = ([ app_name = 'funds' submission_urls = ([ - path('', SubmissionListView.as_view(), name="list"), + path('', SubmissionOverviewView.as_view(), name="overview"), + path('all/', SubmissionListView.as_view(), name="list"), path('<int:pk>/', include([ path('', SubmissionDetailView.as_view(), name="detail"), path('edit/', SubmissionEditView.as_view(), name="edit"), @@ -32,11 +35,23 @@ submission_urls = ([ path('', include('opentech.apply.determinations.urls', namespace="determinations")), path('revisions/', include(revision_urls, namespace="revisions")), ])), - path('rounds/<int:pk>/', SubmissionsByRound.as_view(), name="by_round"), ], 'submissions') +submission_api_urls = ([ + path('', SubmissionList.as_view(), name='list'), + path('<int:pk>/', SubmissionDetail.as_view(), name='detail'), +], 'submissions-api') + + +rounds_urls = ([ + path('', RoundListView.as_view(), name="list"), + path('<int:pk>/', SubmissionsByRound.as_view(), name="detail"), +], 'rounds') + + urlpatterns = [ path('submissions/', include(submission_urls)), - path('search/', SubmissionSearchView.as_view(), name="search"), + path('rounds/', include(rounds_urls)), + path('api/submissions/', include(submission_api_urls)), ] diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index b96a1fbe15c7f7ded3bf76e94318c92c5d5e7017..76e3dbf082deb524cb3d04d00a9d2d15cc49cde9 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -3,6 +3,7 @@ from copy import copy from django.contrib.auth.decorators import login_required from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.db.models import Q from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy @@ -29,50 +30,104 @@ from opentech.apply.users.decorators import staff_required from opentech.apply.utils.views import DelegateableView, ViewDispatcher from .differ import compare -from .forms import ProgressSubmissionForm, UpdateReviewersForm, UpdateSubmissionLeadForm -from .models import ApplicationSubmission, ApplicationRevision, RoundBase, LabBase -from .tables import AdminSubmissionsTable, SubmissionFilter, SubmissionFilterAndSearch +from .forms import ProgressSubmissionForm, ScreeningSubmissionForm, UpdateReviewersForm, UpdateSubmissionLeadForm +from .models import ApplicationSubmission, ApplicationRevision, RoundsAndLabs, RoundBase, LabBase +from .tables import ( + AdminSubmissionsTable, + RoundsTable, + RoundsFilter, + SubmissionFilterAndSearch, + SummarySubmissionsTable, +) from .workflow import STAGE_CHANGE_ACTIONS @method_decorator(staff_required, name='dispatch') -class SubmissionListView(AllActivityContextMixin, SingleTableMixin, FilterView): - template_name = 'funds/submissions.html' +class BaseAdminSubmissionsTable(SingleTableMixin, FilterView): table_class = AdminSubmissionsTable + filterset_class = SubmissionFilterAndSearch + filter_action = '' + + excluded_fields = [] + + @property + def excluded(self): + return { + 'exclude': self.excluded_fields + } + + def get_table_kwargs(self, **kwargs): + return {**self.excluded, **kwargs} - filterset_class = SubmissionFilter + def get_filterset_kwargs(self, filterset_class): + kwargs = super().get_filterset_kwargs(filterset_class) + kwargs.update(self.excluded) + return kwargs def get_queryset(self): return self.filterset_class._meta.model.objects.current().for_table(self.request.user) def get_context_data(self, **kwargs): - active_filters = self.filterset.data - return super().get_context_data(active_filters=active_filters, **kwargs) + kwargs = super().get_context_data(**kwargs) + search_term = self.request.GET.get('query') + kwargs.update( + search_term=search_term, + filter_action=self.filter_action, + ) -@method_decorator(staff_required, name='dispatch') -class SubmissionSearchView(SingleTableMixin, FilterView): - template_name = 'funds/submissions_search.html' - table_class = AdminSubmissionsTable + return super().get_context_data(**kwargs) - filterset_class = SubmissionFilterAndSearch + +class SubmissionOverviewView(AllActivityContextMixin, BaseAdminSubmissionsTable): + template_name = 'funds/submissions_overview.html' + table_class = SummarySubmissionsTable + table_pagination = False + filter_action = reverse_lazy('funds:submissions:list') def get_queryset(self): - return self.filterset_class._meta.model.objects.current().for_table(self.request.user) + return super().get_queryset()[:5] def get_context_data(self, **kwargs): - search_term = self.request.GET.get('query') - - # We have more data than just 'query' - active_filters = len(self.filterset.data) > 1 + base_query = RoundsAndLabs.objects.with_progress().order_by('end_date') + open_rounds = base_query.open()[:6] + open_query = '?round_state=open' + closed_rounds = base_query.closed()[:6] + closed_query = '?round_state=closed' return super().get_context_data( - search_term=search_term, - active_filters=active_filters, + open_rounds=open_rounds, + open_query=open_query, + closed_rounds=closed_rounds, + closed_query=closed_query, **kwargs, ) +class SubmissionListView(AllActivityContextMixin, BaseAdminSubmissionsTable): + template_name = 'funds/submissions.html' + + +class SubmissionsByRound(BaseAdminSubmissionsTable): + template_name = 'funds/submissions_by_round.html' + + excluded_fields = ('round', 'lead', 'fund') + + def get_queryset(self): + # We want to only show lab or Rounds in this view, their base class is Page + try: + self.obj = Page.objects.get(pk=self.kwargs.get('pk')).specific + except Page.DoesNotExist: + raise Http404(_("No Round or Lab found matching the query")) + + if not isinstance(self.obj, (LabBase, RoundBase)): + raise Http404(_("No Round or Lab found matching the query")) + return super().get_queryset().filter(Q(round=self.obj) | Q(page=self.obj)) + + def get_context_data(self, **kwargs): + return super().get_context_data(object=self.obj, **kwargs) + + @method_decorator(staff_required, name='dispatch') class ProgressSubmissionView(DelegatedViewMixin, UpdateView): model = ApplicationSubmission @@ -90,6 +145,26 @@ class ProgressSubmissionView(DelegatedViewMixin, UpdateView): return super().form_valid(form) +@method_decorator(staff_required, name='dispatch') +class ScreeningSubmissionView(DelegatedViewMixin, UpdateView): + model = ApplicationSubmission + form_class = ScreeningSubmissionForm + context_name = 'screening_form' + + def form_valid(self, form): + old = copy(self.get_object()) + response = super().form_valid(form) + # Record activity + messenger( + MESSAGES.SCREENING, + request=self.request, + user=self.request.user, + submission=self.object, + related=str(old.screening_status), + ) + return response + + @method_decorator(staff_required, name='dispatch') class UpdateLeadView(DelegatedViewMixin, UpdateView): model = ApplicationSubmission @@ -140,6 +215,7 @@ class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, Delega model = ApplicationSubmission form_views = [ ProgressSubmissionView, + ScreeningSubmissionView, CommentFormView, UpdateLeadView, UpdateReviewersView, @@ -423,14 +499,10 @@ class RevisionCompareView(DetailView): @method_decorator(staff_required, name='dispatch') -class SubmissionsByRound(DetailView): - model = Page - template_name = 'funds/submissions_by_round.html' +class RoundListView(SingleTableMixin, FilterView): + template_name = 'funds/rounds.html' + table_class = RoundsTable + filterset_class = RoundsFilter - def get_object(self): - # We want to only show lab or Rounds in this view, their base class is Page - obj = super().get_object() - obj = obj.specific - if not isinstance(obj, (LabBase, RoundBase)): - raise Http404(_("No Round or Lab found matching the query")) - return obj + def get_queryset(self): + return RoundsAndLabs.objects.with_progress() diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py index 4f08d21476feb1d0edf9df379844a73aad7cf75e..39cb99ecd230dee979b823c34a8be8502f15e542 100644 --- a/opentech/apply/funds/workflow.py +++ b/opentech/apply/funds/workflow.py @@ -38,13 +38,48 @@ class Workflow(dict): stages.append(phase.stage) return stages + @property + def stepped_phases(self): + phases = defaultdict(list) + for phase in list(self.values()): + phases[phase.step].append(phase) + return phases + + def phases_for(self, user=None): + # Grab the first phase for each step - visible only, the display phase + return [ + phase for phase, *_ in self.stepped_phases.values() + if not user or phase.permissions.can_view(user) + ] + + def previous_visible(self, current, user): + """Find the latest phase that the user has view permissions for""" + display_phase = self.stepped_phases[current.step][0] + phases = self.phases_for() + index = phases.index(display_phase) + for phase in phases[index - 1::-1]: + if phase.permissions.can_view(user): + return phase + class Phase: - def __init__(self, name, display, stage, permissions, step, transitions=dict()): + """ + Phase Names: + display_name = phase name displayed to staff members in the system + public_name = phase name displayed to applicants in the system + future_name = phase_name displayed to applicants if they haven't passed this stage + """ + def __init__(self, name, display, stage, permissions, step, public=None, future=None, transitions=dict()): self.name = name self.display_name = display + if public and future: + raise ValueError("Cant provide both a future and a public name") + + self.public_name = public or self.display_name + self.future_name_staff = future or self.display_name + self.future_name_public = future or self.public_name self.stage = stage - self.permissions = permissions + self.permissions = Permissions(permissions) self.step = step # For building transition methods on the parent @@ -69,6 +104,9 @@ class Phase: def __str__(self): return self.display_name + def __repr__(self): + return f'<Phase {self.display_name} ({self.public_name})>' + class Stage: def __init__(self, name, has_external_review=False): @@ -79,60 +117,48 @@ class Stage: return self.name -class BasePermissions: - def can_edit(self, user: 'User') -> bool: - if user.is_apply_staff: - return self.can_staff_edit(user) - - if user.is_applicant: - return self.can_applicant_edit(user) +class Permissions: + def __init__(self, permissions): + self.permissions = permissions - def can_staff_edit(self, user: 'User') -> bool: - return False + def can_do(self, user, action): + checks = self.permissions.get(action, list()) + return any(check(user) for check in checks) - def can_applicant_edit(self, user: 'User') -> bool: - return False + def can_edit(self, user): + return self.can_do(user, 'edit') - def can_review(self, user: 'User') -> bool: - if user.is_apply_staff: - return self.can_staff_review(user) + def can_review(self, user): + return self.can_do(user, 'review') - if user.is_reviewer: - return self.can_reviewer_review(user) + def can_view(self, user): + return self.can_do(user, 'view') - def can_staff_review(self, user: 'User') -> bool: - return False - def can_reviewer_review(self, user: 'User') -> bool: - return False +staff_can = lambda user: user.is_apply_staff # NOQA +applicant_can = lambda user: user.is_applicant # NOQA -class NoPermissions(BasePermissions): - pass +reviewer_can = lambda user: user.is_reviewer # NOQA -class DefaultPermissions(BasePermissions): - # Other Permissions should inherit from this class - # Staff can review at any time - def can_staff_review(self, user: 'User') -> bool: - return True +def make_permissions(edit=list(), review=list(), view=[staff_can, applicant_can, reviewer_can]): + return { + 'edit': edit, + 'review': review, + 'view': view, + } - def can_staff_edit(self, user: 'User') -> bool: - return True +no_permissions = make_permissions() -class ReviewerReviewPermissions(DefaultPermissions): - def can_reviewer_review(self, user: 'User') -> bool: - return True +default_permissions = make_permissions(edit=[staff_can], review=[staff_can]) +hidden_from_applicant_permissions = make_permissions(edit=[staff_can], review=[staff_can], view=[staff_can, reviewer_can]) -class CanEditPermissions(DefaultPermissions): - def can_applicant_edit(self, user: 'User') -> bool: - return True +reviewer_review_permissions = make_permissions(edit=[staff_can], review=[staff_can, reviewer_can]) - def can_staff_edit(self, user: 'User') -> bool: - # Prevent staff editing whilst with the user for edits - return False +applicant_edit_permissions = make_permissions(edit=[applicant_can], review=[staff_can]) Request = Stage('Request', False) @@ -146,409 +172,487 @@ Proposal = Stage('Proposal', True) INITIAL_STATE = 'in_discussion' -SingleStageDefinition = { - INITIAL_STATE: { - 'transitions': { - 'internal_review': 'Open Review', - 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'more_info': 'Request More Information', - 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'Under Discussion', - 'stage': Request, - 'permissions': DefaultPermissions(), - 'step': 0, - }, - 'more_info': { - 'transitions': { - INITIAL_STATE: { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'More information required', - 'stage': Request, - 'permissions': CanEditPermissions(), - 'step': 0, - }, - 'internal_review': { - 'transitions': { - 'post_review_discussion': 'Close Review', - }, - 'display': 'Internal Review', - 'stage': Request, - 'permissions': DefaultPermissions(), - 'step': 1, +SingleStageDefinition = [ + { + INITIAL_STATE: { + 'transitions': { + 'internal_review': 'Open Review', + 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'more_info': 'Request More Information', + 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Screening', + 'public': 'Application Received', + 'stage': Request, + 'permissions': default_permissions, + }, + 'more_info': { + 'transitions': { + INITIAL_STATE: { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'More information required', + 'stage': Request, + 'permissions': applicant_edit_permissions, + }, }, - 'post_review_discussion': { - 'transitions': { - 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'post_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': Request, - 'permissions': DefaultPermissions(), - 'step': 2, + { + 'internal_review': { + 'transitions': { + 'post_review_discussion': 'Close Review', + }, + 'display': 'Internal Review', + 'public': 'OTF Review', + 'stage': Request, + 'permissions': default_permissions, + }, }, - 'post_review_more_info': { - 'transitions': { - 'post_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'More information required', - 'stage': Request, - 'permissions': CanEditPermissions(), - 'step': 2, + { + 'post_review_discussion': { + 'transitions': { + 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'post_review_more_info': 'Request More Information', + }, + 'display': 'Ready For Discussion', + 'stage': Request, + 'permissions': hidden_from_applicant_permissions, + }, + 'post_review_more_info': { + 'transitions': { + 'post_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'More information required', + 'stage': Request, + 'permissions': applicant_edit_permissions, + }, }, - - 'accepted': { - 'display': 'Accepted', - 'stage': Request, - 'permissions': NoPermissions(), - 'step': 3, + { + 'determination': { + 'transitions': { + 'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Determination', + 'permissions': hidden_from_applicant_permissions, + 'stage': Request, + }, }, - 'rejected': { - 'display': 'Dismissed', - 'stage': Request, - 'permissions': NoPermissions(), - 'step': 3, + { + 'accepted': { + 'display': 'Accepted', + 'future': 'Application Outcome', + 'stage': Request, + 'permissions': no_permissions, + }, + 'rejected': { + 'display': 'Dismissed', + 'stage': Request, + 'permissions': no_permissions, + }, }, -} +] -SingleStageExternalDefinition = { - INITIAL_STATE: { - 'transitions': { - 'ext_internal_review': 'Open Review', - 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'ext_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': RequestExt, - 'permissions': DefaultPermissions(), - 'step': 0, - }, - 'ext_more_info': { - 'transitions': { - INITIAL_STATE: { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - }, - 'display': 'More information required', - 'stage': RequestExt, - 'permissions': CanEditPermissions(), - 'step': 0, - }, - 'ext_internal_review': { - 'transitions': { - 'ext_post_review_discussion': 'Close Review', - }, - 'display': 'Internal Review', - 'stage': RequestExt, - 'permissions': DefaultPermissions(), - 'step': 1, - }, - 'ext_post_review_discussion': { - 'transitions': { - 'ext_external_review': 'Open AC review', - 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'ext_post_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': RequestExt, - 'permissions': DefaultPermissions(), - 'step': 2, +SingleStageExternalDefinition = [ + { + INITIAL_STATE: { + 'transitions': { + 'ext_internal_review': 'Open Review', + 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'ext_more_info': 'Request More Information', + 'ext_determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Screening', + 'public': 'Application Received', + 'stage': RequestExt, + 'permissions': default_permissions, + }, + 'ext_more_info': { + 'transitions': { + INITIAL_STATE: { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestExt, + 'permissions': applicant_edit_permissions, + }, }, - 'ext_post_review_more_info': { - 'transitions': { - 'ext_post_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - }, - 'display': 'More information required', - 'stage': RequestExt, - 'permissions': CanEditPermissions(), - 'step': 2, + { + 'ext_internal_review': { + 'transitions': { + 'ext_post_review_discussion': 'Close Review', + }, + 'display': 'Internal Review', + 'public': 'OTF Review', + 'stage': RequestExt, + 'permissions': default_permissions, + }, }, - 'ext_external_review': { - 'transitions': { - 'ext_post_external_review_discussion': 'Close Review', - }, - 'display': 'Advisory Council Review', - 'stage': RequestExt, - 'permissions': ReviewerReviewPermissions(), - 'step': 3, + { + 'ext_post_review_discussion': { + 'transitions': { + 'ext_external_review': 'Open AC review', + 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'ext_post_review_more_info': 'Request More Information', + 'ext_determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready For Discussion', + 'stage': RequestExt, + 'permissions': hidden_from_applicant_permissions, + }, + 'ext_post_review_more_info': { + 'transitions': { + 'ext_post_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestExt, + 'permissions': applicant_edit_permissions, + }, }, - 'ext_post_external_review_discussion': { - 'transitions': { - 'ext_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'ext_post_external_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': RequestExt, - 'permissions': DefaultPermissions(), - 'step': 4, + { + 'ext_external_review': { + 'transitions': { + 'ext_post_external_review_discussion': 'Close Review', + }, + 'display': 'Advisory Council Review', + 'stage': RequestExt, + 'permissions': reviewer_review_permissions, + }, }, - 'ext_post_external_review_more_info': { - 'transitions': { - 'ext_post_external_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - }, - 'display': 'More information required', - 'stage': RequestExt, - 'permissions': CanEditPermissions(), - 'step': 4, + { + 'ext_post_external_review_discussion': { + 'transitions': { + 'ext_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'ext_post_external_review_more_info': 'Request More Information', + 'ext_determination': {'display': 'Ready For Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Discussion', + 'stage': RequestExt, + 'permissions': hidden_from_applicant_permissions, + }, + 'ext_post_external_review_more_info': { + 'transitions': { + 'ext_post_external_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestExt, + 'permissions': applicant_edit_permissions, + }, }, - - 'ext_accepted': { - 'display': 'Accepted', - 'stage': RequestExt, - 'permissions': NoPermissions(), - 'step': 5, + { + 'ext_determination': { + 'transitions': { + 'ext_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'ext_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Determination', + 'permissions': hidden_from_applicant_permissions, + 'stage': RequestExt, + }, }, - 'ext_rejected': { - 'display': 'Dismissed', - 'stage': RequestExt, - 'permissions': NoPermissions(), - 'step': 5, + { + 'ext_accepted': { + 'display': 'Accepted', + 'future': 'Application Outcome', + 'stage': RequestExt, + 'permissions': no_permissions, + }, + 'ext_rejected': { + 'display': 'Dismissed', + 'stage': RequestExt, + 'permissions': no_permissions, + }, }, -} +] -DoubleStageDefinition = { - INITIAL_STATE: { - 'transitions': { - 'concept_internal_review': 'Open Review', - 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'concept_more_info': 'Request More Information', - 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, +DoubleStageDefinition = [ + { + INITIAL_STATE: { + 'transitions': { + 'concept_internal_review': 'Open Review', + 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_more_info': 'Request More Information', + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_determination': {'display': 'Ready For Preliminary Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Screening', + 'public': 'Concept Note Received', + 'stage': Concept, + 'permissions': default_permissions, + }, + 'concept_more_info': { + 'transitions': { + INITIAL_STATE: { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_determination': {'display': 'Ready For Preliminary Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'More information required', + 'stage': Concept, + 'permissions': applicant_edit_permissions, }, - 'display': 'Under Discussion', - 'stage': Concept, - 'permissions': DefaultPermissions(), - 'step': 0, - }, - 'concept_more_info': { - 'transitions': { - INITIAL_STATE: { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'More information required', - 'stage': Concept, - 'permissions': CanEditPermissions(), - 'step': 0, - }, - 'concept_internal_review': { - 'transitions': { - 'concept_review_discussion': 'Close Review', - 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'Internal Review', - 'stage': Concept, - 'permissions': DefaultPermissions(), - 'step': 1, - }, - 'concept_review_discussion': { - 'transitions': { - 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'concept_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': Concept, - 'permissions': DefaultPermissions(), - 'step': 2, - }, - 'concept_review_more_info': { - 'transitions': { - 'concept_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - }, - 'display': 'More information required', - 'stage': Concept, - 'permissions': CanEditPermissions(), - 'step': 2, - }, - 'invited_to_proposal': { - 'display': 'Concept Accepted', - 'transitions': { - 'draft_proposal': { - 'display': 'Progress', - 'method': 'progress_application', - 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}, - 'conditions': 'not_progressed', - }, - }, - 'stage': Concept, - 'permissions': NoPermissions(), - 'step': 3, }, - 'concept_rejected': { - 'display': 'Dismissed', - 'stage': Concept, - 'permissions': NoPermissions(), - 'step': 3, + { + 'concept_internal_review': { + 'transitions': { + 'concept_review_discussion': 'Close Review', + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Internal Review', + 'public': 'OTF Review', + 'stage': Concept, + 'permissions': default_permissions, + }, }, - 'draft_proposal': { - 'transitions': { - 'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'}, - 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'external_review': 'Open AC review', - }, - 'display': 'Invited for Proposal', - 'stage': Proposal, - 'permissions': CanEditPermissions(), - 'step': 4, + { + 'concept_review_discussion': { + 'transitions': { + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_review_more_info': 'Request More Information', + 'concept_determination': {'display': 'Ready For Preliminary Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Discussion', + 'stage': Concept, + 'permissions': hidden_from_applicant_permissions, + }, + 'concept_review_more_info': { + 'transitions': { + 'concept_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'More information required', + 'stage': Concept, + 'permissions': applicant_edit_permissions, + }, }, - 'proposal_discussion': { - 'transitions': { - 'proposal_internal_review': 'Open Review', - 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'proposal_more_info': 'Request More Information', - 'external_review': 'Open AC review', - }, - 'display': 'Under Discussion', - 'stage': Proposal, - 'permissions': DefaultPermissions(), - 'step': 5, + { + 'concept_determination': { + 'transitions': { + 'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'concept_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Preliminary Determination', + 'permissions': hidden_from_applicant_permissions, + 'stage': Concept, + }, }, - 'proposal_more_info': { - 'transitions': { - 'proposal_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'external_review': 'Open AC review', - }, - 'display': 'More information required', - 'stage': Proposal, - 'permissions': CanEditPermissions(), - 'step': 5, + { + 'invited_to_proposal': { + 'display': 'Concept Accepted', + 'future': 'Preliminary Determination', + 'transitions': { + 'draft_proposal': { + 'display': 'Progress', + 'method': 'progress_application', + 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}, + 'conditions': 'not_progressed', + }, + }, + 'stage': Concept, + 'permissions': no_permissions, + }, + 'concept_rejected': { + 'display': 'Dismissed', + 'stage': Concept, + 'permissions': no_permissions, + }, }, - 'proposal_internal_review': { - 'transitions': { - 'post_proposal_review_discussion': 'Close Review', - }, - 'display': 'Internal Review', - 'stage': Proposal, - 'permissions': DefaultPermissions(), - 'step': 6, + { + 'draft_proposal': { + 'transitions': { + 'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'}, + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'external_review': 'Open AC review', + 'proposal_determination': {'display': 'Ready For Final Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Invited for Proposal', + 'stage': Proposal, + 'permissions': applicant_edit_permissions, + }, }, - 'post_proposal_review_discussion': { - 'transitions': { - 'external_review': 'Open AC review', - 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'post_proposal_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': Proposal, - 'permissions': DefaultPermissions(), - 'step': 7, + { + 'proposal_discussion': { + 'transitions': { + 'proposal_internal_review': 'Open Review', + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_more_info': 'Request More Information', + 'proposal_determination': {'display': 'Ready For Final Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'external_review': 'Open AC review', + }, + 'display': 'Proposal Received', + 'stage': Proposal, + 'permissions': default_permissions, + }, + 'proposal_more_info': { + 'transitions': { + 'proposal_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_determination': {'display': 'Ready For Final Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'external_review': 'Open AC review', + }, + 'display': 'More information required', + 'stage': Proposal, + 'permissions': applicant_edit_permissions, + }, }, - 'post_proposal_review_more_info': { - 'transitions': { - 'post_proposal_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - 'external_review': 'Open AC review', - }, - 'display': 'More information required', - 'stage': Proposal, - 'permissions': CanEditPermissions(), - 'step': 7, + { + 'proposal_internal_review': { + 'transitions': { + 'post_proposal_review_discussion': 'Close Review', + }, + 'display': 'Internal Review', + 'public': 'OTF Review', + 'stage': Proposal, + 'permissions': default_permissions, + }, }, - 'external_review': { - 'transitions': { - 'post_external_review_discussion': 'Close Review', - }, - 'display': 'Advisory Council Review', - 'stage': Proposal, - 'permissions': ReviewerReviewPermissions(), - 'step': 8, + { + 'post_proposal_review_discussion': { + 'transitions': { + 'external_review': 'Open AC review', + 'proposal_determination': {'display': 'Ready For Final Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'post_proposal_review_more_info': 'Request More Information', + }, + 'display': 'Ready for Discussion', + 'stage': Proposal, + 'permissions': hidden_from_applicant_permissions, + }, + 'post_proposal_review_more_info': { + 'transitions': { + 'post_proposal_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + 'external_review': 'Open AC review', + }, + 'display': 'More information required', + 'stage': Proposal, + 'permissions': applicant_edit_permissions, + }, }, - 'post_external_review_discussion': { - 'transitions': { - 'proposal_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, - 'post_external_review_more_info': 'Request More Information', - }, - 'display': 'Under Discussion', - 'stage': Proposal, - 'permissions': DefaultPermissions(), - 'step': 9, + { + 'external_review': { + 'transitions': { + 'post_external_review_discussion': 'Close Review', + }, + 'display': 'Advisory Council Review', + 'stage': Proposal, + 'permissions': reviewer_review_permissions, + }, }, - 'post_external_review_more_info': { - 'transitions': { - 'post_external_review_discussion': { - 'display': 'Submit', - 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, - 'method': 'create_revision', - }, - }, - 'display': 'More information required', - 'stage': Proposal, - 'permissions': CanEditPermissions(), - 'step': 9, + { + 'post_external_review_discussion': { + 'transitions': { + 'proposal_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_determination': {'display': 'Ready For Final Determination', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'post_external_review_more_info': 'Request More Information', + }, + 'display': 'Ready for Discussion', + 'stage': Proposal, + 'permissions': hidden_from_applicant_permissions, + }, + 'post_external_review_more_info': { + 'transitions': { + 'post_external_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': Proposal, + 'permissions': applicant_edit_permissions, + }, }, - 'proposal_accepted': { - 'display': 'Accepted', - 'stage': Proposal, - 'permissions': NoPermissions(), - 'step': 10, + { + 'proposal_determination': { + 'transitions': { + 'proposal_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + 'proposal_rejected': {'display': 'Dismiss', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}}, + }, + 'display': 'Ready for Final Determination', + 'permissions': hidden_from_applicant_permissions, + 'stage': Proposal, + }, }, - 'proposal_rejected': { - 'display': 'Dismissed', - 'stage': Proposal, - 'permissions': NoPermissions(), - 'step': 10, + { + 'proposal_accepted': { + 'display': 'Accepted', + 'future': 'Final Determination', + 'stage': Proposal, + 'permissions': no_permissions, + }, + 'proposal_rejected': { + 'display': 'Dismissed', + 'stage': Proposal, + 'permissions': no_permissions, + }, }, +] -} + +def unpack_phases(phases): + for step, step_data in enumerate(phases): + for name, phase_data in step_data.items(): + yield step, name, phase_data + + +def phase_data(phases): + return { + phase_name: Phase(phase_name, step=step, **phase_data) + for step, phase_name, phase_data in unpack_phases(phases) + } -Request = Workflow('Request', 'single', **{ - phase_name: Phase(phase_name, **phase_data) - for phase_name, phase_data in SingleStageDefinition.items() -}) +Request = Workflow('Request', 'single', **phase_data(SingleStageDefinition)) -RequestExternal = Workflow('Request with external review', 'single_ext', **{ - phase_name: Phase(phase_name, **phase_data) - for phase_name, phase_data in SingleStageExternalDefinition.items() -}) +RequestExternal = Workflow('Request with external review', 'single_ext', **phase_data(SingleStageExternalDefinition)) -ConceptProposal = Workflow('Concept & Proposal', 'double', **{ - phase_name: Phase(phase_name, **phase_data) - for phase_name, phase_data in DoubleStageDefinition.items() -}) +ConceptProposal = Workflow('Concept & Proposal', 'double', **phase_data(DoubleStageDefinition)) WORKFLOWS = { diff --git a/opentech/apply/review/templatetags/review_tags.py b/opentech/apply/review/templatetags/review_tags.py index 444a7e27cbb9fe50fcdabff84e9ec9c03cf39538..e5ef985c5bb9a148cb20a60a4aaccf07593dba08 100644 --- a/opentech/apply/review/templatetags/review_tags.py +++ b/opentech/apply/review/templatetags/review_tags.py @@ -21,7 +21,7 @@ TRAFFIC_LIGHT_COLORS = { } } -TRAFFIC_LIGHT_TEMPLATE = '<span class="traffic-light traffic-light--{color}">{value}</span>' +TRAFFIC_LIGHT_TEMPLATE = '<span aria-label="Traffic light score" class="traffic-light traffic-light--{color}">{value}</span>' @register.filter() diff --git a/opentech/apply/stream_forms/blocks.py b/opentech/apply/stream_forms/blocks.py index 684a692e88e083c2ef4b63f16a75fe162113d62e..d911275742eb4d0f1f8444bce96aa649a2d2563f 100644 --- a/opentech/apply/stream_forms/blocks.py +++ b/opentech/apply/stream_forms/blocks.py @@ -1,5 +1,6 @@ # Credit to https://github.com/BertrandBordage for initial implementation import bleach +from django_bleach.templatetags.bleach_tags import bleach_value from django import forms from django.db.models import BLANK_CHOICE_DASH @@ -50,17 +51,39 @@ class FormFieldBlock(StructBlock): field_kwargs = self.get_field_kwargs(struct_value) return self.get_field_class(struct_value)(**field_kwargs) - def get_context(self, value, parent_context): - context = super().get_context(value, parent_context) - parent_context['data'] = self.format_data(parent_context['data']) or self.no_response() - return context + def serialize(self, value, context): + return { + 'question': value['field_label'], + 'answer': context.get('data'), + 'type': self.name, + } + + def serialize_no_response(self, value, context): + return { + 'question': value['field_label'], + 'answer': 'No Response', + 'type': 'no_response', + } + + def prepare_data(self, value, data, serialize=False): + return bleach_value(str(data)) + + def render(self, value, context): + data = context.get('data') + data = self.prepare_data(value, data, context.get('serialize', False)) + + context.update(data=data or self.no_response()) + + if context.get('serialize'): + if not data: + return self.serialize_no_response(value, context) + return self.serialize(value, context) + + return super().render(value, context) def get_searchable_content(self, value, data): return str(data) - def format_data(self, data): - return data - def no_response(self): return "No response" @@ -273,6 +296,12 @@ class UploadableMediaBlock(OptionalFormFieldBlock): def get_searchable_content(self, value, data): return None + def prepare_data(self, value, data, serialize): + if serialize: + return data.serialize() + + return data + class ImageFieldBlock(UploadableMediaBlock): field_class = forms.ImageField @@ -301,6 +330,11 @@ class MultiFileFieldBlock(UploadableMediaBlock): label = _('Multiple File field') template = 'stream_forms/render_multi_file_field.html' + def prepare_data(self, value, data, serialize): + if serialize: + return [file.serialize() for file in data] + return data + def no_response(self): return [super().no_response()] diff --git a/opentech/apply/stream_forms/files.py b/opentech/apply/stream_forms/files.py index 1d5b8ce5d7df2b780b2e70269fa8bffc227dfe73..8fdf2febd2906c7baf65f19efe2f9dcb45db596e 100644 --- a/opentech/apply/stream_forms/files.py +++ b/opentech/apply/stream_forms/files.py @@ -66,6 +66,12 @@ class StreamFieldFile(File): return self.file.size return self.storage.size(self.name) + def serialize(self): + return { + 'url': self.url, + 'filename': self.filename, + } + def open(self, mode='rb'): if getattr(self, '_file', None) is None: self.file = self.storage.open(self.name, mode) diff --git a/opentech/apply/urls.py b/opentech/apply/urls.py index 27483633b8fb3bad18a4649f396e9a4a7758cc62..deb32700492d24ef9fe3db8d49680f0241fcb757 100644 --- a/opentech/apply/urls.py +++ b/opentech/apply/urls.py @@ -1,9 +1,11 @@ +from django.conf import settings from django.urls import include, path +from .utils import views from .users import urls as users_urls from .dashboard import urls as dashboard_urls -from opentech.urls import urlpatterns as base_urlpatterns +from opentech.urls import base_urlpatterns urlpatterns = [ @@ -14,4 +16,13 @@ urlpatterns = [ path('hijack/', include('hijack.urls', 'hijack')), ] +if settings.DEBUG: + urlpatterns += [ + # Add views for testing 404 and 500 templates + path('test404/', views.page_not_found), + ] + urlpatterns += base_urlpatterns + + +handler404 = 'opentech.apply.utils.views.page_not_found' diff --git a/opentech/apply/utils/__init__.py b/opentech/apply/utils/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..406254cfe657826bdc2a6ce7992d38cb46a4af9f 100644 --- a/opentech/apply/utils/__init__.py +++ b/opentech/apply/utils/__init__.py @@ -0,0 +1 @@ +default_app_config = 'opentech.apply.utils.app.UtilsConfig' diff --git a/opentech/apply/utils/app.py b/opentech/apply/utils/app.py new file mode 100644 index 0000000000000000000000000000000000000000..b7215b4e1eb04ecd21099117b9989161f82be7ad --- /dev/null +++ b/opentech/apply/utils/app.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UtilsConfig(AppConfig): + name = 'opentech.apply.utils' + label = 'apply_utils' diff --git a/opentech/apply/utils/templates/apply/404.html b/opentech/apply/utils/templates/apply/404.html new file mode 100644 index 0000000000000000000000000000000000000000..30b1072c3da83893225f7fe931f2292f5f4b5651 --- /dev/null +++ b/opentech/apply/utils/templates/apply/404.html @@ -0,0 +1,13 @@ +{% extends "base-apply.html" %} +{% load wagtailcore_tags wagtailsettings_tags %} + +{% block title %}{{ settings.utils.SystemMessagesSettings.title_404 }}{% endblock %} + +{% block body_class %}template-404{% endblock %} + +{% block content %} +<div class="wrapper wrapper--small wrapper--inner-space-large"> + <h1>{{ settings.utils.SystemMessagesSettings.title_404 }}</h1> + {{ settings.utils.SystemMessagesSettings.body_404|richtext }} +</div> +{% endblock %} diff --git a/opentech/apply/utils/views.py b/opentech/apply/utils/views.py index d77666e5accbec25b5f044e3b5b315c3915341dc..87932814703c400b72c81e554d1bc1695c9e806e 100644 --- a/opentech/apply/utils/views.py +++ b/opentech/apply/utils/views.py @@ -1,10 +1,17 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator +from django.views import defaults from django.views.generic import DetailView, View from django.views.generic.detail import SingleObjectTemplateResponseMixin from django.views.generic.edit import ModelFormMixin, ProcessFormView +def page_not_found(request, exception=None, template_name='apply/404.html'): + if not request.user.is_authenticated: + template_name = '404.html' + return defaults.page_not_found(request, exception, template_name) + + @method_decorator(login_required, name='dispatch') class ViewDispatcher(View): admin_view: View = None diff --git a/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html b/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html index 1bd9984c700dee25081225c9309e896eb011efed..d45a2dfd5298a870f4c0c8c42a49533f8aebf4db 100644 --- a/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html +++ b/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html @@ -1,5 +1,5 @@ <h4>Get the latest internet freedom news</h4> -<form class="form" action="{% url "newsletter:subscribe" %}" method="post"> +<form class="form" action="{{ PUBLIC_SITE.root_url }}{% url "newsletter:subscribe" %}" method="post"> <div> {% for field in newsletter_form %} <label for="{{ field.id_for_label }}"{% if field.field.required %} required{% endif %}> diff --git a/opentech/public/navigation/templates/navigation/primarynav-apply.html b/opentech/public/navigation/templates/navigation/primarynav-apply.html index a9c9bda2226cb6e045a7409b2e7b0dde80d65577..a48968ebc8b9f5415e12efe286bd9d25af079341 100644 --- a/opentech/public/navigation/templates/navigation/primarynav-apply.html +++ b/opentech/public/navigation/templates/navigation/primarynav-apply.html @@ -2,7 +2,7 @@ <ul class="nav nav--primary" role="menubar"> {% if request.user.is_apply_staff %} {% include "navigation/primarynav-apply-item.html" with name="Dashboard" url="dashboard:dashboard" %} - {% include "navigation/primarynav-apply-item.html" with name="Submissions" url="funds:submissions:list" %} + {% include "navigation/primarynav-apply-item.html" with name="Submissions" url="funds:submissions:overview" %} {% else %} {% include "navigation/primarynav-apply-item.html" with name="Dashboard" url="dashboard:dashboard" %} {% endif %} diff --git a/opentech/public/search/views.py b/opentech/public/search/views.py index 63031455fffca994a04892891a63b22de1fa91d3..a3aa058ae5361db6acfb7cd267ee70bd73ff02d9 100644 --- a/opentech/public/search/views.py +++ b/opentech/public/search/views.py @@ -1,11 +1,17 @@ from django.conf import settings from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.http import Http404 from django.shortcuts import render from wagtail.core.models import Page from wagtail.search.models import Query +from opentech.public.home.models import HomePage + def search(request): + if request.site != HomePage.objects.first().get_site(): + raise Http404 + search_query = request.GET.get('query', None) page = request.GET.get('page', 1) diff --git a/opentech/settings/base.py b/opentech/settings/base.py index a8146b1edc17c7ebfafe7b7482c437d3c28d4e5c..fb39394186581b10c4c316deb34e0a6de93ce67c 100644 --- a/opentech/settings/base.py +++ b/opentech/settings/base.py @@ -73,6 +73,7 @@ INSTALLED_APPS = [ 'opentech.apply.review', 'opentech.apply.determinations', 'opentech.apply.stream_forms', + 'opentech.apply.utils', 'opentech.public.funds', 'opentech.public.home', @@ -116,6 +117,7 @@ INSTALLED_APPS = [ 'django_bleach', 'django_fsm', 'django_pwned_passwords', + 'rest_framework', 'hijack', 'compat', @@ -333,6 +335,11 @@ LOGGING = { 'level': 'INFO', 'propagate': False, }, + 'django': { + 'handlers': ['console', 'sentry'], + 'level': 'ERROR', + 'propagate': False, + }, 'django.request': { 'handlers': ['console', 'sentry'], 'level': 'WARNING', @@ -605,3 +612,15 @@ WEBPACK_LOADER = { COUNTRIES_OVERRIDE = { 'KV': 'Kosovo', } + +# Rest Framework configuration +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ) +} diff --git a/opentech/static_src/src/app/.eslintrc b/opentech/static_src/src/app/.eslintrc new file mode 100644 index 0000000000000000000000000000000000000000..4217ad2a7fec678e4574f84dd1ffbe0ed10bf833 --- /dev/null +++ b/opentech/static_src/src/app/.eslintrc @@ -0,0 +1,31 @@ +{ + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "mocha": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "no-console": "off" + } +} diff --git a/opentech/static_src/src/app/src/App.js b/opentech/static_src/src/app/src/App.js deleted file mode 100644 index 6704bbda9de764f34f2fe86880f136bae946e9bd..0000000000000000000000000000000000000000 --- a/opentech/static_src/src/app/src/App.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { hot } from 'react-hot-loader' - -import './App.scss'; - -class App extends React.Component { - constructor(props){ - super(props); - this.state = { - detailOpen: false - } - } - - detailOpen = (state) => {this.setState({detailOpen: state})} - - render () { - return ( - <div> - <div> - <button className="red-button" onClick={() => this.detailOpen(true)}>Detail View</button> - | - <button onClick={() => this.detailOpen(false)}>List View</button> - </div> - {this.state.detailOpen ? ( - <div><h2>THIS IS REACT</h2></div> - ) : ( - <div dangerouslySetInnerHTML={ {__html: this.props.originalContent} } /> - )} - </div> - )} -} - -export default hot(module)(App) diff --git a/opentech/static_src/src/app/src/App.scss b/opentech/static_src/src/app/src/App.scss deleted file mode 100644 index 57adbe106905eda2913b07b743c6fa7eeabd2954..0000000000000000000000000000000000000000 --- a/opentech/static_src/src/app/src/App.scss +++ /dev/null @@ -1,3 +0,0 @@ -.red-button { - background-color: red; -} diff --git a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js new file mode 100644 index 0000000000000000000000000000000000000000..133ec908ac6d5d229408761c8f6bb708eae82fed --- /dev/null +++ b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux' + +import Switcher from '@components/Switcher'; +import GroupByStatusDetailView from '@containers/GroupByStatusDetailView'; +import { setCurrentSubmissionRound } from '@actions/submissions'; + + +class SubmissionsByRoundApp extends React.Component { + static propTypes = { + roundID: PropTypes.number, + setSubmissionRound: PropTypes.func, + pageContent: PropTypes.node.isRequired, + }; + + + state = { detailOpened: false }; + + componentDidMount() { + this.props.setSubmissionRound(this.props.roundID); + } + + openDetail = () => { + this.setState(state => ({ + style: { ...state.style, display: 'none' } , + detailOpened: true, + })); + } + + closeDetail = () => { + this.setState(state => { + const newStyle = { ...state.style }; + delete newStyle.display; + return { + style: newStyle, + detailOpened: false, + }; + }); + } + + render() { + return ( + <> + <Switcher selector='submissions-by-round-app-react-switcher' open={this.state.detailOpened} handleOpen={this.openDetail} handleClose={this.closeDetail} /> + + <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} /> + + {this.state.detailOpened && + <GroupByStatusDetailView roundId={this.props.roundID} /> + } + </> + ) + } +} + +const mapDispatchToProps = dispatch => { + return { + setSubmissionRound: id => { + dispatch(setCurrentSubmissionRound(id)); + }, + } +}; + +export default hot(module)( + connect(null, mapDispatchToProps)(SubmissionsByRoundApp) +); diff --git a/opentech/static_src/src/app/src/api/index.js b/opentech/static_src/src/app/src/api/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6fcd16016bf03484a9777a09772ecd92b46f4507 --- /dev/null +++ b/opentech/static_src/src/app/src/api/index.js @@ -0,0 +1,6 @@ +import { fetchSubmission, fetchSubmissionsByRound } from '@api/submissions'; + +export default { + fetchSubmissionsByRound, + fetchSubmission, +}; diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js new file mode 100644 index 0000000000000000000000000000000000000000..8d7f95f776558bdb14a68dc874901214de1dcceb --- /dev/null +++ b/opentech/static_src/src/app/src/api/submissions.js @@ -0,0 +1,13 @@ +import { apiFetch } from '@api/utils'; + +export async function fetchSubmissionsByRound(id) { + return apiFetch('/apply/api/submissions/', 'GET', { + 'round': id, + 'page_size': 1000, + }); +} + + +export async function fetchSubmission(id) { + return apiFetch(`/apply/api/submissions/${id}/`, 'GET'); +} diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..094b3281fc9d5ee1ff4d5ea12e8ede48d92f40af --- /dev/null +++ b/opentech/static_src/src/app/src/api/utils.js @@ -0,0 +1,20 @@ +const getBaseUrl = () => { + return process.env.API_BASE_URL; +}; + +export async function apiFetch(path, method = 'GET', params, options) { + const url = new URL(getBaseUrl()); + url.pathname = path; + + if (params !== undefined) { + for (const [paramKey, paramValue] of Object.entries(params)) { + url.searchParams.set(paramKey, paramValue); + } + } + return fetch(url, { + ...options, + method, + mode: 'same-origin', + credentials: 'include' + }); +} diff --git a/opentech/static_src/src/app/src/components/DetailView/index.js b/opentech/static_src/src/app/src/components/DetailView/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b392e211ef577a62aaa64ad8a674b62544cf83cf --- /dev/null +++ b/opentech/static_src/src/app/src/components/DetailView/index.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { withWindowSizeListener } from 'react-window-size-listener'; + +import { clearCurrentSubmission } from '@actions/submissions'; +import DisplayPanel from '@containers/DisplayPanel'; +import SlideInRight from '@components/Transitions/SlideInRight' +import SlideOutLeft from '@components/Transitions/SlideOutLeft' +import { getCurrentSubmissionID } from '@selectors/submissions'; + +import './style.scss'; + +class DetailView extends Component { + static propTypes = { + listing: PropTypes.element.isRequired, + submissionID: PropTypes.number, + windowSize: PropTypes.objectOf(PropTypes.number), + clearSubmission: PropTypes.func.isRequired, + }; + + state = { + listingShown: true, + firstRender: true, + } + + isMobile = (width) => (width ? width : this.props.windowSize.windowWidth) < 1024 + + renderDisplay () { + return <DisplayPanel /> + } + + componentDidUpdate (prevProps, prevState) { + if (this.isMobile()) { + const haveCleared = prevProps.submissionID && !this.props.submissionID + const haveUpdated = !prevProps.submissionID && this.props.submissionID + + if ( haveCleared ) { + this.setState({listingShown: true}) + } else if ( haveUpdated && this.state.firstRender ) { + // Listing automatically updating after update + // clear, but dont run again + this.props.clearSubmission() + this.setState({firstRender: false}) + } else if ( prevProps.submissionID !== this.props.submissionID) { + // Submission has changed and we want to show it + // reset the firstRender so that we can clear it again + this.setState({ + listingShown: false, + firstRender: true, + }) + } + } + } + + render() { + const { listing } = this.props; + + if (this.isMobile()) { + var activeDisplay; + if (this.state.listingShown){ + activeDisplay = ( + <SlideOutLeft key={"listing"}> + {listing} + </SlideOutLeft> + ) + } else { + activeDisplay = ( + <SlideInRight key={"display"}> + { this.renderDisplay() } + </SlideInRight> + ) + } + + return ( + <div className="detail-view"> + { activeDisplay } + </div> + ) + } else { + return ( + <div className="detail-view"> + {listing} + { this.renderDisplay() } + </div> + ) + } + + } +} + +const mapStateToProps = state => ({ + submissionID: getCurrentSubmissionID(state), +}); + +const mapDispatchToProps = { + clearSubmission: clearCurrentSubmission +} + + +export default connect(mapStateToProps, mapDispatchToProps)(withWindowSizeListener(DetailView)); diff --git a/opentech/static_src/src/app/src/components/DetailView/style.scss b/opentech/static_src/src/app/src/components/DetailView/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c70a676efe6257473dd79fc4c59d2ae0214535e --- /dev/null +++ b/opentech/static_src/src/app/src/components/DetailView/style.scss @@ -0,0 +1,24 @@ +.detail-view { + margin: 0 -20px; + overflow-y: overlay; + + @include media-query(tablet-landscape) { + display: grid; + grid-template-columns: 250px 1fr; + } + + @include media-query(desktop) { + // breakout of the wrapper + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + grid-template-columns: 390px 1fr; + } + + @include target-ie11 { + display: flex; + } +} diff --git a/opentech/static_src/src/app/src/components/Listing/index.js b/opentech/static_src/src/app/src/components/Listing/index.js new file mode 100644 index 0000000000000000000000000000000000000000..65f786726b058511d888c230700a004e3c03c841 --- /dev/null +++ b/opentech/static_src/src/app/src/components/Listing/index.js @@ -0,0 +1,124 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ListingGroup from '@components/ListingGroup'; +import ListingItem from '@components/ListingItem'; +import LoadingPanel from '@components/LoadingPanel'; + +import './style.scss'; + +export default class Listing extends React.Component { + static propTypes = { + items: PropTypes.array, + activeItem: PropTypes.number, + isLoading: PropTypes.bool, + error: PropTypes.string, + groupBy: PropTypes.string, + order: PropTypes.arrayOf(PropTypes.string), + onItemSelection: PropTypes.func, + }; + + state = { + orderedItems: [], + }; + + componentDidMount() { + this.orderItems(); + } + + componentDidUpdate(prevProps, prevState) { + // Order items + if (this.props.items !== prevProps.items) { + this.orderItems(); + } + + const oldItem = prevProps.activeItem + const newItem = this.props.activeItem + + // If we have never activated a submission, get the first item + if ( !newItem && !oldItem ) { + const firstGroup = this.state.orderedItems[0] + if ( firstGroup && firstGroup.items[0] ) { + this.setState({firstUpdate: false}) + this.props.onItemSelection(firstGroup.items[0].id) + } + } + } + + renderListItems() { + const { isLoading, error, items, onItemSelection, activeItem } = this.props; + + if (isLoading) { + return ( + <div className="listing__list is-loading"> + <LoadingPanel /> + </div> + ) + } else if (error) { + return ( + <div className="listing__list is-loading"> + <p>Something went wrong. Please try again later.</p> + <p>{ error }</p> + </div> + ) + } else if (items.length === 0) { + return ( + <div className="listing__list is-loading"> + <p>No results found.</p> + </div> + ) + } + + return ( + <ul className="listing__list"> + {this.state.orderedItems.map(group => { + return ( + <ListingGroup key={`listing-group-${group.name}`} item={group}> + {group.items.map(item => { + return <ListingItem + selected={!!activeItem && activeItem===item.id} + onClick={() => onItemSelection(item.id)} + key={`listing-item-${item.id}`} + item={item}/>; + })} + </ListingGroup> + ); + })} + </ul> + ); + } + + getGroupedItems() { + const { groupBy, items } = this.props; + + return items.reduce((tmpItems, v) => { + const groupByValue = v[groupBy]; + if (!(groupByValue in tmpItems)) { + tmpItems[groupByValue] = []; + } + tmpItems[groupByValue].push({...v}); + return tmpItems; + }, {}); + } + + orderItems() { + const groupedItems = this.getGroupedItems(); + const { order = [] } = this.props; + const leftOverKeys = Object.keys(groupedItems).filter(v => !order.includes(v)); + this.setState({ + orderedItems: order.concat(leftOverKeys).filter(key => groupedItems[key] ).map(key => ({ + name: key, + items: groupedItems[key] || [] + })), + }); + } + + render() { + return ( + <div className="listing"> + <div className="listing__header"></div> + {this.renderListItems()} + </div> + ); + } +} diff --git a/opentech/static_src/src/app/src/components/Listing/style.scss b/opentech/static_src/src/app/src/components/Listing/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..97b57fb30221cf84bc81de50bb72f32c7ff9e6c3 --- /dev/null +++ b/opentech/static_src/src/app/src/components/Listing/style.scss @@ -0,0 +1,105 @@ +.listing { + @include target-ie11 { + max-width: 390px; + width: 100%; + } + + &__header { + @include submission-list-item; + height: $listing-header-height; + padding: 20px; + } + + // containing <ul> + &__list { + @include media-query(tablet-landscape) { + // only allow columns to be scrolled on larger screens + height: calc(100vh - var(--header-admin-height) - #{$listing-header-height}); + overflow-y: scroll; + } + + @include media-query(laptop-short) { + // allow for vertical scrolling on laptops + height: calc(100vh - #{$listing-header-height}); + } + + &.is-loading { + padding: 20px; + border-right: 2px solid $color--light-mid-grey; + + p { + margin: 20px 0 20px 20px; + } + + .loading-panel__icon::after { + background: $color--light-grey; + } + } + } + + // inner <li>'s + &__item { + @include submission-list-item; + + &.is-active { + @include target-edge { + margin-left: 8px; + } + + border-right: 2px solid $color--white; + transition: border $transition; + } + + &--heading { + display: flex; + justify-content: space-between; + align-items: center; + background-color: $color--fog; + padding: 15px 20px; + } + } + + // <a> tags + &__link { + display: block; + padding: 30px; + background-color: transparent; + transition: background-color $quick-transition; + position: relative; + color: $color--default; + + &::before { + content: ''; + height: 100%; + width: 0; + position: absolute; + left: 0; + top: 0; + background-color: $color--dark-blue; + transition: width $transition; + } + + &:hover { + background-color: $color--white; + } + + .is-active & { + background-color: $color--white; + + &::before { + width: 8px; + } + } + } + + &__title { + margin: 0; + } + + &__count { + background-color: $color--white; + padding: 0 8px; + border-radius: 5px; + font-size: 14px; + } +} diff --git a/opentech/static_src/src/app/src/components/ListingGroup.js b/opentech/static_src/src/app/src/components/ListingGroup.js new file mode 100644 index 0000000000000000000000000000000000000000..6508a18b13c4c5574109451211c0fcd6c4af8f6f --- /dev/null +++ b/opentech/static_src/src/app/src/components/ListingGroup.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ListingHeading from '@components/ListingHeading'; + + +export default class ListingGroup extends React.Component { + static propTypes = { + children: PropTypes.arrayOf(PropTypes.node), + item: PropTypes.shape({ + name: PropTypes.string, + }), + }; + + render() { + const {item, children} = this.props + return ( + <> + <ListingHeading title={item.name} count={children.length} /> + <ul> + {children} + </ul> + </> + ); + } +} diff --git a/opentech/static_src/src/app/src/components/ListingHeading.js b/opentech/static_src/src/app/src/components/ListingHeading.js new file mode 100644 index 0000000000000000000000000000000000000000..996f7a933e214c8ae0aad300df9f4d1d8fda27ff --- /dev/null +++ b/opentech/static_src/src/app/src/components/ListingHeading.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class ListingHeading extends React.Component { + render() { + return ( + <li className="listing__item listing__item--heading"> + <h5 className="listing__title">{this.props.title}</h5> + <span className="listing__count">{this.props.count}</span> + </li> + ); + } +} + +ListingHeading.propTypes = { + title: PropTypes.string, + count: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), +}; diff --git a/opentech/static_src/src/app/src/components/ListingItem.js b/opentech/static_src/src/app/src/components/ListingItem.js new file mode 100644 index 0000000000000000000000000000000000000000..65969c7deae3a1ddb5e54a1dc0fd6cf19ebacbc8 --- /dev/null +++ b/opentech/static_src/src/app/src/components/ListingItem.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + + +export default class ListingItem extends React.Component { + render() { + const { onClick, item, selected} = this.props; + return ( + <li className={"listing__item " + (selected ? "is-active" : "")}> + <a className="listing__link" onClick={onClick}> + {item.title} + </a> + </li> + ); + } +} + +ListingItem.propTypes = { + item: PropTypes.shape({ + title: PropTypes.string, + }), + onClick: PropTypes.func, + selected: PropTypes.bool, +}; diff --git a/opentech/static_src/src/app/src/components/LoadingPanel/index.js b/opentech/static_src/src/app/src/components/LoadingPanel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cc263937a7eba3378927c1dbd32a1703ce7ca41b --- /dev/null +++ b/opentech/static_src/src/app/src/components/LoadingPanel/index.js @@ -0,0 +1,14 @@ +import React from 'react' + +import './styles.scss'; + +const LoadingIcon = () => { + return ( + <div className="loading-panel"> + <h5>Loading...</h5> + <div className="loading-panel__icon" /> + </div> + ) +} + +export default LoadingIcon diff --git a/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..aaaad97ad9b4bb64522d7e988a23995fa27d2053 --- /dev/null +++ b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss @@ -0,0 +1,40 @@ +.loading-panel { + text-align: center; + + &__icon { + font-size: 10px; + margin: 20px auto; + text-indent: -9999em; + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(to right, $color--dark-grey 10%, transparent 42%); + position: relative; + animation: spin .4s infinite linear; + transform: translateZ(0); + + &::after { + background: $color--white; + width: 75%; + height: 75%; + border-radius: 50%; + content: ''; + margin: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + } +} diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js new file mode 100644 index 0000000000000000000000000000000000000000..06ddea15efbe82c8d0b42798b0c1cd2b1ac7df0f --- /dev/null +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Download from 'images/download.svg'; +import File from 'images/file.svg'; + +const answerType = {answer: PropTypes.string.isRequired} +const arrayAnswerType = {answer: PropTypes.arrayOf(PropTypes.string)} +const fileType = {answer: PropTypes.shape({ + filename: PropTypes.string.isRequired, + url:PropTypes.string.isRequired, +})} + +const ListAnswer = ({Wrapper, answers}) => { + return ( + <ul className={`${Wrapper === FileAnswer ? 'remove-list-style' : ''}`}>{ + answers.map((answer, index) => <li key={index}><Wrapper answer={answer} /></li>) + }</ul> + ) +}; +ListAnswer.propTypes = { + Wrapper: PropTypes.element, + ...arrayAnswerType, +} + +const BasicAnswer = ({answer}) => <p>{ answer }</p>; +BasicAnswer.propTypes = answerType + +const BasicListAnswer = ({answer}) => <ListAnswer Wrapper={BasicAnswer} answers={answer} />; +BasicListAnswer.propTypes = arrayAnswerType + +const RichTextAnswer = ({answer}) => <div dangerouslySetInnerHTML={{ __html: answer }} />; +RichTextAnswer.propTypes = answerType + +const FileAnswer = ({answer}) => ( + <a className="link link--download" href={answer.url}> + <div> + <File /><span>{answer.filename}</span> + </div> + <Download /> + </a> +); +FileAnswer.propTypes = fileType + +const MultiFileAnswer = ({answer}) => <ListAnswer Wrapper={FileAnswer} answers={answer} />; +MultiFileAnswer.propTypes = {answer: PropTypes.arrayOf(fileType)} + +const AddressAnswer = ({answer}) => ( + <div>{ + Object.entries(answer) + .filter(([key, value]) => !!value ) + .map(([key, value]) => <p key={key}>{value}</p> )} + </div> +) +AddressAnswer.propTypes = {answer: PropTypes.objectOf(PropTypes.string)} + + +const answerTypes = { + 'no_response': BasicAnswer, + 'char': BasicAnswer, + 'email': BasicAnswer, + 'name': BasicAnswer, + 'value': BasicAnswer, + 'title': BasicAnswer, + 'full_name': BasicAnswer, + 'duration': BasicAnswer, + 'date': BasicAnswer, + 'checkbox': BasicAnswer, + 'dropdown': BasicAnswer, + 'radios': BasicAnswer, + + // SPECIAL + 'rich_text': RichTextAnswer, + 'address': AddressAnswer, + 'category': BasicListAnswer, + // Files + 'file': FileAnswer, + 'multi_file': MultiFileAnswer, +} + +export const answerPropTypes = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.object), +]) + +const Answer = ({ answer, type }) => { + const AnswerType = answerTypes[type]; + + return <AnswerType answer={answer} />; +} +Answer.propTypes = { + answer: answerPropTypes, + type: PropTypes.string.isRequired, +} + +export default Answer; diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js b/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b917578e041a12a90ea442b59624a7b24bd73ee1 --- /dev/null +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js @@ -0,0 +1,87 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +import Answer, { answerPropTypes } from './answers' +import LoadingPanel from '@components/LoadingPanel'; + +import './styles.scss' + + +const MetaResponse = ({ question, answer, type }) => { + return ( + <div> + <h5>{question}</h5> + <Answer type={type} answer={answer} /> + </div> + ) +} +MetaResponse.propTypes = { + question: PropTypes.string.isRequired, + answer: answerPropTypes, + type: PropTypes.string.isRequired, +} + + +const Response = ({question, answer, type}) => { + return ( + <section> + <h4>{question}</h4> + <Answer type={type} answer={answer} /> + </section> + ) +} +Response.propTypes = { + question: PropTypes.string.isRequired, + answer: answerPropTypes, + type: PropTypes.string.isRequired, +} + + +export default class SubmissionDisplay extends Component { + static propTypes = { + isLoading: PropTypes.bool, + isError: PropTypes.bool, + submission: PropTypes.object, + } + + render() { + if (this.props.isLoading) { + return ( + <div className="display-panel__loading"> + <LoadingPanel /> + </div> + ) + } else if (this.props.isError) { + return ( + <div className="display-panel__loading"> + <p>Something went wrong. Please try again later.</p> + </div> + ) + } else if (this.props.submission === undefined) { + return ( + <div className="display-panel__loading"> + <p>Please select a submission.</p> + </div> + ) + } + const { meta_questions = [], questions = [], stage} = this.props.submission; + + return ( + <div className="application-display"> + <h3>{stage} Information</h3> + + <div className="grid grid--proposal-info"> + {meta_questions.map((response, index) => ( + <MetaResponse key={index} {...response} /> + ))} + </div> + + <div className="rich-text rich-text--answers"> + {questions.map((response, index) => ( + <Response key={index} {...response} /> + ))} + </div> + </div> + ) + } +} diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss b/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..5bb01974ad7f67428f6cced6b0f09f9da9ddad0d --- /dev/null +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss @@ -0,0 +1,11 @@ +.application-display { + @include media-query(tablet-landscape) { + height: calc(100vh - var(--header-admin-height) - #{$listing-header-height} - 40px); + overflow-y: scroll; + } + + @include media-query(laptop-short) { + // allow for vertical scrolling on laptops + height: calc(100vh - #{$listing-header-height} - 40px); + } +} diff --git a/opentech/static_src/src/app/src/components/Switcher/index.js b/opentech/static_src/src/app/src/components/Switcher/index.js new file mode 100644 index 0000000000000000000000000000000000000000..58fabc37430ba43f60dceacf49327f2a3f83652d --- /dev/null +++ b/opentech/static_src/src/app/src/components/Switcher/index.js @@ -0,0 +1,36 @@ +import React from 'react' +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; + +import ArrayIcon from 'images/icon-array.svg' +import GridIcon from 'images/icon-grid.svg'; + +import './styles.scss'; + +class Switcher extends React.Component { + static propTypes = { + handleOpen: PropTypes.func.isRequired, + handleClose: PropTypes.func.isRequired, + selector: PropTypes.string.isRequired, + open: PropTypes.bool, + } + + constructor(props) { + super(props); + this.el = document.getElementById(props.selector); + } + + render() { + const { handleOpen, handleClose, open } = this.props; + + return ReactDOM.createPortal( + <> + <button className={`button button--switcher ${open ? '' : 'is-active'}`} onClick={handleClose} aria-label="Show table"><GridIcon /></button> + <button className={`button button--switcher ${open ? 'is-active' : ''}`} onClick={handleOpen} aria-label="Show grid"><ArrayIcon /></button> + </>, + this.el, + ); + } +} + +export default Switcher diff --git a/opentech/static_src/src/app/src/components/Switcher/styles.scss b/opentech/static_src/src/app/src/components/Switcher/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..8a86bd0441dc50bffb44b26c325145bfdc90dbd4 --- /dev/null +++ b/opentech/static_src/src/app/src/components/Switcher/styles.scss @@ -0,0 +1,24 @@ +.button { + &--switcher { + fill: transparentize($color--white, .5); + transition: fill $transition; + + &:hover, + &:focus { + fill: transparentize($color--white, .25); + } + + &:first-child { + margin-right: 10px; + } + + &.is-active { + fill: $color--white; + } + + svg { + width: 45px; + height: 45px; + } + } +} diff --git a/opentech/static_src/src/app/src/components/Tabber/index.js b/opentech/static_src/src/app/src/components/Tabber/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1b1ee8a357ddc3c527d589305bed96414800805c --- /dev/null +++ b/opentech/static_src/src/app/src/components/Tabber/index.js @@ -0,0 +1,59 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + + + +export const Tab = ({button, children, handleClick}) => <div>{children}</div> +Tab.propTypes = { + button: PropTypes.node, + children: PropTypes.node, + handleClick: PropTypes.func, +} + +class Tabber extends Component { + static propTypes = { + children: PropTypes.arrayOf(PropTypes.element), + } + + constructor() { + super(); + + this.state = { + activeTab: 0 + } + } + + componentDidUpdate(prevProps, prevState) { + const { children } = this.props; + if ( !children[prevState.activeTab].props.children ) { + this.setState({activeTab: children.findIndex(child => child.props.children)}) + } + } + + handleClick = (child) => { + this.setState({ + activeTab: child + }) + } + + render() { + const { children } = this.props; + + return ( + <div className="tabber"> + <div className="tabber__navigation"> + {children.map((child, i) => { + return <a onClick={child.props.handleClick ? child.props.handleClick : () => this.handleClick(i)} className="display-panel__link" key={child.key}>{child.props.button}</a> + }) + } + </div> + <div className="tabber-tab__active"> + { children[this.state.activeTab] } + </div> + </div> + ) + } + +} + +export default Tabber; diff --git a/opentech/static_src/src/app/src/components/Transitions/SlideInRight.js b/opentech/static_src/src/app/src/components/Transitions/SlideInRight.js new file mode 100644 index 0000000000000000000000000000000000000000..3d02d4bf40da82afb2d8e009540f4e26950176ef --- /dev/null +++ b/opentech/static_src/src/app/src/components/Transitions/SlideInRight.js @@ -0,0 +1,40 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import Transition from 'react-transition-group/Transition'; + + +const SlideInRight = ({ children, in: inProp }) => { + const duration = 250; + + const defaultStyle = { + transition: `transform ${duration}ms ease-in-out`, + transform: 'translate3d(0, 0, 0)', + position: 'absolute', + zIndex: 2, + width: '100%' + } + + const transitionStyles = { + entering: { transform: 'translate3d(0, 0, 0)' }, + entered: { transform: 'translate3d(100%, 0, 0)' }, + exiting: { transform: 'translate3d(100%, 0, 0)' }, + exited: { transform: 'translate3d(0, 0, 0)' } + }; + + return ( + <Transition in={inProp} timeout={duration}> + {(state) => ( + <div style={{ ...defaultStyle, ...transitionStyles[state] }}> + {children} + </div> + )} + </Transition> + ) +} + +SlideInRight.propTypes = { + children: PropTypes.node, + in: PropTypes.bool, +} + +export default SlideInRight diff --git a/opentech/static_src/src/app/src/components/Transitions/SlideOutLeft.js b/opentech/static_src/src/app/src/components/Transitions/SlideOutLeft.js new file mode 100644 index 0000000000000000000000000000000000000000..49345eb18fa0ddc1501f9a6ca1b901c1621ea02b --- /dev/null +++ b/opentech/static_src/src/app/src/components/Transitions/SlideOutLeft.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import Transition from 'react-transition-group/Transition'; + + +const SlideOutLeft = ({ children, in: inProp }) => { + const duration = 250; + + const defaultStyle = { + transition: `transform ${duration}ms ease-in-out`, + transform: 'translate3d(0, 0, 0)', + position: 'absolute', + width: '100%' + } + + const transitionStyles = { + entering: { transform: 'translate3d(0, 0, 0)' }, + entered: { transform: 'translate3d(-100%, 0, 0)' }, + exiting: { transform: 'translate3d(-100%, 0, 0)' }, + exited: { transform: 'translate3d(0, 0, 0)' } + }; + + return ( + <Transition in={inProp} timeout={duration}> + {(state) => ( + <div style={{ ...defaultStyle, ...transitionStyles[state] }}> + {children} + </div> + )} + </Transition> + ) +} + +SlideOutLeft.propTypes = { + children: PropTypes.node, + in: PropTypes.bool, +} + +export default SlideOutLeft diff --git a/opentech/static_src/src/app/src/containers/ByStatusListing.js b/opentech/static_src/src/app/src/containers/ByStatusListing.js new file mode 100644 index 0000000000000000000000000000000000000000..873036c9467564588ebc4fba69d8bba81002c29c --- /dev/null +++ b/opentech/static_src/src/app/src/containers/ByStatusListing.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux' + +import Listing from '@components/Listing'; +import { + loadCurrentRound, + setCurrentSubmission, +} from '@actions/submissions'; +import { + getCurrentRound, + getCurrentRoundID, + getCurrentRoundSubmissions, + getCurrentSubmissionID, + getSubmissionsByRoundError, +} from '@selectors/submissions'; + + +const loadData = props => { + props.loadSubmissions(['submissions']) +} + +class ByStatusListing extends React.Component { + static propTypes = { + loadSubmissions: PropTypes.func, + submissions: PropTypes.arrayOf(PropTypes.object), + roundID: PropTypes.number, + round: PropTypes.object, + error: PropTypes.string, + setCurrentItem: PropTypes.func, + activeSubmission: PropTypes.number, + }; + + componentDidMount() { + // Update items if round ID is defined. + if ( this.props.roundID ) { + loadData(this.props) + } + } + + componentDidUpdate(prevProps) { + const { roundID } = this.props; + // Update entries if round ID is changed or is not null. + if (roundID && prevProps.roundID !== roundID) { + console.log('wooop') + loadData(this.props) + } + } + + render() { + const { error, submissions, round, setCurrentItem, activeSubmission } = this.props; + const isLoading = round && round.isFetching + return <Listing + isLoading={isLoading} + error={error} + items={submissions} + activeItem={activeSubmission} + onItemSelection={setCurrentItem} + groupBy={'status'} + order={[ + // TODO: Set the proper order of statuses. + 'post_external_review_discussion', + 'in_discussion', + 'more_info', + 'internal_review', + 'post_review_discussion', + 'post_review_more_info', + 'accepted', + 'rejected', + ]} + />; + } +} + +const mapStateToProps = state => ({ + roundID: getCurrentRoundID(state), + submissions: getCurrentRoundSubmissions(state), + round: getCurrentRound(state), + error: getSubmissionsByRoundError(state), + activeSubmission: getCurrentSubmissionID(state), +}) + +const mapDispatchToProps = dispatch => ({ + loadSubmissions: () => dispatch(loadCurrentRound()), + setCurrentItem: id => dispatch(setCurrentSubmission(id)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ByStatusListing); diff --git a/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js b/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js new file mode 100644 index 0000000000000000000000000000000000000000..75a4a018e0597651c70d9524b9c08c112980da89 --- /dev/null +++ b/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux' + +import { loadCurrentSubmission } from '@actions/submissions' +import { + getCurrentSubmission, + getCurrentSubmissionID, +} from '@selectors/submissions' +import SubmissionDisplay from '@components/SubmissionDisplay'; + +const loadData = props => { + props.loadCurrentSubmission(['questions']) + +} + +class CurrentSubmissionDisplay extends React.Component { + static propTypes = { + submission: PropTypes.object, + submissionID: PropTypes.number, + } + + componentDidMount() { + loadData(this.props) + } + + componentDidUpdate(prevProps) { + if (this.props.submissionID !== prevProps.submissionID ) { + loadData(this.props) + } + } + + render () { + const { submission } = this.props + if ( !submission ) { + return <p>Loading</p> + } + return <SubmissionDisplay + submission={submission} + isLoading={submission.isFetching} + isError={submission.isErrored} /> + } + +} + +const mapStateToProps = state => ({ + submissionID: getCurrentSubmissionID(state), + submission: getCurrentSubmission(state), +}) + + +export default connect(mapStateToProps, {loadCurrentSubmission})(CurrentSubmissionDisplay) diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e09e83f962d083bafa4acb743007b7443aef068b --- /dev/null +++ b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { withWindowSizeListener } from 'react-window-size-listener'; + +import { clearCurrentSubmission } from '@actions/submissions'; +import { + getCurrentSubmission, + getCurrentSubmissionID, + getSubmissionErrorState, + getSubmissionLoadingState, + +} from '@selectors/submissions'; + +import CurrentSubmissionDisplay from '@containers/CurrentSubmissionDisplay' +import Tabber, {Tab} from '@components/Tabber' +import './style.scss'; + + +class DisplayPanel extends React.Component { + static propTypes = { + submissionID: PropTypes.number, + loadSubmission: PropTypes.func, + isLoading: PropTypes.bool, + isError: PropTypes.bool, + clearSubmission: PropTypes.func.isRequired, + windowSize: PropTypes.objectOf(PropTypes.number) + }; + + render() { + const { windowSize: {windowWidth: width} } = this.props; + const { clearSubmission } = this.props; + + const isMobile = width < 1024; + + const submission = <CurrentSubmissionDisplay /> + + let tabs = [ + <Tab button="Notes" key="note"> + <p>Notes</p> + </Tab>, + <Tab button="Status" key="status"> + <p>Status</p> + </Tab> + ] + + if ( isMobile ) { + tabs = [ + <Tab button="Back" key="back" handleClick={ clearSubmission } />, + <Tab button="Application" key="application"> + { submission } + </Tab>, + ...tabs + ] + } + + return ( + <div className="display-panel"> + { !isMobile && ( + <div className="display-panel__column"> + <div className="display-panel__header display-panel__header--spacer"></div> + <div className="display-panel__body"> + { submission } + </div> + </div> + )} + <div className="display-panel__column"> + <div className="display-panel__body"> + <Tabber> + { tabs } + </Tabber> + </div> + </div> + </div> + + ) + } +} + +const mapStateToProps = state => ({ + isLoading: getSubmissionLoadingState(state), + isError: getSubmissionErrorState(state), + submissionID: getCurrentSubmissionID(state), + submission: getCurrentSubmission(state), +}); + +const mapDispatchToProps = { + clearSubmission: clearCurrentSubmission +} + + +export default connect(mapStateToProps, mapDispatchToProps)(withWindowSizeListener(DisplayPanel)); diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss b/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..673a9b9266a9501e4a417f087cc8aa11e3bb97d0 --- /dev/null +++ b/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss @@ -0,0 +1,72 @@ +.display-panel { + background-color: $color--white; + + @include media-query(tablet-landscape) { + display: grid; + grid-template-columns: 1fr 250px; + } + + @include media-query(desktop) { + grid-template-columns: 1fr 390px; + grid-template-rows: 75px 1fr; + } + + @include target-ie11 { + display: flex; + flex-wrap: wrap; + width: 100%; + } + + &__body, + &__header { + @include submission-list-item; + padding: 20px; + } + + // loading container + &__loading { + @include media-query(tablet-portrait) { + height: calc(100vh - var(--header-admin-height) - #{$listing-header-height} - 40px); + } + + // 100vh - listing header - display-panel__body padding + @include media-query(laptop-short) { + height: calc(100vh - #{$listing-header-height} - 40px); + } + } + + &__header { + &--spacer { + display: none; + min-height: 75px; + + @include media-query(tablet-landscape) { + display: block; + } + } + } + + &__links { + display: flex; + align-items: center; + padding: 0; + } + + &__link { + padding: 20px; + } + + &__column { + &:first-child { + @include target-ie11 { + width: 70%; + } + } + + &:last-child { + @include target-ie11 { + width: 30%; + } + } + } +} diff --git a/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js new file mode 100644 index 0000000000000000000000000000000000000000..d0ce02e5378e383edd7bfe8b75010f97870ae939 --- /dev/null +++ b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import DetailView from '@components/DetailView'; +import ByStatusListing from '@containers/ByStatusListing'; + +export default class GroupByStatusDetailView extends React.Component { + render() { + const listing = <ByStatusListing />; + return ( + <DetailView listing={listing} /> + ); + } +} diff --git a/opentech/static_src/src/app/src/index.js b/opentech/static_src/src/app/src/index.js index 3ce0eb6fec8d1cd8ad247e59bb1ba04cb867e793..96cb05daa85321660990598ae80cf8a252dda7d2 100644 --- a/opentech/static_src/src/app/src/index.js +++ b/opentech/static_src/src/app/src/index.js @@ -1,13 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; -import App from './App' +import SubmissionsByRoundApp from './SubmissionsByRoundApp'; +import createStore from '@redux/store'; -const container = document.getElementById('react-app') +const container = document.getElementById('submissions-by-round-react-app'); +const store = createStore(); ReactDOM.render( - <App originalContent={container.innerHTML} />, + <Provider store={store}> + <SubmissionsByRoundApp pageContent={container.innerHTML} roundID={parseInt(container.dataset.roundId)} /> + </Provider>, container ); diff --git a/opentech/static_src/src/app/src/main.scss b/opentech/static_src/src/app/src/main.scss new file mode 100644 index 0000000000000000000000000000000000000000..0508a4af4c6ea528096d39243eafb8a2466b8e49 --- /dev/null +++ b/opentech/static_src/src/app/src/main.scss @@ -0,0 +1,3 @@ +@import 'sass/apply/abstracts/_functions.scss'; +@import 'sass/apply/abstracts/_mixins.scss'; +@import 'sass/apply/abstracts/_variables.scss'; diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js new file mode 100644 index 0000000000000000000000000000000000000000..db35aec0678e3239cec174980b3de9ff6a8d6459 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -0,0 +1,134 @@ +import api from '@api'; +import { + getCurrentSubmission, + getCurrentSubmissionID, + getCurrentRoundID, + getCurrentRound, +} from '@selectors/submissions'; + + +// Submissions by round +export const SET_CURRENT_SUBMISSION_ROUND = 'SET_CURRENT_SUBMISSION_ROUND'; +export const UPDATE_SUBMISSIONS_BY_ROUND = 'UPDATE_SUBMISSIONS_BY_ROUND'; +export const START_LOADING_SUBMISSIONS_BY_ROUND = 'START_LOADING_SUBMISSIONS_BY_ROUND'; +export const FAIL_LOADING_SUBMISSIONS_BY_ROUND = 'FAIL_LOADING_SUBMISSIONS_BY_ROUND'; + +// Submissions +export const SET_CURRENT_SUBMISSION = 'SET_CURRENT_SUBMISSION'; +export const START_LOADING_SUBMISSION = 'START_LOADING_SUBMISSION'; +export const FAIL_LOADING_SUBMISSION = 'FAIL_LOADING_SUBMISSION'; +export const UPDATE_SUBMISSION = 'UPDATE_SUBMISSION'; +export const CLEAR_CURRENT_SUBMISSION = 'CLEAR_CURRENT_SUBMISSION'; + +export const setCurrentSubmissionRound = id => ({ + type: SET_CURRENT_SUBMISSION_ROUND, + id, +}); + +export const setCurrentSubmission = id => ({ + type: SET_CURRENT_SUBMISSION, + id, +}); + +export const loadCurrentRound = (requiredFields=[]) => (dispatch, getState) => { + const round = getCurrentRound(getState()) + + if (round && requiredFields.every(key => round.hasOwnProperty(key))) { + return null + } + + return dispatch(fetchSubmissionsByRound(getCurrentRoundID(getState()))) +} + + +export const fetchSubmissionsByRound = roundID => { + return async function(dispatch) { + dispatch(startLoadingSubmissionsByRound(roundID)); + try { + const response = await api.fetchSubmissionsByRound(roundID); + const json = await response.json(); + if (response.ok) { + dispatch(updateSubmissionsByRound(roundID, json)); + } else { + dispatch(failLoadingSubmissionsByRound(json.meta.error)); + } + } catch (e) { + dispatch(failLoadingSubmissionsByRound(e.message)); + } + }; +}; + + +const updateSubmissionsByRound = (roundID, data) => ({ + type: UPDATE_SUBMISSIONS_BY_ROUND, + roundID, + data, +}); + + +const startLoadingSubmissionsByRound = (roundID) => ({ + type: START_LOADING_SUBMISSIONS_BY_ROUND, + roundID, +}); + + +const failLoadingSubmissionsByRound = (message) => ({ + type: FAIL_LOADING_SUBMISSIONS_BY_ROUND, + message, +}); + + +export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) => { + const submissionID = getCurrentSubmissionID(getState()) + if ( !submissionID ) { + return null + } + const submission = getCurrentSubmission(getState()) + + if (submission && requiredFields.every(key => submission.hasOwnProperty(key))) { + return null + } + + return dispatch(fetchSubmission(getCurrentSubmissionID(getState()))) +} + + +export const fetchSubmission = submissionID => { + return async function(dispatch) { + + dispatch(startLoadingSubmission(submissionID)); + try { + const response = await api.fetchSubmission(submissionID); + const json = await response.json(); + if (response.ok) { + dispatch(updateSubmission(submissionID, json)); + } else { + dispatch(failLoadingSubmission(json.meta.error)); + } + } catch (e) { + dispatch(failLoadingSubmission(e.message)); + } + }; +}; + + +const startLoadingSubmission = submissionID => ({ + type: START_LOADING_SUBMISSION, + submissionID, +}); + +const failLoadingSubmission = submissionID => ({ + type: FAIL_LOADING_SUBMISSION, + submissionID, +}); + + +const updateSubmission = (submissionID, data) => ({ + type: UPDATE_SUBMISSION, + submissionID, + data, +}); + +export const clearCurrentSubmission = () => ({ + type: CLEAR_CURRENT_SUBMISSION, +}); diff --git a/opentech/static_src/src/app/src/redux/reducers/index.js b/opentech/static_src/src/app/src/redux/reducers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d1e5e237467ee34a6e305045c469b99560e3e92b --- /dev/null +++ b/opentech/static_src/src/app/src/redux/reducers/index.js @@ -0,0 +1,9 @@ +import { combineReducers } from 'redux' + +import submissions from '@reducers/submissions'; +import rounds from '@reducers/rounds'; + +export default combineReducers({ + submissions, + rounds, +}); diff --git a/opentech/static_src/src/app/src/redux/reducers/rounds.js b/opentech/static_src/src/app/src/redux/reducers/rounds.js new file mode 100644 index 0000000000000000000000000000000000000000..0f4253b713fa386196a99315fa6221bb47e12630 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/reducers/rounds.js @@ -0,0 +1,82 @@ +import { combineReducers } from 'redux'; + +import { + FAIL_LOADING_SUBMISSIONS_BY_ROUND, + SET_CURRENT_SUBMISSION_ROUND, + START_LOADING_SUBMISSIONS_BY_ROUND, + UPDATE_SUBMISSIONS_BY_ROUND, +} from '@actions/submissions'; + + +function round(state={id: null, submissions: [], isFetching: false}, action) { + switch(action.type) { + case UPDATE_SUBMISSIONS_BY_ROUND: + return { + ...state, + id: action.roundID, + submissions: action.data.results.map(submission => submission.id), + isFetching: false, + }; + case FAIL_LOADING_SUBMISSIONS_BY_ROUND: + return { + ...state, + isFetching: false, + }; + case START_LOADING_SUBMISSIONS_BY_ROUND: + return { + ...state, + id: action.roundID, + isFetching: true, + }; + default: + return state; + } +} + + +function roundsByID(state = {}, action) { + switch(action.type) { + case UPDATE_SUBMISSIONS_BY_ROUND: + case FAIL_LOADING_SUBMISSIONS_BY_ROUND: + case START_LOADING_SUBMISSIONS_BY_ROUND: + return { + ...state, + [action.roundID]: round(state[action.roundID], action) + }; + default: + return state; + } +} + + +function errorMessage(state = null, action) { + switch(action.type) { + case FAIL_LOADING_SUBMISSIONS_BY_ROUND: + return action.message; + case UPDATE_SUBMISSIONS_BY_ROUND: + case START_LOADING_SUBMISSIONS_BY_ROUND: + return null; + default: + return state; + } + +} + + +function currentRound(state = null, action) { + switch(action.type) { + case SET_CURRENT_SUBMISSION_ROUND: + return action.id; + default: + return state; + } +} + + +const rounds = combineReducers({ + byID: roundsByID, + current: currentRound, + error: errorMessage, +}); + +export default rounds; diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js new file mode 100644 index 0000000000000000000000000000000000000000..7563d1e25f0ac19364f82d0c96af3624f2125d4c --- /dev/null +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -0,0 +1,86 @@ +import { combineReducers } from 'redux'; + +import { + CLEAR_CURRENT_SUBMISSION, + FAIL_LOADING_SUBMISSION, + START_LOADING_SUBMISSION, + UPDATE_SUBMISSIONS_BY_ROUND, + UPDATE_SUBMISSION, + SET_CURRENT_SUBMISSION, +} from '@actions/submissions'; + + +function submission(state, action) { + switch(action.type) { + case START_LOADING_SUBMISSION: + return { + ...state, + isFetching: true, + isErrored: false, + }; + case FAIL_LOADING_SUBMISSION: + return { + ...state, + isFetching: false, + isErrored: true, + }; + case UPDATE_SUBMISSION: + return { + ...state, + ...action.data, + isFetching: false, + isErrored: false, + }; + default: + return state; + } +} + + +function submissionsByID(state = {}, action) { + switch(action.type) { + case START_LOADING_SUBMISSION: + case FAIL_LOADING_SUBMISSION: + case UPDATE_SUBMISSION: + return { + ...state, + [action.submissionID]: submission(state[action.submissionID], action), + }; + case UPDATE_SUBMISSIONS_BY_ROUND: + return { + ...state, + ...action.data.results.reduce((newItems, newSubmission) => { + newItems[newSubmission.id] = submission( + state[newSubmission.id], + { + type: UPDATE_SUBMISSION, + data: newSubmission, + } + ); + return newItems; + }, {}), + }; + default: + return state; + } +} + + +function currentSubmission(state = null, action) { + switch(action.type) { + case SET_CURRENT_SUBMISSION: + return action.id; + case CLEAR_CURRENT_SUBMISSION: + return null; + default: + return state; + } +} + + +const submissions = combineReducers({ + byID: submissionsByID, + current: currentSubmission, +}); + +export default submissions; diff --git a/opentech/static_src/src/app/src/redux/selectors/submissions.js b/opentech/static_src/src/app/src/redux/selectors/submissions.js new file mode 100644 index 0000000000000000000000000000000000000000..09124b6896da42c2c177f2e706a8b2528deaca54 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/selectors/submissions.js @@ -0,0 +1,53 @@ +import { createSelector } from 'reselect'; + +const getSubmissions = state => state.submissions.byID; + +const getRounds = state => state.rounds.byID; + +const getCurrentRoundID = state => state.rounds.current; + +const getCurrentRound = createSelector( + [ getCurrentRoundID, getRounds], + (id, rounds) => { + return rounds[id]; + } +); + +const getCurrentSubmissionID = state => state.submissions.current; + + +const getCurrentRoundSubmissions = createSelector( + [ getCurrentRound, getSubmissions], + (round, submissions) => { + const roundSubmissions = round ? round.submissions : []; + return roundSubmissions.map(submissionID => submissions[submissionID]); + } +); + + +const getCurrentSubmission = createSelector( + [ getCurrentSubmissionID, getSubmissions ], + (id, submissions) => { + return submissions[id]; + } +); + +const getSubmissionLoadingState = state => state.submissions.itemLoading === true; + +const getSubmissionErrorState = state => state.submissions.itemLoadingError === true; + +const getSubmissionsByRoundError = state => state.rounds.error; + +const getSubmissionsByRoundLoadingState = state => state.submissions.itemsLoading === true; + +export { + getCurrentRoundID, + getCurrentRound, + getCurrentRoundSubmissions, + getCurrentSubmission, + getCurrentSubmissionID, + getSubmissionsByRoundError, + getSubmissionsByRoundLoadingState, + getSubmissionLoadingState, + getSubmissionErrorState, +}; diff --git a/opentech/static_src/src/app/src/redux/store.js b/opentech/static_src/src/app/src/redux/store.js new file mode 100644 index 0000000000000000000000000000000000000000..ed0b719d6c0ff1968140fc66689ce15d53ddaa07 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/store.js @@ -0,0 +1,25 @@ +import { createStore, applyMiddleware } from 'redux' +import ReduxThunk from 'redux-thunk' +import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly' +import logger from 'redux-logger' + +import rootReducer from '@reducers'; + +const MIDDLEWARE = [ + ReduxThunk, +]; + +if (process.env.NODE_ENV === 'development') { + MIDDLEWARE.push(logger); +} + + +export default initialState => { + const store = createStore( + rootReducer, + composeWithDevTools( + applyMiddleware(...MIDDLEWARE) + ) + ) + return store; +}; diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js index 0a4c392f3cb62c7c39831fbb7ed13c8a3e214653..b6017a03190fc4870e014265d069f79bdd48d734 100644 --- a/opentech/static_src/src/app/webpack.base.config.js +++ b/opentech/static_src/src/app/webpack.base.config.js @@ -1,16 +1,15 @@ -var path = require("path") +var path = require('path'); module.exports = { context: __dirname, - entry: ['./src/index'], + entry: ['@babel/polyfill', './src/index'], output: { - filename: "[name]-[hash].js" + filename: '[name]-[hash].js' }, - plugins: [ - ], // add all common plugins here + plugins: [], module: { rules: [ @@ -19,26 +18,60 @@ module.exports = { loader: 'babel-loader', include: [path.resolve(__dirname, './src')], query: { - presets: ['@babel/preset-react'], + presets: ['@babel/preset-react', '@babel/preset-env'], plugins: [ 'react-hot-loader/babel', '@babel/plugin-proposal-class-properties' ] - } + }, + }, + { + test: /\.js$/, + exclude: /node_modules/, + include: [path.resolve(__dirname, './src')], + loader: 'eslint-loader', + options: { + configFile: path.resolve(__dirname, './.eslintrc'), + }, }, { test: /\.scss$/, - use: [ - 'style-loader', - 'css-loader', - 'sass-loader' - ] + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader', + options: { + sourceMap: true + } + }, { + loader: 'sass-loader', + options: { + sourceMap: true, + data: '@import "main.scss";', + includePaths: [ + path.join(__dirname, 'src') + ] + } + }] + }, + { + test: /\.svg$/, + use: ['@svgr/webpack'] } ] }, resolve: { - modules: ['node_modules'], - extensions: ['.js', '.jsx'] - }, -} + modules: ['node_modules', './src'], + extensions: ['.js', '.jsx'], + alias: { + '@components': path.resolve(__dirname, 'src/components'), + '@containers': path.resolve(__dirname, 'src/containers'), + '@redux': path.resolve(__dirname, 'src/redux'), + '@reducers': path.resolve(__dirname, 'src/redux/reducers'), + '@selectors': path.resolve(__dirname, 'src/redux/selectors'), + '@actions': path.resolve(__dirname, 'src/redux/actions'), + '@api': path.resolve(__dirname, 'src/api'), + } + } +}; diff --git a/opentech/static_src/src/app/webpack.dev.config.js b/opentech/static_src/src/app/webpack.dev.config.js index ca91ad2d1efda7e637e217e7ef2ac12d31e730fb..e609036e52110ccc23ef1cc93158c14893846966 100644 --- a/opentech/static_src/src/app/webpack.dev.config.js +++ b/opentech/static_src/src/app/webpack.dev.config.js @@ -12,6 +12,9 @@ config.plugins = config.plugins.concat([ new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin(), new BundleTracker({filename: './opentech/static_compiled/app/webpack-stats.json'}), + new webpack.EnvironmentPlugin({ + API_BASE_URL: 'http://apply.localhost:8000/', + }), ]) // Add a loader for JSX files with react-hot enabled diff --git a/opentech/static_src/src/app/webpack.prod.config.js b/opentech/static_src/src/app/webpack.prod.config.js index 9b0ff64acf263045ea57e046147492c9f347a7d2..9f85413c07e0cc3b652e0fb7135b675e634dfe07 100644 --- a/opentech/static_src/src/app/webpack.prod.config.js +++ b/opentech/static_src/src/app/webpack.prod.config.js @@ -7,12 +7,10 @@ config.output.path = require('path').resolve('./assets/dist') config.plugins = config.plugins.concat([ new BundleTracker({filename: './opentech/static_compiled/app/webpack-stats-prod.json'}), - - // removes a lot of debugging code in React - new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('production') - }}), + new webpack.EnvironmentPlugin({ + NODE_ENV: 'production', + API_BASE_URL: null , + }), ]) config.optimization = { diff --git a/opentech/static_src/src/images/download.svg b/opentech/static_src/src/images/download.svg new file mode 100644 index 0000000000000000000000000000000000000000..9c729aba1bcd0ea17eb1bb8ea72babcc9366bf40 --- /dev/null +++ b/opentech/static_src/src/images/download.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg viewBox="0 0 16 21" height="21" width="16" xmlns="http://www.w3.org/2000/svg"> + <g stroke-width="3" fill="none" fill-rule="evenodd" stroke-linecap="square"> + <path d="M8.176 13.833V2.167M8.303 14l4.991-4.714M8 14L3.009 9.286M13.824 19.5H2.176" /> + </g> +</svg> diff --git a/opentech/static_src/src/images/file.svg b/opentech/static_src/src/images/file.svg new file mode 100644 index 0000000000000000000000000000000000000000..936d57b3b18cd09fbe4b54a8c053bf21cd707a3b --- /dev/null +++ b/opentech/static_src/src/images/file.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg viewBox="0 0 27 32" height="32" width="27" xmlns="http://www.w3.org/2000/svg"> + <g stroke-width="2" fill="none" fill-rule="evenodd"> + <path d="M1.296 1v29.25H25V9.429h-8.218V1H1.296z" /> + <path d="M5.5 20h15M5.5 15H12" stroke-linecap="square" /> + <path d="M16.828.729l8.551 8.5" /> + </g> +</svg> diff --git a/opentech/static_src/src/images/icon-array.svg b/opentech/static_src/src/images/icon-array.svg new file mode 100644 index 0000000000000000000000000000000000000000..47982a5baa08eb2de4f43ac9a7f01d55dbc000a7 --- /dev/null +++ b/opentech/static_src/src/images/icon-array.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 18h3V5H4v13zM18 5v13h3V5h-3zM8 18h9V5H8v13z"/><path d="M0 0h24v24H0z" fill="none"/></svg> diff --git a/opentech/static_src/src/images/icon-grid.svg b/opentech/static_src/src/images/icon-grid.svg new file mode 100644 index 0000000000000000000000000000000000000000..8b094a0fbd5243d3c68a49b1d5e3b4c9df26fe87 --- /dev/null +++ b/opentech/static_src/src/images/icon-grid.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 14h4v-4H4v4zm0 5h4v-4H4v4zM4 9h4V5H4v4zm5 5h12v-4H9v4zm0 5h12v-4H9v4zM9 5v4h12V5H9z"/><path d="M0 0h24v24H0z" fill="none"/></svg> diff --git a/opentech/static_src/src/javascript/apply/all-submissions-table.js b/opentech/static_src/src/javascript/apply/all-submissions-table.js index 71d6f2087b86be10e79e5b4bbb811a90661c6ec4..f98d014546012f9ff6030911ad36445591430963 100644 --- a/opentech/static_src/src/javascript/apply/all-submissions-table.js +++ b/opentech/static_src/src/javascript/apply/all-submissions-table.js @@ -3,7 +3,7 @@ 'use strict'; // add the toggle arrow before the submission titles - $('.all-submissions__parent td.title').prepend('<span class="all-submissions__toggle js-toggle-submission"><span class="arrow"></span></span>'); + $('.all-submissions-table__parent td.title').prepend('<span class="all-submissions-table__toggle js-toggle-submission"><span class="arrow"></span></span>'); // grab all the toggles const children = Array.prototype.slice.call( @@ -13,7 +13,7 @@ // show/hide the submission child rows children.forEach(function (child) { child.addEventListener('click', (e) => { - $(e.target).closest('.all-submissions__parent').toggleClass('is-expanded'); + $(e.target).closest('.all-submissions-table__parent').toggleClass('is-expanded'); }); }); diff --git a/opentech/static_src/src/javascript/apply/submission-filters.js b/opentech/static_src/src/javascript/apply/submission-filters.js index fe4f36b9c2e7368c7675f98eec0918c616c3dda9..2201fc5d3a2eea39451dedd21ceb1b7d177a5727 100644 --- a/opentech/static_src/src/javascript/apply/submission-filters.js +++ b/opentech/static_src/src/javascript/apply/submission-filters.js @@ -2,15 +2,44 @@ 'use strict'; - const $openButton = $('.js-open-filters'); + // Variables + const $body = $('body'); + const $toggleButton = $('.js-toggle-filters'); const $closeButton = $('.js-close-filters'); const $clearButton = $('.js-clear-filters'); - const $filterList = $('.js-filter-list'); - const $filterWrapper = $('.js-filter-wrapper'); + const filterOpenClass = 'filters-open'; + const filterActiveClass = 'is-active'; + + const $searchInput = $('.js-search-input'); + const $queryInput = $('#id_query'); + const $searchForm = $('.js-search-form'); + + const $filterForm = $('.js-filter-form'); + + const urlParams = new URLSearchParams(window.location.search); + + const persistedParams = ['sort', 'query']; + + // check if the page has a query string and keep filters open if so on desktop + const minimumNumberParams = persistedParams.reduce( + (count, param) => count + urlParams.has(param) ? 1 : 0, + 0 + ); + + if ([...urlParams].length > minimumNumberParams && $(window).width() > 1024) { + $body.addClass(filterOpenClass); + updateButtonText(); + } + + $searchForm.submit((e) => { + e.preventDefault(); + $queryInput.val($searchInput.val()); + $filterForm.submit(); + }); // Add active class to filters - dropdowns are dynamically appended to the dom, // so we have to listen for the event higher up - $('body').on('click', '.select2-dropdown', (e) => { + $body.on('click', '.select2-dropdown', (e) => { // get the id of the dropdown let selectId = e.target.parentElement.parentElement.id; @@ -19,44 +48,122 @@ // if the dropdown contains a clear class, the filters are active if ($(match[0]).find('span.select2-selection__clear').length !== 0) { - match[0].classList.add('is-active'); + match[0].classList.add(filterActiveClass); } else { - match[0].classList.remove('is-active'); + match[0].classList.remove(filterActiveClass); } }); // remove active class on clearing select2 $('.select2').on('select2:unselecting', (e) => { const dropdown = e.target.nextElementSibling.firstChild.firstChild; - if (dropdown.classList.contains('is-active')) { - dropdown.classList.remove('is-active'); + if (dropdown.classList.contains(filterActiveClass)) { + dropdown.classList.remove(filterActiveClass); } }); - // open mobile filters - $openButton.on('click', (e) => { - $('body').addClass('no-scroll'); - $(e.target).next($filterWrapper).addClass('is-open'); - $filterList.addClass('form__filters--mobile'); + // toggle filters + $toggleButton.on('click', () => { + if ($body.hasClass(filterOpenClass)) { + handleClearFilters(); + } + else { + $body.addClass(filterOpenClass); + updateButtonText(); + } }); - // close mobile filters + // close filters on mobile $closeButton.on('click', (e) => { - $('body').removeClass('no-scroll'); - $(e.target).closest($filterWrapper).removeClass('is-open'); - $filterList.removeClass('form__filters--mobile'); + $body.removeClass(filterOpenClass); + updateButtonText(); }); + // redirect to submissions home to clear filters + function handleClearFilters() { + const query = persistedParams.reduce( + (query, param) => query + (urlParams.get(param) ? `&${param}=${urlParams.get(param)}` : ''), '?'); + window.location.href = window.location.href.split('?')[0] + query; + } + + // toggle filters button wording + function updateButtonText() { + if ($body.hasClass(filterOpenClass)) { + $toggleButton.text('Clear filters'); + } + else { + $toggleButton.text('Filters'); + } + } + + // corrects spacing of dropdowns when toggled on mobile + function mobileFilterPadding(element) { + const expanded = 'expanded-filter-element'; + const dropdown = $(element).closest('.select2'); + const openDropdown = $('.select2 .' + expanded); + let dropdownMargin = 0; + + if (openDropdown.length > 0 && !openDropdown.hasClass('select2-container--open')) { + // reset the margin of the select we previously worked + openDropdown.removeClass(expanded); + // store the offset to adjust the new select box (elements above the old dropdown unaffected) + if (dropdown.position().top > openDropdown.position().top) { + dropdownMargin = parseInt(openDropdown.css('marginBottom')); + } + openDropdown.css('margin-bottom', '0px'); + } + + if (dropdown.hasClass('select2-container--open')) { + dropdown.addClass(expanded); + const dropdownID = $(element).closest('.select2-selection').attr('aria-owns'); + // Element which has the height of the select dropdown + const match = $(`ul#${dropdownID}`); + const dropdownHeight = match.outerHeight(true); + + // Element which has the position of the dropdown + const positionalMatch = match.closest('.select2-container'); + + // Pad the bottom of the select box + dropdown.css('margin-bottom', `${dropdownHeight}px`); + + // bump up the dropdown options by height of closed elements + positionalMatch.css('top', positionalMatch.position().top - dropdownMargin); + } + } + // clear all filters $clearButton.on('click', () => { - const dropdowns = document.querySelectorAll('.form__filters--mobile select'); + const dropdowns = document.querySelectorAll('.form__filters select'); dropdowns.forEach(dropdown => { $(dropdown).val(null).trigger('change'); - $('.select2-selection.is-active').removeClass('is-active'); + $('.select2-selection.is-active').removeClass(filterActiveClass); mobileFilterPadding(dropdown); // eslint-disable-line no-undef }); }); -})(jQuery); + $(function () { + // Add active class to select2 checkboxes after page has been filtered + const clearButtons = document.querySelectorAll('.select2-selection__clear'); + clearButtons.forEach(clearButton => { + clearButton.parentElement.parentElement.classList.add(filterActiveClass); + }); + }); + + // reset mobile filters if they're open past the tablet breakpoint + $(window).resize(function resize() { + if ($(window).width() < 1024) { + // close the filters if open when reducing the window size + $('body').removeClass('filters-open'); + // update filter button text + $('.js-toggle-filters').text('Filters'); + + // Correct spacing of dropdowns when toggled + $('.select2').on('click', (e) => { + mobileFilterPadding(e.target); + }); + } + }).trigger('resize'); + +})(jQuery); diff --git a/opentech/static_src/src/javascript/apply/tabs.js b/opentech/static_src/src/javascript/apply/tabs.js index d7ade577561e477f2c9371657ce47d62e9f3e93d..27e6a900499855ef1f662334ca5dcfd0b4a5dbea 100644 --- a/opentech/static_src/src/javascript/apply/tabs.js +++ b/opentech/static_src/src/javascript/apply/tabs.js @@ -7,20 +7,36 @@ return '.js-tabs'; } - constructor() { + constructor(node) { + this.node = node[0]; // The tabs - this.tabItems = Array.prototype.slice.call(document.querySelectorAll('.tab__item:not(.js-tabs-off)')); + this.tabItems = Array.prototype.slice.call(this.node.querySelectorAll('.tab__item:not(.js-tabs-off)')); // The tabs content this.tabsContents = Array.prototype.slice.call(document.querySelectorAll('.tabs__content')); + // The tabs content container + this.tabsContentsContainer = Array.prototype.slice.call(document.querySelectorAll('.js-tabs-content')); + // Active classes this.tabActiveClass = 'tab__item--active'; this.tabContentActiveClass = 'tabs__content--current'; this.defaultSelectedTab = 'tab-1'; + this.addDataAttributes(); this.bindEvents(); } + addDataAttributes() { + // Add data-attrs for multiple tabs + this.tabsContentsContainer.forEach((tabsContent, i) => { + tabsContent.dataset.tabs = i + 1; + }); + + $('.js-tabs').each(function (i) { + $(this).attr('data-tabs', i + 1); + }); + } + bindEvents() { this.updateTabOnLoad(); @@ -48,15 +64,19 @@ tabs(e) { // Find current tab const tab = e.currentTarget; - this.stripTabClasses(); + + const tabContentId = $(tab).closest('.js-tabs').data('tabs'); + this.stripTabClasses(tabContentId); this.addTabClasses(tab); this.updateUrl(tab); } - stripTabClasses() { + stripTabClasses(tabContentId) { // remove active classes from all tabs and tab contents - this.tabItems.forEach(tabItem => tabItem.classList.remove(this.tabActiveClass)); - this.tabsContents.forEach(tabsContent => tabsContent.classList.remove(this.tabContentActiveClass)); + const parents = Array.prototype.slice.call($(`.js-tabs-content[data-tabs=${tabContentId}]`).find('.tabs__content')); + const childTabs = Array.prototype.slice.call($(`.js-tabs[data-tabs=${tabContentId}]`).find('.tab__item')); + childTabs.forEach(tabItem => tabItem.classList.remove(this.tabActiveClass)); + parents.forEach(tabsContent => tabsContent.classList.remove(this.tabContentActiveClass)); } addTabClasses(tab) { @@ -65,10 +85,12 @@ } const tabId = tab.getAttribute('data-tab'); + const tabContentId = $(tab).closest('.js-tabs').data('tabs'); + const parents = $(`.js-tabs-content[data-tabs=${tabContentId}]`); // add active classes to tabs and their respecitve content tab.classList.add(this.tabActiveClass); - document.querySelector(`#${tabId}`).classList.add(this.tabContentActiveClass); + $(parents).find(`#${tabId}`).addClass(this.tabContentActiveClass); } updateUrl(tab) { diff --git a/opentech/static_src/src/javascript/main.js b/opentech/static_src/src/javascript/main.js index 8356fe85e95e43e5fcd325c6c5619b00a7019f0b..9d1e0e9e5022577f26cff13451fdcdcd83e1224f 100644 --- a/opentech/static_src/src/javascript/main.js +++ b/opentech/static_src/src/javascript/main.js @@ -126,67 +126,15 @@ message.classList.add('messages__text--hide'); }); - // Add active class to select2 checkboxes after page has been filtered - document.addEventListener('DOMContentLoaded', () => { - // If there are clear buttons in the dom, it means the filters have been applied - const clearButtons = document.querySelectorAll('.select2-selection__clear'); - clearButtons.forEach(clearButton => { - clearButton.parentElement.parentElement.classList.add('is-active'); - }); - }); - // reset mobile filters if they're open past the tablet breakpoint $(window).resize(function resize() { - if ($(window).width() < 768) { - $('.select2').on('click', (e) => { - mobileFilterPadding(e.target); - }); - } - else { - $('body').removeClass('no-scroll'); - $('.js-filter-wrapper').removeClass('is-open'); - $('.js-filter-list').removeClass('form__filters--mobile'); + if ($(window).width() > 1024) { $('.js-actions-toggle').removeClass('is-active'); $('.js-actions-sidebar').removeClass('is-visible'); $('.tr--parent.is-expanded').removeClass('is-expanded'); } }).trigger('resize'); - function mobileFilterPadding(element) { - const expanded = 'expanded-filter-element'; - const dropdown = $(element).closest('.select2'); - const openDropdown = $('.select2 .' + expanded); - let dropdownMargin = 0; - - if (openDropdown.length > 0 && !openDropdown.hasClass('select2-container--open')) { - // reset the margin of the select we previously worked - openDropdown.removeClass(expanded); - // store the offset to adjust the new select box (elements above the old dropdown unaffected) - if (dropdown.position().top > openDropdown.position().top) { - dropdownMargin = parseInt(openDropdown.css('marginBottom')); - } - openDropdown.css('margin-bottom', '0px'); - } - - if (dropdown.hasClass('select2-container--open')) { - dropdown.addClass(expanded); - const dropdownID = $(element).closest('.select2-selection').attr('aria-owns'); - // Element which has the height of the select dropdown - const match = $(`ul#${dropdownID}`); - const dropdownHeight = match.outerHeight(true); - - // Element which has the position of the dropdown - const positionalMatch = match.closest('.select2-container'); - - // Pad the bottom of the select box - dropdown.css('margin-bottom', `${dropdownHeight}px`); - - // bump up the dropdown options by height of closed elements - positionalMatch.css('top', positionalMatch.position().top - dropdownMargin); - } - } - - $('form').filter('.form__comments').submit(function (e) { var $form = $(this); var formValues = $form.serialize(); @@ -201,4 +149,10 @@ } }); + // Get the header and admin bar height and set custom prop with value + $(window).on('load', function () { + const headerHeight = $('.header').outerHeight(); + const adminbarHeight = $('.admin-bar').outerHeight(); + document.documentElement.style.setProperty('--header-admin-height', headerHeight + adminbarHeight + 'px'); + }); })(jQuery); diff --git a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss index 24509a3ef802fb81c132e6610dc72a471817ad97..a4ac8a42d3d28b1fc951d0226fb1a445087cdc1e 100644 --- a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss +++ b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss @@ -155,3 +155,57 @@ } } +// used for the submission list items in the react app +@mixin submission-list-item { + border-bottom: 2px solid $color--light-mid-grey; + border-right: 2px solid $color--light-mid-grey; +} + +// ie11-specific css +@mixin target-ie11 { + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + @content; + } +} + +// ms-edge specific css +@mixin target-edge { + @supports (-ms-ime-align: auto) { + @content; + } +} + +@mixin table-ordering-styles { + thead { + th { + // ordering + &.desc, + &.asc { + position: relative; + color: $color--dark-grey; + + &::after { + position: absolute; + top: 32px; + margin-left: 3px; + } + + a { + color: inherit; + } + } + + &.desc { + &::after { + @include triangle(top, $color--default, 5px); + } + } + + &.asc { + &::after { + @include triangle(bottom, $color--default, 5px); + } + } + } + } +} diff --git a/opentech/static_src/src/sass/apply/abstracts/_variables.scss b/opentech/static_src/src/sass/apply/abstracts/_variables.scss index 8f6c7113797a6ed556b4e59ef7446e7f88156958..c4e87dfd68b25855e2d32040473b05eebd7d5b49 100644 --- a/opentech/static_src/src/sass/apply/abstracts/_variables.scss +++ b/opentech/static_src/src/sass/apply/abstracts/_variables.scss @@ -1,4 +1,7 @@ // sass-lint:disable no-color-keywords; no-color-hex +:root { + --header-admin-height: 0; +} // Default $color--white: #fff; @@ -23,11 +26,12 @@ $color--mint: #40c2ad; $color--grass: #7dc588; $color--ocean: #1888b1; $color--sky-blue: #e7f2f6; -$color--marine: #177da8; +$color--marine: #14729a; $color--mist: #f3fafe; $color--green: #7dc558; $color--pastel-red: #f1a9a9; $color--pastel-green: #afe6af; +$color--fog: #eff2f5; // Social $color--twitter: #1da6f6; @@ -35,6 +39,7 @@ $color--linkedin: #137ab8; $color--facebook: #396ab5; // Transparent +$color--black-60: rgba(0, 0, 0, .6); $color--black-50: rgba(0, 0, 0, .5); $color--black-40: rgba(0, 0, 0, .4); $color--black-25: rgba(0, 0, 0, .25); @@ -91,13 +96,28 @@ $breakpoints: ( 'small-tablet' '(min-width: 600px)', 'tablet-portrait' '(min-width: 768px)', 'tablet-landscape' '(min-width: 1024px)', + 'laptop-short' '(min-width: 1024px) and (max-height: 1280px)', 'desktop' '(min-width: 1320px)', - 'deskop-wide' '(min-width: 2556px)' + 'desktop-medium' '(min-width: 1920px)', + 'desktop-wide' '(min-width: 2556px)' ); // Transition $transition: .25s ease-out; $quick-transition: .15s ease; +$medium-transition: .5s ease; + +// Delays +$base-delay: 30ms; // Spacing $mobile-gutter: 20px; + +// Filters +$filter-dropdown: '.select2 .select2-selection.select2-selection--single'; + +// listing header/spacer height +$listing-header-height: 75px; + +// Table breakpoint +$table-breakpoint: 'tablet-landscape'; diff --git a/opentech/static_src/src/sass/apply/base/_base.scss b/opentech/static_src/src/sass/apply/base/_base.scss index 64fac05a4b13c45752c01e9ca281c1c86f718327..8ab04c2e5d8a29a34c90e499fe7e255cecb5004d 100644 --- a/opentech/static_src/src/sass/apply/base/_base.scss +++ b/opentech/static_src/src/sass/apply/base/_base.scss @@ -16,6 +16,10 @@ html { -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + @supports(overflow-y: overlay) { + overflow-y: overlay; + } } body { diff --git a/opentech/static_src/src/sass/apply/components/_admin-bar.scss b/opentech/static_src/src/sass/apply/components/_admin-bar.scss index 9cc8a34affd212942794afee61b732f8529eb8b6..ef371d1a796b90145ab925a322066cb3ea3cf1c0 100644 --- a/opentech/static_src/src/sass/apply/components/_admin-bar.scss +++ b/opentech/static_src/src/sass/apply/components/_admin-bar.scss @@ -28,4 +28,12 @@ margin-bottom: 0; } } + + &__meta { + margin: 0 0 10px; + + span { + margin: 0 5px; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_all-rounds-table.scss b/opentech/static_src/src/sass/apply/components/_all-rounds-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..3be90e1d1aeed7bea486547e45a16653f0d757c5 --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_all-rounds-table.scss @@ -0,0 +1,23 @@ +.all-rounds-table { + @include table-ordering-styles; + + thead { + display: none; + + @include media-query($table-breakpoint) { + display: table-header-group; + } + + th { + padding: 25px 10px; + } + } + + tbody { + td { + &.title { + padding-top: 15px; + } + } + } +} diff --git a/opentech/static_src/src/sass/apply/components/_all-submissions.scss b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss similarity index 88% rename from opentech/static_src/src/sass/apply/components/_all-submissions.scss rename to opentech/static_src/src/sass/apply/components/_all-submissions-table.scss index ea72a28ba297d19475de991e4081269086dcd04b..81df77c7fe8888b7d61db64eda57aaaaa8f5850f 100644 --- a/opentech/static_src/src/sass/apply/components/_all-submissions.scss +++ b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss @@ -1,10 +1,9 @@ -// also in _table.scss -$table-breakpoint: 'tablet-landscape'; - -.all-submissions { +.all-submissions-table { + @include table-ordering-styles; $root: &; font-size: 14px; + thead { display: none; @@ -13,37 +12,8 @@ $table-breakpoint: 'tablet-landscape'; } th { - // ordering - &.desc, - &.asc { - position: relative; - color: $color--dark-grey; - - &::after { - position: absolute; - top: 32px; - margin-left: 3px; - } - - a { - color: inherit; - } - } - - &.desc { - &::after { - @include triangle(top, $color--default, 5px); - } - } - - &.asc { - &::after { - @include triangle(bottom, $color--default, 5px); - } - } - &.reviews_stats { // sass-lint:disable-line class-name-format - color: $color--mid-dark-grey; + color: $color--black-60; span { font-size: 13px; @@ -254,7 +224,27 @@ $table-breakpoint: 'tablet-landscape'; } } + &__empty { + td { + padding: 20px; + } + } + &__toggle { padding: 5px 0 5px 5px; } + + &__more { + display: flex; + justify-content: center; + background-color: $color--white; + padding: 20px 25px; + min-height: auto; + + a { + margin: 0; + flex-basis: auto; + font-weight: $weight--semibold; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_button.scss b/opentech/static_src/src/sass/apply/components/_button.scss index 32efcfb5da86bc49602c897f429e5cad93fc46ab..5ebcce8979c9328a65d199918924f6676d478454 100644 --- a/opentech/static_src/src/sass/apply/components/_button.scss +++ b/opentech/static_src/src/sass/apply/components/_button.scss @@ -11,8 +11,8 @@ .form--filters & { width: 100%; - height: 100%; text-align: center; + height: 45px; } } @@ -72,21 +72,34 @@ justify-content: space-between; max-width: 300px; padding: 15px 20px; - margin-bottom: 20px; font-weight: $weight--normal; color: $color--default; background: url('./../../images/filters.svg') $color--white no-repeat 93% center; border: 1px solid $color--light-mid-grey; transition: none; - - &:focus, - &:active, - &:hover { - background: url('./../../images/filters.svg') $color--white no-repeat 93% center; - } + width: 100%; @include media-query(tablet-landscape) { - display: none; + background: none; + padding: 0 10px; + border: 0; + align-items: center; + justify-content: flex-start; + max-width: initial; + width: auto; + + &::before { + content: ''; + background-image: url('./../../images/filters.svg'); + background-color: transparent; + background-position: left center; + transform: rotate(90deg); + background-size: 20px; + width: 20px; + height: 20px; + display: inline-block; + margin-right: 10px; + } } } @@ -96,7 +109,7 @@ &--search { position: absolute; - top: 12px; + top: .65em; right: 10px; svg { diff --git a/opentech/static_src/src/sass/apply/components/_filters.scss b/opentech/static_src/src/sass/apply/components/_filters.scss index 812e3758fdda9463815598c4d655fe4ae916d40f..f6018235d5ad2ecc37b175d9bb8225ac92c9f5c1 100644 --- a/opentech/static_src/src/sass/apply/components/_filters.scss +++ b/opentech/static_src/src/sass/apply/components/_filters.scss @@ -1,11 +1,7 @@ .filters { display: none; - @include media-query(tablet-landscape) { - display: block; - } - - &.is-open { + .filters-open & { position: fixed; top: 0; right: 0; @@ -18,6 +14,26 @@ background: $color--white; } + @include media-query(tablet-landscape) { + display: block; + max-height: 0; + transition: max-height $medium-transition; + transition-delay: $base-delay; + pointer-events: none; + + .filters-open & { + position: relative; + top: auto; + left: auto; + right: auto; + bottom: auto; + height: auto; + background: transparent; + max-height: 85px; + pointer-events: all; + } + } + &__header { display: flex; align-items: center; @@ -36,4 +52,12 @@ } } } + + &__button { + appearance: none; + -webkit-appearance: none; // sass-lint:disable-line no-vendor-prefixes + border: 0; + color: $color--primary; + background: transparent; + } } diff --git a/opentech/static_src/src/sass/apply/components/_form.scss b/opentech/static_src/src/sass/apply/components/_form.scss index 44c94c9aea1d60515fde8b2ce023234235c57936..c7708add8d06a46a2ac14c522915aa821a09bc82 100644 --- a/opentech/static_src/src/sass/apply/components/_form.scss +++ b/opentech/static_src/src/sass/apply/components/_form.scss @@ -17,12 +17,15 @@ } } - &--header-search-desktop { + &--search { position: relative; - width: 280px; + max-width: 300px; + margin-top: $mobile-gutter; + width: 100%; - @include media-query(small-tablet) { - margin-top: 15px; + @include media-query(tablet-landscape) { + max-width: 280px; + margin: 0 0 0 30px; } } @@ -107,8 +110,33 @@ } &__filters { - display: flex; - padding: 10px 0 30px; + #{$filter-dropdown} { + border: 0; + border-top: 1px solid $color--mid-grey; + + &.is-active { + font-weight: $weight--normal; + background-color: transparentize($color--primary, .9); + border-color: $color--mid-grey; + } + + @include media-query(tablet-landscape) { + border: 1px solid $color--mid-grey; + } + } + + @include media-query(tablet-landscape) { + display: flex; + align-items: flex-start; + padding: 10px 0 30px; + opacity: 0; + transition: opacity $transition; + + .filters-open & { + opacity: 1; + transition-delay: $base-delay * 10; + } + } label { display: none; @@ -116,25 +144,19 @@ // so the form can be output in any tag > * { - flex-basis: 225px; - margin-right: 10px; + @include media-query(tablet-landscape) { + flex-basis: 225px; + + &:not(:last-child) { + margin-right: 10px; + } + } } &--mobile { flex-direction: column; padding: 0; - .select2 .select2-selection.select2-selection--single { // sass-lint:disable-line force-element-nesting - border: 0; - border-top: 1px solid $color--mid-grey; - - &.is-active { - font-weight: $weight--normal; - background-color: transparentize($color--primary, .9); - border-color: $color--mid-grey; - } - } - // so the form can be output in any tag > * { flex-basis: auto; @@ -179,6 +201,7 @@ } .form__filters & { + background: $color--white; max-width: 100%; } @@ -385,4 +408,3 @@ max-width: 410px; } } - diff --git a/opentech/static_src/src/sass/apply/components/_input.scss b/opentech/static_src/src/sass/apply/components/_input.scss index e73b1d50d0c1e20e5b140a3c091e9eca3e298bc9..fa64ea285549a1667ce69b28956e986ce9035b1c 100644 --- a/opentech/static_src/src/sass/apply/components/_input.scss +++ b/opentech/static_src/src/sass/apply/components/_input.scss @@ -2,8 +2,8 @@ &--search { width: 100%; padding: 10px; - color: $color--white; + color: $color--dark-grey; background: transparent; - border: 1px solid $color--white; + border: 1px solid $color--mid-dark-grey; } } diff --git a/opentech/static_src/src/sass/apply/components/_link.scss b/opentech/static_src/src/sass/apply/components/_link.scss index 65917654e32eb79eb5e59afb500db9050585f364..9217908648e8a569ebe7e7d86b8769db2ab0d153 100644 --- a/opentech/static_src/src/sass/apply/components/_link.scss +++ b/opentech/static_src/src/sass/apply/components/_link.scss @@ -54,7 +54,7 @@ } &:last-child { - width: 12px; + width: 15px; height: 18px; stroke: $color--white; } diff --git a/opentech/static_src/src/sass/apply/components/_rich-text.scss b/opentech/static_src/src/sass/apply/components/_rich-text.scss index 4457e5494ae076fbaeb38f2d63f8b3b09b8d4fd6..8a0211b4450779c46b90b04dfe3bdb7e7bdd23c1 100644 --- a/opentech/static_src/src/sass/apply/components/_rich-text.scss +++ b/opentech/static_src/src/sass/apply/components/_rich-text.scss @@ -33,6 +33,11 @@ ul { padding-left: 20px; list-style: outside disc; + + &.remove-list-style { + padding: 0; + list-style: none; + } } ol { diff --git a/opentech/static_src/src/sass/apply/components/_round-block.scss b/opentech/static_src/src/sass/apply/components/_round-block.scss new file mode 100644 index 0000000000000000000000000000000000000000..3e5e49227cc55351e3023364c03762adb5ff436e --- /dev/null +++ b/opentech/static_src/src/sass/apply/components/_round-block.scss @@ -0,0 +1,90 @@ +.round-block { + $root: &; + + p { + font-size: 14px; + } + + &__item { + align-items: center; + background-color: $color--white; + border: 1px solid $color--light-mid-grey; + border-bottom: 0; + padding: 25px; + transition: background-color $quick-transition; + min-height: 80px; + + &:last-child { + border-bottom: 1px solid $color--light-mid-grey; + } + + @include media-query(tablet-landscape) { + display: flex; + + &:hover { + background-color: $color--light-grey; + border-right: 1px solid $color--light-grey; + border-left: 1px solid $color--light-grey; + } + + // item spacing + > * { + margin: 0 30px 0 0; + flex-basis: 200px; + } + } + + &--more { + padding: 20px 25px; + justify-content: center; + border-bottom: 1px solid $color--light-mid-grey; + min-height: auto; + + &:hover { + background-color: $color--white; + } + + // show more link + a { + margin: 0; + flex-basis: auto; + font-weight: $weight--semibold; + } + } + } + + &__view { + margin: 0 0 0 auto; + transition: color $quick-transition; + font-weight: $weight--bold; + text-transform: uppercase; + + @include media-query(tablet-landscape) { + color: transparent; + flex-basis: auto; + pointer-events: none; + } + + &:focus, + #{$root}__item:hover & { + color: $color--primary; + pointer-events: all; + } + } + + &__date, + &__determination { + color: $color--primary; + } + + &__title { + font-weight: $weight--semibold; + } + + &__not-found { + margin: 0; + padding: 20px; + background-color: $color--white; + border: 1px solid $color--light-mid-grey; + } +} diff --git a/opentech/static_src/src/sass/apply/components/_section.scss b/opentech/static_src/src/sass/apply/components/_section.scss index 4b7d3979b2b25d4725b21bb407eaec2334a63710..c7142f198c31cafdc2a42addce47023a00251b28 100644 --- a/opentech/static_src/src/sass/apply/components/_section.scss +++ b/opentech/static_src/src/sass/apply/components/_section.scss @@ -8,4 +8,15 @@ margin-bottom: 0; } } + + &--with-options { + display: flex; + flex-direction: column; + + @include media-query(small-tablet) { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + } } diff --git a/opentech/static_src/src/sass/apply/components/_select2.scss b/opentech/static_src/src/sass/apply/components/_select2.scss index 54147955936b9ec77cc96fd6bab97db832395664..dff4cdb83d084a81fd0bb1c4f5c18685167da01c 100644 --- a/opentech/static_src/src/sass/apply/components/_select2.scss +++ b/opentech/static_src/src/sass/apply/components/_select2.scss @@ -1,3 +1,5 @@ +$dropdown-height: 45px; + .select2 { &-container { z-index: 99995; // to override any modals @@ -8,7 +10,7 @@ width: 100% !important; // sass-lint:disable-line no-important .select2-selection--single { - height: 55px; + height: $dropdown-height; border: 1px solid $color--mid-grey; border-radius: 0; @@ -18,27 +20,22 @@ } .select2-selection__clear { - position: absolute; - right: 35px; display: none; - float: none; - - @include media-query(small-tablet) { - display: block; - } } .select2-selection__rendered { padding-left: 15px; - line-height: 55px; + padding-right: 30px; + line-height: $dropdown-height; } .select2-selection__arrow { right: 15px; - height: 53px; + height: $dropdown-height; pointer-events: none; background: url('./../../images/dropdown.svg') transparent no-repeat 95% center; background-size: 8px; + width: 8px; b[role='presentation'] { display: none; @@ -90,7 +87,7 @@ padding: 6px; &::before { - width: 20px; + min-width: 20px; height: 20px; margin-right: 10px; background: $color--white; diff --git a/opentech/static_src/src/sass/apply/components/_table.scss b/opentech/static_src/src/sass/apply/components/_table.scss index 2ff8ebb0e69bf1684e5274926cd8cd39a6e150aa..5bb173ed3f425c1ca9a4f557d5b2414287e5e4b6 100644 --- a/opentech/static_src/src/sass/apply/components/_table.scss +++ b/opentech/static_src/src/sass/apply/components/_table.scss @@ -1,6 +1,4 @@ -// also in _all-submissions.scss -$table-breakpoint: 'tablet-landscape'; - +// base table styles - specific ones in their own scss partial table { width: 100%; background-color: $color--white; @@ -15,7 +13,7 @@ table { text-align: left; a { - color: $color--mid-dark-grey; + color: $color--black-60; transition: color .25s ease-out; } } @@ -78,6 +76,12 @@ table { } } } + + &.title { + a { + font-weight: $weight--bold; + } + } } } @@ -88,5 +92,9 @@ table { @include media-query($table-breakpoint) { padding: 15px 10px; } + + &.title { + padding-left: 20px; + } } } diff --git a/opentech/static_src/src/sass/apply/components/_tabs.scss b/opentech/static_src/src/sass/apply/components/_tabs.scss index 7bfc204205a2a8159a3cbd5bcfa792d5c0004a31..ca285d185d6256f6d2e6ef6db1bee4c6557083f5 100644 --- a/opentech/static_src/src/sass/apply/components/_tabs.scss +++ b/opentech/static_src/src/sass/apply/components/_tabs.scss @@ -11,11 +11,12 @@ &--current { display: inherit; } - } } .tab__item { + $root: &; + @include responsive-font-sizes(12px, 15px); position: relative; padding: 10px; @@ -39,6 +40,32 @@ color: $color--light-blue; } + &--alt { + font-size: map-get($font-sizes, zeta); + font-weight: $weight--semibold; + padding: 20px 10px; + text-transform: none; + display: inline-block; + margin-right: 20px; + border-bottom: 3px solid transparent; + color: $color--mid-dark-grey; + width: auto; + + &:hover { + color: $color--default; + } + + &#{$root}--active { + background-color: transparent; + border-bottom: 3px solid $color--primary; + + &:hover { + color: $color--default; + background-color: transparent; + } + } + } + &--active { color: $color--default; cursor: default; diff --git a/opentech/static_src/src/sass/apply/components/_wrapper.scss b/opentech/static_src/src/sass/apply/components/_wrapper.scss index cf14c2fe9e3c99c34137f3c664e107db9a940445..1d4ac18c0817957a345fd68d67e2b977ca37f3b0 100644 --- a/opentech/static_src/src/sass/apply/components/_wrapper.scss +++ b/opentech/static_src/src/sass/apply/components/_wrapper.scss @@ -265,4 +265,18 @@ &--reviews-table { overflow-x: scroll; } + + &--table-actions { + margin-bottom: 20px; + + @include media-query(tablet-portrait) { + display: flex; + justify-content: space-between; + + } + + @include media-query(tablet-landscape) { + justify-content: flex-end; + } + } } diff --git a/opentech/static_src/src/sass/apply/main.scss b/opentech/static_src/src/sass/apply/main.scss index 745f50807ad3b43d597b5a7f82021f9bec2de050..4423f6ed552181b4708da92017bba897de5f1aa7 100644 --- a/opentech/static_src/src/sass/apply/main.scss +++ b/opentech/static_src/src/sass/apply/main.scss @@ -8,7 +8,8 @@ @import 'base/typography'; // Components -@import 'components/all-submissions'; +@import 'components/all-submissions-table'; +@import 'components/all-rounds-table'; @import 'components/admin-bar'; @import 'components/activity-feed'; @import 'components/comment'; @@ -37,6 +38,7 @@ @import 'components/reviews-list'; @import 'components/reviews-summary'; @import 'components/reviews-sidebar'; +@import 'components/round-block'; @import 'components/select2'; @import 'components/submission-meta'; @import 'components/revision'; diff --git a/opentech/static_src/src/sass/public/abstracts/_variables.scss b/opentech/static_src/src/sass/public/abstracts/_variables.scss index 8f6c7113797a6ed556b4e59ef7446e7f88156958..e501e189b0d40067423ab61d8e127718ec3e15f5 100644 --- a/opentech/static_src/src/sass/public/abstracts/_variables.scss +++ b/opentech/static_src/src/sass/public/abstracts/_variables.scss @@ -92,12 +92,15 @@ $breakpoints: ( 'tablet-portrait' '(min-width: 768px)', 'tablet-landscape' '(min-width: 1024px)', 'desktop' '(min-width: 1320px)', - 'deskop-wide' '(min-width: 2556px)' + 'desktop-wide' '(min-width: 2556px)' ); // Transition $transition: .25s ease-out; $quick-transition: .15s ease; +// Delays +$base-delay: 30ms; + // Spacing $mobile-gutter: 20px; diff --git a/opentech/templates/base.html b/opentech/templates/base.html index 94e1cd306d2ef3fa48167831508a0af03cccee3a..0844af725b963c8d288c791f1eeea958e0afb73d 100644 --- a/opentech/templates/base.html +++ b/opentech/templates/base.html @@ -129,7 +129,7 @@ </div> <div class="header__search"> - <form action="{% url 'search' %}" method="get" role="search" class="form form--header-search-desktop"> + <form action="{{ PUBLIC_SITE.root_url }}{% url 'search' %}" method="get" role="search" class="form form--header-search-desktop"> <button class="button" type="submit" aria-label="Search"> <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg> </button> diff --git a/opentech/urls.py b/opentech/urls.py index 8cb2fe3067cbc3bd1c7e042adb4d2243ae8c6b07..e6845757630e342d14af0be4b2e95fdb6cd03a13 100644 --- a/opentech/urls.py +++ b/opentech/urls.py @@ -14,14 +14,27 @@ from opentech.public import urls as public_urls from opentech.apply.users.urls import public_urlpatterns as user_urls +def apply_cache_control(*patterns): + # Cache-control + cache_length = getattr(settings, 'CACHE_CONTROL_MAX_AGE', None) + + if cache_length: + patterns = decorate_urlpatterns( + patterns, + cache_control(max_age=cache_length) + ) + + return list(patterns) + + urlpatterns = [ path('django-admin/', admin.site.urls), path('admin/', include(wagtailadmin_urls)), path('documents/', include(wagtaildocs_urls)), path('sitemap.xml', sitemap), - path('', include(public_urls)), path('', include((user_urls, 'users_public'))), + path('', include(public_urls)), path('', include('social_django.urls', namespace='social')), path('tinymce/', include('tinymce.urls')), path('select2/', include('django_select2.urls')), @@ -52,18 +65,14 @@ urlpatterns += [ path('', include(wagtail_urls)), ] +urlpatterns = apply_cache_control(*urlpatterns) -# Cache-control -cache_length = getattr(settings, 'CACHE_CONTROL_MAX_AGE', None) - -if cache_length: - urlpatterns = decorate_urlpatterns( - urlpatterns, - cache_control(max_age=cache_length) - ) if settings.DEBUG and settings.DEBUGTOOLBAR: import debug_toolbar urlpatterns = [ path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns + + +base_urlpatterns = [*urlpatterns] diff --git a/package-lock.json b/package-lock.json index beb4fa32aade77e10ca01e4e27c1ac72dbe8c95a..c03d14147002a5f8545753a762f00216e4b07807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } @@ -17,7 +16,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.2.2.tgz", "integrity": "sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/generator": "^7.2.2", @@ -39,7 +37,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -50,7 +47,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.2.tgz", "integrity": "sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==", - "dev": true, "requires": { "@babel/types": "^7.2.2", "jsesc": "^2.5.1", @@ -63,7 +59,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -72,7 +67,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", - "dev": true, "requires": { "@babel/helper-explode-assignable-expression": "^7.1.0", "@babel/types": "^7.0.0" @@ -82,7 +76,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz", "integrity": "sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw==", - "dev": true, "requires": { "@babel/types": "^7.0.0", "esutils": "^2.0.0" @@ -92,7 +85,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz", "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==", - "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.0.0", "@babel/traverse": "^7.1.0", @@ -116,7 +108,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz", "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/types": "^7.0.0", @@ -127,7 +118,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", - "dev": true, "requires": { "@babel/traverse": "^7.1.0", "@babel/types": "^7.0.0" @@ -137,7 +127,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.0.0", "@babel/template": "^7.1.0", @@ -148,7 +137,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -157,7 +145,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz", "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -166,7 +153,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -175,7 +161,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -184,7 +169,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz", "integrity": "sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-simple-access": "^7.1.0", @@ -198,7 +182,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -206,14 +189,12 @@ "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" }, "@babel/helper-regex": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz", "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==", - "dev": true, "requires": { "lodash": "^4.17.10" } @@ -222,7 +203,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-wrap-function": "^7.1.0", @@ -235,7 +215,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz", "integrity": "sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA==", - "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.0.0", "@babel/helper-optimise-call-expression": "^7.0.0", @@ -247,7 +226,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", - "dev": true, "requires": { "@babel/template": "^7.1.0", "@babel/types": "^7.0.0" @@ -257,7 +235,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -266,7 +243,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/template": "^7.1.0", @@ -278,7 +254,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.2.0.tgz", "integrity": "sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==", - "dev": true, "requires": { "@babel/template": "^7.1.2", "@babel/traverse": "^7.1.5", @@ -289,7 +264,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -299,14 +273,12 @@ "@babel/parser": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz", - "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==", - "dev": true + "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-remap-async-to-generator": "^7.1.0", @@ -327,7 +299,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-json-strings": "^7.2.0" @@ -337,7 +308,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz", "integrity": "sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-object-rest-spread": "^7.2.0" @@ -347,7 +317,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" @@ -357,7 +326,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz", "integrity": "sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.0.0", @@ -368,7 +336,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -377,7 +344,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -386,7 +352,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -395,7 +360,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -404,7 +368,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -413,7 +376,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -422,7 +384,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz", "integrity": "sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -433,7 +394,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -442,7 +402,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz", "integrity": "sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "lodash": "^4.17.10" @@ -452,7 +411,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz", "integrity": "sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-define-map": "^7.1.0", @@ -468,7 +426,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -477,7 +434,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz", "integrity": "sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -486,7 +442,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz", "integrity": "sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.0.0", @@ -497,7 +452,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz", "integrity": "sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -506,7 +460,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", - "dev": true, "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -516,7 +469,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz", "integrity": "sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -525,7 +477,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz", "integrity": "sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -535,7 +486,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -544,7 +494,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz", "integrity": "sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -554,7 +503,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz", "integrity": "sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -565,7 +513,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz", "integrity": "sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ==", - "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -575,7 +522,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -585,7 +531,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -594,7 +539,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-replace-supers": "^7.1.0" @@ -604,18 +548,25 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz", "integrity": "sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA==", - "dev": true, "requires": { "@babel/helper-call-delegate": "^7.1.0", "@babel/helper-get-function-arity": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.2.0.tgz", + "integrity": "sha512-YYQFg6giRFMsZPKUM9v+VcHOdfSQdz9jHCx3akAi3UYgyjndmdYGSXylQ/V+HswQt4fL8IklchD9HTsaOCrWQQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-transform-react-display-name": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -624,7 +575,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.2.0.tgz", "integrity": "sha512-h/fZRel5wAfCqcKgq3OhbmYaReo7KkoJBpt8XnvpS7wqaNMqtw5xhxutzcm35iMUWucfAdT/nvGTsWln0JTg2Q==", - "dev": true, "requires": { "@babel/helper-builder-react-jsx": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -635,7 +585,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -645,7 +594,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.2.0.tgz", "integrity": "sha512-A32OkKTp4i5U6aE88GwwcuV4HAprUgHcTq0sSafLxjr6AW0QahrCRCjxogkbbcdtpbXkuTOlgpjophCxb6sh5g==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -655,7 +603,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz", "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==", - "dev": true, "requires": { "regenerator-transform": "^0.13.3" } @@ -664,7 +611,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -673,7 +619,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -682,7 +627,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.0.0" @@ -692,7 +636,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz", "integrity": "sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -702,7 +645,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -711,18 +653,25 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz", "integrity": "sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.0.0", "regexpu-core": "^4.1.3" } }, + "@babel/polyfill": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.2.5.tgz", + "integrity": "sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==", + "requires": { + "core-js": "^2.5.7", + "regenerator-runtime": "^0.12.0" + } + }, "@babel/preset-env": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.2.3.tgz", "integrity": "sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -771,7 +720,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-transform-react-display-name": "^7.0.0", @@ -780,11 +728,18 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, + "@babel/runtime": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", + "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, "@babel/template": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.2.2", @@ -795,7 +750,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/generator": "^7.2.2", @@ -812,7 +766,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -823,7 +776,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.2.tgz", "integrity": "sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.10", @@ -861,6 +813,158 @@ "through2": "^2.0.3" } }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.0.0.tgz", + "integrity": "sha512-PDvHV2WhSGCSExp+eIMEKxYd1Q0SBvXLb4gAOXbdh0dswHFFgXWzxGjCmx5aln4qGrhkuN81khzYzR/44DYaMA==" + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-4.0.3.tgz", + "integrity": "sha512-fpG7AzzJxz1tc8ITYS1jCAt1cq4ydK2R+sx//BMTJgvOjfk91M5GiqFolP8aYTzLcum92IGNAVFS3zEcucOQEA==" + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-4.0.0.tgz", + "integrity": "sha512-nBGVl6LzXTdk1c6w3rMWcjq3mYGz+syWc5b3CdqAiEeY/nswYDoW/cnGUKKC8ofD6/LaG+G/IUnfv3jKoHz43A==" + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-4.0.0.tgz", + "integrity": "sha512-ejQqpTfORy6TT5w1x/2IQkscgfbtNFjitcFDu63GRz7qfhVTYhMdiJvJ1+Aw9hmv9bO4tXThGQDr1IF5lIvgew==" + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.0.0.tgz", + "integrity": "sha512-OE6GT9WRKWqd0Dk6NJ5TYXTF5OxAyn74+c/D+gTLbCXnK2A0luEXuwMbe5zR5Px4A/jow2OeEBboTENl4vtuQg==" + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-4.0.0.tgz", + "integrity": "sha512-QeDRGHXfjYEBTXxV0TsjWmepsL9Up5BOOlMFD557x2JrSiVGUn2myNxHIrHiVW0+nnWnaDcrkjg/jUvbJ5nKCg==" + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-4.0.0.tgz", + "integrity": "sha512-c6eE6ovs14k6dmHKoy26h7iRFhjWNnwYVrDWIPfouVm/gcLIeMw/ME4i91O5LEfaDHs6kTRCcVpbAVbNULZOtw==" + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-4.1.0.tgz", + "integrity": "sha512-uulxdx2p3nrM2BkrtADQHK8IhEzCxdUILfC/ddvFC8tlFWuKiA3ych8C6q0ulyQHq34/3hzz+3rmUbhWF9redg==" + }, + "@svgr/babel-preset": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-4.1.0.tgz", + "integrity": "sha512-Nat5aJ3VO3LE8KfMyIbd3sGWnaWPiFCeWIdEV+lalga0To/tpmzsnPDdnrR9fNYhvSSLJbwhU/lrLYt9wXY0ZQ==", + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^4.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^4.0.3", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^4.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^4.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "^4.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "^4.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "^4.0.0", + "@svgr/babel-plugin-transform-svg-component": "^4.1.0" + } + }, + "@svgr/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-4.1.0.tgz", + "integrity": "sha512-ahv3lvOKuUAcs0KbQ4Jr5fT5pGHhye4ew8jZVS4lw8IQdWrbG/o3rkpgxCPREBk7PShmEoGQpteeXVwp2yExuQ==", + "requires": { + "@svgr/plugin-jsx": "^4.1.0", + "camelcase": "^5.0.0", + "cosmiconfig": "^5.0.7" + }, + "dependencies": { + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + } + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-4.1.0.tgz", + "integrity": "sha512-tdkEZHmigYYiVhIEzycAMKN5aUSpddUnjr6v7bPwaNTFuSyqGUrpCg1JlIGi7PUaaJVHbn6whGQMGUpKOwT5nw==", + "requires": { + "@babel/types": "^7.1.6" + } + }, + "@svgr/plugin-jsx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.1.0.tgz", + "integrity": "sha512-xwu+9TGziuN7cu7p+vhCw2EJIfv8iDNMzn2dR0C7fBYc8q+SRtYTcg4Uyn8ZWh6DM+IZOlVrS02VEMT0FQzXSA==", + "requires": { + "@babel/core": "^7.1.6", + "@svgr/babel-preset": "^4.1.0", + "@svgr/hast-util-to-babel-ast": "^4.1.0", + "rehype-parse": "^6.0.0", + "unified": "^7.0.2", + "vfile": "^3.0.1" + } + }, + "@svgr/plugin-svgo": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-4.0.3.tgz", + "integrity": "sha512-MgL1CrlxvNe+1tQjPUc2bIJtsdJOIE5arbHlPgW+XVWGjMZTUcyNNP8R7/IjM2Iyrc98UJY+WYiiWHrinnY9ZQ==", + "requires": { + "cosmiconfig": "^5.0.7", + "merge-deep": "^3.0.2", + "svgo": "^1.1.1" + } + }, + "@svgr/webpack": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-4.1.0.tgz", + "integrity": "sha512-d09ehQWqLMywP/PT/5JvXwPskPK9QCXUjiSkAHehreB381qExXf5JFCBWhfEyNonRbkIneCeYM99w+Ud48YIQQ==", + "requires": { + "@babel/core": "^7.1.6", + "@babel/plugin-transform-react-constant-elements": "^7.0.0", + "@babel/preset-env": "^7.1.6", + "@babel/preset-react": "^7.0.0", + "@svgr/core": "^4.1.0", + "@svgr/plugin-jsx": "^4.1.0", + "@svgr/plugin-svgo": "^4.0.3", + "loader-utils": "^1.1.0" + } + }, + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + }, + "@types/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz", + "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==" + }, + "@types/unist": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.2.tgz", + "integrity": "sha512-iHI60IbyfQilNubmxsq4zqSjdynlmc2Q/QvH9kjzg9+CCYVVzq1O6tc7VBzSygIwnmOt07w80IG6HDQvjv3Liw==" + }, + "@types/vfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", + "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", + "requires": { + "@types/node": "*", + "@types/unist": "*", + "@types/vfile-message": "*" + } + }, + "@types/vfile-message": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-1.0.1.tgz", + "integrity": "sha512-mlGER3Aqmq7bqR1tTTIVHq8KSAFFRyGbrxuM8C/H82g6k7r2fS+IMEkIu3D7JHzG10NvPdR8DNx0jr0pwpp4dA==", + "requires": { + "@types/node": "*", + "@types/unist": "*" + } + }, "@webassemblyjs/ast": { "version": "1.7.11", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", @@ -1265,6 +1369,16 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -1452,6 +1566,32 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, + "babel-eslint": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", + "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, "babel-loader": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.4.tgz", @@ -1480,6 +1620,11 @@ "now-and-later": "^2.0.0" } }, + "bail": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", + "integrity": "sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1558,8 +1703,7 @@ "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" }, "binary-extensions": { "version": "1.11.0", @@ -1644,6 +1788,11 @@ "multicast-dns-service-types": "^1.1.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1761,7 +1910,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.6.tgz", "integrity": "sha512-kMGKs4BTzRWviZ8yru18xBpx+CyHG9eqgRbj9XbE3IMgtczf4aiA0Y1YCpVdvUieKGZ03kolSPXqTcscBCb9qw==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30000921", "electron-to-chromium": "^1.3.92", @@ -1885,6 +2033,21 @@ "unset-value": "^1.0.0" } }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + } + } + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -1924,14 +2087,18 @@ "caniuse-lite": { "version": "1.0.30000923", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000923.tgz", - "integrity": "sha512-j5ur7eeluOFjjPUkydtXP4KFAsmH3XaQNch5tvWSO+dLHYt5PE+VgJZLWtbVOodfWij6m6zas28T4gB/cLYq1w==", - "dev": true + "integrity": "sha512-j5ur7eeluOFjjPUkydtXP4KFAsmH3XaQNch5tvWSO+dLHYt5PE+VgJZLWtbVOodfWij6m6zas28T4gB/cLYq1w==" }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "ccount": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.3.tgz", + "integrity": "sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw==" + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -2110,6 +2277,16 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2152,6 +2329,11 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -2160,6 +2342,14 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz", + "integrity": "sha512-Cg90/fcK93n0ecgYTAz1jaA3zvnQ0ExlmKY1rdbyHqAx6BHxwoJc+J7HDu0iuQ7ixEs1qaa+WyQ6oeuBpYP1iA==", + "requires": { + "trim": "0.0.1" + } + }, "commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", @@ -2319,11 +2509,38 @@ "is-plain-object": "^2.0.1" } }, + "core-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz", + "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz", + "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -2466,6 +2683,22 @@ } } }, + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, "css-selector-tokenizer": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", @@ -2511,12 +2744,50 @@ } } }, + "css-tree": { + "version": "1.0.0-alpha.28", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz", + "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==", + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "css-url-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz", + "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=" + }, + "css-what": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz", + "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==" + }, "cssesc": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", "dev": true }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + } + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -2583,6 +2854,11 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -2783,6 +3059,30 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, "dom-walk": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", @@ -2795,6 +3095,20 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "duplexer": { "version": "0.1.1", "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -2838,8 +3152,7 @@ "electron-to-chromium": { "version": "1.3.95", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.95.tgz", - "integrity": "sha512-0JZEDKOQAE05EO/4rk3vLAE+PYFI9OLCVLAS4QAq1y+Bb2y1N6MyQJz62ynzHN/y0Ka/nO5jVJcahbCEdfiXLQ==", - "dev": true + "integrity": "sha512-0JZEDKOQAE05EO/4rk3vLAE+PYFI9OLCVLAS4QAq1y+Bb2y1N6MyQJz62ynzHN/y0Ka/nO5jVJcahbCEdfiXLQ==" }, "elliptic": { "version": "6.4.1", @@ -2859,8 +3172,7 @@ "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" }, "encodeurl": { "version": "1.0.2", @@ -2887,6 +3199,11 @@ "tapable": "^1.0.0" } }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -2908,7 +3225,6 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "dev": true, "requires": { "es-to-primitive": "^1.1.1", "function-bind": "^1.1.1", @@ -2921,7 +3237,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -3118,6 +3433,45 @@ } } }, + "eslint-loader": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.1.1.tgz", + "integrity": "sha512-1GrJFfSevQdYpoDzx8mEE2TDWsb/zmFuY09l6hURg1AeFIKQOvZ+vH0UPjzmd1CZIbfTV5HUkMeBmFiDBkgIsQ==", + "dev": true, + "requires": { + "loader-fs-cache": "^1.0.0", + "loader-utils": "^1.0.2", + "object-assign": "^4.0.1", + "object-hash": "^1.1.4", + "rimraf": "^2.6.1" + } + }, + "eslint-plugin-react": { + "version": "7.12.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz", + "integrity": "sha512-1puHJkXJY+oS1t467MjbqjvX53uQ05HXwjqDgdbGBqf5j9eeydI54G3KwiJmWciQ0HTBacIKw2jgwSBSH3yfgQ==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.0.1", + "object.fromentries": "^2.0.0", + "prop-types": "^15.6.2", + "resolve": "^1.9.0" + }, + "dependencies": { + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, "eslint-scope": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", @@ -3182,8 +3536,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -3903,9 +4256,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "optional": true, "requires": { "nan": "^2.9.2", @@ -3914,25 +4267,21 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "bundled": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "bundled": true }, "aproba": { "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true, "optional": true }, "are-we-there-yet": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "version": "1.1.5", + "bundled": true, "optional": true, "requires": { "delegates": "^1.0.0", @@ -3941,76 +4290,64 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "bundled": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "bundled": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "chownr": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "version": "1.1.1", + "bundled": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "bundled": true }, "concat-map": { "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "bundled": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "bundled": true, "optional": true }, "debug": { "version": "2.6.9", - "resolved": false, - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "bundled": true, "optional": true, "requires": { "ms": "2.0.0" } }, "deep-extend": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "version": "0.6.0", + "bundled": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "bundled": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "bundled": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "bundled": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -4018,14 +4355,12 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "bundled": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "bundled": true, "optional": true, "requires": { "aproba": "^1.0.3", @@ -4039,9 +4374,8 @@ } }, "glob": { - "version": "7.1.2", - "resolved": false, - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "version": "7.1.3", + "bundled": true, "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -4054,23 +4388,20 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "bundled": true, "optional": true }, "iconv-lite": { - "version": "0.4.21", - "resolved": false, - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "version": "0.4.24", + "bundled": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.1", - "resolved": false, - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "bundled": true, "optional": true, "requires": { "minimatch": "^3.0.4" @@ -4078,8 +4409,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, "optional": true, "requires": { "once": "^1.3.0", @@ -4088,55 +4418,47 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "bundled": true }, "ini": { "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "bundled": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "bundled": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "bundled": true }, "minipass": { - "version": "2.2.4", - "resolved": false, - "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "version": "2.3.5", + "bundled": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "version": "1.2.1", + "bundled": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -4144,22 +4466,19 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "bundled": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.0.0", - "resolved": false, - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "bundled": true, "optional": true }, "needle": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "version": "2.2.4", + "bundled": true, "optional": true, "requires": { "debug": "^2.1.2", @@ -4168,18 +4487,17 @@ } }, "node-pre-gyp": { - "version": "0.10.0", - "resolved": false, - "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "version": "0.10.3", + "bundled": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -4187,8 +4505,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "bundled": true, "optional": true, "requires": { "abbrev": "1", @@ -4196,15 +4513,13 @@ } }, "npm-bundled": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "version": "1.0.5", + "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", - "resolved": false, - "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "version": "1.2.0", + "bundled": true, "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -4213,8 +4528,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "bundled": true, "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -4225,39 +4539,33 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "bundled": true }, "object-assign": { "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "bundled": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "bundled": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "bundled": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "bundled": true, "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -4266,23 +4574,20 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "bundled": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "bundled": true, "optional": true }, "rc": { - "version": "1.2.7", - "resolved": false, - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "version": "1.2.8", + "bundled": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -4290,16 +4595,14 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "bundled": true, "optional": true } } }, "readable-stream": { "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -4312,53 +4615,45 @@ } }, "rimraf": { - "version": "2.6.2", - "resolved": false, - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.6.3", + "bundled": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": false, - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "bundled": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "bundled": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "bundled": true, "optional": true }, "semver": { - "version": "5.5.0", - "resolved": false, - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "version": "5.6.0", + "bundled": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4367,8 +4662,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -4376,57 +4670,50 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "bundled": true, "optional": true }, "tar": { - "version": "4.4.1", - "resolved": false, - "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "version": "4.4.8", + "bundled": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, "util-deprecate": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "bundled": true, "optional": true }, "wide-align": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "version": "1.1.3", + "bundled": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "bundled": true }, "yallist": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "version": "3.0.3", + "bundled": true } } }, @@ -4663,8 +4950,7 @@ "globals": { "version": "11.7.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", - "dev": true + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==" }, "globby": { "version": "6.1.0", @@ -5198,7 +5484,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5283,6 +5568,34 @@ "minimalistic-assert": "^1.0.1" } }, + "hast-util-from-parse5": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.0.tgz", + "integrity": "sha512-A7ev5OseS/J15214cvDdcI62uwovJO2PB60Xhnq7kaxvvQRFDEccuqbkrFXU03GPBGopdPqlpQBRqIcDS/Fjbg==", + "requires": { + "ccount": "^1.0.3", + "hastscript": "^5.0.0", + "property-information": "^5.0.0", + "web-namespaces": "^1.1.2", + "xtend": "^4.0.1" + } + }, + "hast-util-parse-selector": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz", + "integrity": "sha512-Xyh0v+nHmQvrOqop2Jqd8gOdyQtE8sIP9IQf7mlVDqp924W4w/8Liuguk2L2qei9hARnQSG2m+wAOCxM7npJVw==" + }, + "hastscript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.0.0.tgz", + "integrity": "sha512-xJtuJ8D42Xtq5yJrnDg/KAIxl2cXBXKoiIJwmWX9XMf8113qHTGl/Bf7jEsxmENJ4w6q4Tfl8s/Y6mEZo8x8qw==", + "requires": { + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.2.0", + "property-information": "^5.0.1", + "space-separated-tokens": "^1.0.0" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5436,6 +5749,30 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5623,7 +5960,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -5707,8 +6043,7 @@ "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" }, "is-data-descriptor": { "version": "0.1.4", @@ -5731,8 +6066,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { "version": "0.1.6", @@ -5751,6 +6085,11 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -5861,6 +6200,11 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -5895,7 +6239,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -5924,7 +6267,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, "requires": { "has-symbols": "^1.0.0" } @@ -5991,8 +6333,7 @@ "js-levenshtein": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz", - "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==", - "dev": true + "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==" }, "js-tokens": { "version": "4.0.0", @@ -6016,14 +6357,12 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-schema": { "version": "0.2.3", @@ -6064,7 +6403,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, "requires": { "minimist": "^1.2.0" } @@ -6100,6 +6438,15 @@ "verror": "1.10.0" } }, + "jsx-ast-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "dev": true, + "requires": { + "array-includes": "^3.0.3" + } + }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -6131,6 +6478,11 @@ "es6-weak-map": "^2.0.1" } }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -6199,6 +6551,38 @@ } } }, + "loader-fs-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz", + "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=", + "dev": true, + "requires": { + "find-cache-dir": "^0.1.1", + "mkdirp": "0.5.1" + }, + "dependencies": { + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "^1.0.0" + } + } + } + }, "loader-runner": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz", @@ -6209,7 +6593,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, "requires": { "big.js": "^3.1.3", "emojis-list": "^2.0.0", @@ -6219,8 +6602,7 @@ "json5": { "version": "0.5.1", "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" } } }, @@ -6247,6 +6629,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -6428,6 +6815,11 @@ "safe-buffer": "^5.1.2" } }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==" + }, "media-typer": { "version": "0.3.0", "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6494,6 +6886,72 @@ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, + "merge-deep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", + "integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", + "requires": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "dependencies": { + "clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", + "requires": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + } + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "requires": { + "is-buffer": "^1.0.2" + } + }, + "lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=" + } + } + } + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -6648,7 +7106,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", - "dev": true, "requires": { "for-in": "^0.1.3", "is-extendable": "^0.1.1" @@ -6657,8 +7114,7 @@ "for-in": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=" } } }, @@ -6694,8 +7150,7 @@ "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "multicast-dns": { "version": "6.2.3", @@ -6843,7 +7298,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.2.tgz", "integrity": "sha512-j1gEV/zX821yxdWp/1vBMN0pSUjuH9oGUdLCb4PfUko6ZW7KdRs3Z+QGGwDUhYtSpQvdVVyLd2V0YvLsmdg5jQ==", - "dev": true, "requires": { "semver": "^5.3.0" } @@ -6961,6 +7415,14 @@ "set-blocking": "~2.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -7004,6 +7466,12 @@ } } }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, "object-keys": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", @@ -7039,11 +7507,22 @@ "isobject": "^3.0.0" } }, + "object.fromentries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", + "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.11.0", + "function-bind": "^1.1.1", + "has": "^1.0.1" + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.5.1" @@ -7094,6 +7573,17 @@ "make-iterator": "^1.0.0" } }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -7337,6 +7827,11 @@ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", @@ -7619,8 +8114,7 @@ "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" }, "process": { "version": "0.11.10", @@ -7654,6 +8148,14 @@ "object-assign": "^4.1.1" } }, + "property-information": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.0.1.tgz", + "integrity": "sha512-nAtBDVeSwFM3Ot/YxT7s4NqZmqXI7lLzf46BThvotEtYf2uk2yH0ACYuWQkJ7gxKs49PPtKVY0UlDGkyN9aJlw==", + "requires": { + "xtend": "^4.0.1" + } + }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -7718,6 +8220,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -7853,11 +8360,69 @@ } } }, + "react-is": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz", + "integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==" + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "dev": true + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-redux": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz", + "integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==", + "requires": { + "@babel/runtime": "^7.2.0", + "hoist-non-react-statics": "^3.2.1", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.3" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "requires": { + "react-is": "^16.3.2" + } + } + } + }, + "react-transition-group": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz", + "integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, + "react-window-size-listener": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/react-window-size-listener/-/react-window-size-listener-1.2.3.tgz", + "integrity": "sha512-95lyZTMBBqH0xuhBEP0LshEKlHVF+VHQO7UojfDYmyMoO9jriTAY9Ktr5p9ZF4yR8QKzWZBAUdOEndY/JuMmwA==", + "requires": { + "lodash.debounce": "^3.1.1", + "prop-types": "^15.6.0", + "randomatic": ">=3.0.0" + }, + "dependencies": { + "lodash.debounce": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", + "integrity": "sha1-gSIRw3ipTMKdWqTjNGzwv846ffU=", + "requires": { + "lodash._getnative": "^3.0.0" + } + } + } }, "read-pkg": { "version": "1.1.0", @@ -7945,26 +8510,56 @@ "strip-indent": "^1.0.1" } }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-devtools-extension": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.7.tgz", + "integrity": "sha512-F2GlWMWxCTJGRjJ+GSZcGDcVAj6Pbf77FKb4C9S8eni5Eah6UBGNwxNj8K1MTtmItdZH1Wx+EvIifHN2KKcQrw==", + "dev": true + }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "^0.3.5" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" }, "regenerate-unicode-properties": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", - "dev": true, "requires": { "regenerate": "^1.4.0" } }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + }, "regenerator-transform": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", - "dev": true, "requires": { "private": "^0.1.6" } @@ -7996,7 +8591,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.4.0.tgz", "integrity": "sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA==", - "dev": true, "requires": { "regenerate": "^1.4.0", "regenerate-unicode-properties": "^7.0.0", @@ -8009,14 +8603,12 @@ "regjsgen": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", - "dev": true + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" }, "regjsparser": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "dev": true, "requires": { "jsesc": "~0.5.0" }, @@ -8024,11 +8616,20 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" } } }, + "rehype-parse": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-6.0.0.tgz", + "integrity": "sha512-V2OjMD0xcSt39G4uRdMTqDXXm6HwkUbLMDayYKA/d037j8/OtVSQ+tqKwYWOuyBeoCs/3clXRe30VUjeMDTBSA==", + "requires": { + "hast-util-from-parse5": "^5.0.0", + "parse5": "^5.0.0", + "xtend": "^4.0.1" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -8139,6 +8740,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", @@ -8623,6 +9229,11 @@ "semver": "^5.5.0" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "scheduler": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz", @@ -9084,6 +9695,14 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "space-separated-tokens": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz", + "integrity": "sha512-G3jprCEw+xFEs0ORweLmblJ3XLymGGr6hxZYTYZjIlvDti9vOBUjRQa1Rzjt012aRrocKstHwdNi+F7HguPsEA==", + "requires": { + "trim": "0.0.1" + } + }, "sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -9218,6 +9837,11 @@ "figgy-pudding": "^3.5.1" } }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -9404,6 +10028,32 @@ "es6-symbol": "^3.1.1" } }, + "svgo": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.1.1.tgz", + "integrity": "sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==", + "requires": { + "coa": "~2.0.1", + "colors": "~1.1.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "~0.1.0", + "css-tree": "1.0.0-alpha.28", + "css-url-regex": "^1.1.0", + "csso": "^3.5.0", + "js-yaml": "^3.12.0", + "mkdirp": "~0.5.1", + "object.values": "^1.0.4", + "sax": "~1.2.4", + "stable": "~0.1.6", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "table": { "version": "4.0.3", "resolved": "http://registry.npmjs.org/table/-/table-4.0.3.tgz", @@ -9701,8 +10351,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -9759,6 +10408,11 @@ "punycode": "^1.4.1" } }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", @@ -9767,8 +10421,12 @@ "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "trough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.3.tgz", + "integrity": "sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw==" }, "true-case-path": { "version": "1.0.3", @@ -9872,14 +10530,12 @@ "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" }, "unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, "requires": { "unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-property-aliases-ecmascript": "^1.0.4" @@ -9888,14 +10544,27 @@ "unicode-match-property-value-ecmascript": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz", - "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==", - "dev": true + "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==" }, "unicode-property-aliases-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz", - "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==", - "dev": true + "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==" + }, + "unified": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", + "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", + "requires": { + "@types/unist": "^2.0.0", + "@types/vfile": "^3.0.0", + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^3.0.0", + "x-is-string": "^0.1.0" + } }, "union-value": { "version": "1.0.0", @@ -9956,6 +10625,11 @@ "through2-filter": "^2.0.0" } }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -9968,6 +10642,11 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=" + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -10089,7 +10768,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, "requires": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" @@ -10150,6 +10828,32 @@ "extsprintf": "^1.2.0" } }, + "vfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", + "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", + "requires": { + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + }, "vinyl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", @@ -10238,6 +10942,11 @@ "minimalistic-assert": "^1.0.0" } }, + "web-namespaces": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.2.tgz", + "integrity": "sha512-II+n2ms4mPxK+RnIxRPOw3zwF2jRscdJIUE9BfkKHm4FYEg9+biIoTMnaZF5MpemE3T+VhMLrhbyD4ilkPCSbg==" + }, "webpack": { "version": "4.28.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.28.3.tgz", @@ -10881,6 +11590,11 @@ "mkdirp": "^0.5.1" } }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", diff --git a/package.json b/package.json index 6d744610c1a72832dfece7ac5bceab094a52e4ac..7ad0fcfa4597981cdb4b18bee2f73c21963125a2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "last 2 versions" ], "dependencies": { + "@babel/polyfill": "^7.2.5", + "@svgr/webpack": "^4.1.0", "del": "^3.0.0", "gulp": "^4.0.0", "gulp-babel": "^8.0.0", @@ -22,21 +24,33 @@ "gulp-touch-cmd": "0.0.1", "gulp-uglify": "^3.0.1", "node-sass-import-once": "^1.2.0", + "prop-types": "^15.6.2", "react": "^16.7.0", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "react-redux": "^6.0.0", + "react-transition-group": "^2.5.3", + "react-window-size-listener": "^1.2.3", + "redux": "^4.0.1", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.2.2", "@babel/plugin-proposal-class-properties": "^7.2.3", "@babel/preset-env": "^7.2.3", "@babel/preset-react": "^7.0.0", + "babel-eslint": "^10.0.1", "babel-loader": "^8.0.4", "css-loader": "^2.1.0", + "eslint-loader": "^2.1.1", + "eslint-plugin-react": "^7.12.4", "gulp-eslint": "^5.0.0", "gulp-sass-lint": "^1.4.0", "gulp-sourcemaps": "^2.6.4", "node-sass": "^4.11.0", "react-hot-loader": "^4.6.3", + "redux-devtools-extension": "^2.13.7", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "webpack": "^4.28.3", diff --git a/requirements.txt b/requirements.txt index 25b2bb484519f959a7b91e525edfb1713e75396c..fbc49f76778fdab1ff33dee15eeee2015d01854d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ django~=2.0.0 -djangorestframework==3.7.4 +djangorestframework==3.9.0 django-fsm==2.6.0 wagtail~=2.2.0 psycopg2==2.7.3.1