From 7127cdd7da8d70c81f9b9864d21826532bad7e29 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 24 Jul 2018 00:17:13 +0100
Subject: [PATCH 001/147] Added ModerationCollection and new ModerationRequest
(#32)
---
djangocms_moderation/admin.py | 65 +----
djangocms_moderation/apps.py | 1 -
djangocms_moderation/cms_toolbars.py | 232 ------------------
djangocms_moderation/emails.py | 10 +-
djangocms_moderation/forms.py | 19 +-
djangocms_moderation/helpers.py | 73 ++----
.../migrations/0001_initial.py | 136 ++++++----
.../migrations/0002_auto_20180420_0811.py | 32 ---
.../migrations/0003_auto_20180614_1516.py | 22 --
...pagemoderationrequestaction_is_archived.py | 20 --
.../migrations/0005_auto_20180618_2102.py | 73 ------
.../migrations/0006_auto_20180705_1034.py | 25 --
djangocms_moderation/models.py | 89 +++----
djangocms_moderation/monkeypatches.py | 60 -----
djangocms_moderation/views.py | 58 +----
tests/test_cms_toolbars.py | 157 ------------
tests/test_forms.py | 19 +-
tests/test_helpers.py | 56 -----
tests/test_models.py | 57 ++---
tests/test_moderation_flows.py | 38 +--
tests/test_views.py | 50 +---
tests/utils.py | 35 ++-
22 files changed, 241 insertions(+), 1086 deletions(-)
delete mode 100644 djangocms_moderation/cms_toolbars.py
delete mode 100644 djangocms_moderation/migrations/0002_auto_20180420_0811.py
delete mode 100644 djangocms_moderation/migrations/0003_auto_20180614_1516.py
delete mode 100644 djangocms_moderation/migrations/0004_pagemoderationrequestaction_is_archived.py
delete mode 100644 djangocms_moderation/migrations/0005_auto_20180618_2102.py
delete mode 100644 djangocms_moderation/migrations/0006_auto_20180705_1034.py
delete mode 100644 djangocms_moderation/monkeypatches.py
delete mode 100644 tests/test_cms_toolbars.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 775fe6cc..b0e4f490 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -1,14 +1,12 @@
from __future__ import unicode_literals
from django.conf.urls import url
-from django.contrib import admin, messages
+from django.contrib import admin
from django.core.urlresolvers import reverse
-from django.http import HttpResponseRedirect
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext, ugettext_lazy as _
from cms.admin.placeholderadmin import PlaceholderAdminMixin
-from cms.extensions import PageExtensionAdmin
from cms.models import Page
from adminsortable2.admin import SortableInlineAdminMixin
@@ -20,18 +18,12 @@
ACTION_RESUBMITTED,
)
from .forms import WorkflowStepInlineFormSet
-from .helpers import (
- get_active_moderation_request,
- get_form_submission_for_step,
- get_page_or_404,
- is_moderation_enabled,
-)
+from .helpers import get_form_submission_for_step
from .models import (
ConfirmationFormSubmission,
ConfirmationPage,
- PageModeration,
- PageModerationRequest,
- PageModerationRequestAction,
+ ModerationRequest,
+ ModerationRequestAction,
Role,
Workflow,
WorkflowStep,
@@ -47,13 +39,8 @@
from cms.admin.pageadmin import PageAdmin
-class PageModerationAdmin(PageExtensionAdmin):
- list_display = ['workflow', 'grant_on', 'enabled']
- fields = ['workflow', 'grant_on', 'enabled']
-
-
-class PageModerationRequestActionInline(admin.TabularInline):
- model = PageModerationRequestAction
+class ModerationRequestActionInline(admin.TabularInline):
+ model = ModerationRequestAction
fields = ['show_user', 'message', 'date_taken', 'form_submission']
readonly_fields = fields
verbose_name = _('Action')
@@ -89,11 +76,11 @@ def form_submission(self, obj):
form_submission.short_description = _('Form Submission')
-class PageModerationRequestAdmin(admin.ModelAdmin):
- inlines = [PageModerationRequestActionInline]
- list_display = ['id', 'page', 'language', 'workflow', 'show_status', 'date_sent']
- list_filter = ['language', 'workflow', 'id', 'compliance_number']
- fields = ['id', 'workflow', 'page', 'language', 'is_active', 'show_status', 'compliance_number']
+class ModerationRequestAdmin(admin.ModelAdmin):
+ inlines = [ModerationRequestActionInline]
+ list_display = ['id', 'language', 'collection', 'show_status', 'date_sent']
+ list_filter = ['language', 'collection', 'id', 'compliance_number']
+ fields = ['id', 'collection', 'workflow', 'language', 'is_active', 'show_status', 'compliance_number']
readonly_fields = fields
def has_add_permission(self, request):
@@ -176,11 +163,6 @@ def _url(regex, fn, name, **kwargs):
'approve_request',
action=ACTION_APPROVED,
),
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/select-workflow/$',
- views.select_new_moderation_request,
- 'select_new_moderation',
- ),
_url(
r'^([0-9]+)/([a-z\-]+)/moderation/comments/$',
views.moderation_comments,
@@ -189,28 +171,6 @@ def _url(regex, fn, name, **kwargs):
]
return url_patterns + super(ExtendedPageAdmin, self).get_urls()
- def publish_page(self, request, page_id, language):
- page = get_page_or_404(page_id, language)
-
- if not is_moderation_enabled(page):
- return super(ExtendedPageAdmin, self).publish_page(request, page_id, language)
-
- active_request = get_active_moderation_request(page, language)
-
- if active_request and active_request.is_approved():
- # The moderation request has been approved.
- # Let the user publish the page.
- return super(ExtendedPageAdmin, self).publish_page(request, page_id, language)
- elif active_request:
- message = ugettext('This page is currently undergoing moderation '
- 'and can\'t be published until all parties have approved it.')
- else:
- message = ugettext('You need to submit this page for moderation before publishing.')
-
- messages.warning(request, message)
- path = page.get_absolute_url(language, fallback=True)
- return HttpResponseRedirect(path)
-
class ConfirmationPageAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
view_on_site = True
@@ -267,8 +227,7 @@ def form_data(self, obj):
admin.site._registry[Page] = ExtendedPageAdmin(Page, admin.site)
-admin.site.register(PageModeration, PageModerationAdmin)
-admin.site.register(PageModerationRequest, PageModerationRequestAdmin)
+admin.site.register(ModerationRequest, ModerationRequestAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(Workflow, WorkflowAdmin)
diff --git a/djangocms_moderation/apps.py b/djangocms_moderation/apps.py
index f34a5099..1e6fedd2 100644
--- a/djangocms_moderation/apps.py
+++ b/djangocms_moderation/apps.py
@@ -9,6 +9,5 @@ class ModerationConfig(AppConfig):
verbose_name = _('django CMS Moderation')
def ready(self):
- import djangocms_moderation.monkeypatches
import djangocms_moderation.handlers
import djangocms_moderation.signals # noqa: F401
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
deleted file mode 100644
index e66606a6..00000000
--- a/djangocms_moderation/cms_toolbars.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.utils.functional import cached_property
-from django.utils.translation import ugettext_lazy as _
-
-from cms.api import get_page_draft
-from cms.toolbar.items import (
- Button,
- Dropdown,
- DropdownToggleButton,
- ModalButton,
-)
-from cms.toolbar_base import CMSToolbar
-from cms.toolbar_pool import toolbar_pool
-from cms.utils import page_permissions
-
-from .helpers import get_active_moderation_request, is_moderation_enabled
-from .models import PageModeration
-from .monkeypatches import set_current_language
-from .utils import get_admin_url
-
-
-from . import conf # isort:skip
-
-
-try:
- PageToolbar = toolbar_pool.toolbars['cms.cms_toolbars.PageToolbar']
-except: # noqa: F722
- from cms.cms_toolbars import PageToolbar
-
-
-class ExtendedPageToolbar(PageToolbar):
- class Media:
- js = ('djangocms_moderation/js/dist/bundle.moderation.min.js',)
- css = {
- 'all': ('djangocms_moderation/css/moderation.css',)
- }
-
- def __init__(self, *args, **kwargs):
- super(ExtendedPageToolbar, self).__init__(*args, **kwargs)
- set_current_language(self.current_lang)
-
- @cached_property
- def moderation_request(self):
- if not self.page:
- return None
- return get_active_moderation_request(self.page, self.current_lang)
-
- @cached_property
- def moderation_workflow(self):
- if not self.page:
- return None
- if self.moderation_request:
- return self.moderation_request.workflow
- return None
-
- @cached_property
- def is_moderation_enabled(self):
- if not self.page:
- return False
- return is_moderation_enabled(self.page)
-
- def get_cancel_moderation_button(self):
- cancel_request_url = get_admin_url(
- name='cms_moderation_cancel_request',
- language=self.current_lang,
- args=(self.page.pk, self.current_lang),
- )
- return ModalButton(name=_('Cancel request'), url=cancel_request_url)
-
- def user_can_publish(self):
- moderation_request = self.moderation_request
- user = self.request.user
-
- if moderation_request and moderation_request.user_can_take_moderation_action(user):
- return True
- return super(ExtendedPageToolbar, self).user_can_publish()
-
- def add_publish_button(self, classes=('cms-btn-action', 'cms-btn-publish', 'cms-btn-publish-active',)):
- """
- Lets work out what button should we display to the user.
- We need to consider the moderation, e.g. if it is enabled,
- we need to display moderation buttons instead of publish ones
- """
- page = self.page
-
- if not self.user_can_publish() or not self.is_moderation_enabled:
- # Page has no pending changes
- # OR user has no permission to publish
- # OR page has disabled moderation
- return super(ExtendedPageToolbar, self).add_publish_button(classes)
-
- moderation_request = self.moderation_request
-
- if moderation_request and moderation_request.is_approved():
- return super(ExtendedPageToolbar, self).add_publish_button(classes)
- elif moderation_request:
- # We have an active moderation request ongoing.
- user = self.request.user
- container = Dropdown(side=self.toolbar.RIGHT)
- container.add_primary_button(
- DropdownToggleButton(name=_('Moderation'))
- )
-
- if page.is_published(self.current_lang):
- container.buttons.append(
- Button(name=_('View differences'), url='#', extra_classes=('js-cms-moderation-view-diff',))
- )
-
- if moderation_request.user_can_resubmit(user):
- # This is a content author, able to edit and resubmit the
- # changes for another moderation cycle
- resubmit_request_url = get_admin_url(
- name='cms_moderation_resubmit_request',
- language=self.current_lang,
- args=(page.pk, self.current_lang),
- )
- container.buttons.append(
- ModalButton(name=_('Resubmit changes for moderation'), url=resubmit_request_url)
- )
- elif moderation_request.user_can_take_moderation_action(user):
- # Now we have a moderator, able to Approve or Reject changes
- approve_request_url = get_admin_url(
- name='cms_moderation_approve_request',
- language=self.current_lang,
- args=(page.pk, self.current_lang),
- )
- container.buttons.append(
- ModalButton(name=_('Approve changes'), url=approve_request_url)
- )
-
- reject_request_url = get_admin_url(
- name='cms_moderation_reject_request',
- language=self.current_lang,
- args=(page.pk, self.current_lang),
- )
- container.buttons.append(
- ModalButton(name=_('Send for rework'), url=reject_request_url)
- )
-
- # Anyone should be able to cancel the moderation request
- container.buttons.append(self.get_cancel_moderation_button())
-
- if moderation_request.user_can_view_comments(user):
- comment_url = get_admin_url(
- name='cms_moderation_comments',
- language=self.current_lang,
- args=(page.pk, self.current_lang),
- )
- container.buttons.append(
- ModalButton(name=_('View comments'), url=comment_url)
- )
-
- self.toolbar.add_item(container)
- else:
- if conf.ENABLE_WORKFLOW_OVERRIDE:
- view_name = 'cms_moderation_select_new_moderation'
- else:
- view_name = 'cms_moderation_new_request'
-
- new_request_url = get_admin_url(
- name=view_name,
- language=self.current_lang,
- args=(page.pk, self.current_lang),
- )
- self.toolbar.add_modal_button(
- name=_('Submit for moderation'),
- url=new_request_url,
- side=self.toolbar.RIGHT,
- )
-
- def get_publish_button(self, classes=None):
- if not self.is_moderation_enabled:
- return super(ExtendedPageToolbar, self).get_publish_button(classes)
-
- button = super(ExtendedPageToolbar, self).get_publish_button(['cms-btn-publish'])
- container = Dropdown(side=self.toolbar.RIGHT)
- container.add_primary_button(
- DropdownToggleButton(name=_('Moderation'))
- )
- container.buttons.extend(button.buttons)
- container.buttons.append(self.get_cancel_moderation_button())
- return container
-
-
-class PageModerationToolbar(CMSToolbar):
-
- def populate(self):
- """ Adds Moderation link to Page Toolbar Menu
- """
- # always use draft if we have a page
- page = get_page_draft(self.request.current_page)
-
- if page:
- can_change = page_permissions.user_can_change_page(self.request.user, page)
- else:
- can_change = False
-
- if not can_change:
- return
-
- page_menu = self.toolbar.get_menu('page')
-
- if not page_menu or page_menu.disabled:
- return
-
- try:
- extension = PageModeration.objects.get(extended_object_id=page.pk)
- except PageModeration.DoesNotExist:
- extension = None
-
- opts = PageModeration._meta
-
- url_args = []
-
- if extension:
- url_name = '{}_{}_{}'.format(opts.app_label, opts.model_name, 'change')
- url_args.append(extension.pk)
- else:
- url_name = '{}_{}_{}'.format(opts.app_label, opts.model_name, 'add')
-
- url = get_admin_url(url_name, self.current_lang, args=url_args)
-
- if not extension:
- url += '?extended_object=%s' % page.pk
- not_edit_mode = not self.toolbar.edit_mode_active
- page_menu.add_modal_item(_('Moderation'), url=url, disabled=not_edit_mode)
-
-
-toolbar_pool.toolbars['cms.cms_toolbars.PageToolbar'] = ExtendedPageToolbar
-toolbar_pool.register(PageModerationToolbar)
diff --git a/djangocms_moderation/emails.py b/djangocms_moderation/emails.py
index 4bdbf9bf..9c88cb55 100644
--- a/djangocms_moderation/emails.py
+++ b/djangocms_moderation/emails.py
@@ -31,9 +31,9 @@
def _send_email(request, action, recipients, subject, template):
- page = request.page
+ obj = request.content_object
edit_on = get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
- page_url = page.get_absolute_url(request.language) + '?' + edit_on
+ page_url = obj.get_absolute_url(request.language) + '?' + edit_on
author_name = request.get_first_action().get_by_user_name()
if action.to_user_id:
@@ -43,10 +43,10 @@ def _send_email(request, action, recipients, subject, template):
else:
moderator_name = ''
- site = page.node.site
- admin_url = reverse('admin:djangocms_moderation_pagemoderationrequest_change', args=(request.pk,))
+ site = obj.node.site
+ admin_url = reverse('admin:djangocms_moderation_moderationrequest_change', args=(request.pk,))
context = {
- 'page': page,
+ 'page': obj,
'page_url': get_absolute_url(page_url, site),
'author_name': author_name,
'by_user_name': action.get_by_user_name(),
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 35450311..f20f9068 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -8,8 +8,6 @@
from adminsortable2.admin import CustomInlineFormSet
from .constants import ACTION_CANCELLED, ACTION_REJECTED, ACTION_RESUBMITTED
-from .helpers import get_page_moderation_workflow
-from .models import Workflow
class WorkflowStepInlineFormSet(CustomInlineFormSet):
@@ -75,7 +73,7 @@ def configure_moderator_field(self):
def save(self):
self.workflow.submit_new_request(
- page=self.page,
+ obj=self.page,
by_user=self.user,
to_user=self.cleaned_data.get('moderator'),
language=self.language,
@@ -118,18 +116,3 @@ def save(self):
to_user=self.cleaned_data.get('moderator'),
message=self.cleaned_data['message'],
)
-
-
-class SelectModerationForm(forms.Form):
- required_css_class = 'required'
-
- workflow = forms.ModelChoiceField(
- label=_('workflow to trigger'),
- queryset=Workflow.objects.all(),
- required=True,
- )
-
- def __init__(self, *args, **kwargs):
- page = kwargs.pop('page')
- super(SelectModerationForm, self).__init__(*args, **kwargs)
- self.fields['workflow'].initial = get_page_moderation_workflow(page)
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index 7808d0d7..ecaf6855 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -1,16 +1,6 @@
-from django.shortcuts import get_object_or_404
+from django.contrib.contenttypes.models import ContentType
-from cms.models import Page
-
-from .models import (
- ConfirmationFormSubmission,
- PageModeration,
- PageModerationRequest,
- Workflow,
-)
-
-
-from . import conf # isort:skip
+from .models import ConfirmationFormSubmission, ModerationRequest, Workflow
def get_default_workflow():
@@ -21,62 +11,39 @@ def get_default_workflow():
return workflow
-def get_page_moderation_settings(page):
- moderation = PageModeration.objects.for_page(page)
- return moderation
-
-
-def get_page_moderation_workflow(page):
- moderation = get_page_moderation_settings(page)
-
- if moderation:
- workflow = moderation.workflow
- else:
- workflow = get_default_workflow()
- return workflow
-
-
-def get_workflow_or_none(pk):
- try:
- return Workflow.objects.get(pk=pk)
- except Workflow.DoesNotExist:
- return None
+def get_moderation_workflow():
+ # TODO for now just return default, this would need to depend on the collection
+ # Might be as well not needed in 4.x, leaving it here for now
+ return get_default_workflow()
-def get_active_moderation_request(page, language):
+def get_active_moderation_request(obj, language):
+ content_type = ContentType.objects.get_for_model(obj)
try:
- return PageModerationRequest.objects.get(
- page=page,
+ return ModerationRequest.objects.get(
+ content_type=content_type,
+ object_id=obj.pk,
language=language,
is_active=True,
)
- except PageModerationRequest.DoesNotExist:
+ except ModerationRequest.DoesNotExist:
return None
-def get_page_or_404(page_id, language):
- return get_object_or_404(
- Page,
- pk=page_id,
+def get_page_or_404(obj_id, language):
+ """
+ TODO is this needed in 4.x?
+ """
+ content_type = ContentType.objects.get(app_label="cms", model="page") # how do we get this
+
+ return content_type.get_object_for_this_type(
+ pk=obj_id,
is_page_type=False,
publisher_is_draft=True,
title_set__language=language,
)
-def is_moderation_enabled(page):
- page_moderation_extension = get_page_moderation_settings(page)
-
- try:
- is_enabled = page_moderation_extension.enabled
- except AttributeError:
- is_enabled = True
-
- if conf.ENABLE_WORKFLOW_OVERRIDE:
- return is_enabled and Workflow.objects.exists()
- return is_enabled and bool(get_page_moderation_workflow(page))
-
-
def get_form_submission_for_step(active_request, current_step):
lookup = (
ConfirmationFormSubmission
diff --git a/djangocms_moderation/migrations/0001_initial.py b/djangocms_moderation/migrations/0001_initial.py
index ac5a6225..d8cde7ef 100644
--- a/djangocms_moderation/migrations/0001_initial.py
+++ b/djangocms_moderation/migrations/0001_initial.py
@@ -1,39 +1,74 @@
# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-07-17 15:49
from __future__ import unicode_literals
-from django.db import migrations, models
+import cms.models.fields
from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+from djangocms_moderation import conf
+from djangocms_moderation.constants import ACTION_CHOICES
class Migration(migrations.Migration):
+ initial = True
+
dependencies = [
- ('cms', '0016_auto_20160608_1535'),
- ('auth', '0006_require_contenttypes_0002'),
+ ('auth', '0008_alter_user_username_max_length'),
+ ('cms', '0020_old_tree_cleanup'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
- name='PageModeration',
+ name='ConfirmationFormSubmission',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('grant_on', models.IntegerField(default=5, verbose_name='grant on', choices=[(1, 'Current page'), (2, 'Page children (immediate)'), (3, 'Page and children (immediate)'), (4, 'Page descendants'), (5, 'Page and descendants')])),
- ('extended_object', models.OneToOneField(editable=False, to='cms.Page')),
- ('public_extension', models.OneToOneField(related_name='draft_extension', null=True, editable=False, to='djangocms_moderation.PageModeration')),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('data', models.TextField(blank=True, editable=False)),
+ ('submitted_at', models.DateTimeField(auto_now_add=True)),
+ ('by_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='by user')),
],
options={
- 'abstract': False,
+ 'verbose_name': 'Confirmation Form Submission',
+ 'verbose_name_plural': 'Confirmation Form Submissions',
},
),
migrations.CreateModel(
- name='PageModerationRequest',
+ name='ConfirmationPage',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('language', models.CharField(max_length=5, verbose_name='language', choices=settings.LANGUAGES)),
- ('is_active', models.BooleanField(default=False, db_index=True)),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50, verbose_name='name')),
+ ('content_type', models.CharField(choices=[('plain', 'Plain'), ('form', 'Form')], default='form', max_length=50, verbose_name='Content Type')),
+ ('template', models.CharField(choices=conf.CONFIRMATION_PAGE_TEMPLATES, default=conf.DEFAULT_CONFIRMATION_PAGE_TEMPLATE, max_length=100, verbose_name='Template')),
+ ('content', cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, slotname='confirmation_content', to='cms.Placeholder')),
+ ],
+ options={
+ 'verbose_name': 'Confirmation Page',
+ 'verbose_name_plural': 'Confirmation Pages',
+ },
+ ),
+ migrations.CreateModel(
+ name='ModerationCollection',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=128, verbose_name='name')),
+ ('is_locked', models.BooleanField(default=False, verbose_name='is locked')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ModerationRequest',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('object_id', models.PositiveIntegerField()),
+ ('language', models.CharField(choices=settings.LANGUAGES, max_length=5, verbose_name='language')),
+ ('is_active', models.BooleanField(db_index=True, default=False)),
('date_sent', models.DateTimeField(auto_now_add=True, verbose_name='date sent')),
- ('page', models.ForeignKey(verbose_name='page', to='cms.Page')),
+ ('compliance_number', models.CharField(blank=True, editable=False, max_length=32, null=True, unique=True, verbose_name='compliance number')),
+ ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_requests', to='djangocms_moderation.ModerationCollection')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'verbose_name': 'Request',
@@ -41,28 +76,30 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
- name='PageModerationRequestAction',
+ name='ModerationRequestAction',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('action', models.CharField(max_length=30, verbose_name='status', choices=[('start', 'Started'), ('rejected', 'Rejected'), ('approved', 'Approved'), ('cancelled', 'Cancelled'), ('finished', 'Finished')])),
- ('message', models.TextField(verbose_name='message', blank=True)),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('action', models.CharField(choices=ACTION_CHOICES, max_length=30, verbose_name='status')),
+ ('message', models.TextField(blank=True, verbose_name='message')),
('date_taken', models.DateTimeField(auto_now_add=True, verbose_name='date taken')),
- ('by_user', models.ForeignKey(related_name='+', verbose_name='by user', to=settings.AUTH_USER_MODEL)),
- ('request', models.ForeignKey(related_name='actions', verbose_name='request', to='djangocms_moderation.PageModerationRequest')),
+ ('is_archived', models.BooleanField(default=False)),
+ ('by_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='by user')),
+ ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to='djangocms_moderation.ModerationRequest', verbose_name='request')),
],
options={
- 'ordering': ('date_taken',),
'verbose_name': 'Action',
'verbose_name_plural': 'Actions',
+ 'ordering': ('date_taken',),
},
),
migrations.CreateModel(
name='Role',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120, verbose_name='name')),
- ('group', models.ForeignKey(verbose_name='group', blank=True, to='auth.Group', null=True)),
- ('user', models.ForeignKey(verbose_name='user', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+ ('confirmation_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.ConfirmationPage', verbose_name='confirmation page')),
+ ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group', verbose_name='group')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'Role',
@@ -72,9 +109,12 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Workflow',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('name', models.CharField(unique=True, max_length=120, verbose_name='name')),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=120, unique=True, verbose_name='name')),
('is_default', models.BooleanField(default=False, verbose_name='is default')),
+ ('identifier', models.CharField(blank=True, default='', help_text="Identifier is a 'free' field you could use for internal purposes. For example, it could be used as a workflow specific prefix of a compliance number", max_length=128, verbose_name='identifier')),
+ ('requires_compliance_number', models.BooleanField(default=False, help_text='Does the Compliance number need to be generated before the moderation request is approved? Please select the compliance number backend below', verbose_name='requires compliance number?')),
+ ('compliance_number_backend', models.CharField(choices=conf.COMPLIANCE_NUMBER_BACKENDS, default=conf.DEFAULT_COMPLIANCE_NUMBER_BACKEND, max_length=255, verbose_name='compliance number backend')),
],
options={
'verbose_name': 'Workflow',
@@ -84,45 +124,59 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='WorkflowStep',
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_required', models.BooleanField(default=True, verbose_name='is mandatory')),
('order', models.PositiveIntegerField()),
- ('role', models.ForeignKey(related_name='+', verbose_name='role', to='djangocms_moderation.Role')),
- ('workflow', models.ForeignKey(related_name='steps', verbose_name='workflow', to='djangocms_moderation.Workflow')),
+ ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.Role', verbose_name='role')),
+ ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='djangocms_moderation.Workflow', verbose_name='workflow')),
],
options={
- 'ordering': ('order',),
'verbose_name': 'Step',
'verbose_name_plural': 'Steps',
+ 'ordering': ('order',),
},
),
migrations.AddField(
- model_name='pagemoderationrequestaction',
+ model_name='moderationrequestaction',
name='step_approved',
- field=models.ForeignKey(verbose_name='step approved', blank=True, to='djangocms_moderation.WorkflowStep', null=True),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.WorkflowStep', verbose_name='step approved'),
),
migrations.AddField(
- model_name='pagemoderationrequestaction',
+ model_name='moderationrequestaction',
name='to_role',
- field=models.ForeignKey(related_name='+', verbose_name='to role', blank=True, to='djangocms_moderation.Role', null=True),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.Role', verbose_name='to role'),
),
migrations.AddField(
- model_name='pagemoderationrequestaction',
+ model_name='moderationrequestaction',
name='to_user',
- field=models.ForeignKey(related_name='+', verbose_name='to user', blank=True, to=settings.AUTH_USER_MODEL, null=True),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='to user'),
),
migrations.AddField(
- model_name='pagemoderationrequest',
+ model_name='moderationcollection',
name='workflow',
- field=models.ForeignKey(related_name='requests', verbose_name='workflow', to='djangocms_moderation.Workflow'),
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_collections', to='djangocms_moderation.Workflow', verbose_name='workflow'),
),
migrations.AddField(
- model_name='pagemoderation',
- name='workflow',
- field=models.ForeignKey(related_name='+', verbose_name='workflow', to='djangocms_moderation.Workflow'),
+ model_name='confirmationformsubmission',
+ name='confirmation_page',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.ConfirmationPage', verbose_name='confirmation page'),
+ ),
+ migrations.AddField(
+ model_name='confirmationformsubmission',
+ name='for_step',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.WorkflowStep', verbose_name='for step'),
+ ),
+ migrations.AddField(
+ model_name='confirmationformsubmission',
+ name='request',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_submissions', to='djangocms_moderation.ModerationRequest', verbose_name='request'),
),
migrations.AlterUniqueTogether(
name='workflowstep',
unique_together=set([('role', 'workflow')]),
),
+ migrations.AlterUniqueTogether(
+ name='confirmationformsubmission',
+ unique_together=set([('request', 'for_step')]),
+ ),
]
diff --git a/djangocms_moderation/migrations/0002_auto_20180420_0811.py b/djangocms_moderation/migrations/0002_auto_20180420_0811.py
deleted file mode 100644
index 2f56e79f..00000000
--- a/djangocms_moderation/migrations/0002_auto_20180420_0811.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.11 on 2018-04-20 07:11
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-from djangocms_moderation import conf
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('djangocms_moderation', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='pagemoderation',
- name='enabled',
- field=models.BooleanField(default=True, verbose_name='enable moderation for page'),
- ),
- migrations.AddField(
- model_name='pagemoderationrequest',
- name='compliance_number',
- field=models.CharField(blank=True, max_length=32, null=True, unique=True, editable=False),
- ),
- migrations.AddField(
- model_name='workflow',
- name='compliance_number_backend',
- field=models.CharField(choices=conf.COMPLIANCE_NUMBER_BACKENDS, default=conf.DEFAULT_COMPLIANCE_NUMBER_BACKEND, max_length=255),
- ),
- ]
diff --git a/djangocms_moderation/migrations/0003_auto_20180614_1516.py b/djangocms_moderation/migrations/0003_auto_20180614_1516.py
deleted file mode 100644
index 4872493c..00000000
--- a/djangocms_moderation/migrations/0003_auto_20180614_1516.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-06-14 14:16
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-from djangocms_moderation.constants import ACTION_CHOICES
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('djangocms_moderation', '0002_auto_20180420_0811'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='pagemoderationrequestaction',
- name='action',
- field=models.CharField(choices=ACTION_CHOICES, max_length=30, verbose_name='status'),
- ),
- ]
diff --git a/djangocms_moderation/migrations/0004_pagemoderationrequestaction_is_archived.py b/djangocms_moderation/migrations/0004_pagemoderationrequestaction_is_archived.py
deleted file mode 100644
index ee3560c1..00000000
--- a/djangocms_moderation/migrations/0004_pagemoderationrequestaction_is_archived.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-06-14 14:19
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('djangocms_moderation', '0003_auto_20180614_1516'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='pagemoderationrequestaction',
- name='is_archived',
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/djangocms_moderation/migrations/0005_auto_20180618_2102.py b/djangocms_moderation/migrations/0005_auto_20180618_2102.py
deleted file mode 100644
index 4abd7c7b..00000000
--- a/djangocms_moderation/migrations/0005_auto_20180618_2102.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-06-18 20:02
-from __future__ import unicode_literals
-
-import cms.models.fields
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-from djangocms_moderation import conf
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('cms', '0020_old_tree_cleanup'),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('djangocms_moderation', '0004_pagemoderationrequestaction_is_archived'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='ConfirmationFormSubmission',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('data', models.TextField(blank=True, editable=False)),
- ('submitted_at', models.DateTimeField(auto_now_add=True)),
- ('by_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='by user')),
- ],
- options={
- 'verbose_name': 'Confirmation Form Submission',
- 'verbose_name_plural': 'Confirmation Form Submissions',
- },
- ),
- migrations.CreateModel(
- name='ConfirmationPage',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=50, verbose_name='name')),
- ('content_type', models.CharField(choices=[('plain', 'Plain'), ('form', 'Form')], default='form', max_length=50, verbose_name='Content Type')),
- ('template', models.CharField(choices=conf.CONFIRMATION_PAGE_TEMPLATES, default=conf.DEFAULT_CONFIRMATION_PAGE_TEMPLATE, max_length=100, verbose_name='Template')),
- ('content', cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, slotname='confirmation_content', to='cms.Placeholder')),
- ],
- options={
- 'verbose_name': 'Confirmation Page',
- 'verbose_name_plural': 'Confirmation Pages',
- },
- ),
- migrations.AddField(
- model_name='confirmationformsubmission',
- name='confirmation_page',
- field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.ConfirmationPage', verbose_name='confirmation page'),
- ),
- migrations.AddField(
- model_name='confirmationformsubmission',
- name='for_step',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.WorkflowStep', verbose_name='for step'),
- ),
- migrations.AddField(
- model_name='confirmationformsubmission',
- name='request',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_submissions', to='djangocms_moderation.PageModerationRequest', verbose_name='request'),
- ),
- migrations.AddField(
- model_name='role',
- name='confirmation_page',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.ConfirmationPage', verbose_name='confirmation page'),
- ),
- migrations.AlterUniqueTogether(
- name='confirmationformsubmission',
- unique_together=set([('request', 'for_step')]),
- ),
- ]
diff --git a/djangocms_moderation/migrations/0006_auto_20180705_1034.py b/djangocms_moderation/migrations/0006_auto_20180705_1034.py
deleted file mode 100644
index ae8fb816..00000000
--- a/djangocms_moderation/migrations/0006_auto_20180705_1034.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-07-05 09:34
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('djangocms_moderation', '0005_auto_20180618_2102'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='workflow',
- name='identifier',
- field=models.CharField(blank=True, default='', help_text="Identifier is a 'free' field you could use for internal purposes. For example, it could be used as a workflow specific prefix of a compliance number", max_length=128),
- ),
- migrations.AddField(
- model_name='workflow',
- name='requires_compliance_number',
- field=models.BooleanField(default=False, help_text='Does the Compliance number need to be generated before the moderation request is approved? Please select the compliance number backend below'),
- ),
- ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 82f3e2db..5731a619 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -5,6 +5,8 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models, transaction
@@ -12,12 +14,9 @@
from django.utils.functional import cached_property
from django.utils.translation import ugettext, ugettext_lazy as _
-from cms.extensions import PageExtension
-from cms.extensions.extension_pool import extension_pool
from cms.models.fields import PlaceholderField
from .emails import notify_request_author, notify_requested_moderator
-from .managers import PageModerationManager
from .utils import generate_compliance_number
@@ -192,7 +191,7 @@ def get_active_request(self, page, language):
try:
active_request = lookup.get()
- except PageModerationRequest.DoesNotExist:
+ except ModerationRequest.DoesNotExist:
active_request = None
return active_request
@@ -201,9 +200,9 @@ def has_active_request(self, page, language):
return lookup.exists()
@transaction.atomic
- def submit_new_request(self, by_user, page, language, message='', to_user=None):
+ def submit_new_request(self, by_user, obj, language, message='', to_user=None):
request = self.requests.create(
- page=page,
+ content_object=obj,
language=language,
is_active=True,
workflow=self,
@@ -266,64 +265,35 @@ def get_next_required(self):
return self.get_next(cache=False, is_required=True)
-@python_2_unicode_compatible
-class PageModeration(PageExtension):
- ACCESS_CHOICES = (
- (constants.ACCESS_PAGE, _('Current page')),
- (constants.ACCESS_CHILDREN, _('Page children (immediate)')),
- (constants.ACCESS_PAGE_AND_CHILDREN, _('Page and children (immediate)')),
- (constants.ACCESS_DESCENDANTS, _('Page descendants')),
- (constants.ACCESS_PAGE_AND_DESCENDANTS, _('Page and descendants')),
- )
-
+class ModerationCollection(models.Model):
+ name = models.CharField(verbose_name=_('name'), max_length=128)
workflow = models.ForeignKey(
to=Workflow,
verbose_name=_('workflow'),
- related_name='+',
- )
- grant_on = models.IntegerField(
- verbose_name=_('grant on'),
- choices=ACCESS_CHOICES,
- default=constants.ACCESS_PAGE_AND_DESCENDANTS,
+ related_name='moderation_collections',
)
- enabled = models.BooleanField(
- verbose_name=_('enable moderation for page'),
- default=True,
- )
-
- objects = PageModerationManager()
-
- def __str__(self):
- return self.extended_object.get_page_title()
-
- @cached_property
- def page(self):
- return self.get_page()
-
- def copy_relations(self, oldinstance, language):
- self.workflow_id = oldinstance.workflow_id
+ # TODO: proper implementations and handlers coming later for is_locked
+ is_locked = models.BooleanField(verbose_name=_('is locked'), default=False)
@python_2_unicode_compatible
-class PageModerationRequest(models.Model):
- page = models.ForeignKey(
- to='cms.Page',
- verbose_name=_('page'),
- limit_choices_to={
- 'is_page_type': False,
- 'publisher_is_draft': True,
- },
+class ModerationRequest(models.Model):
+ collection = models.ForeignKey(
+ to=ModerationCollection,
+ related_name='moderation_requests',
+ on_delete=models.CASCADE
+ )
+ content_type = models.ForeignKey(
+ ContentType,
+ on_delete=models.CASCADE,
)
+ object_id = models.PositiveIntegerField()
+ content_object = GenericForeignKey('content_type', 'object_id')
language = models.CharField(
verbose_name=_('language'),
max_length=5,
choices=settings.LANGUAGES,
)
- workflow = models.ForeignKey(
- to=Workflow,
- verbose_name=_('workflow'),
- related_name='requests',
- )
is_active = models.BooleanField(
default=False,
db_index=True,
@@ -348,7 +318,7 @@ class Meta:
def __str__(self):
return "{} {}".format(
self.pk,
- self.page.get_page_title(self.language)
+ self.content_object.pk
)
@cached_property
@@ -358,6 +328,10 @@ def author(self):
"""
return self.get_first_action().by_user
+ @cached_property
+ def workflow(self):
+ return self.collection.workflow
+
def has_pending_step(self):
return self.get_pending_steps().exists()
@@ -505,7 +479,7 @@ def set_compliance_number(self):
@python_2_unicode_compatible
-class PageModerationRequestAction(models.Model):
+class ModerationRequestAction(models.Model):
action = models.CharField(
verbose_name=_('status'),
max_length=30,
@@ -549,7 +523,7 @@ class PageModerationRequestAction(models.Model):
blank=True,
)
request = models.ForeignKey(
- to=PageModerationRequest,
+ to=ModerationRequest,
verbose_name=_('request'),
related_name='actions',
)
@@ -603,12 +577,12 @@ def save(self, **kwargs):
if next_step:
self.to_role_id = next_step.role_id
- super(PageModerationRequestAction, self).save(**kwargs)
+ super(ModerationRequestAction, self).save(**kwargs)
class ConfirmationFormSubmission(models.Model):
request = models.ForeignKey(
- to=PageModerationRequest,
+ to=ModerationRequest,
verbose_name=_('request'),
related_name='form_submissions',
on_delete=models.CASCADE,
@@ -651,6 +625,3 @@ def get_by_user_name(self):
def get_form_data(self):
return json.loads(self.data)
-
-
-extension_pool.register(PageModeration)
diff --git a/djangocms_moderation/monkeypatches.py b/djangocms_moderation/monkeypatches.py
deleted file mode 100644
index 4f03f6b7..00000000
--- a/djangocms_moderation/monkeypatches.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from functools import wraps
-from threading import local
-
-from django.utils.decorators import available_attrs
-from django.utils.translation import get_language
-
-from cms.utils import page_permissions
-
-from .helpers import get_active_moderation_request
-
-
-# thread local support
-_thread_locals = local()
-
-
-def set_current_language(user):
- _thread_locals.request_language = user
-
-
-def get_request_language():
- return getattr(_thread_locals, 'request_language', get_language())
-
-
-def user_can_change_page(func):
- @wraps(func, assigned=available_attrs(func))
- def wrapper(user, page, site=None):
- can_change = func(user, page, site=site)
-
- if can_change:
- active_request = get_active_moderation_request(page, get_request_language())
-
- # If there is an active moderation request, the only user
- # who can edit the content is the original content author after
- # his moderation request has been rejected by any of the moderators.
- # The reason for this that content author should be able to
- # resubmit the changes for another review as a part of the same
- # moderation request.
- if active_request and not active_request.user_can_resubmit(user):
- return False
- return can_change
- return wrapper
-
-
-def user_can_view_page_draft(func):
- @wraps(func, assigned=available_attrs(func))
- def wrapper(user, page, site=None):
- can_view_page_draft = func(user, page, site=site)
-
- # get active request for page
- active_request = get_active_moderation_request(page, get_request_language())
-
- # check if user is part of the active request, if yes, return True
- if active_request and active_request.user_can_moderate(user):
- return True
- return can_view_page_draft
- return wrapper
-
-
-page_permissions.user_can_change_page = user_can_change_page(page_permissions.user_can_change_page)
-page_permissions.user_can_view_page_draft = user_can_view_page_draft(page_permissions.user_can_view_page_draft)
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 967ed1ed..92568a9a 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -13,21 +13,16 @@
from cms.utils.urlutils import add_url_parameters
-from .forms import (
- ModerationRequestForm,
- SelectModerationForm,
- UpdateModerationRequestForm,
-)
+from .forms import ModerationRequestForm, UpdateModerationRequestForm
from .helpers import (
get_active_moderation_request,
- get_page_moderation_workflow,
+ get_moderation_workflow,
get_page_or_404,
- get_workflow_or_none,
)
from .models import (
ConfirmationFormSubmission,
ConfirmationPage,
- PageModerationRequest,
+ ModerationRequest,
)
from .utils import get_admin_url
@@ -93,10 +88,8 @@ def dispatch(self, request, *args, **kwargs):
elif self.action != constants.ACTION_STARTED:
# All except for the new request endpoint require an active moderation request
return HttpResponseBadRequest('Page does not have an active moderation request.')
- elif request.GET.get('workflow'):
- self.workflow = get_workflow_or_none(request.GET.get('workflow'))
else:
- self.workflow = get_page_moderation_workflow(self.page)
+ self.workflow = get_moderation_workflow()
if not self.workflow:
return HttpResponseBadRequest('No moderation workflow exists for page.')
@@ -119,7 +112,7 @@ def get_form_kwargs(self):
return kwargs
def get_context_data(self, **kwargs):
- opts = PageModerationRequest._meta
+ opts = ModerationRequest._meta
form_submission_opts = ConfirmationFormSubmission._meta
if self.active_request:
@@ -178,47 +171,6 @@ def get_context_data(self, **kwargs):
)
-class SelectModerationView(FormView):
-
- form_class = SelectModerationForm
- template_name = 'djangocms_moderation/select_workflow_form.html'
-
- def dispatch(self, request, *args, **kwargs):
- self.page_id = args[0]
- self.current_lang = args[1]
- return super(SelectModerationView, self).dispatch(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super(SelectModerationView, self).get_context_data(**kwargs)
- context.update({
- 'has_change_permission': True,
- 'root_path': reverse('admin:index'),
- 'adminform': context['form'],
- 'is_popup': True,
- })
- return context
-
- def get_form_kwargs(self):
- kwargs = super(SelectModerationView, self).get_form_kwargs()
- kwargs['page'] = get_page_or_404(self.page_id, self.current_lang)
- return kwargs
-
- def form_valid(self, form):
- selected_workflow = form.cleaned_data['workflow']
- redirect_url = add_url_parameters(
- get_admin_url(
- name='cms_moderation_new_request',
- language=self.current_lang,
- args=(self.page_id, self.current_lang),
- ),
- workflow=selected_workflow.pk
- )
- return HttpResponseRedirect(redirect_url)
-
-
-select_new_moderation_request = SelectModerationView.as_view()
-
-
class ModerationCommentsView(ListView):
template_name = 'djangocms_moderation/comment_list.html'
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
deleted file mode 100644
index 3ba13db8..00000000
--- a/tests/test_cms_toolbars.py
+++ /dev/null
@@ -1,157 +0,0 @@
-from mock import patch
-
-from django.test.client import RequestFactory
-from django.utils.encoding import force_text
-
-from cms.api import create_page, publish_page
-from cms.constants import PUBLISHER_STATE_DIRTY
-from cms.middleware.toolbar import ToolbarMiddleware
-from cms.toolbar.items import ButtonList, Dropdown, ModalItem
-from cms.utils.conf import get_cms_setting
-
-from djangocms_moderation.models import PageModeration
-from djangocms_moderation.utils import get_admin_url
-
-from .utils import BaseViewTestCase
-
-
-class BaseToolbarTest(BaseViewTestCase):
-
- def setup_toolbar(self, page, user, is_edit_mode=True):
- page.set_publisher_state('en', state=PUBLISHER_STATE_DIRTY) # make page dirty
-
- if is_edit_mode:
- edit_mode = get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
- else:
- edit_mode = get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF')
-
- request = RequestFactory().get('{}?{}'.format(page.get_absolute_url('en'), edit_mode))
- request.current_page = page
- request.user = user
- request.session = self.client.session
- ToolbarMiddleware().process_request(request)
- self.toolbar = request.toolbar
- self.toolbar.populate()
- self.toolbar.post_template_populate()
- self.toolbar_left_items = self.toolbar.get_left_items()
- self.toolbar_right_items = self.toolbar.get_right_items()
-
-
-class ExtendedPageToolbarTest(BaseToolbarTest):
-
- def test_show_publish_button_if_moderation_disabled_for_page(self):
- self.wf1.is_default = False
- self.wf1.save() # making all workflows not default, therefore by design moderation is disabled
- new_page = create_page(title='New Page', template='page.html', language='en', published=True,)
- self.setup_toolbar(new_page, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, ButtonList)], [])
- self.assertTrue([button for button in buttons if force_text(button.name) == 'Publish page changes'])
-
- def test_show_resubmit_button_if_moderation_request_is_rejected(self):
- self.setup_toolbar(self.pg5, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, Dropdown)], [])
- self.assertEqual(len(buttons), 4)
- self.assertEqual(force_text(buttons[0].name), 'View differences')
- self.assertEqual(force_text(buttons[1].name), 'Resubmit changes for moderation')
- self.assertEqual(force_text(buttons[2].name), 'Cancel request')
- self.assertEqual(force_text(buttons[3].name), 'View comments')
-
- def test_show_moderation_dropdown_if_moderation_request_non_published_page(self):
- self.setup_toolbar(self.pg1, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, Dropdown)], [])
- self.assertEqual(len(buttons), 4)
- # View difference button is not present
- self.assertEqual(force_text(buttons[0].name), 'Approve changes')
- self.assertEqual(force_text(buttons[1].name), 'Send for rework')
- self.assertEqual(force_text(buttons[2].name), 'Cancel request')
- self.assertEqual(force_text(buttons[3].name), 'View comments')
-
- def test_show_moderation_dropdown_if_moderation_request_previously_published_page(self):
- self.setup_toolbar(self.pg5, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, Dropdown)], [])
- self.assertEqual(len(buttons), 4)
- # View difference button is present
- self.assertEqual(force_text(buttons[0].name), 'View differences')
- self.assertEqual(force_text(buttons[1].name), 'Resubmit changes for moderation')
- self.assertEqual(force_text(buttons[2].name), 'Cancel request')
- self.assertEqual(force_text(buttons[3].name), 'View comments')
-
- def test_show_moderation_dropdown_with_no_actions_for_non_role_user(self):
- self.setup_toolbar(self.pg1, self.user3)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, Dropdown)], [])
- self.assertEqual(len(buttons), 2)
- # `self.pg1` has never been published so we don't show View diff button
- self.assertEqual(force_text(buttons[0].name), 'Cancel request')
- self.assertEqual(force_text(buttons[1].name), 'View comments')
-
- def test_show_submit_for_moderation_button_if_page_is_dirty(self):
- new_page = create_page(title='New Page', template='page.html', language='en', published=True,)
- self.setup_toolbar(new_page, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, ButtonList)], [])
- self.assertTrue([button for button in buttons if force_text(button.name) == 'Submit for moderation'])
-
- def test_submit_for_moderation_button_with_default_settings(self):
- new_page = create_page(title='New Page', template='page.html', language='en', published=True,)
- self.setup_toolbar(new_page, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, ButtonList)], [])
- submit_for_moderation_button = [
- button for button in buttons if force_text(button.name) == 'Submit for moderation'
- ][0]
- url = get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(new_page.pk, 'en'),
- )
- self.assertEqual(submit_for_moderation_button.url, url)
-
- @patch('djangocms_moderation.conf.ENABLE_WORKFLOW_OVERRIDE', True)
- def test_submit_for_moderation_button_with_override_settings(self):
- new_page = create_page(title='New Page', template='page.html', language='en', published=True,)
- self.setup_toolbar(new_page, self.user)
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, ButtonList)], [])
- submit_for_moderation_button = [
- button for button in buttons if force_text(button.name) == 'Submit for moderation'
- ][0]
- url = get_admin_url(
- name='cms_moderation_select_new_moderation',
- language='en',
- args=(new_page.pk, 'en'),
- )
- self.assertEqual(submit_for_moderation_button.url, url)
-
- def test_publish_button_after_moderation_request_approved(self):
- self.setup_toolbar(self.pg3, self.user) # pg3 => moderation request is approved
- publish_page(page=self.pg3, user=self.user, language="en")
- buttons = sum([item.buttons for item in self.toolbar_right_items if isinstance(item, Dropdown)], [])
- self.assertEqual(len(buttons), 2)
- self.assertEqual(force_text(buttons[0].name), 'Publish page changes')
- self.assertEqual(force_text(buttons[1].name), 'Cancel request')
-
-
-class PageModerationToolbarTest(BaseToolbarTest):
-
- def test_moderation_menu_add_rendered(self):
- new_page = create_page(title='New Page', template='page.html', language='en',)
- self.setup_toolbar(new_page, self.user)
- page_menu = self.toolbar.menus['page']
- opts = PageModeration._meta
- url = get_admin_url(
- name='{}_{}_{}'.format(opts.app_label, opts.model_name, 'add'),
- language='en',
- args=[],
- )
- url += '?extended_object=%s' % new_page.pk
- self.assertEqual(len(page_menu.find_items(ModalItem, url=url)), 1)
-
- def test_moderation_menu_change_rendered(self):
- new_page = create_page(title='New Page', template='page.html', language='en',)
- extension = PageModeration.objects.create(extended_object=new_page, enabled=True, workflow=self.wf1,)
- self.setup_toolbar(new_page, self.user)
- page_menu = self.toolbar.menus['page']
- opts = PageModeration._meta
- url = get_admin_url(
- name='{}_{}_{}'.format(opts.app_label, opts.model_name, 'change'),
- language='en',
- args=[extension.pk],
- )
- self.assertEqual(len(page_menu.find_items(ModalItem, url=url)), 1)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 5be2d86e..90f9ecdd 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -6,10 +6,8 @@
from djangocms_moderation import constants
from djangocms_moderation.forms import (
ModerationRequestForm,
- SelectModerationForm,
UpdateModerationRequestForm,
)
-from djangocms_moderation.models import Workflow
from .utils import BaseTestCase
@@ -48,7 +46,7 @@ def test_form_save(self):
self.assertTrue(form.is_valid())
form.save()
form.workflow.submit_new_request.assert_called_once_with(
- page=self.pg2,
+ obj=self.pg2,
by_user=self.user,
to_user=None,
language='en',
@@ -126,18 +124,3 @@ def test_form_save(self):
to_user=None,
message='Approved message',
)
-
-
-class SelectModerationFormTest(BaseTestCase):
-
- def test_form_init(self):
- form = SelectModerationForm(page=self.pg1,)
- self.assertIn('workflow', form.fields)
- field_workflow = form.fields['workflow']
- self.assertQuerysetEqual(
- field_workflow.queryset,
- Workflow.objects.all(),
- transform=lambda x: x,
- ordered=False,
- )
- self.assertEqual(field_workflow.initial, self.wf1)
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index a3b35f59..d78534f3 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,37 +1,18 @@
import json
-from mock import patch
-
-from django.test import override_settings
from djangocms_moderation.helpers import (
get_active_moderation_request,
get_form_submission_for_step,
get_page_or_404,
- get_workflow_or_none,
- is_moderation_enabled,
)
from djangocms_moderation.models import (
ConfirmationFormSubmission,
ConfirmationPage,
- PageModeration,
- Workflow,
)
from .utils import BaseTestCase
-class GetWorkflowOrNoneTest(BaseTestCase):
-
- def test_existing_workflow(self):
- workflow = Workflow.objects.get(pk=1)
- self.assertEqual(get_workflow_or_none(1), workflow)
- workflow = Workflow.objects.get(pk=2)
- self.assertEqual(get_workflow_or_none(2), workflow)
-
- def test_non_existing_workflow(self):
- self.assertIsNone(get_workflow_or_none(10))
-
-
class GetCurrentModerationRequestTest(BaseTestCase):
def test_existing_moderation_request(self):
@@ -49,43 +30,6 @@ def test_returns_page(self):
self.assertEqual(get_page_or_404(self.pg1.pk, 'en'), self.pg1)
-class IsModerationEnabledTest(BaseTestCase):
-
- @override_settings(CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE=True)
- def test_returns_true_with_override_no_moderation_object(self):
- self.assertTrue(is_moderation_enabled(self.pg1))
-
- @override_settings(CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE=True)
- def test_returns_true_with_override_moderation_object_enabled(self):
- PageModeration.objects.create(extended_object=self.pg1, enabled=True, workflow=self.wf1,)
- self.assertTrue(is_moderation_enabled(self.pg1))
-
- @override_settings(CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE=True)
- def test_returns_false_with_override_moderation_object_disabled(self):
- PageModeration.objects.create(extended_object=self.pg1, enabled=False, workflow=self.wf1,)
- self.assertFalse(is_moderation_enabled(self.pg1))
-
- @override_settings(CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE=True)
- def test_returns_false_with_override_no_workflows(self):
- Workflow.objects.all().delete()
- self.assertFalse(is_moderation_enabled(self.pg1))
-
- def test_returns_true_default_settings_has_default_workflow(self):
- self.assertTrue(is_moderation_enabled(self.pg1))
-
- def test_returns_true_default_settings_moderation_object_enabled(self):
- PageModeration.objects.create(extended_object=self.pg1, enabled=True, workflow=self.wf1,)
- self.assertTrue(is_moderation_enabled(self.pg1))
-
- def test_returns_false_default_settings_moderation_object_disabled(self):
- PageModeration.objects.create(extended_object=self.pg1, enabled=False, workflow=self.wf1,)
- self.assertFalse(is_moderation_enabled(self.pg1))
-
- @patch('djangocms_moderation.helpers.get_page_moderation_workflow', return_value=None)
- def test_returns_false_default_settings_no_workflow(self, mock_gpmw):
- self.assertFalse(is_moderation_enabled(self.pg1))
-
-
class GetFormSubmissions(BaseTestCase):
def test_returns_form_submission_for_step(self):
diff --git a/tests/test_models.py b/tests/test_models.py
index 593bc1a6..1103042f 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,18 +1,17 @@
import json
from mock import patch
+from unittest import skip
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
-from cms.api import create_page
-
from djangocms_moderation import constants
from djangocms_moderation.models import (
ConfirmationFormSubmission,
ConfirmationPage,
- PageModerationRequest,
- PageModerationRequestAction,
+ ModerationRequest,
+ ModerationRequestAction,
Role,
Workflow,
WorkflowStep,
@@ -64,17 +63,18 @@ def test_multiple_defaults_validation_error(self):
def test_first_step(self):
self.assertEqual(self.wf1.first_step, self.wf1st1)
+ @skip('4.0 rework TBC')
@patch('djangocms_moderation.models.notify_requested_moderator')
def test_submit_new_request(self, mock_nrm):
request = self.wf1.submit_new_request(
by_user=self.user,
- page=self.pg3,
+ obj=self.pg3,
language='en',
message='Some message',
)
self.assertQuerysetEqual(
request.actions.all(),
- PageModerationRequestAction.objects.filter(request=request),
+ ModerationRequestAction.objects.filter(request=request),
transform=lambda x: x,
ordered=False,
)
@@ -94,7 +94,7 @@ def test_get_next_required(self):
self.assertIsNone(self.wf1st3.get_next_required())
-class PageModerationRequestTest(BaseTestCase):
+class ModerationRequestTest(BaseTestCase):
def test_has_pending_step(self):
self.assertTrue(self.moderation_request1.has_pending_step())
@@ -244,20 +244,11 @@ def test_user_can_moderate(self):
self.assertFalse(self.moderation_request3.user_can_moderate(temp_user))
# check that it doesn't allow access to users that aren't part of this moderation request
- self.pg5 = create_page(title='Page 5', template='page.html', language='en',)
- self.user4 = User.objects.create_superuser(username='test4', email='test4@test.com', password='test4',)
- self.role4 = Role.objects.create(name='Role 4', user=self.user4,)
- self.wf4 = Workflow.objects.create(pk=4, name='Workflow 4',)
- self.wf4st1 = self.wf4.steps.create(role=self.role4, is_required=True, order=1,)
- self.wf4st2 = self.wf4.steps.create(role=self.role1, is_required=False, order=2,)
- self.moderation_request4 = PageModerationRequest.objects.create(
- page=self.pg5, language='en', workflow=self.wf4, is_active=True,)
- self.moderation_request4.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
-
+ user4 = User.objects.create_superuser(username='test4', email='test4@test.com', password='test4',)
self.assertTrue(self.moderation_request4.user_can_moderate(self.user))
- self.assertFalse(self.moderation_request4.user_can_moderate(self.user2))
- self.assertFalse(self.moderation_request4.user_can_moderate(self.user3))
- self.assertTrue(self.moderation_request4.user_can_moderate(self.user4))
+ self.assertTrue(self.moderation_request4.user_can_moderate(self.user2))
+ self.assertTrue(self.moderation_request4.user_can_moderate(self.user3))
+ self.assertFalse(self.moderation_request4.user_can_moderate(user4))
@patch('djangocms_moderation.models.notify_request_author')
@patch('djangocms_moderation.models.notify_requested_moderator')
@@ -372,11 +363,11 @@ def test_rejection_makes_the_previous_actions_archived(self):
def test_compliance_number(self, mock_uuid):
mock_uuid.return_value = 'abc123'
- request = PageModerationRequest.objects.create(
- page=self.pg1,
+ request = ModerationRequest.objects.create(
+ content_object=self.pg1,
language='en',
is_active=True,
- workflow=self.wf1,
+ collection=self.collection1,
)
self.assertEqual(mock_uuid.call_count, 0)
@@ -386,10 +377,10 @@ def test_compliance_number(self, mock_uuid):
def test_compliance_number_sequential_number_backend(self):
self.wf2.compliance_number_backend = 'djangocms_moderation.backends.sequential_number_backend'
- request = PageModerationRequest.objects.create(
- page=self.pg1,
+ request = ModerationRequest.objects.create(
+ content_object=self.pg1,
language='en',
- workflow=self.wf2,
+ collection=self.collection2,
)
request.refresh_from_db()
self.assertIsNone(request.compliance_number)
@@ -405,10 +396,10 @@ def test_compliance_number_sequential_number_with_identifier_prefix_backend(self
)
self.wf2.identifier = 'SSO'
- request = PageModerationRequest.objects.create(
- page=self.pg1,
+ request = ModerationRequest.objects.create(
+ content_object=self.pg1,
language='en',
- workflow=self.wf2,
+ collection=self.collection2,
)
request.refresh_from_db()
self.assertIsNone(request.compliance_number)
@@ -419,7 +410,7 @@ def test_compliance_number_sequential_number_with_identifier_prefix_backend(self
self.assertEqual(request.compliance_number, expected)
-class PageModerationRequestActionTest(BaseTestCase):
+class ModerationRequestActionTest(BaseTestCase):
def test_get_by_user_name(self):
action = self.moderation_request3.actions.last()
@@ -439,10 +430,10 @@ def test_save_when_to_user_passed(self):
self.assertEqual(new_action.to_role, self.role2)
def test_save_when_to_user_not_passed_and_action_started(self):
- new_request = PageModerationRequest.objects.create(
- page=self.pg2,
+ new_request = ModerationRequest.objects.create(
+ content_object=self.pg2,
language='en',
- workflow=self.wf1,
+ collection=self.collection1,
is_active=True,
)
new_action = new_request.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
diff --git a/tests/test_moderation_flows.py b/tests/test_moderation_flows.py
index 967fce46..ee5bf221 100644
--- a/tests/test_moderation_flows.py
+++ b/tests/test_moderation_flows.py
@@ -1,3 +1,5 @@
+from unittest import skip
+
from django.contrib.auth.models import User
from django.test import TestCase
@@ -6,8 +8,8 @@
from djangocms_moderation import constants
from djangocms_moderation.models import (
- PageModerationRequest,
- PageModerationRequestAction,
+ ModerationRequest,
+ ModerationRequestAction,
Role,
Workflow,
)
@@ -67,6 +69,7 @@ def _resubmit_moderation_request(self, user, message='Test message - resubmit'):
def _cancel_moderation_request(self, user, message='Test message - cancel'):
return self._process_moderation_request(user, 'cancel', message)
+ @skip('4.0 rework TBC')
def test_approve_moderation_workflow(self):
"""
This case tests the following workflow:
@@ -77,20 +80,20 @@ def test_approve_moderation_workflow(self):
5. compliance number is generated
6. author publishes the page and workflow is done
"""
- self.assertFalse(PageModerationRequest.objects.exists())
- self.assertFalse(PageModerationRequestAction.objects.exists())
+ self.assertFalse(ModerationRequest.objects.exists())
+ self.assertFalse(ModerationRequestAction.objects.exists())
# Lets create a new moderation request
self._new_moderation_request(self.author)
- moderation_request = PageModerationRequest.objects.get() # It exists
- action = PageModerationRequestAction.objects.get()
+ moderation_request = ModerationRequest.objects.get() # It exists
+ action = ModerationRequestAction.objects.get()
self.assertEqual(action.action, constants.ACTION_STARTED)
response = self._approve_moderation_request(self.moderator_1)
self.assertEqual(response.status_code, 200)
- second_action = PageModerationRequestAction.objects.last()
+ second_action = ModerationRequestAction.objects.last()
self.assertTrue(second_action.action, constants.ACTION_APPROVED)
self.assertTrue(second_action.message, 'Test message - approved')
# Compliance number is not generated yet
@@ -110,7 +113,7 @@ def test_approve_moderation_workflow(self):
compliance_number = moderation_request.compliance_number
self.assertIsNotNone(compliance_number)
- third_action = PageModerationRequestAction.objects.last()
+ third_action = ModerationRequestAction.objects.last()
self.assertTrue(third_action.action, constants.ACTION_APPROVED)
self.assertTrue(second_action.message, 'message #2')
@@ -122,10 +125,11 @@ def test_approve_moderation_workflow(self):
moderation_request.refresh_from_db()
# Moderation request is finished and last action is recorded
self.assertFalse(moderation_request.is_active)
- last_action = PageModerationRequestAction.objects.last()
+ last_action = ModerationRequestAction.objects.last()
self.assertTrue(last_action.action, constants.ACTION_FINISHED)
self.assertEqual(moderation_request.compliance_number, compliance_number)
+ @skip('4.0 rework TBC')
def test_reject_moderation_workflow(self):
"""
This case tests the following workflow:
@@ -136,15 +140,15 @@ def test_reject_moderation_workflow(self):
5. all is approved by moderator_1 and moderator_2
6. author cancels the request
"""
- self.assertFalse(PageModerationRequest.objects.exists())
- self.assertFalse(PageModerationRequestAction.objects.exists())
+ self.assertFalse(ModerationRequest.objects.exists())
+ self.assertFalse(ModerationRequestAction.objects.exists())
# Lets create a new moderation request
self._new_moderation_request(self.author)
- moderation_request = PageModerationRequest.objects.get() # It exists
+ moderation_request = ModerationRequest.objects.get() # It exists
- action = PageModerationRequestAction.objects.get()
+ action = ModerationRequestAction.objects.get()
self.assertEqual(action.action, constants.ACTION_STARTED)
# moderator_1 will approve it now
@@ -159,12 +163,12 @@ def test_reject_moderation_workflow(self):
# Make sure that after the rejection, this moderation request is still active
self.assertTrue(moderation_request.is_active)
- third_action = PageModerationRequestAction.objects.last()
+ third_action = ModerationRequestAction.objects.last()
self.assertTrue(third_action.action, constants.ACTION_REJECTED)
self.assertTrue(third_action.message, 'Please, less swearing')
# Lets check that we now have 2 archived actions. First and second one
- self.assertEqual(2, PageModerationRequestAction.objects.filter(is_archived=True).count())
+ self.assertEqual(2, ModerationRequestAction.objects.filter(is_archived=True).count())
# Now the original author can make amends and resubmit
self._resubmit_moderation_request(self.author)
@@ -173,7 +177,7 @@ def test_reject_moderation_workflow(self):
moderation_request.refresh_from_db()
self.assertTrue(moderation_request.is_active)
- last_action = PageModerationRequestAction.objects.last()
+ last_action = ModerationRequestAction.objects.last()
self.assertTrue(last_action.action, constants.ACTION_RESUBMITTED)
self._approve_moderation_request(self.moderator_1)
@@ -185,5 +189,5 @@ def test_reject_moderation_workflow(self):
moderation_request.refresh_from_db()
self.assertFalse(moderation_request.is_active)
- last_action = PageModerationRequestAction.objects.last()
+ last_action = ModerationRequestAction.objects.last()
self.assertTrue(last_action.action, constants.ACTION_CANCELLED)
diff --git a/tests/test_views.py b/tests/test_views.py
index 852da6f7..04b222da 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,5 +1,6 @@
import json
from mock import patch
+from unittest import skip
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
@@ -9,7 +10,6 @@
from djangocms_moderation import constants
from djangocms_moderation.forms import (
ModerationRequestForm,
- SelectModerationForm,
UpdateModerationRequestForm,
)
from djangocms_moderation.models import (
@@ -54,6 +54,7 @@ def test_new_request_view_with_form(self):
title=_('Submit for moderation')
)
+ @skip('4.0 rework TBC')
def test_new_request_view_with_form_workflow_passed_param(self):
response = self.client.get(
'{}?{}'.format(
@@ -154,6 +155,7 @@ def test_get_form_kwargs(self):
self.assertEqual(kwargs.get('workflow'), view.workflow)
self.assertEqual(kwargs.get('active_request'), view.active_request)
+ @skip('4.0 rework TBC')
def test_form_valid(self):
response = self.client.post(get_admin_url(
name='cms_moderation_new_request',
@@ -175,6 +177,7 @@ def test_throws_error_moderation_already_exists(self):
self.assertEqual(response.status_code, 400)
self.assertEqual(response.content, b'Page already has an active moderation request.')
+ @skip('4.0 rework TBC')
def test_throws_error_invalid_workflow_passed(self):
response = self.client.get('{}?{}'.format(
get_admin_url(
@@ -217,7 +220,7 @@ def test_throws_error_forbidden_user(self):
self.assertEqual(response.status_code, 403)
self.assertEqual(response.content, b'User is not allowed to update request.')
- @patch('djangocms_moderation.views.get_page_moderation_workflow', return_value=None)
+ @patch('djangocms_moderation.views.get_moderation_workflow', return_value=None)
def test_throws_error_if_workflow_has_not_been_resolved(self, mock_gpmw):
response = self.client.get(get_admin_url(
name='cms_moderation_new_request',
@@ -303,49 +306,6 @@ def test_renders_all_form_submissions(self):
self.assertQuerysetEqual(form_submissions, results, transform=lambda x: x, ordered=False)
-class SelectModerationViewTest(BaseViewTestCase):
-
- def test_renders_view_with_form(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_select_new_moderation',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- view = response.context_data['view']
- form = response.context_data['adminform']
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template_name[0], 'djangocms_moderation/select_workflow_form.html')
- self.assertEqual(view.page_id, str(self.pg1.pk))
- self.assertEqual(view.current_lang, 'en')
- self.assertIsInstance(form, SelectModerationForm)
-
- def test_get_form_kwargs(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_select_new_moderation',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- view = response.context_data['view']
- kwargs = view.get_form_kwargs()
- self.assertEqual(kwargs.get('page'), self.pg1)
-
- def test_form_valid(self):
- response = self.client.post(get_admin_url(
- name='cms_moderation_select_new_moderation',
- language='en',
- args=(self.pg1.pk, 'en')
- ), {'workflow': self.wf2.pk})
- form_valid_redirect_url = '{}?{}'.format(
- get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ),
- 'workflow={}'.format(self.wf2.pk)
- )
- self.assertEqual(response.url, form_valid_redirect_url)
-
-
class ModerationCommentsViewTest(BaseViewTestCase):
def test_comment_list(self):
diff --git a/tests/utils.py b/tests/utils.py
index 5a250b3f..36dee07c 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,7 +4,12 @@
from cms.api import create_page
from djangocms_moderation import constants
-from djangocms_moderation.models import PageModerationRequest, Role, Workflow
+from djangocms_moderation.models import (
+ ModerationCollection,
+ ModerationRequest,
+ Role,
+ Workflow,
+)
class BaseTestCase(TestCase):
@@ -51,17 +56,21 @@ def setUpTestData(cls):
cls.wf3st2 = cls.wf3.steps.create(role=cls.role3, is_required=False, order=2,)
# create page moderation requests and actions
- cls.moderation_request1 = PageModerationRequest.objects.create(
- page=cls.pg1, language='en', workflow=cls.wf1, is_active=True,)
+ cls.collection1 = ModerationCollection.objects.create(name='Collection 1', workflow=cls.wf1)
+ cls.collection2 = ModerationCollection.objects.create(name='Collection 2', workflow=cls.wf2)
+ cls.collection3 = ModerationCollection.objects.create(name='Collection 3', workflow=cls.wf3)
+
+ cls.moderation_request1 = ModerationRequest.objects.create(
+ content_object=cls.pg1, language='en', collection=cls.collection1, is_active=True,)
cls.moderation_request1.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
- PageModerationRequest.objects.create(
- page=cls.pg1, language='en', workflow=cls.wf1, is_active=False,)
- PageModerationRequest.objects.create(
- page=cls.pg2, language='en', workflow=cls.wf2, is_active=False,)
+ ModerationRequest.objects.create(
+ content_object=cls.pg1, language='en', collection=cls.collection1, is_active=False,)
+ ModerationRequest.objects.create(
+ content_object=cls.pg2, language='en', collection=cls.collection2, is_active=False,)
- cls.moderation_request2 = PageModerationRequest.objects.create(
- page=cls.pg3, language='en', workflow=cls.wf2, is_active=True,)
+ cls.moderation_request2 = ModerationRequest.objects.create(
+ content_object=cls.pg3, language='en', collection=cls.collection2, is_active=True,)
cls.moderation_request2.actions.create(
by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request2.actions.create(
@@ -69,8 +78,8 @@ def setUpTestData(cls):
cls.moderation_request2.actions.create(
by_user=cls.user, action=constants.ACTION_APPROVED, step_approved=cls.wf2st2,)
- cls.moderation_request3 = PageModerationRequest.objects.create(
- page=cls.pg4, language='en', workflow=cls.wf3, is_active=True,)
+ cls.moderation_request3 = ModerationRequest.objects.create(
+ content_object=cls.pg4, language='en', collection=cls.collection3, is_active=True,)
cls.moderation_request3.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request3.actions.create(
by_user=cls.user,
@@ -79,8 +88,8 @@ def setUpTestData(cls):
step_approved=cls.wf3st1,
)
# This request will be rejected
- cls.moderation_request4 = PageModerationRequest.objects.create(
- page=cls.pg5, language='en', workflow=cls.wf3, is_active=True,)
+ cls.moderation_request4 = ModerationRequest.objects.create(
+ content_object=cls.pg5, language='en', collection=cls.collection3, is_active=True,)
cls.moderation_request4.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request4.actions.create(by_user=cls.user2, action=constants.ACTION_REJECTED)
From af3a37acaf9e40709b8f2cd0b816dc8911730fb6 Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Fri, 27 Jul 2018 14:26:31 +0100
Subject: [PATCH 002/147] Integrated moderation with app registration system
(#35)
---
.circleci/config.yml | 67 ++++++-------------------
djangocms_moderation/cms_config.py | 26 ++++++++++
tests/requirements.txt | 1 +
tests/settings.py | 3 ++
tests/test_app_registration.py | 78 ++++++++++++++++++++++++++++++
tests/test_forms.py | 2 +-
tests/test_handlers.py | 2 +-
tests/test_helpers.py | 2 +-
tests/test_models.py | 2 +-
tests/test_views.py | 2 +-
tests/utils/__init__.py | 0
tests/utils/app_1/__init__.py | 1 +
tests/utils/app_1/apps.py | 7 +++
tests/utils/app_1/cms_config.py | 10 ++++
tests/utils/app_1/models.py | 15 ++++++
tests/utils/app_2/__init__.py | 1 +
tests/utils/app_2/apps.py | 7 +++
tests/utils/app_2/cms_config.py | 10 ++++
tests/utils/app_2/models.py | 15 ++++++
tests/{utils.py => utils/base.py} | 0
tox.ini | 8 ++-
21 files changed, 196 insertions(+), 63 deletions(-)
create mode 100644 djangocms_moderation/cms_config.py
create mode 100644 tests/test_app_registration.py
create mode 100644 tests/utils/__init__.py
create mode 100644 tests/utils/app_1/__init__.py
create mode 100644 tests/utils/app_1/apps.py
create mode 100644 tests/utils/app_1/cms_config.py
create mode 100644 tests/utils/app_1/models.py
create mode 100644 tests/utils/app_2/__init__.py
create mode 100644 tests/utils/app_2/apps.py
create mode 100644 tests/utils/app_2/cms_config.py
create mode 100644 tests/utils/app_2/models.py
rename tests/{utils.py => utils/base.py} (100%)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e2a1e571..a489597f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,17 +1,5 @@
version: 2.0
-py27default: &py27default
- docker:
- - image: circleci/python:2.7
- steps:
- - setup_remote_docker:
- docker_layer_caching: true
- - checkout
- - restore_cache:
- keys: py27-
- - run: docker load -i caches/py27.tar || true
- - run: docker run py27 tox -e $CIRCLE_STAGE
-
py34default: &py34default
docker:
- image: circleci/python:3.4
@@ -49,10 +37,6 @@ py36default: &py36default
- run: docker load -i caches/py36.tar || true
- run: docker run py36 tox -e $CIRCLE_STAGE
-py27_requires: &py27_requires
- requires:
- - py27_base
-
py34_requires: &py34_requires
requires:
- py34_base
@@ -66,20 +50,6 @@ py36_requires: &py36_requires
- py36_base
jobs:
- py27_base:
- docker:
- - image: circleci/python:2.7
- steps:
- - checkout
- - setup_remote_docker:
- docker_layer_caching: true
- - run: docker build -f .circleci/Dockerfile --build-arg PYTHON_VERSION=2.7 -t py27 .
- - run: mkdir caches
- - run: docker save -o caches/py27.tar py27
- - save_cache:
- key: py27-{{ .Environment.CIRCLE_SHA1 }}
- paths:
- - "caches/"
py34_base:
docker:
- image: circleci/python:3.4
@@ -139,21 +109,17 @@ jobs:
keys: py35-
- run: docker load -i caches/py35.tar || true
- run: docker run py35 gulp lint
- py27-dj111-sqlite-cms35:
- <<: *py27default
- py34-dj111-sqlite-cms35:
+ py34-dj111-sqlite-cms4:
<<: *py34default
- py35-dj111-sqlite-cms35:
+ py35-dj111-sqlite-cms4:
<<: *py35default
- py36-dj111-sqlite-cms35:
+ py36-dj111-sqlite-cms4:
<<: *py36default
- py36-dj111-sqlite-cms36:
- <<: *py36default
- py34-dj20-sqlite-cms36:
+ py34-dj20-sqlite-cms4:
<<: *py34default
- py35-dj20-sqlite-cms36:
+ py35-dj20-sqlite-cms4:
<<: *py35default
- py36-dj20-sqlite-cms36:
+ py36-dj20-sqlite-cms4:
<<: *py36default
#######################
@@ -162,7 +128,6 @@ workflows:
version: 2
build:
jobs:
- - py27_base
- py34_base
- py35_base
- py36_base
@@ -175,27 +140,23 @@ workflows:
- eslint:
requires:
- py35_base
- - py27-dj111-sqlite-cms35:
- requires:
- - py27_base
- - py34-dj111-sqlite-cms35:
+ - py34-dj111-sqlite-cms4:
requires:
- py34_base
- - py35-dj111-sqlite-cms35:
+ - py35-dj111-sqlite-cms4:
requires:
- py35_base
- - py36-dj111-sqlite-cms35:
- requires:
- - py36_base
- - py36-dj111-sqlite-cms36:
+ - py36-dj111-sqlite-cms4:
requires:
- py36_base
- - py34-dj20-sqlite-cms36:
+ - py34-dj20-sqlite-cms4:
requires:
- py34_base
- - py35-dj20-sqlite-cms36:
+ - py35-dj20-sqlite-cms4:
requires:
- py35_base
- - py36-dj20-sqlite-cms36:
+ - py36-dj20-sqlite-cms4:
requires:
- py36_base
+
+
diff --git a/djangocms_moderation/cms_config.py b/djangocms_moderation/cms_config.py
new file mode 100644
index 00000000..0ff35c3c
--- /dev/null
+++ b/djangocms_moderation/cms_config.py
@@ -0,0 +1,26 @@
+from django.core.exceptions import ImproperlyConfigured
+
+from cms.app_base import CMSAppExtension
+
+
+class ModerationExtension(CMSAppExtension):
+
+ def __init__(self):
+ self.moderated_models = []
+
+ def configure_app(self, cms_config):
+ versioning_enabled = getattr(cms_config, 'djangocms_versioning_enabled', False)
+ moderated_models = getattr(cms_config, 'moderated_models', [])
+ versioning_models = getattr(cms_config, 'versioning_models', [])
+
+ if not versioning_enabled:
+ raise ImproperlyConfigured('Versioning needs to be enabled for Moderation')
+
+ for moderated_model in moderated_models:
+ if moderated_model not in versioning_models:
+ raise ImproperlyConfigured(
+ 'Moderated models need to be Versionable, please include every '
+ 'model that needs to be moderated in versioning_models entry'
+ )
+
+ self.moderated_models.extend(moderated_models)
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 990b53c4..9ea1f073 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -5,3 +5,4 @@ flake8
isort
aldryn-forms>=3.0.3
mock
+-e git+git://github.com/divio/djangocms-versioning.git@master#egg=djangocms-versioning
diff --git a/tests/settings.py b/tests/settings.py
index 75ae77df..770a6acd 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -1,5 +1,8 @@
HELPER_SETTINGS = {
'INSTALLED_APPS': [
+ 'tests.utils.app_1',
+ 'tests.utils.app_2',
+ 'djangocms_versioning',
'filer',
'easy_thumbnails',
'absolute',
diff --git a/tests/test_app_registration.py b/tests/test_app_registration.py
new file mode 100644
index 00000000..c44d50f3
--- /dev/null
+++ b/tests/test_app_registration.py
@@ -0,0 +1,78 @@
+try:
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+from unittest import TestCase
+
+from django.apps import apps
+from django.core.exceptions import ImproperlyConfigured
+
+from cms import app_registration
+from cms.test_utils.testcases import CMSTestCase
+from cms.utils.setup import setup_cms_apps
+
+from djangocms_moderation.cms_config import ModerationExtension
+
+from .utils.app_1.models import TestModel3, TestModel4
+from .utils.app_2.models import TestModel1, TestModel2
+
+
+class CMSConfigTest(CMSTestCase, TestCase):
+
+ def setUp(self):
+ app_registration.get_cms_extension_apps.cache_clear()
+ app_registration.get_cms_config_apps.cache_clear()
+
+ def test_missing_versioning_enabled(self):
+ extension = ModerationExtension()
+ cms_config = Mock(
+ moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
+ djangocms_moderation_enabled=True,
+ djangocms_versioning_enabled=False,
+ app_config=Mock(label='blah_cms_config')
+ )
+
+ with self.assertRaises(ImproperlyConfigured):
+ extension.configure_app(cms_config)
+
+ def test_moderated_model_not_in_versioning_models(self):
+ extension = ModerationExtension()
+ cms_config = Mock(
+ djangocms_moderation_enabled=True,
+ moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
+ versioning_models=[TestModel3, TestModel4],
+ app_config=Mock(label='blah_cms_config')
+ )
+
+ with self.assertRaises(ImproperlyConfigured):
+ extension.configure_app(cms_config)
+
+ def test_valid_cms_config(self):
+ extension = ModerationExtension()
+ cms_config = Mock(
+ djangocms_moderation_enabled=True,
+ moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
+ versioning_models=[TestModel1, TestModel2, TestModel3, TestModel4],
+ app_config=Mock(label='blah_cms_config')
+ )
+
+ extension.configure_app(cms_config)
+ self.assertTrue(TestModel1 in extension.moderated_models)
+ self.assertTrue(TestModel2 in extension.moderated_models)
+ self.assertTrue(TestModel3 in extension.moderated_models)
+ self.assertTrue(TestModel4 in extension.moderated_models)
+
+
+class CMSConfigIntegrationTest(CMSTestCase):
+
+ def setUp(self):
+ app_registration.get_cms_extension_apps.cache_clear()
+ app_registration.get_cms_config_apps.cache_clear()
+
+ def test_config_with_two_apps(self):
+ setup_cms_apps()
+ moderation_config = apps.get_app_config('djangocms_moderation')
+ registered_model = moderation_config.cms_extension.moderated_models
+
+ self.assertEqual(len(registered_model), 4)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 90f9ecdd..c622e4a8 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -9,7 +9,7 @@
UpdateModerationRequestForm,
)
-from .utils import BaseTestCase
+from .utils.base import BaseTestCase
class ModerationRequestFormTest(BaseTestCase):
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index ea51631f..cb240cb4 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -9,7 +9,7 @@
ConfirmationPage,
)
-from .utils import BaseTestCase
+from .utils.base import BaseTestCase
class ModerationConfirmationFormSubmissionTest(BaseTestCase):
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index d78534f3..f7e7210e 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -10,7 +10,7 @@
ConfirmationPage,
)
-from .utils import BaseTestCase
+from .utils.base import BaseTestCase
class GetCurrentModerationRequestTest(BaseTestCase):
diff --git a/tests/test_models.py b/tests/test_models.py
index 1103042f..d69c6b4a 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -17,7 +17,7 @@
WorkflowStep,
)
-from .utils import BaseTestCase
+from .utils.base import BaseTestCase
class RoleTest(BaseTestCase):
diff --git a/tests/test_views.py b/tests/test_views.py
index 04b222da..c3ae1b71 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -18,7 +18,7 @@
)
from djangocms_moderation.utils import get_admin_url
-from .utils import BaseViewTestCase
+from .utils.base import BaseViewTestCase
class ModerationRequestViewTest(BaseViewTestCase):
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/utils/app_1/__init__.py b/tests/utils/app_1/__init__.py
new file mode 100644
index 00000000..03f81427
--- /dev/null
+++ b/tests/utils/app_1/__init__.py
@@ -0,0 +1 @@
+default_app_config = "tests.utils.app_1.apps.App1Config"
diff --git a/tests/utils/app_1/apps.py b/tests/utils/app_1/apps.py
new file mode 100644
index 00000000..d7a19c05
--- /dev/null
+++ b/tests/utils/app_1/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class App1Config(AppConfig):
+ name = 'tests.utils.app_1'
+ label = 'app_1'
+ verbose_name = "django app with cms_config for integration test"
diff --git a/tests/utils/app_1/cms_config.py b/tests/utils/app_1/cms_config.py
new file mode 100644
index 00000000..bb1b63fd
--- /dev/null
+++ b/tests/utils/app_1/cms_config.py
@@ -0,0 +1,10 @@
+from cms.app_base import CMSAppConfig
+
+from .models import TestModel3, TestModel4
+
+
+class CMSApp1Config(CMSAppConfig):
+ djangocms_versioning_enabled = True
+ djangocms_moderation_enabled = True
+ versioning_models = [TestModel3, TestModel4]
+ moderated_models = [TestModel3, TestModel4]
diff --git a/tests/utils/app_1/models.py b/tests/utils/app_1/models.py
new file mode 100644
index 00000000..d58a1a5e
--- /dev/null
+++ b/tests/utils/app_1/models.py
@@ -0,0 +1,15 @@
+from django.db import models
+
+from djangocms_versioning.models import BaseVersion
+
+
+class GrouperModel(models.Model):
+ content = models.CharField(max_length=255)
+
+
+class TestModel3(BaseVersion):
+ content = models.ForeignKey(GrouperModel)
+
+
+class TestModel4(BaseVersion):
+ content = models.ForeignKey(GrouperModel)
diff --git a/tests/utils/app_2/__init__.py b/tests/utils/app_2/__init__.py
new file mode 100644
index 00000000..ff4e78c0
--- /dev/null
+++ b/tests/utils/app_2/__init__.py
@@ -0,0 +1 @@
+default_app_config = "tests.utils.app_2.apps.App2Config"
diff --git a/tests/utils/app_2/apps.py b/tests/utils/app_2/apps.py
new file mode 100644
index 00000000..d42552ed
--- /dev/null
+++ b/tests/utils/app_2/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class App2Config(AppConfig):
+ name = 'tests.utils.app_2'
+ label = 'app_2'
+ verbose_name = "Another django app with cms_config for integration test"
diff --git a/tests/utils/app_2/cms_config.py b/tests/utils/app_2/cms_config.py
new file mode 100644
index 00000000..2b5827f0
--- /dev/null
+++ b/tests/utils/app_2/cms_config.py
@@ -0,0 +1,10 @@
+from cms.app_base import CMSAppConfig
+
+from .models import TestModel1, TestModel2
+
+
+class CMSApp2Config(CMSAppConfig):
+ djangocms_versioning_enabled = True
+ djangocms_moderation_enabled = True
+ versioning_models = [TestModel1, TestModel2]
+ moderated_models = [TestModel1, TestModel2]
diff --git a/tests/utils/app_2/models.py b/tests/utils/app_2/models.py
new file mode 100644
index 00000000..5e92c245
--- /dev/null
+++ b/tests/utils/app_2/models.py
@@ -0,0 +1,15 @@
+from django.db import models
+
+from djangocms_versioning.models import BaseVersion
+
+
+class GrouperModel(models.Model):
+ content = models.CharField(max_length=255)
+
+
+class TestModel1(BaseVersion):
+ content = models.ForeignKey(GrouperModel)
+
+
+class TestModel2(BaseVersion):
+ content = models.ForeignKey(GrouperModel)
diff --git a/tests/utils.py b/tests/utils/base.py
similarity index 100%
rename from tests/utils.py
rename to tests/utils/base.py
diff --git a/tox.ini b/tox.ini
index 719d970d..bc6de0aa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,8 +2,8 @@
envlist =
flake8
isort
- py{27,34,35,36}-dj111-sqlite-cms{35,36}
- py{34,35,36}-dj20-sqlite-cms36
+ py{34,35,36}-dj111-sqlite-cms4
+ py{34,35,36}-dj20-sqlite-cms4
skip_missing_interpreters=True
@@ -14,11 +14,9 @@ deps =
dj111: Django>=1.11,<2.0
dj20: Django>=2.0,<2.1
- cms35: django-cms>=3.5,<3.6
- cms36: https://github.com/divio/django-cms/archive/develop.zip
+ cms4: https://github.com/divio/django-cms/archive/release/4.0.x.zip
basepython =
- py27: python2.7
py34: python3.4
py35: python3.5
py36: python3.6
From 3178ac2dfda8d3da29284ffc3a439189811bc187 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 31 Jul 2018 22:25:27 +0100
Subject: [PATCH 003/147] Collection methods 1.0.x (#34)
---
djangocms_moderation/exceptions.py | 6 ++
.../0002_moderationcollection_author.py | 28 +++++++
djangocms_moderation/models.py | 37 +++++++++
tests/test_models.py | 76 ++++++++++++++++++-
tests/utils/base.py | 14 +++-
5 files changed, 156 insertions(+), 5 deletions(-)
create mode 100644 djangocms_moderation/exceptions.py
create mode 100644 djangocms_moderation/migrations/0002_moderationcollection_author.py
diff --git a/djangocms_moderation/exceptions.py b/djangocms_moderation/exceptions.py
new file mode 100644
index 00000000..abf1e860
--- /dev/null
+++ b/djangocms_moderation/exceptions.py
@@ -0,0 +1,6 @@
+class ObjectAlreadyInCollection(Exception):
+ pass
+
+
+class CollectionIsLocked(Exception):
+ pass
diff --git a/djangocms_moderation/migrations/0002_moderationcollection_author.py b/djangocms_moderation/migrations/0002_moderationcollection_author.py
new file mode 100644
index 00000000..152690fa
--- /dev/null
+++ b/djangocms_moderation/migrations/0002_moderationcollection_author.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-07-18 14:01
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('djangocms_moderation', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='moderationcollection',
+ name='author',
+ field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ preserve_default=False,
+ ),
+ migrations.AlterUniqueTogether(
+ name='moderationrequest',
+ unique_together=set([('collection', 'object_id', 'content_type')]),
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 5731a619..453577dd 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -17,6 +17,7 @@
from cms.models.fields import PlaceholderField
from .emails import notify_request_author, notify_requested_moderator
+from .exceptions import CollectionIsLocked, ObjectAlreadyInCollection
from .utils import generate_compliance_number
@@ -267,6 +268,12 @@ def get_next_required(self):
class ModerationCollection(models.Model):
name = models.CharField(verbose_name=_('name'), max_length=128)
+ author = models.ForeignKey(
+ to=settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ related_name='+',
+ on_delete=models.CASCADE,
+ )
workflow = models.ForeignKey(
to=Workflow,
verbose_name=_('workflow'),
@@ -275,6 +282,35 @@ class ModerationCollection(models.Model):
# TODO: proper implementations and handlers coming later for is_locked
is_locked = models.BooleanField(verbose_name=_('is locked'), default=False)
+ def add_object(self, content_object):
+ """
+ Add object to the ModerationRequest in this collection.
+ :return:
+ """
+ if self.is_locked:
+ raise CollectionIsLocked(
+ "Can't add the object to the collection, because it is locked"
+ )
+
+ content_type = ContentType.objects.get_for_model(content_object)
+ # Object can ever be part of only one collection
+ existing_request_exists = ModerationRequest.objects.filter(
+ content_type=content_type,
+ object_id=content_object.pk,
+ ).exists()
+
+ if not existing_request_exists:
+ return self.moderation_requests.create(
+ content_type=content_type,
+ object_id=content_object.pk,
+ collection=self,
+ )
+ else:
+ raise ObjectAlreadyInCollection(
+ "{} is already part of existing moderation request which is part "
+ "of another active collection".format(content_object)
+ )
+
@python_2_unicode_compatible
class ModerationRequest(models.Model):
@@ -314,6 +350,7 @@ class ModerationRequest(models.Model):
class Meta:
verbose_name = _('Request')
verbose_name_plural = _('Requests')
+ unique_together = ('collection', 'object_id', 'content_type')
def __str__(self):
return "{} {}".format(
diff --git a/tests/test_models.py b/tests/test_models.py
index d69c6b4a..c90d7af6 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -3,13 +3,21 @@
from unittest import skip
from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
+from cms.api import create_page
+
from djangocms_moderation import constants
+from djangocms_moderation.exceptions import (
+ CollectionIsLocked,
+ ObjectAlreadyInCollection,
+)
from djangocms_moderation.models import (
ConfirmationFormSubmission,
ConfirmationPage,
+ ModerationCollection,
ModerationRequest,
ModerationRequestAction,
Role,
@@ -364,7 +372,7 @@ def test_compliance_number(self, mock_uuid):
mock_uuid.return_value = 'abc123'
request = ModerationRequest.objects.create(
- content_object=self.pg1,
+ content_object=self.pg4,
language='en',
is_active=True,
collection=self.collection1,
@@ -501,3 +509,69 @@ def test_get_by_user_name(self):
confirmation_page=self.cp,
)
self.assertEqual(cfs.get_by_user_name(), self.user.username)
+
+
+class ModerationCollectionTest(BaseTestCase):
+ def setUp(self):
+ self.collection1 = ModerationCollection.objects.create(
+ author=self.user, name='My collection 1', workflow=self.wf1
+ )
+ self.collection2 = ModerationCollection.objects.create(
+ author=self.user, name='My collection 2', workflow=self.wf1
+ )
+
+ self.page1 = create_page(title='My page 1', template='page.html', language='en',)
+ self.page2 = create_page(title='My page 2', template='page.html', language='en',)
+
+ def _moderation_requests_count(self, obj, collection=None):
+ """
+ How many moderation requests are there [for a given collection]
+ :return:
+ """
+ content_type = ContentType.objects.get_for_model(obj)
+ queryset = ModerationRequest.objects.filter(
+ content_type=content_type,
+ object_id=obj.pk,
+ )
+ if collection:
+ queryset = queryset.filter(collection=collection)
+ return queryset.count()
+
+ def test_add_object(self):
+ self.assertEqual(0, self._moderation_requests_count(self.page1))
+ # Add `page1` to `collection1`
+ self.collection1.add_object(self.page1)
+ self.assertEqual(1, self._moderation_requests_count(self.page1))
+ self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
+
+ # Adding the same object to the same collection will raise an exception
+ with self.assertRaises(ObjectAlreadyInCollection):
+ self.collection1.add_object(self.page1)
+
+ self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
+ self.assertEqual(1, self._moderation_requests_count(self.page1))
+
+ # This should not work as `page1` is already part of `collection1`
+ with self.assertRaises(ObjectAlreadyInCollection):
+ self.collection2.add_object(self.page1)
+
+ # We can add `page2` to the `collection1` as it is not there yet
+ self.assertEqual(0, self._moderation_requests_count(self.page2))
+ self.collection1.add_object(self.page2)
+ self.assertEqual(1, self._moderation_requests_count(self.page2))
+ self.assertEqual(1, self._moderation_requests_count(self.page2, self.collection1))
+ self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
+
+ def test_create_moderation_request_from_content_object_locked_collection(self):
+ # This works, as the collection is not locked
+ self.collection2.add_object(self.page1)
+ self.assertEqual(1, self._moderation_requests_count(self.page1))
+
+ # Now, let's lock the collection, so we can't add to it anymore
+ self.collection2.is_locked = True
+ self.collection2.save()
+
+ with self.assertRaises(CollectionIsLocked):
+ self.collection2.add_object(self.page1)
+
+ self.assertEqual(1, self._moderation_requests_count(self.page1))
diff --git a/tests/utils/base.py b/tests/utils/base.py
index 36dee07c..8c78bce8 100644
--- a/tests/utils/base.py
+++ b/tests/utils/base.py
@@ -56,16 +56,22 @@ def setUpTestData(cls):
cls.wf3st2 = cls.wf3.steps.create(role=cls.role3, is_required=False, order=2,)
# create page moderation requests and actions
- cls.collection1 = ModerationCollection.objects.create(name='Collection 1', workflow=cls.wf1)
- cls.collection2 = ModerationCollection.objects.create(name='Collection 2', workflow=cls.wf2)
- cls.collection3 = ModerationCollection.objects.create(name='Collection 3', workflow=cls.wf3)
+ cls.collection1 = ModerationCollection.objects.create(
+ author=cls.user, name='Collection 1', workflow=cls.wf1
+ )
+ cls.collection2 = ModerationCollection.objects.create(
+ author=cls.user, name='Collection 2', workflow=cls.wf2
+ )
+ cls.collection3 = ModerationCollection.objects.create(
+ author=cls.user, name='Collection 3', workflow=cls.wf3
+ )
cls.moderation_request1 = ModerationRequest.objects.create(
content_object=cls.pg1, language='en', collection=cls.collection1, is_active=True,)
cls.moderation_request1.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
ModerationRequest.objects.create(
- content_object=cls.pg1, language='en', collection=cls.collection1, is_active=False,)
+ content_object=cls.pg3, language='en', collection=cls.collection1, is_active=False,)
ModerationRequest.objects.create(
content_object=cls.pg2, language='en', collection=cls.collection2, is_active=False,)
From 83ff5284813a84fc63d9966cae3d402a3aa16d6b Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Thu, 2 Aug 2018 11:22:33 +0100
Subject: [PATCH 004/147] Added collection admin views 1.0.x (#36)
---
djangocms_moderation/admin.py | 82 +++++++++++++++++--
.../migrations/0003_auto_20180724_1521.py | 26 ++++++
djangocms_moderation/models.py | 5 ++
.../moderation_request_change_list.html | 11 +++
4 files changed, 118 insertions(+), 6 deletions(-)
create mode 100644 djangocms_moderation/migrations/0003_auto_20180724_1521.py
create mode 100644 djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index b0e4f490..95618341 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -22,6 +22,7 @@
from .models import (
ConfirmationFormSubmission,
ConfirmationPage,
+ ModerationCollection,
ModerationRequest,
ModerationRequestAction,
Role,
@@ -77,16 +78,40 @@ def form_submission(self, obj):
class ModerationRequestAdmin(admin.ModelAdmin):
+ actions = None # remove `delete_selected` for now, it will be handled later
inlines = [ModerationRequestActionInline]
- list_display = ['id', 'language', 'collection', 'show_status', 'date_sent']
- list_filter = ['language', 'collection', 'id', 'compliance_number']
- fields = ['id', 'collection', 'workflow', 'language', 'is_active', 'show_status', 'compliance_number']
+ list_display = ['id', 'content_type', 'get_title', 'collection', 'get_preview_link', 'get_status']
+ list_filter = ['collection']
+ fields = ['id', 'collection', 'workflow', 'is_active', 'get_status']
readonly_fields = fields
+ change_list_template = 'djangocms_moderation/moderation_request_change_list.html'
+
+ def get_title(self, obj):
+ return obj.content_object
+ get_title.short_description = _('Title')
+
+ def get_preview_link(self, obj):
+ # TODO this will return Version object preview link once implemented
+ return "Link placeholder"
+ get_preview_link.short_description = _('Preview')
def has_add_permission(self, request):
return False
- def show_status(self, obj):
+ def changelist_view(self, request, extra_context=None):
+ # If we filter by a specific collection, we want to add this collection
+ # to the context
+ collection_id = request.GET.get('collection__id__exact')
+ if collection_id:
+ try:
+ collection = ModerationCollection.objects.get(pk=int(collection_id))
+ except (ValueError, ModerationCollection.DoesNotExist):
+ pass
+ else:
+ extra_context = dict(collection=collection)
+ return super(ModerationRequestAdmin, self).changelist_view(request, extra_context)
+
+ def get_status(self, obj):
if obj.is_approved():
status = ugettext('Ready for publishing')
elif obj.is_active and obj.has_pending_step():
@@ -102,7 +127,7 @@ def show_status(self, obj):
}
status = ugettext('%(action)s by %(name)s') % message_data
return status
- show_status.short_description = _('Status')
+ get_status.short_description = _('Status')
class RoleAdmin(admin.ModelAdmin):
@@ -123,7 +148,51 @@ def get_extra(self, request, obj=None, **kwargs):
class WorkflowAdmin(admin.ModelAdmin):
inlines = [WorkflowStepInline]
list_display = ['name', 'is_default']
- fields = ['name', 'is_default', 'identifier', 'requires_compliance_number', 'compliance_number_backend']
+ fields = [
+ 'name',
+ 'is_default',
+ 'identifier',
+ 'requires_compliance_number',
+ 'compliance_number_backend',
+ ]
+
+
+class ModerationCollectionAdmin(admin.ModelAdmin):
+ actions = None # remove `delete_selected` for now, it will be handled later
+ list_display = [
+ 'id',
+ 'get_name_with_requests_link',
+ 'get_moderator',
+ 'workflow',
+ 'status',
+ 'is_locked',
+ 'date_created',
+ ]
+
+ def get_name_with_requests_link(self, obj):
+ """
+ Name of the collection should link to the list of associated
+ moderation requests
+ """
+ return format_html(
+ '{}',
+ reverse('admin:djangocms_moderation_moderationrequest_changelist'),
+ obj.pk,
+ obj.name,
+ )
+ get_name_with_requests_link.short_description = _('Name')
+
+ def get_moderator(self, obj):
+ return obj.author
+ get_moderator.short_description = _('Moderator')
+
+ def status(self, obj):
+ # TODO more statuses to come in the future, once implemented.
+ # It will very likely be a ModerationCollection.status field
+ if obj.is_locked:
+ return _("In review")
+ return _("Collection")
+ status.short_description = _('Status')
class ExtendedPageAdmin(PageAdmin):
@@ -228,6 +297,7 @@ def form_data(self, obj):
admin.site._registry[Page] = ExtendedPageAdmin(Page, admin.site)
admin.site.register(ModerationRequest, ModerationRequestAdmin)
+admin.site.register(ModerationCollection, ModerationCollectionAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(Workflow, WorkflowAdmin)
diff --git a/djangocms_moderation/migrations/0003_auto_20180724_1521.py b/djangocms_moderation/migrations/0003_auto_20180724_1521.py
new file mode 100644
index 00000000..63533099
--- /dev/null
+++ b/djangocms_moderation/migrations/0003_auto_20180724_1521.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-07-24 14:21
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('djangocms_moderation', '0002_moderationcollection_author'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='moderationcollection',
+ name='date_created',
+ field=models.DateTimeField(auto_now_add=True),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='moderationcollection',
+ name='date_modified',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 453577dd..16c9d795 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -281,6 +281,11 @@ class ModerationCollection(models.Model):
)
# TODO: proper implementations and handlers coming later for is_locked
is_locked = models.BooleanField(verbose_name=_('is locked'), default=False)
+ date_created = models.DateTimeField(auto_now_add=True)
+ date_modified = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return self.name
def add_object(self, content_object):
"""
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
new file mode 100644
index 00000000..ee4addab
--- /dev/null
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -0,0 +1,11 @@
+{% extends "admin/change_list.html" %}
+
+{% block content %}
+{{ block.super }}
+
+{% comment %}
+ TODO This template will be overridden to provide the required functionality.
+ For now, lets just output the selected collection
+{% endcomment %}
+Selected collection: {{ collection }}
+{% endblock %}
From 465cd74cb6560429f8f64c358bf5a5c901181441 Mon Sep 17 00:00:00 2001
From: Paulo Alvarado
Date: Mon, 6 Aug 2018 19:16:40 +0100
Subject: [PATCH 005/147] Use master aldryn-forms branch
---
tests/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 9ea1f073..e31ee985 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -3,6 +3,6 @@ tox
coverage
flake8
isort
-aldryn-forms>=3.0.3
+-e git+git://github.com/aldryn/aldryn-forms.git@master#egg=aldryn-forms
mock
-e git+git://github.com/divio/djangocms-versioning.git@master#egg=djangocms-versioning
From 4f4570734ec761286f345917eb645b836b59bb9c Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Mon, 6 Aug 2018 19:16:56 +0100
Subject: [PATCH 006/147] Rework app registration integration with versioning
(#37)
---
djangocms_moderation/cms_config.py | 13 +++++----
tests/test_app_registration.py | 42 ++++++++++++++----------------
tests/utils/app_1/cms_config.py | 20 +++++++++++---
tests/utils/app_1/models.py | 16 +++++++-----
tests/utils/app_2/cms_config.py | 22 ++++++++++++----
tests/utils/app_2/models.py | 16 +++++++-----
6 files changed, 78 insertions(+), 51 deletions(-)
diff --git a/djangocms_moderation/cms_config.py b/djangocms_moderation/cms_config.py
index 0ff35c3c..f4e1e5db 100644
--- a/djangocms_moderation/cms_config.py
+++ b/djangocms_moderation/cms_config.py
@@ -1,3 +1,4 @@
+from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from cms.app_base import CMSAppExtension
@@ -11,16 +12,18 @@ def __init__(self):
def configure_app(self, cms_config):
versioning_enabled = getattr(cms_config, 'djangocms_versioning_enabled', False)
moderated_models = getattr(cms_config, 'moderated_models', [])
- versioning_models = getattr(cms_config, 'versioning_models', [])
if not versioning_enabled:
raise ImproperlyConfigured('Versioning needs to be enabled for Moderation')
- for moderated_model in moderated_models:
- if moderated_model not in versioning_models:
+ versioning_extension = apps.get_app_config('djangocms_versioning').cms_extension
+
+ for model in moderated_models:
+ # @todo replace this with a to be provided func from versioning_extensions
+ if model not in versioning_extension.versionables_by_content:
raise ImproperlyConfigured(
- 'Moderated models need to be Versionable, please include every '
- 'model that needs to be moderated in versioning_models entry'
+ 'Moderated model %s need to be Versionable, please include every model that '
+ 'needs to be moderated in djangocms_versioning VersionableItem entry' % model
)
self.moderated_models.extend(moderated_models)
diff --git a/tests/test_app_registration.py b/tests/test_app_registration.py
index c44d50f3..bca219b6 100644
--- a/tests/test_app_registration.py
+++ b/tests/test_app_registration.py
@@ -4,6 +4,7 @@
from mock import Mock
from unittest import TestCase
+from unittest.mock import patch
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
@@ -14,8 +15,8 @@
from djangocms_moderation.cms_config import ModerationExtension
-from .utils.app_1.models import TestModel3, TestModel4
-from .utils.app_2.models import TestModel1, TestModel2
+from .utils.app_1.models import App1PostContent, App1TitleContent
+from .utils.app_2.models import App2PostContent, App2TitleContent
class CMSConfigTest(CMSTestCase, TestCase):
@@ -27,41 +28,31 @@ def setUp(self):
def test_missing_versioning_enabled(self):
extension = ModerationExtension()
cms_config = Mock(
- moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
+ moderated_models=[App1PostContent, App1TitleContent, App2PostContent, App2TitleContent],
djangocms_moderation_enabled=True,
djangocms_versioning_enabled=False,
app_config=Mock(label='blah_cms_config')
)
- with self.assertRaises(ImproperlyConfigured):
+ err_msg = 'Versioning needs to be enabled for Moderation'
+ with self.assertRaisesMessage(ImproperlyConfigured, err_msg):
extension.configure_app(cms_config)
- def test_moderated_model_not_in_versioning_models(self):
+ @patch('django.apps.apps.get_app_config')
+ def test_model_not_in_versionables_by_content(self, get_app_config):
extension = ModerationExtension()
cms_config = Mock(
+ moderated_models=[App1PostContent],
djangocms_moderation_enabled=True,
- moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
- versioning_models=[TestModel3, TestModel4],
+ djangocms_versioning_enabled=True,
app_config=Mock(label='blah_cms_config')
)
- with self.assertRaises(ImproperlyConfigured):
- extension.configure_app(cms_config)
+ err_msg = 'Moderated model %s need to be Versionable, please include every model that ' \
+ 'needs to be moderated in djangocms_versioning VersionableItem entry' % App1PostContent
- def test_valid_cms_config(self):
- extension = ModerationExtension()
- cms_config = Mock(
- djangocms_moderation_enabled=True,
- moderated_models=[TestModel1, TestModel2, TestModel3, TestModel4],
- versioning_models=[TestModel1, TestModel2, TestModel3, TestModel4],
- app_config=Mock(label='blah_cms_config')
- )
-
- extension.configure_app(cms_config)
- self.assertTrue(TestModel1 in extension.moderated_models)
- self.assertTrue(TestModel2 in extension.moderated_models)
- self.assertTrue(TestModel3 in extension.moderated_models)
- self.assertTrue(TestModel4 in extension.moderated_models)
+ with self.assertRaisesMessage(ImproperlyConfigured, err_msg):
+ extension.configure_app(cms_config)
class CMSConfigIntegrationTest(CMSTestCase):
@@ -69,10 +60,15 @@ class CMSConfigIntegrationTest(CMSTestCase):
def setUp(self):
app_registration.get_cms_extension_apps.cache_clear()
app_registration.get_cms_config_apps.cache_clear()
+ self.moderated_models = (App2PostContent, App2TitleContent,
+ App1PostContent, App1TitleContent)
def test_config_with_two_apps(self):
setup_cms_apps()
moderation_config = apps.get_app_config('djangocms_moderation')
registered_model = moderation_config.cms_extension.moderated_models
+ for model in self.moderated_models:
+ self.assertIn(model, registered_model)
+
self.assertEqual(len(registered_model), 4)
diff --git a/tests/utils/app_1/cms_config.py b/tests/utils/app_1/cms_config.py
index bb1b63fd..83ec92c4 100644
--- a/tests/utils/app_1/cms_config.py
+++ b/tests/utils/app_1/cms_config.py
@@ -1,10 +1,22 @@
from cms.app_base import CMSAppConfig
-from .models import TestModel3, TestModel4
+from djangocms_versioning.datastructures import VersionableItem
+
+from .models import App1PostContent, App1TitleContent
class CMSApp1Config(CMSAppConfig):
- djangocms_versioning_enabled = True
djangocms_moderation_enabled = True
- versioning_models = [TestModel3, TestModel4]
- moderated_models = [TestModel3, TestModel4]
+ djangocms_versioning_enabled = True
+ moderated_models = (App1PostContent, App1TitleContent)
+
+ versioning = [
+ VersionableItem(
+ content_model=App1PostContent,
+ grouper_field_name='post'
+ ),
+ VersionableItem(
+ content_model=App1TitleContent,
+ grouper_field_name='title'
+ )
+ ]
diff --git a/tests/utils/app_1/models.py b/tests/utils/app_1/models.py
index d58a1a5e..c0dd2ec2 100644
--- a/tests/utils/app_1/models.py
+++ b/tests/utils/app_1/models.py
@@ -1,15 +1,17 @@
from django.db import models
-from djangocms_versioning.models import BaseVersion
+class App1Post(models.Model):
+ pass
-class GrouperModel(models.Model):
- content = models.CharField(max_length=255)
+class App1PostContent(models.Model):
+ post = models.ForeignKey(App1Post, on_delete=models.CASCADE)
-class TestModel3(BaseVersion):
- content = models.ForeignKey(GrouperModel)
+class App1Title(models.Model):
+ pass
-class TestModel4(BaseVersion):
- content = models.ForeignKey(GrouperModel)
+
+class App1TitleContent(models.Model):
+ title = models.ForeignKey(App1Title, on_delete=models.CASCADE)
diff --git a/tests/utils/app_2/cms_config.py b/tests/utils/app_2/cms_config.py
index 2b5827f0..425d28a6 100644
--- a/tests/utils/app_2/cms_config.py
+++ b/tests/utils/app_2/cms_config.py
@@ -1,10 +1,22 @@
from cms.app_base import CMSAppConfig
-from .models import TestModel1, TestModel2
+from djangocms_versioning.datastructures import VersionableItem
+from .models import App2PostContent, App2TitleContent
-class CMSApp2Config(CMSAppConfig):
- djangocms_versioning_enabled = True
+
+class CMSApp1Config(CMSAppConfig):
djangocms_moderation_enabled = True
- versioning_models = [TestModel1, TestModel2]
- moderated_models = [TestModel1, TestModel2]
+ djangocms_versioning_enabled = True
+ moderated_models = (App2PostContent, App2TitleContent)
+
+ versioning = [
+ VersionableItem(
+ content_model=App2PostContent,
+ grouper_field_name='post'
+ ),
+ VersionableItem(
+ content_model=App2TitleContent,
+ grouper_field_name='title'
+ )
+ ]
diff --git a/tests/utils/app_2/models.py b/tests/utils/app_2/models.py
index 5e92c245..692ba5a5 100644
--- a/tests/utils/app_2/models.py
+++ b/tests/utils/app_2/models.py
@@ -1,15 +1,17 @@
from django.db import models
-from djangocms_versioning.models import BaseVersion
+class App2Post(models.Model):
+ pass
-class GrouperModel(models.Model):
- content = models.CharField(max_length=255)
+class App2PostContent(models.Model):
+ post = models.ForeignKey(App2Post, on_delete=models.CASCADE)
-class TestModel1(BaseVersion):
- content = models.ForeignKey(GrouperModel)
+class App2Title(models.Model):
+ pass
-class TestModel2(BaseVersion):
- content = models.ForeignKey(GrouperModel)
+
+class App2TitleContent(models.Model):
+ title = models.ForeignKey(App2Title, on_delete=models.CASCADE)
From 51bbd9064c1f9ecf78b9c28129deab9b647d5115 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 7 Aug 2018 14:11:12 +0100
Subject: [PATCH 007/147] Added submit collection for moderation (#38)
---
djangocms_moderation/admin.py | 32 +++-
djangocms_moderation/emails.py | 36 ++--
djangocms_moderation/forms.py | 57 +++---
djangocms_moderation/models.py | 60 ++++---
.../emails/moderation-request/request.txt | 19 +-
.../moderation_request_change_list.html | 10 ++
.../djangocms_moderation/request_form.html | 2 +-
djangocms_moderation/views.py | 55 +++++-
tests/test_forms.py | 74 +++-----
tests/test_models.py | 88 +++++----
tests/test_views.py | 170 +++++++-----------
11 files changed, 322 insertions(+), 281 deletions(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 95618341..9e405ead 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -109,23 +109,33 @@ def changelist_view(self, request, extra_context=None):
pass
else:
extra_context = dict(collection=collection)
+ if collection.allow_submit_for_moderation:
+ submit_for_moderation_url = reverse(
+ 'admin:cms_moderation_submit_collection_for_moderation',
+ args=(collection_id,)
+ )
+ extra_context['submit_for_moderation_url'] = submit_for_moderation_url
return super(ModerationRequestAdmin, self).changelist_view(request, extra_context)
def get_status(self, obj):
+ last_action = obj.get_last_action()
if obj.is_approved():
status = ugettext('Ready for publishing')
elif obj.is_active and obj.has_pending_step():
next_step = obj.get_next_required()
role = next_step.role.name
status = ugettext('Pending %(role)s approval') % {'role': role}
- else:
- last_action = obj.get_last_action()
+ elif last_action:
+ # We can have moderation requests without any action (e.g. the
+ # ones not submitted for moderation yet)
user_name = last_action.get_by_user_name()
message_data = {
'action': last_action.get_action_display(),
'name': user_name,
}
status = ugettext('%(action)s by %(name)s') % message_data
+ else:
+ status = ugettext('Ready for submission')
return status
get_status.short_description = _('Status')
@@ -194,6 +204,19 @@ def status(self, obj):
return _("Collection")
status.short_description = _('Status')
+ def get_urls(self):
+ def _url(regex, fn, name, **kwargs):
+ return url(regex, self.admin_site.admin_view(fn), kwargs=kwargs, name=name)
+
+ url_patterns = [
+ _url(
+ '^(?P\d+)/submit-for-review/$',
+ views.submit_collection_for_moderation,
+ name="cms_moderation_submit_collection_for_moderation",
+ ),
+ ]
+ return url_patterns + super(ModerationCollectionAdmin, self).get_urls()
+
class ExtendedPageAdmin(PageAdmin):
@@ -203,11 +226,6 @@ def _url(regex, fn, name, **kwargs):
return url(regex, self.admin_site.admin_view(fn), kwargs=kwargs, name=name)
url_patterns = [
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/new/$',
- views.new_moderation_request,
- 'new_request',
- ),
_url(
r'^([0-9]+)/([a-z\-]+)/moderation/resubmit/$',
views.resubmit_moderation_request,
diff --git a/djangocms_moderation/emails.py b/djangocms_moderation/emails.py
index 9c88cb55..c68b210e 100644
--- a/djangocms_moderation/emails.py
+++ b/djangocms_moderation/emails.py
@@ -4,12 +4,7 @@
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.utils.encoding import force_text
-from django.utils.translation import (
- override as force_language,
- ugettext_lazy as _,
-)
-
-from cms.utils.conf import get_cms_setting
+from django.utils.translation import ugettext_lazy as _
from .utils import get_absolute_url
@@ -30,12 +25,7 @@
}
-def _send_email(request, action, recipients, subject, template):
- obj = request.content_object
- edit_on = get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
- page_url = obj.get_absolute_url(request.language) + '?' + edit_on
- author_name = request.get_first_action().get_by_user_name()
-
+def _send_email(collection, action, recipients, subject, template):
if action.to_user_id:
moderator_name = action.get_to_user_name()
elif action.to_role_id:
@@ -43,23 +33,19 @@ def _send_email(request, action, recipients, subject, template):
else:
moderator_name = ''
- site = obj.node.site
- admin_url = reverse('admin:djangocms_moderation_moderationrequest_change', args=(request.pk,))
+ admin_url = reverse('admin:djangocms_moderation_moderationcollection_change', args=(collection.pk,))
context = {
- 'page': obj,
- 'page_url': get_absolute_url(page_url, site),
- 'author_name': author_name,
+ 'collection': collection,
+ 'author_name': collection.author_name,
'by_user_name': action.get_by_user_name(),
'moderator_name': moderator_name,
- 'job_id': request.pk,
- 'comment': request.get_last_action().message,
- 'admin_url': get_absolute_url(admin_url, site),
+ 'admin_url': get_absolute_url(admin_url),
}
template = 'djangocms_moderation/emails/moderation-request/{}'.format(template)
- with force_language(request.language):
- subject = force_text(subject)
- content = render_to_string(template, context)
+ # TODO What language should the email be sent in? e.g. `with force_language(lang):`
+ subject = force_text(subject)
+ content = render_to_string(template, context)
message = EmailMessage(
subject=subject,
@@ -88,7 +74,7 @@ def notify_request_author(request, action):
return status
-def notify_requested_moderator(request, action):
+def notify_collection_moderators(collection, action):
if action.to_user_id and not action.to_user.email:
return 0
@@ -102,7 +88,7 @@ def notify_requested_moderator(request, action):
return 0
status = _send_email(
- request=request,
+ collection=collection,
action=action,
recipients=recipients,
subject=_('Review requested'),
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index f20f9068..62b27bd6 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -41,7 +41,7 @@ def validate_unique(self):
selected_roles.append(selected_role.pk)
-class ModerationRequestForm(forms.Form):
+class UpdateModerationRequestForm(forms.Form):
moderator = forms.ModelChoiceField(
label=_('moderator'),
queryset=get_user_model().objects.none(),
@@ -60,28 +60,8 @@ def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.workflow = kwargs.pop('workflow')
self.active_request = kwargs.pop('active_request')
- super(ModerationRequestForm, self).__init__(*args, **kwargs)
-
- if 'moderator' in self.fields:
- self.configure_moderator_field()
-
- def configure_moderator_field(self):
- next_role = self.workflow.first_step.role
- users = next_role.get_users_queryset().exclude(pk=self.user.pk)
- self.fields['moderator'].empty_label = ugettext('Any {role}').format(role=next_role.name)
- self.fields['moderator'].queryset = users
-
- def save(self):
- self.workflow.submit_new_request(
- obj=self.page,
- by_user=self.user,
- to_user=self.cleaned_data.get('moderator'),
- language=self.language,
- message=self.cleaned_data['message'],
- )
-
-
-class UpdateModerationRequestForm(ModerationRequestForm):
+ super(UpdateModerationRequestForm, self).__init__(*args, **kwargs)
+ self.configure_moderator_field()
def configure_moderator_field(self):
# For cancelling and rejecting, we don't need to display a moderator
@@ -116,3 +96,34 @@ def save(self):
to_user=self.cleaned_data.get('moderator'),
message=self.cleaned_data['message'],
)
+
+
+class SubmitCollectionForModerationForm(forms.Form):
+ moderator = forms.ModelChoiceField(
+ label=_('moderator'),
+ queryset=get_user_model().objects.none(),
+ required=False,
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.collection = kwargs.pop('collection')
+ self.user = kwargs.pop('user')
+ super(SubmitCollectionForModerationForm, self).__init__(*args, **kwargs)
+ self.configure_moderator_field()
+
+ def configure_moderator_field(self):
+ next_role = self.collection.workflow.first_step.role
+ users = next_role.get_users_queryset().exclude(pk=self.user.pk)
+ self.fields['moderator'].empty_label = ugettext('Any {role}').format(role=next_role.name)
+ self.fields['moderator'].queryset = users
+
+ def clean(self):
+ if not self.collection.allow_submit_for_moderation:
+ self.add_error(None, _("This collection can't be submitted for a review"))
+ return super(SubmitCollectionForModerationForm, self).clean()
+
+ def save(self):
+ self.collection.submit_for_moderation(
+ by_user=self.user,
+ to_user=self.cleaned_data.get('moderator'),
+ )
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 16c9d795..9140da53 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -16,7 +16,7 @@
from cms.models.fields import PlaceholderField
-from .emails import notify_request_author, notify_requested_moderator
+from .emails import notify_collection_moderators
from .exceptions import CollectionIsLocked, ObjectAlreadyInCollection
from .utils import generate_compliance_number
@@ -200,23 +200,6 @@ def has_active_request(self, page, language):
lookup = self._lookup_active_request(page, language)
return lookup.exists()
- @transaction.atomic
- def submit_new_request(self, by_user, obj, language, message='', to_user=None):
- request = self.requests.create(
- content_object=obj,
- language=language,
- is_active=True,
- workflow=self,
- )
- new_action = request.actions.create(
- by_user=by_user,
- to_user=to_user,
- action=constants.ACTION_STARTED,
- message=message,
- )
- notify_requested_moderator(request, new_action)
- return request
-
@python_2_unicode_compatible
class WorkflowStep(models.Model):
@@ -287,6 +270,37 @@ class ModerationCollection(models.Model):
def __str__(self):
return self.name
+ @property
+ def author_name(self):
+ return self.author.get_full_name() or self.author.get_username()
+
+ def submit_for_moderation(self, by_user, to_user=None):
+ """
+ Submit all the moderation requests belonging to this collection for
+ moderation and mark the collection as locked
+ """
+ for moderation_request in self.moderation_requests.all():
+ action = moderation_request.actions.create(
+ by_user=by_user,
+ to_user=to_user,
+ action=constants.ACTION_STARTED,
+ )
+ # Lock the collection as it has been now submitted for moderation
+ self.is_locked = True
+ self.save(update_fields=['is_locked'])
+ # It is fine to pass any `action` from any moderation_request.actions
+ # above as it will have the same moderators
+ notify_collection_moderators(collection=self, action=action)
+
+ @property
+ def allow_submit_for_moderation(self):
+ """
+ Can this collection submitted for moderation?
+ :return:
+ """
+ # TODO limited check for now, consider makings this a model field
+ return not self.is_locked and self.moderation_requests.exists()
+
def add_object(self, content_object):
"""
Add object to the ModerationRequest in this collection.
@@ -412,7 +426,7 @@ def update_status(self, action, by_user, message='', to_user=None):
)
self.save(update_fields=['is_active'])
- new_action = self.actions.create(
+ self.actions.create(
by_user=by_user,
to_user=to_user,
action=action,
@@ -420,10 +434,6 @@ def update_status(self, action, by_user, message='', to_user=None):
step_approved=step_approved,
)
- if new_action.to_user_id or new_action.to_role_id:
- notify_requested_moderator(self, new_action)
- notify_request_author(self, new_action)
-
if self.should_set_compliance_number():
self.set_compliance_number()
@@ -507,7 +517,7 @@ def user_can_moderate(self, user):
return False
def user_is_author(self, user):
- return user == self.get_first_action().by_user
+ return user == self.author
def user_can_view_comments(self, user):
return self.user_is_author(user) or self.user_can_moderate(user)
@@ -597,7 +607,7 @@ def get_to_user_name(self):
return self._get_user_name(self.to_user)
def _get_user_name(self, user):
- return user.get_full_name() or getattr(user, user.USERNAME_FIELD)
+ return user.get_full_name() or user.get_username()
def save(self, **kwargs):
"""
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
index c4501a8c..01c003c3 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
@@ -1,14 +1,29 @@
{% load i18n %}
-{% blocktrans %}
-Hello {{ moderator_name }}, {{ by_user_name }} has requested your approval for changes in {{ page_url }}.
+{% blocktrans with collection.name as collection_name %}
+Hello {{ moderator_name }}, {{ by_user_name }} has requested your approval for
+changes in the collection {{ collection_name }}.
{% endblocktrans %}
+
+{% trans 'Items included in this collection:' %}
+
+
+{% for moderation_request in collection.moderation_requests.all %}
+ -
+ {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+
+{% endfor %}
+
+
+
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
{% endif %}
+{% if job_id %}
{% trans 'Job ID' %}: {{ job_id }}
+{% endif %}
{% if admin_url %}
{% trans 'Admin Url' %}:
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
index ee4addab..8516d60a 100644
--- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -1,4 +1,14 @@
{% extends "admin/change_list.html" %}
+{% load i18n %}
+
+{% block object-tools-items %}
+{{ block.super }}
+{% if submit_for_moderation_url %}
+
+ {% trans 'Submit for review' %}
+
+{% endif %}
+{% endblock %}
{% block content %}
{{ block.super }}
diff --git a/djangocms_moderation/templates/djangocms_moderation/request_form.html b/djangocms_moderation/templates/djangocms_moderation/request_form.html
index c7ded030..546d3902 100644
--- a/djangocms_moderation/templates/djangocms_moderation/request_form.html
+++ b/djangocms_moderation/templates/djangocms_moderation/request_form.html
@@ -2,7 +2,7 @@
{% load i18n admin_urls %}
{% block content %}
- {% if errors %}
+ {% if errors or adminform.non_field_errors %}
{% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 92568a9a..135bb288 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -13,7 +13,10 @@
from cms.utils.urlutils import add_url_parameters
-from .forms import ModerationRequestForm, UpdateModerationRequestForm
+from .forms import (
+ SubmitCollectionForModerationForm,
+ UpdateModerationRequestForm,
+)
from .helpers import (
get_active_moderation_request,
get_moderation_workflow,
@@ -22,6 +25,7 @@
from .models import (
ConfirmationFormSubmission,
ConfirmationPage,
+ ModerationCollection,
ModerationRequest,
)
from .utils import get_admin_url
@@ -135,13 +139,6 @@ def get_context_data(self, **kwargs):
return context
-new_moderation_request = ModerationRequestView.as_view(
- action=constants.ACTION_STARTED,
- page_title=_('Submit for moderation'),
- form_class=ModerationRequestForm,
- success_message=_('The page has been sent for moderation.'),
-)
-
cancel_moderation_request = ModerationRequestView.as_view(
action=constants.ACTION_CANCELLED,
page_title=_('Cancel request'),
@@ -240,3 +237,45 @@ def moderation_confirmation_page(request, confirmation_id):
reviewed=True,
)
return render(request, confirmation_page_instance.template, context)
+
+
+class SubmitCollectionForModeration(FormView):
+ template_name = 'djangocms_moderation/request_form.html'
+ form_class = SubmitCollectionForModerationForm
+ collection = None # Populated in dispatch method
+
+ def dispatch(self, request, *args, **kwargs):
+ self.collection = get_object_or_404(
+ ModerationCollection,
+ pk=self.kwargs['collection_id'],
+ )
+ return super(SubmitCollectionForModeration, self).dispatch(request, *args, **kwargs)
+
+ def get_form_kwargs(self):
+ kwargs = super(SubmitCollectionForModeration, self).get_form_kwargs()
+ kwargs['collection'] = self.collection
+ kwargs['user'] = self.request.user
+ return kwargs
+
+ def form_valid(self, form):
+ form.save()
+ messages.success(self.request, _("Your collection has been submitted for a review"))
+ # Redirect back to the collection filtered moderation request change list
+ redirect_url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
+ redirect_url = "{}?collection__id__exact={}".format(
+ redirect_url,
+ self.collection.id
+ )
+ return HttpResponseRedirect(redirect_url)
+
+ def get_context_data(self, **kwargs):
+ context = super(SubmitCollectionForModeration, self).get_context_data(**kwargs)
+ context.update({
+ 'opts': ModerationCollection._meta,
+ 'title': _('Submit collection for review'),
+ 'adminform': context['form'],
+ })
+ return context
+
+
+submit_collection_for_moderation = SubmitCollectionForModeration.as_view()
diff --git a/tests/test_forms.py b/tests/test_forms.py
index c622e4a8..28cd3eb3 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -1,59 +1,18 @@
-from mock import MagicMock
+import mock
from django.contrib.auth.models import User
from django.forms import HiddenInput
from djangocms_moderation import constants
from djangocms_moderation.forms import (
- ModerationRequestForm,
+ SubmitCollectionForModerationForm,
UpdateModerationRequestForm,
)
+from djangocms_moderation.models import ModerationCollection
from .utils.base import BaseTestCase
-class ModerationRequestFormTest(BaseTestCase):
-
- def test_form_init(self):
- form = ModerationRequestForm(
- action=constants.ACTION_STARTED,
- language='en',
- page=self.pg2,
- user=self.user,
- workflow=self.wf1,
- active_request=None,
- )
- self.assertIn('moderator', form.fields)
- field_moderator = form.fields['moderator']
- self.assertEqual(field_moderator.empty_label, 'Any Role 1')
- self.assertQuerysetEqual(field_moderator.queryset, User.objects.none())
-
- def test_form_save(self):
- data = {
- 'moderator': None,
- 'message': 'Some message'
- }
- form = ModerationRequestForm(
- data,
- action=constants.ACTION_STARTED,
- language='en',
- page=self.pg2,
- user=self.user,
- workflow=self.wf1,
- active_request=None,
- )
- form.workflow.submit_new_request = MagicMock()
- self.assertTrue(form.is_valid())
- form.save()
- form.workflow.submit_new_request.assert_called_once_with(
- obj=self.pg2,
- by_user=self.user,
- to_user=None,
- language='en',
- message='Some message',
- )
-
-
class UpdateModerationRequestFormTest(BaseTestCase):
def test_form_init_approved_action(self):
@@ -65,7 +24,6 @@ def test_form_init_approved_action(self):
workflow=self.wf1,
active_request=self.moderation_request1,
)
- self.assertIsInstance(form, ModerationRequestForm)
field_moderator = form.fields['moderator']
self.assertEqual(field_moderator.empty_label, 'Any Role 2')
self.assertQuerysetEqual(
@@ -115,7 +73,7 @@ def test_form_save(self):
workflow=self.wf1,
active_request=self.moderation_request1,
)
- form.active_request.update_status = MagicMock()
+ form.active_request.update_status = mock.MagicMock()
self.assertTrue(form.is_valid())
form.save()
form.active_request.update_status.assert_called_once_with(
@@ -124,3 +82,27 @@ def test_form_save(self):
to_user=None,
message='Approved message',
)
+
+
+class SubmitCollectionForModerationFormTest(BaseTestCase):
+ @mock.patch.object(ModerationCollection, 'allow_submit_for_moderation')
+ def test_form_is_invalid_if_collection_cant_be_submitted_for_review(self, allow_submit_mock):
+ data = {
+ 'moderator': None,
+ }
+
+ allow_submit_mock.__get__ = mock.Mock(return_value=False)
+ form = SubmitCollectionForModerationForm(
+ data,
+ collection=self.collection1,
+ user=self.user,
+ )
+ self.assertFalse(form.is_valid())
+
+ allow_submit_mock.__get__ = mock.Mock(return_value=True)
+ form = SubmitCollectionForModerationForm(
+ data,
+ collection=self.collection1,
+ user=self.user,
+ )
+ self.assertTrue(form.is_valid())
diff --git a/tests/test_models.py b/tests/test_models.py
index c90d7af6..de6e086b 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,6 +1,5 @@
import json
from mock import patch
-from unittest import skip
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
@@ -71,23 +70,6 @@ def test_multiple_defaults_validation_error(self):
def test_first_step(self):
self.assertEqual(self.wf1.first_step, self.wf1st1)
- @skip('4.0 rework TBC')
- @patch('djangocms_moderation.models.notify_requested_moderator')
- def test_submit_new_request(self, mock_nrm):
- request = self.wf1.submit_new_request(
- by_user=self.user,
- obj=self.pg3,
- language='en',
- message='Some message',
- )
- self.assertQuerysetEqual(
- request.actions.all(),
- ModerationRequestAction.objects.filter(request=request),
- transform=lambda x: x,
- ordered=False,
- )
- self.assertEqual(mock_nrm.call_count, 1)
-
class WorkflowStepTest(BaseTestCase):
@@ -258,9 +240,7 @@ def test_user_can_moderate(self):
self.assertTrue(self.moderation_request4.user_can_moderate(self.user3))
self.assertFalse(self.moderation_request4.user_can_moderate(user4))
- @patch('djangocms_moderation.models.notify_request_author')
- @patch('djangocms_moderation.models.notify_requested_moderator')
- def test_update_status_action_approved(self, mock_nrm, mock_nra):
+ def test_update_status_action_approved(self):
self.moderation_request1.update_status(
action=constants.ACTION_APPROVED,
by_user=self.user,
@@ -268,12 +248,8 @@ def test_update_status_action_approved(self, mock_nrm, mock_nra):
)
self.assertTrue(self.moderation_request1.is_active)
self.assertEqual(len(self.moderation_request1.actions.filter(is_archived=False)), 2)
- self.assertEqual(mock_nrm.call_count, 1)
- self.assertEqual(mock_nra.call_count, 1)
- @patch('djangocms_moderation.models.notify_request_author')
- @patch('djangocms_moderation.models.notify_requested_moderator')
- def test_update_status_action_rejected(self, mock_nrm, mock_nra):
+ def test_update_status_action_rejected(self):
self.moderation_request1.update_status(
action=constants.ACTION_REJECTED,
by_user=self.user,
@@ -282,14 +258,7 @@ def test_update_status_action_rejected(self, mock_nrm, mock_nra):
self.assertTrue(self.moderation_request1.is_active)
self.assertEqual(len(self.moderation_request1.actions.all()), 2)
- self.assertEqual(mock_nra.call_count, 1)
- # No need to notify the moderator, as this is assigned back to the
- # content author
- self.assertFalse(mock_nrm.called)
-
- @patch('djangocms_moderation.models.notify_request_author')
- @patch('djangocms_moderation.models.notify_requested_moderator')
- def test_update_status_action_resubmitted(self, mock_nrm, mock_nra):
+ def test_update_status_action_resubmitted(self):
self.moderation_request1.update_status(
action=constants.ACTION_RESUBMITTED,
by_user=self.user,
@@ -298,9 +267,6 @@ def test_update_status_action_resubmitted(self, mock_nrm, mock_nra):
self.assertTrue(self.moderation_request1.is_active)
self.assertEqual(len(self.moderation_request1.actions.all()), 2)
- self.assertEqual(mock_nra.call_count, 1)
- self.assertEqual(mock_nrm.call_count, 1)
-
def test_compliance_number_is_generated(self):
self.wf1.requires_compliance_number = True
self.assertTrue(self.moderation_request1.has_required_pending_steps())
@@ -523,6 +489,52 @@ def setUp(self):
self.page1 = create_page(title='My page 1', template='page.html', language='en',)
self.page2 = create_page(title='My page 2', template='page.html', language='en',)
+ def test_allow_submit_for_moderation(self):
+ self.collection1.is_locked = False
+ self.collection1.save()
+ # This is false, as we don't have any moderation requests in this collection
+ self.assertFalse(self.collection1.allow_submit_for_moderation)
+
+ ModerationRequest.objects.create(
+ content_object=self.pg1, collection=self.collection1, is_active=True
+ )
+ self.assertTrue(self.collection1.allow_submit_for_moderation)
+
+ self.collection1.is_locked = True
+ self.collection1.save()
+ self.assertFalse(self.collection1.allow_submit_for_moderation)
+
+ @patch('djangocms_moderation.models.notify_collection_moderators')
+ def test_submit_for_moderation(self, mock_ncm):
+ ModerationRequest.objects.create(
+ content_object=self.pg1, language='en', collection=self.collection1
+ )
+ ModerationRequest.objects.create(
+ content_object=self.pg3, language='en', collection=self.collection1
+ )
+
+ self.assertFalse(
+ ModerationRequestAction.objects.filter(
+ request__collection=self.collection1
+ ).exists()
+ )
+
+ self.collection1.is_locked = False
+ self.collection1.save()
+
+ self.collection1.submit_for_moderation(self.user, None)
+ self.assertEqual(1, mock_ncm.call_count)
+
+ self.collection1.refresh_from_db()
+ # Collection should lock itself
+ self.assertTrue(self.collection1.is_locked)
+ # We will now have 2 actions with status STARTED.
+ self.assertEqual(
+ 2, ModerationRequestAction.objects.filter(
+ request__collection=self.collection1, action=constants.ACTION_STARTED
+ ).count()
+ )
+
def _moderation_requests_count(self, obj, collection=None):
"""
How many moderation requests are there [for a given collection]
@@ -562,7 +574,7 @@ def test_add_object(self):
self.assertEqual(1, self._moderation_requests_count(self.page2, self.collection1))
self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
- def test_create_moderation_request_from_content_object_locked_collection(self):
+ def test_add_object_locked_collection(self):
# This works, as the collection is not locked
self.collection2.add_object(self.page1)
self.assertEqual(1, self._moderation_requests_count(self.page1))
diff --git a/tests/test_views.py b/tests/test_views.py
index c3ae1b71..70ce9cd9 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,20 +1,18 @@
import json
-from mock import patch
-from unittest import skip
+import mock
from django.contrib.auth.models import User
+from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from cms.utils.urlutils import add_url_parameters
from djangocms_moderation import constants
-from djangocms_moderation.forms import (
- ModerationRequestForm,
- UpdateModerationRequestForm,
-)
+from djangocms_moderation.forms import UpdateModerationRequestForm
from djangocms_moderation.models import (
ConfirmationFormSubmission,
ConfirmationPage,
+ ModerationCollection,
)
from djangocms_moderation.utils import get_admin_url
@@ -36,46 +34,6 @@ def _assert_render(self, response, page, action, workflow, active_request, form_
self.assertEqual(response.context_data['title'], title)
self.assertIsInstance(form, form_cls)
- def test_new_request_view_with_form(self):
- response = self.client.get(
- get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- )
- )
- self._assert_render(
- response=response,
- page=self.pg2,
- action=constants.ACTION_STARTED,
- active_request=None,
- workflow=self.wf1,
- form_cls=ModerationRequestForm,
- title=_('Submit for moderation')
- )
-
- @skip('4.0 rework TBC')
- def test_new_request_view_with_form_workflow_passed_param(self):
- response = self.client.get(
- '{}?{}'.format(
- get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- ),
- 'workflow={}'.format(self.wf2.pk)
- )
- )
- self._assert_render(
- response=response,
- page=self.pg2,
- action=constants.ACTION_STARTED,
- active_request=None,
- workflow=self.wf2,
- form_cls=ModerationRequestForm,
- title=_('Submit for moderation')
- )
-
def test_cancel_request_view_with_form(self):
response = self.client.get(get_admin_url(
name='cms_moderation_cancel_request',
@@ -140,56 +98,6 @@ def test_approve_request_view_with_form(self):
title=_('Approve changes')
)
- def test_get_form_kwargs(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- ))
- view = response.context_data['view']
- kwargs = view.get_form_kwargs()
- self.assertEqual(kwargs.get('action'), view.action)
- self.assertEqual(kwargs.get('language'), view.language)
- self.assertEqual(kwargs.get('page'), view.page)
- self.assertEqual(kwargs.get('user'), view.request.user)
- self.assertEqual(kwargs.get('workflow'), view.workflow)
- self.assertEqual(kwargs.get('active_request'), view.active_request)
-
- @skip('4.0 rework TBC')
- def test_form_valid(self):
- response = self.client.post(get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- ), {'moderator': '', 'message': 'Some review message'})
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'reloadBrowser') # check html part
-
- def test_throws_error_moderation_already_exists(self):
- response = self.client.get('{}?{}'.format(
- get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ),
- 'workflow={}'.format(self.wf1.pk) # pg1 => active request
- ))
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.content, b'Page already has an active moderation request.')
-
- @skip('4.0 rework TBC')
- def test_throws_error_invalid_workflow_passed(self):
- response = self.client.get('{}?{}'.format(
- get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- ),
- 'workflow=10' # pg2 => no active requests, 10 => workflow does not exist
- ))
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.content, b'No moderation workflow exists for page.')
-
def test_throws_no_active_moderation_request(self):
response = self.client.get(get_admin_url(
name='cms_moderation_cancel_request',
@@ -220,16 +128,6 @@ def test_throws_error_forbidden_user(self):
self.assertEqual(response.status_code, 403)
self.assertEqual(response.content, b'User is not allowed to update request.')
- @patch('djangocms_moderation.views.get_moderation_workflow', return_value=None)
- def test_throws_error_if_workflow_has_not_been_resolved(self, mock_gpmw):
- response = self.client.get(get_admin_url(
- name='cms_moderation_new_request',
- language='en',
- args=(self.pg2.pk, 'en')
- ))
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.content, b'No moderation workflow exists for page.')
-
def _create_confirmation_page(self, moderation_request):
# First delete all the form submissions for the passed moderation_request
# This will make sure there are no form submissions
@@ -389,3 +287,63 @@ def test_renders_post_view(self):
reviewed=True,
)
self.assertEqual(response.context['redirect_url'], redirect_url)
+
+
+class SubmitCollectionForModerationViewTest(BaseViewTestCase):
+ def setUp(self):
+ super(SubmitCollectionForModerationViewTest, self).setUp()
+ self.url = reverse(
+ 'admin:cms_moderation_submit_collection_for_moderation',
+ args=(self.collection2.pk,)
+ )
+ request_change_list_url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
+ self.request_change_list_url = "{}?collection__id__exact={}".format(
+ request_change_list_url,
+ self.collection2.pk
+ )
+
+ @mock.patch.object(ModerationCollection, 'submit_for_moderation')
+ def test_submit_collection_for_moderation(self, submit_mock):
+ response = self.client.get(self.url)
+ self.assertEqual(200, response.status_code)
+
+ response = self.client.post(self.url)
+ assert submit_mock.called
+ self.assertEqual(302, response.status_code)
+ self.assertEqual(self.request_change_list_url, response.url)
+
+
+class ModerationRequestChangeListView(BaseViewTestCase):
+ def setUp(self):
+ super(ModerationRequestChangeListView, self).setUp()
+ self.collection_submit_url = reverse(
+ 'admin:cms_moderation_submit_collection_for_moderation',
+ args=(self.collection2.pk,)
+ )
+ self.url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
+ self.url_with_filter = "{}?collection__id__exact={}".format(
+ self.url, self.collection2.pk
+ )
+
+ def test_change_list_view_should_contain_collection_object(self):
+ response = self.client.get(self.url)
+ self.assertEqual(200, response.status_code)
+ self.assertNotIn('collection', response.context)
+
+ response = self.client.get(self.url_with_filter)
+ self.assertEqual(200, response.status_code)
+ self.assertEqual(response.context['collection'], self.collection2)
+
+ @mock.patch.object(ModerationCollection, 'allow_submit_for_moderation')
+ def test_change_list_view_should_contain_submit_collection_url(self, allow_submit_mock):
+ response = self.client.get(self.url)
+ self.assertEqual(200, response.status_code)
+ self.assertNotIn('submit_for_moderation_url', response.context)
+
+ allow_submit_mock.__get__ = mock.Mock(return_value=False)
+ response = self.client.get(self.url_with_filter)
+ self.assertNotIn('submit_for_moderation_url', response.context)
+
+ allow_submit_mock.__get__ = mock.Mock(return_value=True)
+ response = self.client.get(self.url_with_filter)
+ self.assertIn('submit_for_moderation_url', response.context)
From 9307979bfbf8e9ce4b94718412506bbb2b4b2a1a Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Thu, 9 Aug 2018 11:50:54 +0100
Subject: [PATCH 008/147] Logic to add single item to collection (#39)
---
djangocms_moderation/admin.py | 60 +--
djangocms_moderation/cms_config.py | 1 -
djangocms_moderation/cms_toolbars.py | 46 +++
djangocms_moderation/forms.py | 69 ++++
djangocms_moderation/models.py | 13 +
.../item_to_collection.html | 73 ++++
.../djangocms_moderation/request_form.html | 2 +-
djangocms_moderation/views.py | 218 +++--------
tests/test_app_registration.py | 7 +-
tests/test_views.py | 353 ++++++------------
10 files changed, 380 insertions(+), 462 deletions(-)
create mode 100644 djangocms_moderation/cms_toolbars.py
create mode 100644 djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 9e405ead..0b6ce7ca 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -7,16 +7,9 @@
from django.utils.translation import ugettext, ugettext_lazy as _
from cms.admin.placeholderadmin import PlaceholderAdminMixin
-from cms.models import Page
from adminsortable2.admin import SortableInlineAdminMixin
-from .constants import (
- ACTION_APPROVED,
- ACTION_CANCELLED,
- ACTION_REJECTED,
- ACTION_RESUBMITTED,
-)
from .forms import WorkflowStepInlineFormSet
from .helpers import get_form_submission_for_step
from .models import (
@@ -34,12 +27,6 @@
from . import views # isort:skip
-try:
- PageAdmin = admin.site._registry[Page].__class__
-except KeyError:
- from cms.admin.pageadmin import PageAdmin
-
-
class ModerationRequestActionInline(admin.TabularInline):
model = ModerationRequestAction
fields = ['show_user', 'message', 'date_taken', 'form_submission']
@@ -214,49 +201,13 @@ def _url(regex, fn, name, **kwargs):
views.submit_collection_for_moderation,
name="cms_moderation_submit_collection_for_moderation",
),
- ]
- return url_patterns + super(ModerationCollectionAdmin, self).get_urls()
-
-
-class ExtendedPageAdmin(PageAdmin):
-
- def get_urls(self):
- def _url(regex, fn, name, **kwargs):
- name = 'cms_moderation_{}'.format(name)
- return url(regex, self.admin_site.admin_view(fn), kwargs=kwargs, name=name)
-
- url_patterns = [
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/resubmit/$',
- views.resubmit_moderation_request,
- 'resubmit_request',
- action=ACTION_RESUBMITTED,
- ),
_url(
- r'^([0-9]+)/([a-z\-]+)/moderation/cancel/$',
- views.cancel_moderation_request,
- 'cancel_request',
- action=ACTION_CANCELLED,
- ),
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/reject/$',
- views.reject_moderation_request,
- 'reject_request',
- action=ACTION_REJECTED,
- ),
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/approve/$',
- views.approve_moderation_request,
- 'approve_request',
- action=ACTION_APPROVED,
- ),
- _url(
- r'^([0-9]+)/([a-z\-]+)/moderation/comments/$',
- views.moderation_comments,
- 'comments',
- ),
+ r'^item/add-item/$',
+ views.add_item_to_collection,
+ name='cms_moderation_item_to_collection',
+ )
]
- return url_patterns + super(ExtendedPageAdmin, self).get_urls()
+ return url_patterns + super(ModerationCollectionAdmin, self).get_urls()
class ConfirmationPageAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
@@ -313,7 +264,6 @@ def form_data(self, obj):
form_data.short_description = _('Form Data')
-admin.site._registry[Page] = ExtendedPageAdmin(Page, admin.site)
admin.site.register(ModerationRequest, ModerationRequestAdmin)
admin.site.register(ModerationCollection, ModerationCollectionAdmin)
admin.site.register(Role, RoleAdmin)
diff --git a/djangocms_moderation/cms_config.py b/djangocms_moderation/cms_config.py
index f4e1e5db..efbc01cc 100644
--- a/djangocms_moderation/cms_config.py
+++ b/djangocms_moderation/cms_config.py
@@ -17,7 +17,6 @@ def configure_app(self, cms_config):
raise ImproperlyConfigured('Versioning needs to be enabled for Moderation')
versioning_extension = apps.get_app_config('djangocms_versioning').cms_extension
-
for model in moderated_models:
# @todo replace this with a to be provided func from versioning_extensions
if model not in versioning_extension.versionables_by_content:
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
new file mode 100644
index 00000000..cd935a1c
--- /dev/null
+++ b/djangocms_moderation/cms_toolbars.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+from django.utils.translation import ugettext_lazy as _
+
+from cms.api import get_page_draft
+from cms.toolbar_base import CMSToolbar
+from cms.toolbar_pool import toolbar_pool
+from cms.utils.urlutils import add_url_parameters
+
+from .utils import get_admin_url
+
+
+class ModerationToolbar(CMSToolbar):
+ class Media:
+ js = ('djangocms_moderation/js/dist/bundle.moderation.min.js',)
+ css = {
+ 'all': ('djangocms_moderation/css/moderation.css',)
+ }
+
+ def post_template_populate(self):
+ """
+ @TODO replace page object with generic content object
+ :return:
+ """
+ super(ModerationToolbar, self).post_template_populate()
+ page = get_page_draft(self.request.current_page)
+
+ if not page:
+ return None
+
+ url = add_url_parameters(
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language=self.current_lang,
+ args=()
+ ),
+ content_object_id=page.pk
+ )
+
+ self.toolbar.add_modal_button(
+ name=_('Submit for moderation'),
+ url=url,
+ side=self.toolbar.RIGHT,
+ )
+
+
+toolbar_pool.register(ModerationToolbar)
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 62b27bd6..ab918efd 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -1,13 +1,17 @@
from __future__ import unicode_literals
from django import forms
+from django.contrib import admin
+from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
from django.contrib.auth import get_user_model
+from django.contrib.contenttypes.models import ContentType
from django.forms.forms import NON_FIELD_ERRORS
from django.utils.translation import ugettext, ugettext_lazy as _
from adminsortable2.admin import CustomInlineFormSet
from .constants import ACTION_CANCELLED, ACTION_REJECTED, ACTION_RESUBMITTED
+from .models import ModerationCollection, ModerationRequest
class WorkflowStepInlineFormSet(CustomInlineFormSet):
@@ -98,6 +102,71 @@ def save(self):
)
+class CollectionItemForm(forms.Form):
+
+ collection = forms.ModelChoiceField(
+ queryset=ModerationCollection.objects.filter(is_locked=False),
+ required=True
+ )
+ content_type = forms.ModelChoiceField(
+ queryset=ContentType.objects.filter(app_label="cms", model="page"),
+ required=True,
+ widget=forms.HiddenInput(),
+ )
+ content_object_id = forms.IntegerField()
+
+ def set_collection_widget(self, request):
+ related_modeladmin = admin.site._registry.get(ModerationCollection)
+ dbfield = ModerationRequest._meta.get_field('collection')
+ formfield = self.fields['collection']
+ formfield.widget = RelatedFieldWidgetWrapper(
+ formfield.widget,
+ dbfield.rel,
+ admin_site=admin.site,
+ can_add_related=related_modeladmin.has_add_permission(request),
+ can_change_related=related_modeladmin.has_change_permission(request),
+ can_delete_related=related_modeladmin.has_delete_permission(request),
+ )
+
+ def clean(self):
+ """
+ Validates content_object_id: Checks that a given content_object_id has
+ a content_object and it is not currently part of any ModerationRequest
+
+ :return:
+ """
+ if self.errors:
+ return self.cleaned_data
+
+ content_type = self.cleaned_data['content_type']
+
+ try:
+ content_object = content_type.get_object_for_this_type(
+ pk=self.cleaned_data['content_object_id'],
+ is_page_type=False,
+ publisher_is_draft=True,
+ )
+ except content_type.model_class().DoesNotExist:
+ content_object = None
+
+ if not content_object:
+ raise forms.ValidationError(_('Invalid content_object_id, does not exist'))
+
+ request_with_object_exists = ModerationRequest.objects.filter(
+ content_type=content_type,
+ object_id=content_object.pk,
+ ).exists()
+
+ if request_with_object_exists:
+ raise forms.ValidationError(_(
+ "{} is already part of existing moderation request which is part "
+ "of another active collection".format(content_object)
+ ))
+
+ self.cleaned_data['content_object'] = content_object
+ return self.cleaned_data
+
+
class SubmitCollectionForModerationForm(forms.Form):
moderator = forms.ModelChoiceField(
label=_('moderator'),
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 9140da53..fc0d10b3 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -330,6 +330,19 @@ def add_object(self, content_object):
"of another active collection".format(content_object)
)
+ def _add_object(self, content_object):
+ """
+ Add object to the ModerationRequest in this collection.
+ Requires validation from .forms.CollectionItemForm
+ :return:
+ """
+ content_type = ContentType.objects.get_for_model(content_object)
+ return self.moderation_requests.create(
+ content_type=content_type,
+ object_id=content_object.pk,
+ collection=self,
+ )
+
@python_2_unicode_compatible
class ModerationRequest(models.Model):
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
new file mode 100644
index 00000000..045874bc
--- /dev/null
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -0,0 +1,73 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+{% load static %}
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/request_form.html b/djangocms_moderation/templates/djangocms_moderation/request_form.html
index 546d3902..4fc0a38a 100644
--- a/djangocms_moderation/templates/djangocms_moderation/request_form.html
+++ b/djangocms_moderation/templates/djangocms_moderation/request_form.html
@@ -34,7 +34,7 @@
-
+
{% endblock %}
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 135bb288..599443e9 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -1,201 +1,83 @@
from __future__ import unicode_literals
-from django.contrib import messages
+from django.contrib import admin, messages
+from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
-from django.http import (
- HttpResponseBadRequest,
- HttpResponseForbidden,
- HttpResponseRedirect,
-)
+from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.utils.translation import ugettext_lazy as _
-from django.views.generic import FormView, ListView
+from django.views.generic import FormView
+from cms.models import Page
from cms.utils.urlutils import add_url_parameters
-from .forms import (
- SubmitCollectionForModerationForm,
- UpdateModerationRequestForm,
-)
-from .helpers import (
- get_active_moderation_request,
- get_moderation_workflow,
- get_page_or_404,
-)
-from .models import (
- ConfirmationFormSubmission,
- ConfirmationPage,
- ModerationCollection,
- ModerationRequest,
-)
+from .forms import CollectionItemForm, SubmitCollectionForModerationForm
+from .models import ConfirmationPage, ModerationCollection
from .utils import get_admin_url
from . import constants # isort:skip
-class ModerationRequestView(FormView):
-
- action = None
- page_title = None
- success_message = None
- template_name = 'djangocms_moderation/request_form.html'
+class CollectionItemView(FormView):
+ template_name = 'djangocms_moderation/item_to_collection.html'
+ form_class = CollectionItemForm
success_template_name = 'djangocms_moderation/request_finalized.html'
- def dispatch(self, request, *args, **kwargs):
- user = request.user
- self.page_id = args[0]
- self.language = args[1]
- self.page = get_page_or_404(self.page_id, self.language)
- self.workflow = None
- self.active_request = get_active_moderation_request(self.page, self.language)
-
- if self.active_request:
- self.workflow = self.active_request.workflow
-
- needs_ongoing = self.action in (constants.ACTION_APPROVED, constants.ACTION_REJECTED)
-
- if self.action == constants.ACTION_STARTED:
- # Can't start new request if there's one already.
- return HttpResponseBadRequest('Page already has an active moderation request.')
- elif self.active_request.is_approved() and needs_ongoing:
- # Can't reject or approve a moderation request whose steps have all
- # already been approved.
- return HttpResponseBadRequest('Moderation request has already been approved.')
- elif needs_ongoing and not self.active_request.user_can_take_moderation_action(user):
- # User can't approve or reject a request where he's not part of the workflow
- return HttpResponseForbidden('User is not allowed to update request.')
- elif self.action == constants.ACTION_APPROVED:
- next_step = self.active_request.user_get_step(self.request.user)
- confirmation_is_valid = True
-
- if next_step and next_step.role:
- confirmation_page_instance = next_step.role.confirmation_page
- else:
- confirmation_page_instance = None
-
- if confirmation_page_instance:
- confirmation_is_valid = confirmation_page_instance.is_valid(
- active_request=self.active_request,
- for_step=next_step,
- is_reviewed=request.GET.get('reviewed'),
- )
-
- if not confirmation_is_valid:
- redirect_url = add_url_parameters(
- confirmation_page_instance.get_absolute_url(),
- content_view=True,
- page=self.page_id,
- language=self.language,
- )
- return HttpResponseRedirect(redirect_url)
- elif self.action != constants.ACTION_STARTED:
- # All except for the new request endpoint require an active moderation request
- return HttpResponseBadRequest('Page does not have an active moderation request.')
- else:
- self.workflow = get_moderation_workflow()
+ def get_form_kwargs(self):
+ kwargs = super(CollectionItemView, self).get_form_kwargs()
+ kwargs['initial'].update({
+ 'content_object_id': self.request.GET.get('content_object_id'),
+ 'content_type': ContentType.objects.get_for_model(Page).pk,
+ })
+ collection_id = self.request.GET.get('collection_id')
- if not self.workflow:
- return HttpResponseBadRequest('No moderation workflow exists for page.')
- return super(ModerationRequestView, self).dispatch(request, *args, **kwargs)
+ if collection_id:
+ kwargs['initial']['collection'] = collection_id
+ return kwargs
def form_valid(self, form):
- form.save()
- context = self.get_context_data(form=form)
- messages.success(self.request, self.success_message)
- return render(self.request, self.success_template_name, context)
+ content_object = form.cleaned_data['content_object']
+ collection = form.cleaned_data['collection']
+ collection.add_object(content_object)
+ messages.success(self.request, _('Item successfully added to moderation collection'))
+ return render(self.request, self.success_template_name, {})
- def get_form_kwargs(self):
- kwargs = super(ModerationRequestView, self).get_form_kwargs()
- kwargs['action'] = self.action
- kwargs['language'] = self.language
- kwargs['page'] = self.page
- kwargs['user'] = self.request.user
- kwargs['workflow'] = self.workflow
- kwargs['active_request'] = self.active_request
- return kwargs
+ def get_form(self, **kwargs):
+ form = super(CollectionItemView, self).get_form(**kwargs)
+ form.set_collection_widget(self.request)
+ return form
def get_context_data(self, **kwargs):
- opts = ModerationRequest._meta
- form_submission_opts = ConfirmationFormSubmission._meta
-
- if self.active_request:
- form_submissions = self.active_request.form_submissions.all()
+ """
+ Gets collection_id from params or from the first collection in the list
+ when no ?collection_id is not supplied
+
+ Always gets content_object_list from a collection at a time
+ """
+ context = super(CollectionItemView, self).get_context_data(**kwargs)
+ opts_meta = ModerationCollection._meta
+ collection_id = self.request.GET.get('collection_id')
+
+ if collection_id:
+ collection = ModerationCollection.objects.get(pk=collection_id)
+ content_object_list = collection.moderation_requests.all()
else:
- form_submissions = []
+ content_object_list = []
- context = super(ModerationRequestView, self).get_context_data(**kwargs)
+ model_admin = admin.site._registry[ModerationCollection]
context.update({
- 'title': self.page_title,
- 'has_change_permission': True,
- 'opts': opts,
- 'root_path': reverse('admin:index'),
- 'app_label': opts.app_label,
- 'adminform': context['form'],
- 'is_popup': True,
- 'form_submissions': form_submissions,
- 'form_submission_opts': form_submission_opts,
+ 'content_object_list': content_object_list,
+ 'opts': opts_meta,
+ 'title': _('Add to collection'),
+ 'form': self.get_form(),
+ 'media': model_admin.media,
})
- return context
-
-
-cancel_moderation_request = ModerationRequestView.as_view(
- action=constants.ACTION_CANCELLED,
- page_title=_('Cancel request'),
- form_class=UpdateModerationRequestForm,
- success_message=_('The moderation request has been cancelled.'),
-)
-
-reject_moderation_request = ModerationRequestView.as_view(
- action=constants.ACTION_REJECTED,
- page_title=_('Send for rework'),
- form_class=UpdateModerationRequestForm,
- success_message=_('The moderation request has been sent for rework.'),
-)
-
-approve_moderation_request = ModerationRequestView.as_view(
- action=constants.ACTION_APPROVED,
- page_title=_('Approve changes'),
- form_class=UpdateModerationRequestForm,
- success_message=_('The changes have been approved.'),
-)
-
-resubmit_moderation_request = ModerationRequestView.as_view(
- action=constants.ACTION_RESUBMITTED,
- page_title=_('Resubmit changes'),
- form_class=UpdateModerationRequestForm,
- success_message=_('The request has been re-submitted.'),
-)
-
-
-class ModerationCommentsView(ListView):
- template_name = 'djangocms_moderation/comment_list.html'
-
- def dispatch(self, request, page_id, language, *args, **kwargs):
- page_obj = get_page_or_404(page_id, language)
- self.active_request = get_active_moderation_request(page_obj, language)
-
- if not self.active_request.user_can_view_comments(request.user):
- return HttpResponseForbidden('User is not allowed to view comments.')
-
- return super(ModerationCommentsView, self).dispatch(
- request, page_id, language, *args, **kwargs
- )
-
- def get_queryset(self):
- return self.active_request.actions.all()
-
- def get_context_data(self, **kwargs):
- context = super(ModerationCommentsView, self).get_context_data(**kwargs)
- context.update({
- 'title': _('View Comments'),
- 'is_popup': True,
- })
return context
-moderation_comments = ModerationCommentsView.as_view()
+add_item_to_collection = CollectionItemView.as_view()
def moderation_confirmation_page(request, confirmation_id):
diff --git a/tests/test_app_registration.py b/tests/test_app_registration.py
index bca219b6..4da9719e 100644
--- a/tests/test_app_registration.py
+++ b/tests/test_app_registration.py
@@ -60,8 +60,11 @@ class CMSConfigIntegrationTest(CMSTestCase):
def setUp(self):
app_registration.get_cms_extension_apps.cache_clear()
app_registration.get_cms_config_apps.cache_clear()
- self.moderated_models = (App2PostContent, App2TitleContent,
- App1PostContent, App1TitleContent)
+
+ self.moderated_models = (
+ App2PostContent, App2TitleContent,
+ App1PostContent, App1TitleContent
+ )
def test_config_with_two_apps(self):
setup_cms_apps()
diff --git a/tests/test_views.py b/tests/test_views.py
index 70ce9cd9..38af5a58 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,292 +1,175 @@
-import json
import mock
from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from cms.utils.urlutils import add_url_parameters
-from djangocms_moderation import constants
-from djangocms_moderation.forms import UpdateModerationRequestForm
-from djangocms_moderation.models import (
- ConfirmationFormSubmission,
- ConfirmationPage,
- ModerationCollection,
-)
+from djangocms_moderation.forms import CollectionItemForm
+from djangocms_moderation.models import ModerationCollection, ModerationRequest
from djangocms_moderation.utils import get_admin_url
from .utils.base import BaseViewTestCase
-class ModerationRequestViewTest(BaseViewTestCase):
+class CollectionItemViewTest(BaseViewTestCase):
- def _assert_render(self, response, page, action, workflow, active_request, form_cls, title):
- view = response.context_data['view']
- form = response.context_data['adminform']
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template_name[0], 'djangocms_moderation/request_form.html')
- self.assertEqual(view.language, 'en')
- self.assertEqual(view.page, page)
- self.assertEqual(view.action, action)
- self.assertEqual(view.workflow, workflow)
- self.assertEqual(view.active_request, active_request)
- self.assertEqual(response.context_data['title'], title)
- self.assertIsInstance(form, form_cls)
-
- def test_cancel_request_view_with_form(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_cancel_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- self._assert_render(
- response=response,
- page=self.pg1,
- action=constants.ACTION_CANCELLED,
- active_request=self.moderation_request1,
- workflow=self.wf1,
- form_cls=UpdateModerationRequestForm,
- title=_('Cancel request')
- )
+ def setUp(self):
- def test_reject_request_view_with_form(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_reject_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- self._assert_render(
- response=response,
- page=self.pg1,
- action=constants.ACTION_REJECTED,
- active_request=self.moderation_request1,
- workflow=self.wf1,
- form_cls=UpdateModerationRequestForm,
- title=_('Send for rework')
+ self.user = User.objects.create_user(
+ username='test1', email='test1@test.com', password='test1', is_staff=True
)
- def test_resubmit_request_view_with_form(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_resubmit_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- self._assert_render(
- response=response,
- page=self.pg1,
- action=constants.ACTION_RESUBMITTED,
- active_request=self.moderation_request1,
- workflow=self.wf1,
- form_cls=UpdateModerationRequestForm,
- title=_('Resubmit changes')
+ self.collection_1 = ModerationCollection.objects.create(
+ author=self.user, name='My collection 1', workflow=self.wf1
)
-
- def test_approve_request_view_with_form(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_approve_request',
- language='en',
- args=(self.pg1.pk, 'en')
- ))
- self._assert_render(
- response=response,
- page=self.pg1,
- action=constants.ACTION_APPROVED,
- active_request=self.moderation_request1,
- workflow=self.wf1,
- form_cls=UpdateModerationRequestForm,
- title=_('Approve changes')
+ self.collection_2 = ModerationCollection.objects.create(
+ author=self.user, name='My collection 2', workflow=self.wf1
)
- def test_throws_no_active_moderation_request(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_cancel_request',
- language='en',
- args=(self.pg2.pk, 'en') # pg2 => no active requests
- ))
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.content, b'Page does not have an active moderation request.')
-
- def test_throws_error_already_approved(self):
- response = self.client.get(get_admin_url(
- name='cms_moderation_approve_request',
- language='en',
- args=(self.pg3.pk, 'en') # pg3 => active request with all approved steps
- ))
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.content, b'Moderation request has already been approved.')
-
- def test_throws_error_forbidden_user(self):
- from django.contrib.auth.models import User
- user = User.objects.create_user(username='test1', email='test1@test.com', password='test1', is_staff=True)
- self.client.force_login(user)
- response = self.client.get(get_admin_url(
- name='cms_moderation_approve_request',
- language='en',
- args=(self.pg1.pk, 'en') # pg1 => active request
- ))
- self.assertEqual(response.status_code, 403)
- self.assertEqual(response.content, b'User is not allowed to update request.')
-
- def _create_confirmation_page(self, moderation_request):
- # First delete all the form submissions for the passed moderation_request
- # This will make sure there are no form submissions
- # attached with the passed moderation_request
- moderation_request.form_submissions.all().delete()
- self.cp = ConfirmationPage.objects.create(
- name='Checklist Form',
- )
- self.role1.confirmation_page = self.cp
- self.role1.save()
+ self.content_type = ContentType.objects.get_for_model(self.pg1)
- def test_redirects_to_confirmation_page_if_invalid_check(self):
- self._create_confirmation_page(self.moderation_request1)
- response = self.client.get(
- get_admin_url(
- name='cms_moderation_approve_request',
- language='en',
- args=(self.pg1.pk, 'en')
- )
- )
- redirect_url = add_url_parameters(
- self.cp.get_absolute_url(),
- content_view=True,
- page=self.pg1.pk,
- language='en',
- )
- self.assertEqual(response.status_code, 302) # redirection
- self.assertEqual(response.url, redirect_url)
-
- def test_does_not_redirect_to_confirmation_page_if_valid_check(self):
- self._create_confirmation_page(self.moderation_request1)
- ConfirmationFormSubmission.objects.create(
- request=self.moderation_request1,
- for_step=self.wf1st1,
- by_user=self.user,
- data=json.dumps([{'label': 'Question 1', 'answer': 'Yes'}]),
- confirmation_page=self.cp,
- )
- response = self.client.get(
- get_admin_url(
- name='cms_moderation_approve_request',
- language='en',
- args=(self.pg1.pk, 'en')
- )
- )
- self._assert_render(
- response=response,
- page=self.pg1,
- action=constants.ACTION_APPROVED,
- active_request=self.moderation_request1,
- workflow=self.wf1,
- form_cls=UpdateModerationRequestForm,
- title=_('Approve changes')
- )
+ def _assert_render(self, response):
+ form = response.context_data['form']
- def test_renders_all_form_submissions(self):
- self._create_confirmation_page(self.moderation_request1)
- ConfirmationFormSubmission.objects.create(
- request=self.moderation_request1,
- for_step=self.wf1st1,
- by_user=self.user,
- data=json.dumps([{'label': 'Question 1', 'answer': 'Yes'}]),
- confirmation_page=self.cp,
- )
- response = self.client.get(
+ self.assertIsInstance(form, CollectionItemForm)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template_name[0], 'djangocms_moderation/item_to_collection.html')
+
+ self.assertEqual(response.context_data['title'], _('Add to collection'))
+
+ def test_add_object_to_collections(self):
+ ModerationRequest.objects.all().delete()
+ self.client.force_login(self.user)
+ response = self.client.post(
get_admin_url(
- name='cms_moderation_approve_request',
+ name='cms_moderation_item_to_collection',
language='en',
- args=(self.pg1.pk, 'en')
+ args=()
+ ), {'collection': self.collection_1.pk,
+ 'content_type': self.content_type.pk,
+ 'content_object_id': self.pg1.pk
+ }
)
- )
- form_submissions = response.context_data['form_submissions']
- results = ConfirmationFormSubmission.objects.filter(request=self.moderation_request1)
- self.assertQuerysetEqual(form_submissions, results, transform=lambda x: x, ordered=False)
+ self.assertEqual(response.status_code, 200)
+ # self.assertContains(response, 'reloadBrowser')
+
+ content_type = ContentType.objects.get_for_model(self.pg1)
+ moderation_request = ModerationRequest.objects.filter(
+ content_type=content_type,
+ object_id=self.pg1.pk,
+ )[0]
-class ModerationCommentsViewTest(BaseViewTestCase):
+ self.assertEqual(moderation_request.collection, self.collection_1)
- def test_comment_list(self):
- response = self.client.get(
+ def test_invalid_content_already_in_collection(self):
+ # add object
+ self.collection_1._add_object(self.pg1)
+
+ self.client.force_login(self.user)
+
+ response = self.client.post(
get_admin_url(
- name='cms_moderation_comments',
+ name='cms_moderation_item_to_collection',
language='en',
- args=(self.pg3.pk, 'en')
- )
- )
+ args=()
+ ), {'collection': self.collection_1.pk,
+ 'content_type': self.content_type.pk,
+ 'content_object_id': self.pg1.pk})
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context_data['object_list'].count(), 3)
-
- def test_comment_list_protected(self):
- new_user = User.objects.create_superuser(
- username='new_user', email='new_user@test.com', password='test'
+ self.assertIn(
+ "is already part of existing moderation request which is part",
+ response.context_data['form'].errors['__all__'][0]
)
- self.client.force_login(new_user)
- response = self.client.get(
+ def test_non_existing_content_object(self):
+ self.client.force_login(self.user)
+ content_type = ContentType.objects.get_for_model(self.pg1)
+ response = self.client.post(
get_admin_url(
- name='cms_moderation_comments',
+ name='cms_moderation_item_to_collection',
language='en',
- args=(self.pg3.pk, 'en')
- )
- )
-
- self.assertEqual(response.status_code, 403)
+ args=()
+ ), {'collection': self.collection_1.pk,
+ 'content_type': content_type.pk,
+ 'content_object_id': 9000})
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(
+ 'Invalid content_object_id, does not exist',
+ response.context_data['form'].errors['__all__'][0]
+ )
-class ModerationConfirmationPageTest(BaseViewTestCase):
+ def test_exclude_locked_collections(self):
+ ModerationRequest.objects.all().delete()
+ self.collection_1.is_locked = True
+ self.collection_1.save()
- def setUp(self):
- super(ModerationConfirmationPageTest, self).setUp()
- self.cp = ConfirmationPage.objects.create(
- name='Checklist Form',
- )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language='en',
+ args=()
+ ), {'collection': self.collection_1.pk, 'content_object_id': self.pg1.pk})
- def test_renders_build_view(self):
- response = self.client.get(self.cp.get_absolute_url())
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.templates[0].name, self.cp.template)
+ # locked collection are not part of the list
self.assertEqual(
- response.context['CONFIRMATION_BASE_TEMPLATE'],
- 'djangocms_moderation/base_confirmation_build.html',
+ "Select a valid choice. That choice is not one of the available choices.",
+ response.context_data['form'].errors['collection'][0]
)
- def test_renders_content_view(self):
+ def test_list_content_objects_from_collection_id_param(self):
+ ModerationRequest.objects.all().delete()
+
+ self.collection_1._add_object(self.pg1)
+ self.collection_2._add_object(self.pg2)
+
+ self.client.force_login(self.user)
response = self.client.get(
add_url_parameters(
- self.cp.get_absolute_url(),
- content_view=True,
- page=self.pg1.pk,
- language='en',
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language='en',
+ args=()
+ ), collection_id=self.collection_2.pk
)
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.templates[0].name, self.cp.template)
- self.assertEqual(response.context['CONFIRMATION_BASE_TEMPLATE'], 'djangocms_moderation/base_confirmation.html')
- def test_renders_post_view(self):
- response = self.client.post(
+ moderation_requests = ModerationRequest.objects.filter(collection=self.collection_2)
+ # moderation request is content_object
+ for mod_request in moderation_requests:
+ self.assertTrue(mod_request in response.context_data['content_object_list'])
+
+ def test_content_object_id_from_params(self):
+ self.client.force_login(self.user)
+ response = self.client.get(
add_url_parameters(
- self.cp.get_absolute_url(),
- content_view=True,
- page=self.pg1.pk,
- language='en',
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language='en',
+ args=()
+ ), content_object_id=self.pg1.pk
)
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.templates[0].name, self.cp.template)
- self.assertEqual(response.context['CONFIRMATION_BASE_TEMPLATE'], 'djangocms_moderation/base_confirmation.html')
- self.assertTrue(response.context['submitted'])
- redirect_url = add_url_parameters(
+
+ form = response.context_data['form']
+ self.assertEqual(self.pg1.pk, int(form.initial['content_object_id']))
+
+ def test_authenticated_users_only(self):
+ response = self.client.get(
get_admin_url(
- name='cms_moderation_approve_request',
+ name='cms_moderation_item_to_collection',
language='en',
- args=(self.pg1.pk, 'en'),
- ),
- reviewed=True,
+ args=()
+ )
)
- self.assertEqual(response.context['redirect_url'], redirect_url)
+
+ self.assertEqual(response.status_code, 302)
class SubmitCollectionForModerationViewTest(BaseViewTestCase):
From c3b39f80a64fbea983ddcdf7a781a95dc349979e Mon Sep 17 00:00:00 2001
From: Vadim Sikora
Date: Thu, 9 Aug 2018 17:02:20 +0200
Subject: [PATCH 009/147] FIL-518 use django jquery when listening to change
event on collection
Django loads own jQuery when rendering "add-related" widgets and
triggers change event on them in their own jQuery. Since these are not
native browser events we can't listen to them via other jQuery.
---
.../item_to_collection.html | 32 +++++++++----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
index 045874bc..9f752c07 100644
--- a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -50,24 +50,24 @@ {% trans "Add to existing collection" %}
-
{% endblock %}
From d8ec17efe45ec3a88fb884b991525ac5232ebb53 Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Tue, 14 Aug 2018 10:46:14 +0100
Subject: [PATCH 010/147] Make toolbar collection item aware (#41)
---
djangocms_moderation/cms_toolbars.py | 43 +++++++++++------
tests/test_cms_toolbars.py | 71 ++++++++++++++++++++++++++++
tests/utils/app_1/cms_config.py | 6 ++-
tests/utils/app_2/cms_config.py | 6 ++-
4 files changed, 108 insertions(+), 18 deletions(-)
create mode 100644 tests/test_cms_toolbars.py
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index cd935a1c..92ab46cc 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from cms.api import get_page_draft
@@ -6,6 +7,7 @@
from cms.toolbar_pool import toolbar_pool
from cms.utils.urlutils import add_url_parameters
+from .models import ModerationRequest
from .utils import get_admin_url
@@ -27,20 +29,33 @@ def post_template_populate(self):
if not page:
return None
- url = add_url_parameters(
- get_admin_url(
- name='cms_moderation_item_to_collection',
- language=self.current_lang,
- args=()
- ),
- content_object_id=page.pk
- )
-
- self.toolbar.add_modal_button(
- name=_('Submit for moderation'),
- url=url,
- side=self.toolbar.RIGHT,
- )
+ try:
+ content_type = ContentType.objects.get_for_model(page)
+ moderation_request = ModerationRequest.objects.get(
+ content_type=content_type,
+ object_id=page.pk,
+ )
+ self.toolbar.add_modal_button(
+ name=_('In Moderation "%s"' % moderation_request.collection.name),
+ url='#',
+ disabled=True,
+ side=self.toolbar.RIGHT,
+ )
+ except ModerationRequest.DoesNotExist:
+ url = add_url_parameters(
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language=self.current_lang,
+ args=()
+ ),
+ content_object_id=page.pk
+ )
+
+ self.toolbar.add_modal_button(
+ name=_('Submit for moderation'),
+ url=url,
+ side=self.toolbar.RIGHT,
+ )
toolbar_pool.register(ModerationToolbar)
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
new file mode 100644
index 00000000..b40f9b7b
--- /dev/null
+++ b/tests/test_cms_toolbars.py
@@ -0,0 +1,71 @@
+from django.contrib.auth.models import AnonymousUser
+from django.test.client import RequestFactory
+
+from cms.middleware.toolbar import ToolbarMiddleware
+from cms.toolbar.toolbar import CMSToolbar
+from cms.utils.conf import get_cms_setting
+
+from djangocms_moderation.cms_toolbars import ModerationToolbar
+from djangocms_moderation.models import ModerationRequest
+
+from .utils.base import BaseTestCase
+
+
+class TestCMSToolbars(BaseTestCase):
+
+ def get_page_request(self, page, user, path=None, edit=False,
+ preview=False, structure=False, lang_code='en', disable=False):
+ if not path:
+ path = page.get_absolute_url()
+
+ if edit:
+ path += '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
+
+ if structure:
+ path += '?%s' % get_cms_setting('CMS_TOOLBAR_URL__BUILD')
+
+ if preview:
+ path += '?preview'
+
+ request = RequestFactory().get(path)
+ request.session = {}
+ request.user = user
+ request.LANGUAGE_CODE = lang_code
+ if edit:
+ request.GET = {'edit': None}
+ else:
+ request.GET = {'edit_off': None}
+ if disable:
+ request.GET[get_cms_setting('CMS_TOOLBAR_URL__DISABLE')] = None
+ request.current_page = page
+ mid = ToolbarMiddleware()
+ mid.process_request(request)
+ if hasattr(request, 'toolbar'):
+ request.toolbar.populate()
+ return request
+
+ def test_submit_for_moderation(self):
+ ModerationRequest.objects.all().delete()
+
+ request = self.get_page_request(self.pg1, AnonymousUser(), '/')
+ toolbar = CMSToolbar(request)
+ toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
+ toolbar.populate()
+ toolbar.post_template_populate()
+
+ self.assertEquals(
+ toolbar.toolbar.get_right_items()[0].buttons[0].name,
+ 'Submit for moderation'
+ )
+
+ def test_page_in_moderation(self):
+ request = self.get_page_request(self.pg1, AnonymousUser(), '/')
+ toolbar = CMSToolbar(request)
+ toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
+ toolbar.populate()
+ toolbar.post_template_populate()
+
+ self.assertEquals(
+ toolbar.toolbar.get_right_items()[0].buttons[0].name,
+ 'In Moderation "%s"' % self.collection1.name
+ )
diff --git a/tests/utils/app_1/cms_config.py b/tests/utils/app_1/cms_config.py
index 83ec92c4..a73937c2 100644
--- a/tests/utils/app_1/cms_config.py
+++ b/tests/utils/app_1/cms_config.py
@@ -13,10 +13,12 @@ class CMSApp1Config(CMSAppConfig):
versioning = [
VersionableItem(
content_model=App1PostContent,
- grouper_field_name='post'
+ grouper_field_name='post',
+ copy_function=lambda x: x
),
VersionableItem(
content_model=App1TitleContent,
- grouper_field_name='title'
+ grouper_field_name='title',
+ copy_function=lambda x: x
)
]
diff --git a/tests/utils/app_2/cms_config.py b/tests/utils/app_2/cms_config.py
index 425d28a6..801eadc3 100644
--- a/tests/utils/app_2/cms_config.py
+++ b/tests/utils/app_2/cms_config.py
@@ -13,10 +13,12 @@ class CMSApp1Config(CMSAppConfig):
versioning = [
VersionableItem(
content_model=App2PostContent,
- grouper_field_name='post'
+ grouper_field_name='post',
+ copy_function=lambda x: x
),
VersionableItem(
content_model=App2TitleContent,
- grouper_field_name='title'
+ grouper_field_name='title',
+ copy_function=lambda x: x
)
]
From 97a405c865140eeed56a4b91a889a521f60026cb Mon Sep 17 00:00:00 2001
From: Noel da Costa
Date: Mon, 20 Aug 2018 11:52:13 +0100
Subject: [PATCH 011/147] Collection locks and status (#42)
---
djangocms_moderation/admin.py | 15 +----
djangocms_moderation/constants.py | 11 ++++
djangocms_moderation/forms.py | 10 +--
.../migrations/0001_initial.py | 16 +++--
.../0002_moderationcollection_author.py | 28 ---------
.../migrations/0003_auto_20180724_1521.py | 26 --------
djangocms_moderation/models.py | 53 ++++------------
.../moderation_request_change_list.html | 4 +-
tests/test_forms.py | 2 +-
tests/test_models.py | 63 +++----------------
tests/test_views.py | 25 ++++----
11 files changed, 71 insertions(+), 182 deletions(-)
delete mode 100644 djangocms_moderation/migrations/0002_moderationcollection_author.py
delete mode 100644 djangocms_moderation/migrations/0003_auto_20180724_1521.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 0b6ce7ca..d8745f28 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -96,12 +96,12 @@ def changelist_view(self, request, extra_context=None):
pass
else:
extra_context = dict(collection=collection)
- if collection.allow_submit_for_moderation:
- submit_for_moderation_url = reverse(
+ if collection.allow_submit_for_review:
+ submit_for_review_url = reverse(
'admin:cms_moderation_submit_collection_for_moderation',
args=(collection_id,)
)
- extra_context['submit_for_moderation_url'] = submit_for_moderation_url
+ extra_context['submit_for_review_url'] = submit_for_review_url
return super(ModerationRequestAdmin, self).changelist_view(request, extra_context)
def get_status(self, obj):
@@ -162,7 +162,6 @@ class ModerationCollectionAdmin(admin.ModelAdmin):
'get_moderator',
'workflow',
'status',
- 'is_locked',
'date_created',
]
@@ -183,14 +182,6 @@ def get_moderator(self, obj):
return obj.author
get_moderator.short_description = _('Moderator')
- def status(self, obj):
- # TODO more statuses to come in the future, once implemented.
- # It will very likely be a ModerationCollection.status field
- if obj.is_locked:
- return _("In review")
- return _("Collection")
- status.short_description = _('Status')
-
def get_urls(self):
def _url(regex, fn, name, **kwargs):
return url(regex, self.admin_site.admin_view(fn), kwargs=kwargs, name=name)
diff --git a/djangocms_moderation/constants.py b/djangocms_moderation/constants.py
index c70b58af..98d9c2ab 100644
--- a/djangocms_moderation/constants.py
+++ b/djangocms_moderation/constants.py
@@ -42,3 +42,14 @@
CONTENT_TYPE_PLAIN = 'plain'
CONTENT_TYPE_FORM = 'form'
+
+# masks for Collectoin STATUS
+COLLECTING = 'COLLECTING'
+IN_REVIEW = 'IN_REVIEW'
+ARCHIVED = 'ARCHIVED'
+
+STATUS_CHOICES = (
+ (COLLECTING, _('Collecting')),
+ (IN_REVIEW, _('In Review')),
+ (ARCHIVED, _('Archived')),
+)
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index ab918efd..35d8e629 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -10,7 +10,7 @@
from adminsortable2.admin import CustomInlineFormSet
-from .constants import ACTION_CANCELLED, ACTION_REJECTED, ACTION_RESUBMITTED
+from .constants import ACTION_CANCELLED, ACTION_REJECTED, ACTION_RESUBMITTED, COLLECTING
from .models import ModerationCollection, ModerationRequest
@@ -105,7 +105,7 @@ def save(self):
class CollectionItemForm(forms.Form):
collection = forms.ModelChoiceField(
- queryset=ModerationCollection.objects.filter(is_locked=False),
+ queryset=ModerationCollection.objects.filter(status=COLLECTING),
required=True
)
content_type = forms.ModelChoiceField(
@@ -169,7 +169,7 @@ def clean(self):
class SubmitCollectionForModerationForm(forms.Form):
moderator = forms.ModelChoiceField(
- label=_('moderator'),
+ label=_('Select review group'),
queryset=get_user_model().objects.none(),
required=False,
)
@@ -187,12 +187,12 @@ def configure_moderator_field(self):
self.fields['moderator'].queryset = users
def clean(self):
- if not self.collection.allow_submit_for_moderation:
+ if not self.collection.allow_submit_for_review:
self.add_error(None, _("This collection can't be submitted for a review"))
return super(SubmitCollectionForModerationForm, self).clean()
def save(self):
- self.collection.submit_for_moderation(
+ self.collection.submit_for_review(
by_user=self.user,
to_user=self.cleaned_data.get('moderator'),
)
diff --git a/djangocms_moderation/migrations/0001_initial.py b/djangocms_moderation/migrations/0001_initial.py
index d8cde7ef..5a435fe6 100644
--- a/djangocms_moderation/migrations/0001_initial.py
+++ b/djangocms_moderation/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-07-17 15:49
+# Generated by Django 1.11.13 on 2018-08-16 08:57
from __future__ import unicode_literals
import cms.models.fields
@@ -7,8 +7,8 @@
from django.db import migrations, models
import django.db.models.deletion
-from djangocms_moderation import conf
-from djangocms_moderation.constants import ACTION_CHOICES
+from djangocms_moderation import conf
+from djangocms_moderation.constants import ACTION_CHOICES, STATUS_CHOICES
class Migration(migrations.Migration):
@@ -18,6 +18,7 @@ class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('cms', '0020_old_tree_cleanup'),
+ ('cms', '0028_remove_page_placeholders'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
@@ -55,7 +56,10 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, verbose_name='name')),
- ('is_locked', models.BooleanField(default=False, verbose_name='is locked')),
+ ('status', models.CharField(choices=STATUS_CHOICES, db_index=True, default='COLLECTING', max_length=10)),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_modified', models.DateTimeField(auto_now=True)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='author')),
],
),
migrations.CreateModel(
@@ -175,6 +179,10 @@ class Migration(migrations.Migration):
name='workflowstep',
unique_together=set([('role', 'workflow')]),
),
+ migrations.AlterUniqueTogether(
+ name='moderationrequest',
+ unique_together=set([('collection', 'object_id', 'content_type')]),
+ ),
migrations.AlterUniqueTogether(
name='confirmationformsubmission',
unique_together=set([('request', 'for_step')]),
diff --git a/djangocms_moderation/migrations/0002_moderationcollection_author.py b/djangocms_moderation/migrations/0002_moderationcollection_author.py
deleted file mode 100644
index 152690fa..00000000
--- a/djangocms_moderation/migrations/0002_moderationcollection_author.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-07-18 14:01
-from __future__ import unicode_literals
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('djangocms_moderation', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='moderationcollection',
- name='author',
- field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='author'),
- preserve_default=False,
- ),
- migrations.AlterUniqueTogether(
- name='moderationrequest',
- unique_together=set([('collection', 'object_id', 'content_type')]),
- ),
- ]
diff --git a/djangocms_moderation/migrations/0003_auto_20180724_1521.py b/djangocms_moderation/migrations/0003_auto_20180724_1521.py
deleted file mode 100644
index 63533099..00000000
--- a/djangocms_moderation/migrations/0003_auto_20180724_1521.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.13 on 2018-07-24 14:21
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('djangocms_moderation', '0002_moderationcollection_author'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='moderationcollection',
- name='date_created',
- field=models.DateTimeField(auto_now_add=True),
- preserve_default=False,
- ),
- migrations.AddField(
- model_name='moderationcollection',
- name='date_modified',
- field=models.DateTimeField(auto_now=True),
- ),
- ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index fc0d10b3..b429d8c4 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -17,7 +17,6 @@
from cms.models.fields import PlaceholderField
from .emails import notify_collection_moderators
-from .exceptions import CollectionIsLocked, ObjectAlreadyInCollection
from .utils import generate_compliance_number
@@ -262,8 +261,12 @@ class ModerationCollection(models.Model):
verbose_name=_('workflow'),
related_name='moderation_collections',
)
- # TODO: proper implementations and handlers coming later for is_locked
- is_locked = models.BooleanField(verbose_name=_('is locked'), default=False)
+ status = models.CharField(
+ max_length=10,
+ choices=constants.STATUS_CHOICES,
+ default=constants.COLLECTING,
+ db_index=True,
+ )
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
@@ -274,10 +277,10 @@ def __str__(self):
def author_name(self):
return self.author.get_full_name() or self.author.get_username()
- def submit_for_moderation(self, by_user, to_user=None):
+ def submit_for_review(self, by_user, to_user=None):
"""
Submit all the moderation requests belonging to this collection for
- moderation and mark the collection as locked
+ review and mark the collection as locked
"""
for moderation_request in self.moderation_requests.all():
action = moderation_request.actions.create(
@@ -286,51 +289,21 @@ def submit_for_moderation(self, by_user, to_user=None):
action=constants.ACTION_STARTED,
)
# Lock the collection as it has been now submitted for moderation
- self.is_locked = True
- self.save(update_fields=['is_locked'])
+ self.status = constants.IN_REVIEW
+ self.save(update_fields=['status'])
# It is fine to pass any `action` from any moderation_request.actions
# above as it will have the same moderators
notify_collection_moderators(collection=self, action=action)
@property
- def allow_submit_for_moderation(self):
+ def allow_submit_for_review(self):
"""
- Can this collection submitted for moderation?
+ Can this collection be submitted for review?
:return:
"""
- # TODO limited check for now, consider makings this a model field
- return not self.is_locked and self.moderation_requests.exists()
+ return self.status == constants.COLLECTING and self.moderation_requests.exists()
def add_object(self, content_object):
- """
- Add object to the ModerationRequest in this collection.
- :return:
- """
- if self.is_locked:
- raise CollectionIsLocked(
- "Can't add the object to the collection, because it is locked"
- )
-
- content_type = ContentType.objects.get_for_model(content_object)
- # Object can ever be part of only one collection
- existing_request_exists = ModerationRequest.objects.filter(
- content_type=content_type,
- object_id=content_object.pk,
- ).exists()
-
- if not existing_request_exists:
- return self.moderation_requests.create(
- content_type=content_type,
- object_id=content_object.pk,
- collection=self,
- )
- else:
- raise ObjectAlreadyInCollection(
- "{} is already part of existing moderation request which is part "
- "of another active collection".format(content_object)
- )
-
- def _add_object(self, content_object):
"""
Add object to the ModerationRequest in this collection.
Requires validation from .forms.CollectionItemForm
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
index 8516d60a..185b403f 100644
--- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -3,9 +3,9 @@
{% block object-tools-items %}
{{ block.super }}
-{% if submit_for_moderation_url %}
+{% if submit_for_review_url %}
- {% trans 'Submit for review' %}
+ {% trans 'Submit for review' %}
{% endif %}
{% endblock %}
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 28cd3eb3..f21126bf 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -85,7 +85,7 @@ def test_form_save(self):
class SubmitCollectionForModerationFormTest(BaseTestCase):
- @mock.patch.object(ModerationCollection, 'allow_submit_for_moderation')
+ @mock.patch.object(ModerationCollection, 'allow_submit_for_review')
def test_form_is_invalid_if_collection_cant_be_submitted_for_review(self, allow_submit_mock):
data = {
'moderator': None,
diff --git a/tests/test_models.py b/tests/test_models.py
index de6e086b..416cc1e0 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -9,10 +9,6 @@
from cms.api import create_page
from djangocms_moderation import constants
-from djangocms_moderation.exceptions import (
- CollectionIsLocked,
- ObjectAlreadyInCollection,
-)
from djangocms_moderation.models import (
ConfirmationFormSubmission,
ConfirmationPage,
@@ -489,23 +485,23 @@ def setUp(self):
self.page1 = create_page(title='My page 1', template='page.html', language='en',)
self.page2 = create_page(title='My page 2', template='page.html', language='en',)
- def test_allow_submit_for_moderation(self):
- self.collection1.is_locked = False
+ def test_allow_submit_for_review(self):
+ self.collection1.status = constants.COLLECTING
self.collection1.save()
# This is false, as we don't have any moderation requests in this collection
- self.assertFalse(self.collection1.allow_submit_for_moderation)
+ self.assertFalse(self.collection1.allow_submit_for_review)
ModerationRequest.objects.create(
content_object=self.pg1, collection=self.collection1, is_active=True
)
- self.assertTrue(self.collection1.allow_submit_for_moderation)
+ self.assertTrue(self.collection1.allow_submit_for_review)
- self.collection1.is_locked = True
+ self.collection1.status = constants.IN_REVIEW
self.collection1.save()
- self.assertFalse(self.collection1.allow_submit_for_moderation)
+ self.assertFalse(self.collection1.allow_submit_for_review)
@patch('djangocms_moderation.models.notify_collection_moderators')
- def test_submit_for_moderation(self, mock_ncm):
+ def test_submit_for_review(self, mock_ncm):
ModerationRequest.objects.create(
content_object=self.pg1, language='en', collection=self.collection1
)
@@ -519,15 +515,15 @@ def test_submit_for_moderation(self, mock_ncm):
).exists()
)
- self.collection1.is_locked = False
+ self.collection1.status = constants.COLLECTING
self.collection1.save()
- self.collection1.submit_for_moderation(self.user, None)
+ self.collection1.submit_for_review(self.user, None)
self.assertEqual(1, mock_ncm.call_count)
self.collection1.refresh_from_db()
# Collection should lock itself
- self.assertTrue(self.collection1.is_locked)
+ self.assertEquals(self.collection1.status, constants.IN_REVIEW)
# We will now have 2 actions with status STARTED.
self.assertEqual(
2, ModerationRequestAction.objects.filter(
@@ -548,42 +544,3 @@ def _moderation_requests_count(self, obj, collection=None):
if collection:
queryset = queryset.filter(collection=collection)
return queryset.count()
-
- def test_add_object(self):
- self.assertEqual(0, self._moderation_requests_count(self.page1))
- # Add `page1` to `collection1`
- self.collection1.add_object(self.page1)
- self.assertEqual(1, self._moderation_requests_count(self.page1))
- self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
-
- # Adding the same object to the same collection will raise an exception
- with self.assertRaises(ObjectAlreadyInCollection):
- self.collection1.add_object(self.page1)
-
- self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
- self.assertEqual(1, self._moderation_requests_count(self.page1))
-
- # This should not work as `page1` is already part of `collection1`
- with self.assertRaises(ObjectAlreadyInCollection):
- self.collection2.add_object(self.page1)
-
- # We can add `page2` to the `collection1` as it is not there yet
- self.assertEqual(0, self._moderation_requests_count(self.page2))
- self.collection1.add_object(self.page2)
- self.assertEqual(1, self._moderation_requests_count(self.page2))
- self.assertEqual(1, self._moderation_requests_count(self.page2, self.collection1))
- self.assertEqual(1, self._moderation_requests_count(self.page1, self.collection1))
-
- def test_add_object_locked_collection(self):
- # This works, as the collection is not locked
- self.collection2.add_object(self.page1)
- self.assertEqual(1, self._moderation_requests_count(self.page1))
-
- # Now, let's lock the collection, so we can't add to it anymore
- self.collection2.is_locked = True
- self.collection2.save()
-
- with self.assertRaises(CollectionIsLocked):
- self.collection2.add_object(self.page1)
-
- self.assertEqual(1, self._moderation_requests_count(self.page1))
diff --git a/tests/test_views.py b/tests/test_views.py
index 38af5a58..1104a4b5 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -10,6 +10,7 @@
from djangocms_moderation.forms import CollectionItemForm
from djangocms_moderation.models import ModerationCollection, ModerationRequest
from djangocms_moderation.utils import get_admin_url
+from djangocms_moderation import constants
from .utils.base import BaseViewTestCase
@@ -67,7 +68,7 @@ def test_add_object_to_collections(self):
def test_invalid_content_already_in_collection(self):
# add object
- self.collection_1._add_object(self.pg1)
+ self.collection_1.add_object(self.pg1)
self.client.force_login(self.user)
@@ -104,11 +105,13 @@ def test_non_existing_content_object(self):
response.context_data['form'].errors['__all__'][0]
)
- def test_exclude_locked_collections(self):
+ def test_prevent_locked_collections(self):
+ """
+ from being selected when adding to collection
+ """
ModerationRequest.objects.all().delete()
- self.collection_1.is_locked = True
+ self.collection_1.status = constants.IN_REVIEW
self.collection_1.save()
-
self.client.force_login(self.user)
response = self.client.post(
get_admin_url(
@@ -126,8 +129,8 @@ def test_exclude_locked_collections(self):
def test_list_content_objects_from_collection_id_param(self):
ModerationRequest.objects.all().delete()
- self.collection_1._add_object(self.pg1)
- self.collection_2._add_object(self.pg2)
+ self.collection_1.add_object(self.pg1)
+ self.collection_2.add_object(self.pg2)
self.client.force_login(self.user)
response = self.client.get(
@@ -185,7 +188,7 @@ def setUp(self):
self.collection2.pk
)
- @mock.patch.object(ModerationCollection, 'submit_for_moderation')
+ @mock.patch.object(ModerationCollection, 'submit_for_review')
def test_submit_collection_for_moderation(self, submit_mock):
response = self.client.get(self.url)
self.assertEqual(200, response.status_code)
@@ -217,16 +220,16 @@ def test_change_list_view_should_contain_collection_object(self):
self.assertEqual(200, response.status_code)
self.assertEqual(response.context['collection'], self.collection2)
- @mock.patch.object(ModerationCollection, 'allow_submit_for_moderation')
+ @mock.patch.object(ModerationCollection, 'allow_submit_for_review')
def test_change_list_view_should_contain_submit_collection_url(self, allow_submit_mock):
response = self.client.get(self.url)
self.assertEqual(200, response.status_code)
- self.assertNotIn('submit_for_moderation_url', response.context)
+ self.assertNotIn('submit_for_review_url', response.context)
allow_submit_mock.__get__ = mock.Mock(return_value=False)
response = self.client.get(self.url_with_filter)
- self.assertNotIn('submit_for_moderation_url', response.context)
+ self.assertNotIn('submit_for_review_url', response.context)
allow_submit_mock.__get__ = mock.Mock(return_value=True)
response = self.client.get(self.url_with_filter)
- self.assertIn('submit_for_moderation_url', response.context)
+ self.assertIn('submit_for_review_url', response.context)
From 1c9287bfafe3de2309be67549521fd8c68a606c6 Mon Sep 17 00:00:00 2001
From: Mateusz Kamycki
Date: Mon, 20 Aug 2018 16:33:12 +0200
Subject: [PATCH 012/147] Fixed Circle CI caching problem (#45)
---
.circleci/config.yml | 59 ++++++++++++++++-------------------
djangocms_moderation/forms.py | 7 ++++-
tests/test_views.py | 2 +-
3 files changed, 34 insertions(+), 34 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a489597f..28e2b07e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,9 +7,9 @@ py34default: &py34default
- setup_remote_docker:
docker_layer_caching: true
- checkout
- - restore_cache:
- keys: py34-
- - run: docker load -i caches/py34.tar || true
+ - attach_workspace:
+ at: /tmp/images
+ - run: docker load -i /tmp/images/py34.tar || true
- run: docker run py34 tox -e $CIRCLE_STAGE
py35default: &py35default
@@ -19,9 +19,9 @@ py35default: &py35default
- setup_remote_docker:
docker_layer_caching: true
- checkout
- - restore_cache:
- keys: py35-
- - run: docker load -i caches/py35.tar || true
+ - attach_workspace:
+ at: /tmp/images
+ - run: docker load -i /tmp/images/py35.tar || true
- run: docker run py35 tox -e $CIRCLE_STAGE
@@ -32,9 +32,9 @@ py36default: &py36default
- setup_remote_docker:
docker_layer_caching: true
- checkout
- - restore_cache:
- keys: py36-
- - run: docker load -i caches/py36.tar || true
+ - attach_workspace:
+ at: /tmp/images
+ - run: docker load -i /tmp/images/py36.tar || true
- run: docker run py36 tox -e $CIRCLE_STAGE
py34_requires: &py34_requires
@@ -58,12 +58,11 @@ jobs:
- setup_remote_docker:
docker_layer_caching: true
- run: docker build -f .circleci/Dockerfile --build-arg PYTHON_VERSION=3.4 -t py34 .
- - run: mkdir caches
- - run: docker save -o caches/py34.tar py34
- - save_cache:
- key: py34-{{ .Environment.CIRCLE_SHA1 }}
- paths:
- - "caches/"
+ - run: mkdir images
+ - run: docker save -o images/py34.tar py34
+ - persist_to_workspace:
+ root: images
+ paths: py34.tar
py36_base:
docker:
- image: circleci/python:3.6
@@ -72,12 +71,11 @@ jobs:
- setup_remote_docker:
docker_layer_caching: true
- run: docker build -f .circleci/Dockerfile --build-arg PYTHON_VERSION=3.6 -t py36 .
- - run: mkdir caches
- - run: docker save -o caches/py36.tar py36
- - save_cache:
- key: py36-{{ .Environment.CIRCLE_SHA1 }}
- paths:
- - "caches/"
+ - run: mkdir images
+ - run: docker save -o images/py36.tar py36
+ - persist_to_workspace:
+ root: images
+ paths: py36.tar
py35_base:
docker:
@@ -87,12 +85,11 @@ jobs:
- setup_remote_docker:
docker_layer_caching: true
- run: docker build -f .circleci/Dockerfile --build-arg PYTHON_VERSION=3.5 -t py35 .
- - run: mkdir caches
- - run: docker save -o caches/py35.tar py35
- - save_cache:
- key: py35-{{ .Environment.CIRCLE_SHA1 }}
- paths:
- - "caches/"
+ - run: mkdir images
+ - run: docker save -o images/py35.tar py35
+ - persist_to_workspace:
+ root: images
+ paths: py35.tar
flake8:
<<: *py35default
@@ -105,9 +102,9 @@ jobs:
- setup_remote_docker:
docker_layer_caching: true
- checkout
- - restore_cache:
- keys: py35-
- - run: docker load -i caches/py35.tar || true
+ - attach_workspace:
+ at: /tmp/images
+ - run: docker load -i /tmp/images/py35.tar || true
- run: docker run py35 gulp lint
py34-dj111-sqlite-cms4:
<<: *py34default
@@ -158,5 +155,3 @@ workflows:
- py36-dj20-sqlite-cms4:
requires:
- py36_base
-
-
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 35d8e629..5dd8df2e 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -10,7 +10,12 @@
from adminsortable2.admin import CustomInlineFormSet
-from .constants import ACTION_CANCELLED, ACTION_REJECTED, ACTION_RESUBMITTED, COLLECTING
+from .constants import (
+ ACTION_CANCELLED,
+ ACTION_REJECTED,
+ ACTION_RESUBMITTED,
+ COLLECTING,
+)
from .models import ModerationCollection, ModerationRequest
diff --git a/tests/test_views.py b/tests/test_views.py
index 1104a4b5..d55ec07f 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -7,10 +7,10 @@
from cms.utils.urlutils import add_url_parameters
+from djangocms_moderation import constants
from djangocms_moderation.forms import CollectionItemForm
from djangocms_moderation.models import ModerationCollection, ModerationRequest
from djangocms_moderation.utils import get_admin_url
-from djangocms_moderation import constants
from .utils.base import BaseViewTestCase
From 9afd53ddba1523bc574d028cbd182529d2a15e6e Mon Sep 17 00:00:00 2001
From: Noel da Costa
Date: Tue, 21 Aug 2018 16:56:19 +0100
Subject: [PATCH 013/147] Added pre-flight check (#46)
---
djangocms_moderation/admin.py | 36 +++++++++++++-
djangocms_moderation/admin_actions.py | 34 +++++++++++++
djangocms_moderation/models.py | 13 +++++
djangocms_moderation/views.py | 4 ++
tests/test_admin_actions.py | 71 +++++++++++++++++++++++++++
tests/test_models.py | 22 +++++++++
tests/test_views.py | 11 ++---
tests/utils/base.py | 13 ++++-
8 files changed, 196 insertions(+), 8 deletions(-)
create mode 100644 djangocms_moderation/admin_actions.py
create mode 100644 tests/test_admin_actions.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index d8745f28..7fbad9f3 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -3,6 +3,7 @@
from django.conf.urls import url
from django.contrib import admin
from django.core.urlresolvers import reverse
+from django.http import Http404
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext, ugettext_lazy as _
@@ -10,6 +11,7 @@
from adminsortable2.admin import SortableInlineAdminMixin
+from .admin_actions import publish_selected
from .forms import WorkflowStepInlineFormSet
from .helpers import get_form_submission_for_step
from .models import (
@@ -65,7 +67,7 @@ def form_submission(self, obj):
class ModerationRequestAdmin(admin.ModelAdmin):
- actions = None # remove `delete_selected` for now, it will be handled later
+ actions = [publish_selected]
inlines = [ModerationRequestActionInline]
list_display = ['id', 'content_type', 'get_title', 'collection', 'get_preview_link', 'get_status']
list_filter = ['collection']
@@ -85,6 +87,16 @@ def get_preview_link(self, obj):
def has_add_permission(self, request):
return False
+ def get_actions(self, request):
+ actions = super().get_actions(request)
+ # If there is nothing to publish, then remove `publish_selected` action
+ if 'publish_selected' in actions and (
+ not hasattr(request, '_collection') or
+ not request._collection.allow_pre_flight(request.user)
+ ):
+ del actions['publish_selected']
+ return actions
+
def changelist_view(self, request, extra_context=None):
# If we filter by a specific collection, we want to add this collection
# to the context
@@ -92,6 +104,7 @@ def changelist_view(self, request, extra_context=None):
if collection_id:
try:
collection = ModerationCollection.objects.get(pk=int(collection_id))
+ request._collection = collection
except (ValueError, ModerationCollection.DoesNotExist):
pass
else:
@@ -102,12 +115,22 @@ def changelist_view(self, request, extra_context=None):
args=(collection_id,)
)
extra_context['submit_for_review_url'] = submit_for_review_url
+ else:
+ # If no collection id, then don't show all requests
+ # as each collection's actions, buttons and privileges may differ
+ raise Http404
+
return super(ModerationRequestAdmin, self).changelist_view(request, extra_context)
def get_status(self, obj):
last_action = obj.get_last_action()
if obj.is_approved():
status = ugettext('Ready for publishing')
+
+ # TODO: consider published status for version e.g.:
+ # elif obj.content_object.is_published():
+ # status = ugettext('Published')
+
elif obj.is_active and obj.has_pending_step():
next_step = obj.get_next_required()
role = next_step.role.name
@@ -164,6 +187,17 @@ class ModerationCollectionAdmin(admin.ModelAdmin):
'status',
'date_created',
]
+ editonly_fields = ('status',) # fields editable only on EDIT
+ addonly_fields = ('workflow',) # fields editable only on CREATE
+
+ def get_readonly_fields(self, request, obj=None):
+ """
+ Override to provide editonly_fields and addonly_fields functionality
+ """
+ if obj: # Editing an existing object
+ return self.readonly_fields + self.addonly_fields
+ else: # Adding a new object
+ return self.readonly_fields + self.editonly_fields
def get_name_with_requests_link(self, obj):
"""
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
new file mode 100644
index 00000000..37934984
--- /dev/null
+++ b/djangocms_moderation/admin_actions.py
@@ -0,0 +1,34 @@
+from django.contrib import messages
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext_lazy as _, ungettext
+
+
+def publish_selected(modeladmin, request, queryset):
+ if request.user != request._collection.author:
+ raise PermissionDenied
+
+ num_published_requests = 0
+ for moderation_request in queryset.all():
+ if moderation_request.is_approved():
+ num_published_requests += 1
+ publish_content_object(moderation_request.content_object)
+
+ # notify the UI of the action results
+ messages.success(
+ request,
+ ungettext(
+ '%(count)d request successfully published',
+ '%(count)d requests successfully published',
+ num_published_requests
+ ) % {
+ 'count': num_published_requests
+ },
+ )
+
+
+publish_selected.short_description = _("Publish selected requests")
+
+
+def publish_content_object(content_object):
+ # TODO: e.g.moderation_request.content_object.publish(request.user)
+ return True
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index b429d8c4..be8bd5a3 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -303,6 +303,19 @@ def allow_submit_for_review(self):
"""
return self.status == constants.COLLECTING and self.moderation_requests.exists()
+ def allow_pre_flight(self, user):
+ """
+ Is this collection ready for pre-flight?
+ :return:
+ """
+ if self.status != constants.IN_REVIEW or user != self.author:
+ return False
+ moderation_requests = self.moderation_requests.filter(is_active=True)
+ for moderation_request in moderation_requests:
+ if moderation_request.is_approved():
+ return True
+ return False
+
def add_object(self, content_object):
"""
Add object to the ModerationRequest in this collection.
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 599443e9..1de4b0a8 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -26,6 +26,7 @@ class CollectionItemView(FormView):
def get_form_kwargs(self):
kwargs = super(CollectionItemView, self).get_form_kwargs()
+ # TODO: replace page object with Version object
kwargs['initial'].update({
'content_object_id': self.request.GET.get('content_object_id'),
'content_type': ContentType.objects.get_for_model(Page).pk,
@@ -81,6 +82,9 @@ def get_context_data(self, **kwargs):
def moderation_confirmation_page(request, confirmation_id):
+ """
+ This is an implementation of Aldryn-forms to provide a review confirmation page
+ """
confirmation_page_instance = get_object_or_404(ConfirmationPage, pk=confirmation_id)
content_view = bool(request.GET.get('content_view'))
page_id = request.GET.get('page')
diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py
new file mode 100644
index 00000000..b9c189a3
--- /dev/null
+++ b/tests/test_admin_actions.py
@@ -0,0 +1,71 @@
+import mock
+
+from django.contrib.admin import ACTION_CHECKBOX_NAME
+from django.urls import reverse
+
+from cms.api import create_page
+
+from djangocms_moderation import constants
+from djangocms_moderation.models import (
+ ModerationCollection,
+ ModerationRequest,
+ Workflow,
+)
+
+from .utils.base import BaseTestCase
+
+
+class AdminActionTest(BaseTestCase):
+
+ def setUp(self):
+ self.wf = Workflow.objects.create(name='Workflow Test',)
+ self.collection = ModerationCollection.objects.create(
+ author=self.user, name='Collection Admin Actions', workflow=self.wf, status=constants.IN_REVIEW
+ )
+
+ pg1 = create_page(title='Page 1', template='page.html', language='en',)
+ pg2 = create_page(title='Page 2', template='page.html', language='en',)
+
+ self.mr1 = ModerationRequest.objects.create(
+ content_object=pg1, language='en', collection=self.collection, is_active=True,)
+
+ self.wfst = self.wf.steps.create(role=self.role1, is_required=True, order=1,)
+
+ # this moderation request is approved
+ self.mr1.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
+ self.mr1.actions.create(
+ by_user=self.user,
+ to_user=self.user2,
+ action=constants.ACTION_APPROVED,
+ step_approved=self.wfst,
+ )
+
+ # this moderation request is not approved
+ self.mr2 = ModerationRequest.objects.create(
+ content_object=pg2, language='en', collection=self.collection, is_active=True,)
+ self.mr2.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
+
+ self.url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
+ self.url_with_filter = "{}?collection__id__exact={}".format(
+ self.url, self.collection.pk
+ )
+
+ self.client.force_login(self.user)
+
+ @mock.patch('djangocms_moderation.admin_actions.publish_content_object')
+ def test_publish_selected(self, mock_publish_content_object):
+
+ fixtures = [self.mr1, self.mr2]
+ data = {
+ 'action': 'publish_selected',
+ 'select_across': 0,
+ 'index': 0,
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+ self.client.post(self.url_with_filter, data, follow=True)
+ self.assertTrue(self.mr1.is_approved())
+ self.assertFalse(self.mr2.is_approved())
+
+ assert mock_publish_content_object.called
+ # check it has been called only once, i.e. with the approved mr1
+ mock_publish_content_object.assert_called_once_with(self.mr1.content_object)
diff --git a/tests/test_models.py b/tests/test_models.py
index 416cc1e0..df40d778 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -500,6 +500,28 @@ def test_allow_submit_for_review(self):
self.collection1.save()
self.assertFalse(self.collection1.allow_submit_for_review)
+ def test_allow_pre_flight(self):
+ self.collection4.status = constants.COLLECTING
+ self.collection4.save()
+
+ # This is false, as there are no approved requests and status is COLLECTING
+ self.assertFalse(self.collection4.allow_pre_flight(self.user))
+
+ self.collection4.status = constants.IN_REVIEW
+ self.collection4.save()
+
+ # This is false, as there are no approved requests
+ self.assertFalse(self.collection4.allow_pre_flight(self.user))
+
+ self.moderation_request5.update_status(
+ action=constants.ACTION_APPROVED,
+ by_user=self.user,
+ message='Approved',
+ )
+
+ self.assertTrue(self.collection4.allow_pre_flight(self.user))
+ self.assertFalse(self.collection4.allow_pre_flight(self.user2))
+
@patch('djangocms_moderation.models.notify_collection_moderators')
def test_submit_for_review(self, mock_ncm):
ModerationRequest.objects.create(
diff --git a/tests/test_views.py b/tests/test_views.py
index d55ec07f..5fcb0dcd 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -211,21 +211,20 @@ def setUp(self):
self.url, self.collection2.pk
)
- def test_change_list_view_should_contain_collection_object(self):
+ def test_change_list_view_should_404_if_not_filtered(self):
response = self.client.get(self.url)
+ self.assertEqual(404, response.status_code)
+
+ response = self.client.get(self.url_with_filter)
self.assertEqual(200, response.status_code)
- self.assertNotIn('collection', response.context)
+ def test_change_list_view_should_contain_collection_object(self):
response = self.client.get(self.url_with_filter)
self.assertEqual(200, response.status_code)
self.assertEqual(response.context['collection'], self.collection2)
@mock.patch.object(ModerationCollection, 'allow_submit_for_review')
def test_change_list_view_should_contain_submit_collection_url(self, allow_submit_mock):
- response = self.client.get(self.url)
- self.assertEqual(200, response.status_code)
- self.assertNotIn('submit_for_review_url', response.context)
-
allow_submit_mock.__get__ = mock.Mock(return_value=False)
response = self.client.get(self.url_with_filter)
self.assertNotIn('submit_for_review_url', response.context)
diff --git a/tests/utils/base.py b/tests/utils/base.py
index 8c78bce8..f82bbb49 100644
--- a/tests/utils/base.py
+++ b/tests/utils/base.py
@@ -20,13 +20,15 @@ def setUpTestData(cls):
cls.wf1 = Workflow.objects.create(pk=1, name='Workflow 1', is_default=True,)
cls.wf2 = Workflow.objects.create(pk=2, name='Workflow 2',)
cls.wf3 = Workflow.objects.create(pk=3, name='Workflow 3',)
+ cls.wf4 = Workflow.objects.create(pk=4, name='Workflow 4',)
# create pages
cls.pg1 = create_page(title='Page 1', template='page.html', language='en',)
cls.pg2 = create_page(title='Page 2', template='page.html', language='en',)
cls.pg3 = create_page(title='Page 3', template='page.html', language='en', published=True)
cls.pg4 = create_page(title='Page 4', template='page.html', language='en',)
- cls.pg5 = create_page(title='Page 4', template='page.html', language='en', published=True)
+ cls.pg5 = create_page(title='Page 5', template='page.html', language='en', published=True)
+ cls.pg6 = create_page(title='Page 5', template='page.html', language='en',)
# create users, groups and roles
cls.user = User.objects.create_superuser(
@@ -55,6 +57,8 @@ def setUpTestData(cls):
cls.wf3st1 = cls.wf3.steps.create(role=cls.role1, is_required=True, order=1,)
cls.wf3st2 = cls.wf3.steps.create(role=cls.role3, is_required=False, order=2,)
+ cls.wf4st4 = cls.wf4.steps.create(role=cls.role1, is_required=True, order=1,)
+
# create page moderation requests and actions
cls.collection1 = ModerationCollection.objects.create(
author=cls.user, name='Collection 1', workflow=cls.wf1
@@ -65,6 +69,9 @@ def setUpTestData(cls):
cls.collection3 = ModerationCollection.objects.create(
author=cls.user, name='Collection 3', workflow=cls.wf3
)
+ cls.collection4 = ModerationCollection.objects.create(
+ author=cls.user, name='Collection 4', workflow=cls.wf4
+ )
cls.moderation_request1 = ModerationRequest.objects.create(
content_object=cls.pg1, language='en', collection=cls.collection1, is_active=True,)
@@ -99,6 +106,10 @@ def setUpTestData(cls):
cls.moderation_request4.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request4.actions.create(by_user=cls.user2, action=constants.ACTION_REJECTED)
+ cls.moderation_request5 = ModerationRequest.objects.create(
+ content_object=cls.pg6, language='en', collection=cls.collection4, is_active=True,)
+ cls.moderation_request5.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
+
class BaseViewTestCase(BaseTestCase):
From 8bbf16146eb0894a653ede3bd4ea3a0bab4804f3 Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Tue, 21 Aug 2018 18:03:01 +0100
Subject: [PATCH 014/147] Delete selected moderation requests (remove items
from collection) (#44)
---
README.rst | 2 +-
djangocms_moderation/admin.py | 7 ++----
djangocms_moderation/admin_actions.py | 22 ++++++++++++++++++
tests/test_admin_actions.py | 32 +++++++++++++++++++++++++++
4 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/README.rst b/README.rst
index 302fc510..d724bd27 100644
--- a/README.rst
+++ b/README.rst
@@ -9,7 +9,7 @@ Installation
Requirements
============
-django CMS Moderation requires that you have a django CMS 3.4.3 (or higher) project already running and set up.
+django CMS Moderation requires that you have a django CMS 4.0 (or higher) project already running and set up.
To install
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 7fbad9f3..c3303565 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -11,7 +11,7 @@
from adminsortable2.admin import SortableInlineAdminMixin
-from .admin_actions import publish_selected
+from .admin_actions import delete_selected, publish_selected
from .forms import WorkflowStepInlineFormSet
from .helpers import get_form_submission_for_step
from .models import (
@@ -39,9 +39,6 @@ class ModerationRequestActionInline(admin.TabularInline):
def has_add_permission(self, request):
return False
- def has_delete_permission(self, request, obj=None):
- return False
-
def show_user(self, obj):
_name = obj.get_by_user_name()
return ugettext('By {user}').format(user=_name)
@@ -67,7 +64,7 @@ def form_submission(self, obj):
class ModerationRequestAdmin(admin.ModelAdmin):
- actions = [publish_selected]
+ actions = [delete_selected, publish_selected]
inlines = [ModerationRequestActionInline]
list_display = ['id', 'content_type', 'get_title', 'collection', 'get_preview_link', 'get_status']
list_filter = ['collection']
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
index 37934984..1cd69bd0 100644
--- a/djangocms_moderation/admin_actions.py
+++ b/djangocms_moderation/admin_actions.py
@@ -3,6 +3,28 @@
from django.utils.translation import ugettext_lazy as _, ungettext
+def delete_selected(modeladmin, request, queryset):
+ if not modeladmin.has_delete_permission(request):
+ raise PermissionDenied
+
+ if queryset.exclude(collection__author=request.user).exists():
+ raise PermissionDenied
+
+ num_deleted_requests = queryset.count()
+ queryset.delete()
+
+ messages.success(
+ request,
+ ungettext(
+ '%(count)d request successfully deleted',
+ '%(count)d requests successfully deleted',
+ num_deleted_requests
+ ) % {
+ 'count': num_deleted_requests
+ },
+ )
+
+
def publish_selected(modeladmin, request, queryset):
if request.user != request._collection.author:
raise PermissionDenied
diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py
index b9c189a3..06d119d4 100644
--- a/tests/test_admin_actions.py
+++ b/tests/test_admin_actions.py
@@ -6,6 +6,7 @@
from cms.api import create_page
from djangocms_moderation import constants
+from djangocms_moderation.admin import ModerationRequestAdmin
from djangocms_moderation.models import (
ModerationCollection,
ModerationRequest,
@@ -52,6 +53,37 @@ def setUp(self):
self.client.force_login(self.user)
+ @mock.patch.object(ModerationRequestAdmin, 'has_delete_permission')
+ def test_delete_selected(self, mock_has_delete_permission):
+ mock_has_delete_permission.return_value = True
+ self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
+
+ fixtures = [self.mr1, self.mr2]
+ data = {
+ 'action': 'delete_selected',
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+ # This user is not the collection author
+ self.client.force_login(self.user2)
+ response = self.client.post(self.url_with_filter, data)
+ self.assertEqual(response.status_code, 403)
+ # Nothing is deleted
+ self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
+
+ # Now lets try with collection author, but without delete permission
+ mock_has_delete_permission.return_value = False
+ self.client.force_login(self.user)
+ response = self.client.post(self.url_with_filter, data)
+ self.assertEqual(response.status_code, 403)
+ # Nothing is deleted
+ self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
+
+ mock_has_delete_permission.return_value = True
+ self.client.force_login(self.user)
+ response = self.client.post(self.url_with_filter, data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 0)
+
@mock.patch('djangocms_moderation.admin_actions.publish_content_object')
def test_publish_selected(self, mock_publish_content_object):
From 14083e16d12937042d9ded2010b1360715bd5f9d Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Mon, 3 Sep 2018 15:24:13 +0100
Subject: [PATCH 015/147] Bulk actions (#47)
---
djangocms_moderation/admin.py | 131 +++++++---
djangocms_moderation/admin_actions.py | 172 ++++++++++++-
djangocms_moderation/cms_toolbars.py | 3 +-
djangocms_moderation/emails.py | 64 ++---
djangocms_moderation/forms.py | 3 +-
djangocms_moderation/helpers.py | 3 +-
djangocms_moderation/models.py | 37 ++-
.../emails/moderation-request/approved.txt | 20 +-
.../emails/moderation-request/cancelled.txt | 20 +-
.../emails/moderation-request/rejected.txt | 24 +-
.../emails/moderation-request/request.txt | 6 +-
.../moderation_request_change_list.html | 24 +-
tests/test_admin.py | 151 ++++++++++++
tests/test_admin_actions.py | 228 ++++++++++++++++--
tests/test_app_registration.py | 2 +
tests/test_forms.py | 4 +-
tests/test_models.py | 56 +++--
tests/test_views.py | 4 +-
tests/utils/app_1/models.py | 6 +
tests/utils/app_2/models.py | 6 +
tests/utils/base.py | 6 +-
21 files changed, 813 insertions(+), 157 deletions(-)
create mode 100644 tests/test_admin.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index c3303565..525457be 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -11,7 +11,14 @@
from adminsortable2.admin import SortableInlineAdminMixin
-from .admin_actions import delete_selected, publish_selected
+from .admin_actions import (
+ approve_selected,
+ delete_selected,
+ publish_selected,
+ reject_selected,
+ resubmit_selected,
+)
+from .constants import ARCHIVED, IN_REVIEW
from .forms import WorkflowStepInlineFormSet
from .helpers import get_form_submission_for_step
from .models import (
@@ -64,14 +71,26 @@ def form_submission(self, obj):
class ModerationRequestAdmin(admin.ModelAdmin):
- actions = [delete_selected, publish_selected]
+ actions = [ # filtered out in `self.get_actions`
+ delete_selected,
+ publish_selected,
+ approve_selected,
+ reject_selected,
+ resubmit_selected,
+ ]
inlines = [ModerationRequestActionInline]
list_display = ['id', 'content_type', 'get_title', 'collection', 'get_preview_link', 'get_status']
- list_filter = ['collection']
fields = ['id', 'collection', 'workflow', 'is_active', 'get_status']
readonly_fields = fields
change_list_template = 'djangocms_moderation/moderation_request_change_list.html'
+ def has_module_permission(self, request):
+ """
+ Don't display Requests in the admin index as they should be accessed
+ and filtered through the Collection list view
+ """
+ return False
+
def get_title(self, obj):
return obj.content_object
get_title.short_description = _('Title')
@@ -85,14 +104,53 @@ def has_add_permission(self, request):
return False
def get_actions(self, request):
+ """
+ By default, all actions are enabled. But we need to only keep the actions
+ which have a moderation requests ready for.
+ E.g. if there are no moderation requests ready to be published,
+ we don't need to keep the `publish_selected` action
+ """
+ try:
+ collection = request._collection
+ except AttributeError:
+ # If we are not in the collection aware list, then don't
+ # offer any bulk actions
+ return {}
+
actions = super().get_actions(request)
- # If there is nothing to publish, then remove `publish_selected` action
- if 'publish_selected' in actions and (
- not hasattr(request, '_collection') or
- not request._collection.allow_pre_flight(request.user)
- ):
- del actions['publish_selected']
- return actions
+ actions_to_keep = []
+
+ if collection.status in [IN_REVIEW, ARCHIVED]:
+ # Keep track how many actions we've added in the below loop (_actions_kept).
+ # If we added all of them (_max_to_keep), we can exit the for loop
+ if collection.status == IN_REVIEW:
+ _max_to_keep = 4 # publish_selected, approve_selected, reject_selected, resubmit_selected
+ else:
+ # If the collection is archived, then no other action than
+ # `publish_selected` is possible.
+ _max_to_keep = 1 # publish_selected
+
+ for mr in collection.moderation_requests.all():
+ if len(actions_to_keep) == _max_to_keep:
+ break # We have found all the actions, so no need to loop anymore
+ if 'publish_selected' not in actions_to_keep:
+ if mr.is_approved() and request.user == collection.author:
+ actions_to_keep.append('publish_selected')
+ if collection.status == IN_REVIEW and 'approve_selected' not in actions_to_keep:
+ if mr.user_can_take_moderation_action(request.user):
+ actions_to_keep.append('approve_selected')
+ actions_to_keep.append('reject_selected')
+ if collection.status == IN_REVIEW and 'resubmit_selected' not in actions_to_keep:
+ if mr.user_can_resubmit(request.user):
+ actions_to_keep.append('resubmit_selected')
+
+ # Only collection author can delete moderation requests
+ if collection.author == request.user:
+ actions_to_keep.append('delete_selected')
+
+ return {
+ key: value for key, value in actions.items() if key in actions_to_keep
+ }
def changelist_view(self, request, extra_context=None):
# If we filter by a specific collection, we want to add this collection
@@ -106,7 +164,7 @@ def changelist_view(self, request, extra_context=None):
pass
else:
extra_context = dict(collection=collection)
- if collection.allow_submit_for_review:
+ if collection.allow_submit_for_review(user=request.user):
submit_for_review_url = reverse(
'admin:cms_moderation_submit_collection_for_moderation',
args=(collection_id,)
@@ -117,30 +175,32 @@ def changelist_view(self, request, extra_context=None):
# as each collection's actions, buttons and privileges may differ
raise Http404
- return super(ModerationRequestAdmin, self).changelist_view(request, extra_context)
+ return super().changelist_view(request, extra_context)
def get_status(self, obj):
+ # We can have moderation requests without any action (e.g. the
+ # ones not submitted for moderation yet)
last_action = obj.get_last_action()
- if obj.is_approved():
- status = ugettext('Ready for publishing')
-
- # TODO: consider published status for version e.g.:
- # elif obj.content_object.is_published():
- # status = ugettext('Published')
-
- elif obj.is_active and obj.has_pending_step():
- next_step = obj.get_next_required()
- role = next_step.role.name
- status = ugettext('Pending %(role)s approval') % {'role': role}
- elif last_action:
- # We can have moderation requests without any action (e.g. the
- # ones not submitted for moderation yet)
- user_name = last_action.get_by_user_name()
- message_data = {
- 'action': last_action.get_action_display(),
- 'name': user_name,
- }
- status = ugettext('%(action)s by %(name)s') % message_data
+
+ if last_action:
+ if obj.is_approved():
+ status = ugettext('Ready for publishing')
+ # TODO: consider published status for version e.g.:
+ # elif obj.content_object.is_published():
+ # status = ugettext('Published')
+ elif obj.is_rejected():
+ status = ugettext('Pending author rework')
+ elif obj.is_active and obj.has_pending_step():
+ next_step = obj.get_next_required()
+ role = next_step.role.name
+ status = ugettext('Pending %(role)s approval') % {'role': role}
+ else:
+ user_name = last_action.get_by_user_name()
+ message_data = {
+ 'action': last_action.get_action_display(),
+ 'name': user_name,
+ }
+ status = ugettext('%(action)s by %(name)s') % message_data
else:
status = ugettext('Ready for submission')
return status
@@ -179,6 +239,7 @@ class ModerationCollectionAdmin(admin.ModelAdmin):
list_display = [
'id',
'get_name_with_requests_link',
+ 'job_id',
'get_moderator',
'workflow',
'status',
@@ -229,7 +290,7 @@ def _url(regex, fn, name, **kwargs):
name='cms_moderation_item_to_collection',
)
]
- return url_patterns + super(ModerationCollectionAdmin, self).get_urls()
+ return url_patterns + super().get_urls()
class ConfirmationPageAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
@@ -246,7 +307,7 @@ def _url(regex, fn, name, **kwargs):
name='cms_moderation_confirmation_page',
),
]
- return url_patterns + super(ConfirmationPageAdmin, self).get_urls()
+ return url_patterns + super().get_urls()
class ConfirmationFormSubmissionAdmin(admin.ModelAdmin):
@@ -264,7 +325,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
- return super(ConfirmationFormSubmissionAdmin, self).change_view(
+ return super().change_view(
request, object_id, form_url, extra_context=extra_context,
)
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
index 1cd69bd0..e6f37403 100644
--- a/djangocms_moderation/admin_actions.py
+++ b/djangocms_moderation/admin_actions.py
@@ -2,6 +2,157 @@
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _, ungettext
+from djangocms_moderation import constants
+from djangocms_moderation.emails import (
+ notify_collection_author,
+ notify_collection_moderators,
+)
+
+
+def resubmit_selected(modeladmin, request, queryset):
+ """
+ Validate and re-submit all the selected moderation requests for
+ moderation and notify reviewers via email.
+ """
+ resubmitted_requests = []
+
+ for mr in queryset.all():
+ if mr.user_can_resubmit(request.user):
+ resubmitted_requests.append(mr)
+ mr.update_status(
+ action=constants.ACTION_RESUBMITTED,
+ by_user=request.user,
+ )
+
+ if resubmitted_requests:
+ # Lets notify reviewers. TODO task queue?
+ notify_collection_moderators(
+ collection=request._collection,
+ moderation_requests=resubmitted_requests,
+ # We can take any action here, as all the requests are in the same
+ # stage of moderation - at the beginning
+ action_obj=resubmitted_requests[0].get_last_action()
+ )
+
+ messages.success(
+ request,
+ ungettext(
+ '%(count)d request successfully resubmitted for review',
+ '%(count)d requests successfully resubmitted for review',
+ len(resubmitted_requests)
+ ) % {
+ 'count': len(resubmitted_requests)
+ },
+ )
+resubmit_selected.short_description = _("Resubmit changes for review") # noqa: E305
+
+
+def reject_selected(modeladmin, request, queryset):
+ """
+ Validate and reject all the selected moderation requests and notify
+ the author about these requests
+ """
+ rejected_requests = []
+
+ for moderation_request in queryset.all():
+ if moderation_request.user_can_take_moderation_action(request.user):
+ rejected_requests.append(moderation_request)
+ moderation_request.update_status(
+ action=constants.ACTION_REJECTED,
+ by_user=request.user,
+ )
+
+ # Now we need to notify collection reviewers and moderator. TODO task queue?
+ if rejected_requests:
+ notify_collection_author(
+ collection=request._collection,
+ moderation_requests=rejected_requests,
+ action=constants.ACTION_REJECTED,
+ by_user=request.user,
+ )
+
+ messages.success(
+ request,
+ ungettext(
+ '%(count)d request successfully submitted for rework',
+ '%(count)d requests successfully submitted for rework',
+ len(rejected_requests)
+ ) % {
+ 'count': len(rejected_requests)
+ },
+ )
+reject_selected.short_description = _('Submit for rework') # noqa: E305
+
+
+def approve_selected(modeladmin, request, queryset):
+ """
+ Validate and approve all the selected moderation requests and notify
+ the author and reviewers.
+
+ When bulk approving, we need to check for the next line of reviewers and
+ notify them about the pending moderation requests assigned to them.
+
+ Because this is a bulk action, we need to group the approved_requests
+ by the action.step_approved, so we notify the correct reviewers.
+
+ For example, if some requests are in the first stage of approval,
+ and some in the second, then the reviewers we need to notify are
+ different per request, depending on which stage the request is in
+ """
+ approved_requests = []
+ # Variable we are using to group the requests by action.step_approved
+ request_action_mapping = dict()
+
+ for mr in queryset.all():
+ if mr.user_can_take_moderation_action(request.user):
+ approved_requests.append(mr)
+ mr.update_status(
+ action=constants.ACTION_APPROVED,
+ by_user=request.user,
+ )
+ action = mr.get_last_action()
+ if action.to_user_id or action.to_role_id:
+ # We group the moderation requests by step_approved.pk.
+ # Sometimes it can be None, in which case they can be grouped
+ # together and we use "0" as a key
+ step_approved_key = str(action.step_approved.pk if action.step_approved else 0)
+ if step_approved_key not in request_action_mapping:
+ request_action_mapping[step_approved_key] = [mr]
+ request_action_mapping['action_' + step_approved_key] = action
+ else:
+ request_action_mapping[step_approved_key].append(mr)
+
+ if approved_requests: # TODO task queue?
+ # Lets notify the collection author about the approval
+ notify_collection_author(
+ collection=request._collection,
+ moderation_requests=approved_requests,
+ action=constants.ACTION_APPROVED,
+ by_user=request.user,
+ )
+
+ # Notify reviewers
+ for key, moderation_requests in sorted(request_action_mapping.items(), key=lambda x: x[0]):
+ if not key.startswith('action_'):
+ notify_collection_moderators(
+ collection=request._collection,
+ moderation_requests=moderation_requests,
+ action_obj=request_action_mapping['action_' + key]
+ )
+
+ messages.success(
+ request,
+ ungettext(
+ '%(count)d request successfully approved',
+ '%(count)d requests successfully approved',
+ len(approved_requests)
+ ) % {
+ 'count': len(approved_requests)
+ },
+ )
+
+ post_bulk_actions(request._collection)
+
def delete_selected(modeladmin, request, queryset):
if not modeladmin.has_delete_permission(request):
@@ -11,8 +162,16 @@ def delete_selected(modeladmin, request, queryset):
raise PermissionDenied
num_deleted_requests = queryset.count()
- queryset.delete()
+ if num_deleted_requests: # TODO task queue?
+ notify_collection_author(
+ collection=request._collection,
+ moderation_requests=[mr for mr in queryset],
+ action=constants.ACTION_CANCELLED,
+ by_user=request.user,
+ )
+
+ queryset.delete()
messages.success(
request,
ungettext(
@@ -24,6 +183,9 @@ def delete_selected(modeladmin, request, queryset):
},
)
+ post_bulk_actions(request._collection)
+delete_selected.short_description = _('Cancel selected') # noqa: E305
+
def publish_selected(modeladmin, request, queryset):
if request.user != request._collection.author:
@@ -47,8 +209,14 @@ def publish_selected(modeladmin, request, queryset):
},
)
+ post_bulk_actions(request._collection)
+publish_selected.short_description = _("Publish selected requests") # noqa: E305
+
-publish_selected.short_description = _("Publish selected requests")
+def post_bulk_actions(collection):
+ if collection.should_be_archived():
+ collection.status = constants.ARCHIVED
+ collection.save(update_fields=['status'])
def publish_content_object(content_object):
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 92ab46cc..0f0c63da 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -2,7 +2,6 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
-from cms.api import get_page_draft
from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool
from cms.utils.urlutils import add_url_parameters
@@ -24,7 +23,7 @@ def post_template_populate(self):
:return:
"""
super(ModerationToolbar, self).post_template_populate()
- page = get_page_draft(self.request.current_page)
+ page = self.request.current_page
if not page:
return None
diff --git a/djangocms_moderation/emails.py b/djangocms_moderation/emails.py
index c68b210e..0ccd7f10 100644
--- a/djangocms_moderation/emails.py
+++ b/djangocms_moderation/emails.py
@@ -19,27 +19,32 @@
email_subjects = {
- constants.ACTION_APPROVED: _('Changes Approved'),
- constants.ACTION_CANCELLED: _('Request for moderation cancelled'),
- constants.ACTION_REJECTED: _('Changes Rejected'),
+ constants.ACTION_APPROVED: _('Approved moderation requests'),
+ constants.ACTION_REJECTED: _('Rejected moderation requests'),
+ constants.ACTION_CANCELLED: _('Request for moderation deleted'),
}
-def _send_email(collection, action, recipients, subject, template):
- if action.to_user_id:
- moderator_name = action.get_to_user_name()
- elif action.to_role_id:
- moderator_name = action.to_role.name
- else:
- moderator_name = ''
+def _send_email(
+ collection,
+ moderation_requests,
+ recipients,
+ subject,
+ template,
+ by_user
+):
+ admin_url = "{}?collection__id__exact={}".format(
+ reverse('admin:djangocms_moderation_moderationrequest_changelist'),
+ collection.id
+ )
- admin_url = reverse('admin:djangocms_moderation_moderationcollection_change', args=(collection.pk,))
context = {
'collection': collection,
+ 'moderation_requests': moderation_requests,
'author_name': collection.author_name,
- 'by_user_name': action.get_by_user_name(),
- 'moderator_name': moderator_name,
'admin_url': get_absolute_url(admin_url),
+ 'job_id': collection.job_id,
+ 'by_user': by_user,
}
template = 'djangocms_moderation/emails/moderation-request/{}'.format(template)
@@ -56,32 +61,28 @@ def _send_email(collection, action, recipients, subject, template):
return message.send()
-def notify_request_author(request, action):
- if action.action not in email_subjects:
- # TODO: FINISH THIS
- return 0
-
- if not request.author.email:
- return 0
+def notify_collection_author(collection, moderation_requests, action, by_user):
+ if action not in email_subjects or not collection.author.email:
+ return
status = _send_email(
- request=request,
- action=action,
- recipients=[request.author.email],
- subject=email_subjects[action.action],
- template='{}.txt'.format(action.action),
+ collection=collection,
+ moderation_requests=moderation_requests,
+ recipients=[collection.author.email],
+ subject=email_subjects[action],
+ template='{}.txt'.format(action),
+ by_user=by_user,
)
return status
-def notify_collection_moderators(collection, action):
- if action.to_user_id and not action.to_user.email:
+def notify_collection_moderators(collection, moderation_requests, action_obj):
+ if action_obj.to_user_id and not action_obj.to_user.email:
return 0
-
try:
- recipients = [action.to_user.email]
+ recipients = [action_obj.to_user.email]
except AttributeError:
- users = action.to_role.get_users_queryset().exclude(email='')
+ users = action_obj.to_role.get_users_queryset().exclude(email='')
recipients = users.values_list('email', flat=True)
if not recipients:
@@ -89,9 +90,10 @@ def notify_collection_moderators(collection, action):
status = _send_email(
collection=collection,
- action=action,
+ moderation_requests=moderation_requests,
recipients=recipients,
subject=_('Review requested'),
template='request.txt',
+ by_user=action_obj.by_user
)
return status
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 5dd8df2e..4ae23c1b 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -149,7 +149,6 @@ def clean(self):
content_object = content_type.get_object_for_this_type(
pk=self.cleaned_data['content_object_id'],
is_page_type=False,
- publisher_is_draft=True,
)
except content_type.model_class().DoesNotExist:
content_object = None
@@ -192,7 +191,7 @@ def configure_moderator_field(self):
self.fields['moderator'].queryset = users
def clean(self):
- if not self.collection.allow_submit_for_review:
+ if not self.collection.allow_submit_for_review(user=self.user):
self.add_error(None, _("This collection can't be submitted for a review"))
return super(SubmitCollectionForModerationForm, self).clean()
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index ecaf6855..0dfed959 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -39,8 +39,7 @@ def get_page_or_404(obj_id, language):
return content_type.get_object_for_this_type(
pk=obj_id,
is_page_type=False,
- publisher_is_draft=True,
- title_set__language=language,
+ pagecontent_set__language=language,
)
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index be8bd5a3..a7f17e81 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -273,6 +273,10 @@ class ModerationCollection(models.Model):
def __str__(self):
return self.name
+ @property
+ def job_id(self):
+ return "{}".format(self.pk)
+
@property
def author_name(self):
return self.author.get_full_name() or self.author.get_username()
@@ -293,28 +297,35 @@ def submit_for_review(self, by_user, to_user=None):
self.save(update_fields=['status'])
# It is fine to pass any `action` from any moderation_request.actions
# above as it will have the same moderators
- notify_collection_moderators(collection=self, action=action)
+ notify_collection_moderators(
+ collection=self,
+ moderation_requests=self.moderation_requests.all(),
+ action_obj=action,
+ )
- @property
- def allow_submit_for_review(self):
+ def allow_submit_for_review(self, user):
"""
Can this collection be submitted for review?
:return:
"""
- return self.status == constants.COLLECTING and self.moderation_requests.exists()
+ return all([
+ self.author == user,
+ self.status == constants.COLLECTING,
+ self.moderation_requests.exists(),
+ ])
- def allow_pre_flight(self, user):
+ def should_be_archived(self):
"""
- Is this collection ready for pre-flight?
+ Collection should be archived if all moderation requests are moderated
:return:
"""
- if self.status != constants.IN_REVIEW or user != self.author:
+ if self.status in [constants.COLLECTING, constants.ARCHIVED]:
return False
- moderation_requests = self.moderation_requests.filter(is_active=True)
- for moderation_request in moderation_requests:
- if moderation_request.is_approved():
- return True
- return False
+ # TODO this is not efficient, is there a better way?
+ for mr in self.moderation_requests.all():
+ if not mr.is_approved():
+ return False
+ return True
def add_object(self, content_object):
"""
@@ -369,6 +380,7 @@ class Meta:
verbose_name = _('Request')
verbose_name_plural = _('Requests')
unique_together = ('collection', 'object_id', 'content_type')
+ ordering = ['id']
def __str__(self):
return "{} {}".format(
@@ -496,7 +508,6 @@ def user_can_take_moderation_action(self, user):
return False
pending_steps = self.get_pending_steps().select_related('role')
-
for step in pending_steps.iterator():
is_assigned = step.role.user_is_assigned(user)
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
index df41922e..4bfcc35f 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
@@ -1,14 +1,30 @@
{% load i18n %}
-{% blocktrans %}
-Hello {{ author_name }}, {{ by_user_name }} has approved your changes in {{ page_url }}.
+{% blocktrans with collection.author_name as collection_author and collection.name as collection_name %}
+Hello {{ collection_author }}, {{ by_user }} has approved some moderation
+requests in the collection {{ collection_name }}.
{% endblocktrans %}
+
+{% trans 'Approved moderation requests' %}:
+
+
+
+{% for moderation_request in moderation_requests %}
+ -
+ {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+
+{% endfor %}
+
+
+
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
{% endif %}
+{% if job_id %}
{% trans 'Job ID' %}: {{ job_id }}
+{% endif %}
{% if admin_url %}
{% trans 'Admin Url' %}:
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
index f9210c95..c1f0ae98 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
@@ -1,17 +1,31 @@
{% load i18n %}
-{% blocktrans %}
-Hello {{ author_name }}, {{ by_user_name }} has cancelled your request for moderation on {{ page_url }}.
+{% blocktrans with collection.author_name as collection_author and collection.name as collection_name %}
+Hello {{ collection_author }}, the following moderation requests in the collection
+{{ collection_name }} have been cancelled
{% endblocktrans %}
+
+{% trans 'Cancelled moderation requests' %}:
+
+
+
+{% for moderation_request in moderation_requests %}
+ -
+ {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+
+{% endfor %}
+
+
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
{% endif %}
+{% if job_id %}
{% trans 'Job ID' %}: {{ job_id }}
+{% endif %}
{% if admin_url %}
{% trans 'Admin Url' %}:
{{ admin_url }}
{% endif %}
-
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
index cc7f4f9e..2995502b 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
@@ -1,16 +1,32 @@
{% load i18n %}
-{% blocktrans %}
-Hello {{ author_name }}, {{ by_user_name }} has rejected your changes in {{ page_url }}.
-
-You can resubmit the necessary changes for another review, or cancel the moderation request.
+{% blocktrans with collection.author_name as collection_author and collection.name as collection_name %}
+Hello {{ collection_author }}, {{ by_user }} has rejected some moderation
+requests in the collection {{ collection_name }}.
{% endblocktrans %}
+
+{% trans 'You can resubmit the necessary changes for another review, or remove the moderation requests from the collection.' %}
+
+
+
+{% trans 'Rejected moderation requests' %}:
+
+
+
+{% for moderation_request in moderation_requests %}
+ -
+ {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+
+{% endfor %}
+
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
{% endif %}
+{% if job_id %}
{% trans 'Job ID' %}: {{ job_id }}
+{% endif %}
{% if admin_url %}
{% trans 'Admin Url' %}:
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
index 01c003c3..f3767b0c 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
@@ -1,14 +1,14 @@
{% load i18n %}
{% blocktrans with collection.name as collection_name %}
-Hello {{ moderator_name }}, {{ by_user_name }} has requested your approval for
+Hello, {{ by_user }} has requested your approval for
changes in the collection {{ collection_name }}.
{% endblocktrans %}
-{% trans 'Items included in this collection:' %}
+{% trans 'Items included in this request' %}:
-{% for moderation_request in collection.moderation_requests.all %}
+{% for moderation_request in moderation_requests %}
-
{{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
index 185b403f..e1500e07 100644
--- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -1,21 +1,23 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
+{% block content_title %}
+ {% if collection %}
+ {{ collection.name }}
+
+ {% trans 'Job ID' %}: {{ collection.job_id }}
+ {% trans 'Status' %}: {{ collection.get_status_display }}
+ {% trans 'Workflow' %}: {{ collection.workflow.name }}
+ {% trans 'Author' %}: {{ collection.author_name }}
+
+ {% endif %}
+{% endblock %}
+
{% block object-tools-items %}
{{ block.super }}
{% if submit_for_review_url %}
-
- {% trans 'Submit for review' %}
+ {% trans 'Submit collection for review' %}
{% endif %}
{% endblock %}
-
-{% block content %}
-{{ block.super }}
-
-{% comment %}
- TODO This template will be overridden to provide the required functionality.
- For now, lets just output the selected collection
-{% endcomment %}
-Selected collection: {{ collection }}
-{% endblock %}
diff --git a/tests/test_admin.py b/tests/test_admin.py
new file mode 100644
index 00000000..a6be7bb8
--- /dev/null
+++ b/tests/test_admin.py
@@ -0,0 +1,151 @@
+from django.contrib import admin
+from django.urls import reverse
+
+from cms.api import create_page
+
+from djangocms_moderation import constants
+from djangocms_moderation.admin import ModerationRequestAdmin
+from djangocms_moderation.constants import ACTION_REJECTED
+from djangocms_moderation.models import (
+ ModerationCollection,
+ ModerationRequest,
+ Workflow,
+)
+
+from .utils.base import BaseTestCase
+
+
+class MockRequest:
+ GET = {}
+
+
+class ModerationRequestAdminTestCase(BaseTestCase):
+ def setUp(self):
+ self.wf = Workflow.objects.create(name='Workflow Test',)
+ self.collection = ModerationCollection.objects.create(
+ author=self.user, name='Collection Admin Actions', workflow=self.wf, status=constants.IN_REVIEW
+ )
+
+ pg1 = create_page(title='Page 1', template='page.html', language='en',)
+ pg2 = create_page(title='Page 2', template='page.html', language='en',)
+
+ self.mr1 = ModerationRequest.objects.create(
+ content_object=pg1, language='en', collection=self.collection, is_active=True,)
+
+ self.wfst = self.wf.steps.create(role=self.role2, is_required=True, order=1,)
+
+ # this moderation request is approved
+ self.mr1.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
+ self.mr1action2 = self.mr1.actions.create(
+ by_user=self.user,
+ to_user=self.user2,
+ action=constants.ACTION_APPROVED,
+ step_approved=self.wfst,
+ )
+
+ # this moderation request is not approved
+ self.mr2 = ModerationRequest.objects.create(
+ content_object=pg2, language='en', collection=self.collection, is_active=True,)
+ self.mr2.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
+
+ self.url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
+ self.url_with_filter = "{}?collection__id__exact={}".format(
+ self.url, self.collection.pk
+ )
+ self.mra = ModerationRequestAdmin(ModerationRequest, admin.AdminSite())
+
+ def test_delete_selected_action_visibility(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertIn('delete_selected', actions)
+
+ # user2 won't be able to delete requests, as they are not the collection
+ # author
+ mock_request.user = self.user2
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('delete_selected', actions)
+
+ def test_publish_selected_action_visibility(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ actions = self.mra.get_actions(request=mock_request)
+ # mr1 request is approved, so user1 can see the publish selected option
+ self.assertIn('publish_selected', actions)
+
+ # user2 should not be able to see it
+ mock_request.user = self.user2
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('publish_selected', actions)
+
+ # if there are no approved requests, user can't see the button either
+ mock_request.user = self.user
+ self.mr1.get_last_action().delete()
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('publish_selected', actions)
+
+ def test_approve_and_reject_selected_action_visibility(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ actions = self.mra.get_actions(request=mock_request)
+ # mr1 is not a moderator for collection1 so he can't approve or reject
+ # anything
+ self.assertNotIn('approve_selected', actions)
+ self.assertNotIn('reject_selected', actions)
+
+ # user2 is moderator and there is 1 unapproved request
+ mock_request.user = self.user2
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertIn('approve_selected', actions)
+ self.assertIn('reject_selected', actions)
+
+ # now everything is approved, so not even user2 can see the actions
+ self.mr2.delete()
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('approve_selected', actions)
+ self.assertNotIn('reject_selected', actions)
+
+ def test_resubmit_selected_action_visibility(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ actions = self.mra.get_actions(request=mock_request)
+ # There is nothing set to re-work, so user can't see the resubmit action
+ self.assertNotIn('resubmit_selected', actions)
+
+ self.mr1action2.action = ACTION_REJECTED
+ self.mr1action2.save()
+ actions = self.mra.get_actions(request=mock_request)
+ # There is 1 mr to rework now, so user can do it
+ self.assertIn('resubmit_selected', actions)
+
+ # user2 can't, as they are not the author of the request
+ mock_request.user = self.user2
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('resubmit_selected', actions)
+
+ def test_in_review_status_is_considered(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ self.collection.status = constants.ARCHIVED
+ self.collection.save()
+
+ actions = self.mra.get_actions(request=mock_request)
+ # for self.user, the publish_selected should be available even if
+ # collection status is ARCHIVED
+ self.assertIn('publish_selected', actions)
+
+ mock_request.user = self.user2
+ actions = self.mra.get_actions(request=mock_request)
+ # mr2 request is not approved, so user2 should see the
+ # approve_selected option, but the collection is not in IN_REVIEW
+ self.assertNotIn('approve_selected', actions)
+
+ self.collection.status = constants.IN_REVIEW
+ self.collection.save()
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertIn('approve_selected', actions)
diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py
index 06d119d4..125cf6c6 100644
--- a/tests/test_admin_actions.py
+++ b/tests/test_admin_actions.py
@@ -7,9 +7,11 @@
from djangocms_moderation import constants
from djangocms_moderation.admin import ModerationRequestAdmin
+from djangocms_moderation.constants import ACTION_REJECTED
from djangocms_moderation.models import (
ModerationCollection,
ModerationRequest,
+ Role,
Workflow,
)
@@ -21,7 +23,10 @@ class AdminActionTest(BaseTestCase):
def setUp(self):
self.wf = Workflow.objects.create(name='Workflow Test',)
self.collection = ModerationCollection.objects.create(
- author=self.user, name='Collection Admin Actions', workflow=self.wf, status=constants.IN_REVIEW
+ author=self.user,
+ name='Collection Admin Actions',
+ workflow=self.wf,
+ status=constants.IN_REVIEW,
)
pg1 = create_page(title='Page 1', template='page.html', language='en',)
@@ -30,16 +35,13 @@ def setUp(self):
self.mr1 = ModerationRequest.objects.create(
content_object=pg1, language='en', collection=self.collection, is_active=True,)
- self.wfst = self.wf.steps.create(role=self.role1, is_required=True, order=1,)
+ self.wfst1 = self.wf.steps.create(role=self.role1, is_required=True, order=1,)
+ self.wfst2 = self.wf.steps.create(role=self.role2, is_required=True, order=1,)
# this moderation request is approved
self.mr1.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
- self.mr1.actions.create(
- by_user=self.user,
- to_user=self.user2,
- action=constants.ACTION_APPROVED,
- step_approved=self.wfst,
- )
+ self.mr1.update_status(constants.ACTION_APPROVED, self.user)
+ self.mr1.update_status(constants.ACTION_APPROVED, self.user2)
# this moderation request is not approved
self.mr2 = ModerationRequest.objects.create(
@@ -54,7 +56,9 @@ def setUp(self):
self.client.force_login(self.user)
@mock.patch.object(ModerationRequestAdmin, 'has_delete_permission')
- def test_delete_selected(self, mock_has_delete_permission):
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
+ def test_delete_selected(self, notify_author_mock, notify_moderators_mock, mock_has_delete_permission):
mock_has_delete_permission.return_value = True
self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
@@ -65,8 +69,7 @@ def test_delete_selected(self, mock_has_delete_permission):
}
# This user is not the collection author
self.client.force_login(self.user2)
- response = self.client.post(self.url_with_filter, data)
- self.assertEqual(response.status_code, 403)
+ self.client.post(self.url_with_filter, data)
# Nothing is deleted
self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
@@ -78,26 +81,221 @@ def test_delete_selected(self, mock_has_delete_permission):
# Nothing is deleted
self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 2)
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.IN_REVIEW)
+
mock_has_delete_permission.return_value = True
self.client.force_login(self.user)
response = self.client.post(self.url_with_filter, data)
self.assertEqual(response.status_code, 302)
self.assertEqual(ModerationRequest.objects.filter(collection=self.collection).count(), 0)
+ notify_author_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr1, self.mr2],
+ action=constants.ACTION_CANCELLED,
+ by_user=self.user,
+ )
+
+ self.assertFalse(notify_moderators_mock.called)
+
+ # All moderation requests were deleted, so collection should be archived
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.ARCHIVED)
+
@mock.patch('djangocms_moderation.admin_actions.publish_content_object')
def test_publish_selected(self, mock_publish_content_object):
fixtures = [self.mr1, self.mr2]
data = {
'action': 'publish_selected',
- 'select_across': 0,
- 'index': 0,
ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
}
self.client.post(self.url_with_filter, data, follow=True)
- self.assertTrue(self.mr1.is_approved())
- self.assertFalse(self.mr2.is_approved())
assert mock_publish_content_object.called
# check it has been called only once, i.e. with the approved mr1
mock_publish_content_object.assert_called_once_with(self.mr1.content_object)
+
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
+ def test_approve_selected(self, notify_author_mock, notify_moderators_mock):
+ fixtures = [self.mr1, self.mr2]
+ data = {
+ 'action': 'approve_selected',
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+ self.assertFalse(self.mr2.is_approved())
+ self.assertTrue(self.mr1.is_approved())
+
+ self.client.post(self.url_with_filter, data)
+
+ notify_author_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action=self.mr2.get_last_action().action,
+ by_user=self.user,
+ )
+
+ notify_moderators_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action_obj=self.mr2.get_last_action(),
+ )
+ notify_author_mock.reset_mock()
+ notify_moderators_mock.reset_mock()
+
+ # There are 2 steps so we need to approve both to get mr2 approved
+ self.assertFalse(self.mr2.is_approved())
+ self.assertTrue(self.mr1.is_approved())
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.IN_REVIEW)
+
+ self.client.force_login(self.user2)
+ self.client.post(self.url_with_filter, data)
+
+ self.assertTrue(self.mr2.is_approved())
+ self.assertTrue(self.mr1.is_approved())
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.ARCHIVED)
+
+ notify_author_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action=self.mr2.get_last_action().action,
+ by_user=self.user2,
+ )
+ self.assertFalse(notify_moderators_mock.called)
+
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
+ def test_reject_selected(self, notify_author_mock, notify_moderators_mock):
+ fixtures = [self.mr1, self.mr2]
+ data = {
+ 'action': 'reject_selected',
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+ self.assertFalse(self.mr2.is_approved())
+ self.assertFalse(self.mr2.is_rejected())
+ self.assertTrue(self.mr1.is_approved())
+
+ self.client.post(self.url_with_filter, data)
+
+ self.assertFalse(self.mr2.is_approved())
+ self.assertTrue(self.mr2.is_rejected())
+ self.assertTrue(self.mr1.is_approved())
+
+ self.assertFalse(notify_moderators_mock.called)
+
+ notify_author_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action=self.mr2.get_last_action().action,
+ by_user=self.user,
+ )
+
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.IN_REVIEW)
+
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
+ def test_resubmit_selected(self, notify_author_mock, notify_moderators_mock):
+ self.mr2.update_status(
+ action=ACTION_REJECTED,
+ by_user=self.user
+ )
+
+ fixtures = [self.mr1, self.mr2]
+ data = {
+ 'action': 'resubmit_selected',
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+ self.assertTrue(self.mr2.is_rejected())
+ self.assertTrue(self.mr1.is_approved())
+
+ self.client.post(self.url_with_filter, data)
+
+ self.assertFalse(self.mr2.is_rejected())
+ self.assertTrue(self.mr1.is_approved())
+
+ self.assertFalse(notify_author_mock.called)
+ notify_moderators_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action_obj=self.mr2.get_last_action(),
+ )
+
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.IN_REVIEW)
+
+ @mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
+ def test_approve_selected_sends_correct_emails(self, notify_moderators_mock):
+ role4 = Role.objects.create(user=self.user)
+ # Add two more steps
+ self.wf.steps.create(role=self.role3, is_required=True, order=1,)
+ self.wf.steps.create(role=role4, is_required=True, order=1,)
+ self.user.groups.add(self.group)
+
+ # Add one more, partially approved request
+ pg3 = create_page(title='Page 3', template='page.html', language='en', )
+ self.mr3 = ModerationRequest.objects.create(
+ content_object=pg3, language='en', collection=self.collection, is_active=True,)
+ self.mr3.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
+ self.mr3.update_status(by_user=self.user, action=constants.ACTION_APPROVED,)
+ self.mr3.update_status(by_user=self.user2, action=constants.ACTION_APPROVED,)
+
+ self.user.groups.add(self.group)
+
+ fixtures = [self.mr1, self.mr2, self.mr3]
+ data = {
+ 'action': 'approve_selected',
+ ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
+ }
+
+ # First post as `self.user` should notify mr1 and mr2 and mr3 moderators
+ self.client.post(self.url_with_filter, data)
+ # The notify email will be send accordingly. As mr1 and mr3 are in the
+ # different stages of approval compared to mr 2,
+ # we need to send 2 emails to appropriate moderators
+ self.assertEqual(notify_moderators_mock.call_count, 2)
+ self.assertEqual(
+ notify_moderators_mock.call_args_list[0],
+ mock.call(collection=self.collection,
+ moderation_requests=[self.mr1, self.mr3],
+ action_obj=self.mr1.get_last_action()
+ )
+ )
+
+ self.assertEqual(
+ notify_moderators_mock.call_args_list[1],
+ mock.call(collection=self.collection,
+ moderation_requests=[self.mr2],
+ action_obj=self.mr2.get_last_action(),
+ )
+ )
+ self.assertFalse(self.mr1.is_approved())
+ self.assertFalse(self.mr3.is_approved())
+
+ notify_moderators_mock.reset_mock()
+ self.client.post(self.url_with_filter, data)
+ # Second post approves m3 and mr1, but as this is the last stage of
+ # the approval, there is no need for notification emails anymore
+ self.assertEqual(notify_moderators_mock.call_count, 0)
+ self.assertTrue(self.mr1.is_approved())
+ self.assertTrue(self.mr3.is_approved())
+
+ self.client.force_login(self.user2)
+ # user2 can approve only 1 request, mr2, so one notification email
+ # should go out
+ self.client.post(self.url_with_filter, data)
+ notify_moderators_mock.assert_called_once_with(
+ collection=self.collection,
+ moderation_requests=[self.mr2],
+ action_obj=self.mr2.get_last_action(),
+ )
+
+ self.user.groups.remove(self.group)
+
+ # Not all request have been fully approved
+ self.collection.refresh_from_db()
+ self.assertEqual(self.collection.status, constants.IN_REVIEW)
diff --git a/tests/test_app_registration.py b/tests/test_app_registration.py
index 4da9719e..6838abd2 100644
--- a/tests/test_app_registration.py
+++ b/tests/test_app_registration.py
@@ -8,6 +8,7 @@
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
+from django.test import ignore_warnings
from cms import app_registration
from cms.test_utils.testcases import CMSTestCase
@@ -55,6 +56,7 @@ def test_model_not_in_versionables_by_content(self, get_app_config):
extension.configure_app(cms_config)
+@ignore_warnings(module='djangocms_versioning.helpers')
class CMSConfigIntegrationTest(CMSTestCase):
def setUp(self):
diff --git a/tests/test_forms.py b/tests/test_forms.py
index f21126bf..e7243c14 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -91,7 +91,7 @@ def test_form_is_invalid_if_collection_cant_be_submitted_for_review(self, allow_
'moderator': None,
}
- allow_submit_mock.__get__ = mock.Mock(return_value=False)
+ allow_submit_mock.return_value = False
form = SubmitCollectionForModerationForm(
data,
collection=self.collection1,
@@ -99,7 +99,7 @@ def test_form_is_invalid_if_collection_cant_be_submitted_for_review(self, allow_
)
self.assertFalse(form.is_valid())
- allow_submit_mock.__get__ = mock.Mock(return_value=True)
+ allow_submit_mock.return_value = True
form = SubmitCollectionForModerationForm(
data,
collection=self.collection1,
diff --git a/tests/test_models.py b/tests/test_models.py
index df40d778..8eecdecc 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -381,7 +381,6 @@ def test_compliance_number_sequential_number_with_identifier_prefix_backend(self
class ModerationRequestActionTest(BaseTestCase):
-
def test_get_by_user_name(self):
action = self.moderation_request3.actions.last()
self.assertEqual(action.get_by_user_name(), self.user.username)
@@ -485,42 +484,49 @@ def setUp(self):
self.page1 = create_page(title='My page 1', template='page.html', language='en',)
self.page2 = create_page(title='My page 2', template='page.html', language='en',)
- def test_allow_submit_for_review(self):
+ def test_job_id(self):
+ self.assertEqual(str(self.collection1.pk), self.collection1.job_id)
+ self.assertEqual(str(self.collection2.pk), self.collection2.job_id)
+
+ @patch.object(ModerationRequest, 'is_approved')
+ def test_should_be_archived(self, is_approved_mock):
self.collection1.status = constants.COLLECTING
self.collection1.save()
- # This is false, as we don't have any moderation requests in this collection
- self.assertFalse(self.collection1.allow_submit_for_review)
+ self.assertFalse(self.collection1.should_be_archived())
- ModerationRequest.objects.create(
- content_object=self.pg1, collection=self.collection1, is_active=True
- )
- self.assertTrue(self.collection1.allow_submit_for_review)
+ self.collection1.status = constants.ARCHIVED
+ self.collection1.save()
+ self.assertFalse(self.collection1.should_be_archived())
self.collection1.status = constants.IN_REVIEW
self.collection1.save()
- self.assertFalse(self.collection1.allow_submit_for_review)
-
- def test_allow_pre_flight(self):
- self.collection4.status = constants.COLLECTING
- self.collection4.save()
+ self.assertTrue(self.collection1.should_be_archived())
- # This is false, as there are no approved requests and status is COLLECTING
- self.assertFalse(self.collection4.allow_pre_flight(self.user))
+ ModerationRequest.objects.create(
+ content_object=self.pg1, collection=self.collection1, is_active=True
+ )
+ is_approved_mock.return_value = False
+ self.assertFalse(self.collection1.should_be_archived())
- self.collection4.status = constants.IN_REVIEW
- self.collection4.save()
+ is_approved_mock.return_value = True
+ self.assertTrue(self.collection1.should_be_archived())
- # This is false, as there are no approved requests
- self.assertFalse(self.collection4.allow_pre_flight(self.user))
+ def test_allow_submit_for_review(self):
+ self.collection1.status = constants.COLLECTING
+ self.collection1.save()
+ # This is false, as we don't have any moderation requests in this collection
+ self.assertFalse(self.collection1.allow_submit_for_review(user=self.user))
- self.moderation_request5.update_status(
- action=constants.ACTION_APPROVED,
- by_user=self.user,
- message='Approved',
+ ModerationRequest.objects.create(
+ content_object=self.pg1, collection=self.collection1, is_active=True
)
+ self.assertTrue(self.collection1.allow_submit_for_review(user=self.user))
+ # Only collection author can submit
+ self.assertFalse(self.collection1.allow_submit_for_review(user=self.user2))
- self.assertTrue(self.collection4.allow_pre_flight(self.user))
- self.assertFalse(self.collection4.allow_pre_flight(self.user2))
+ self.collection1.status = constants.IN_REVIEW
+ self.collection1.save()
+ self.assertFalse(self.collection1.allow_submit_for_review(user=self.user))
@patch('djangocms_moderation.models.notify_collection_moderators')
def test_submit_for_review(self, mock_ncm):
diff --git a/tests/test_views.py b/tests/test_views.py
index 5fcb0dcd..af099a19 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -225,10 +225,10 @@ def test_change_list_view_should_contain_collection_object(self):
@mock.patch.object(ModerationCollection, 'allow_submit_for_review')
def test_change_list_view_should_contain_submit_collection_url(self, allow_submit_mock):
- allow_submit_mock.__get__ = mock.Mock(return_value=False)
+ allow_submit_mock.return_value = False
response = self.client.get(self.url_with_filter)
self.assertNotIn('submit_for_review_url', response.context)
- allow_submit_mock.__get__ = mock.Mock(return_value=True)
+ allow_submit_mock.return_value = True
response = self.client.get(self.url_with_filter)
self.assertIn('submit_for_review_url', response.context)
diff --git a/tests/utils/app_1/models.py b/tests/utils/app_1/models.py
index c0dd2ec2..158bf8a6 100644
--- a/tests/utils/app_1/models.py
+++ b/tests/utils/app_1/models.py
@@ -8,6 +8,9 @@ class App1Post(models.Model):
class App1PostContent(models.Model):
post = models.ForeignKey(App1Post, on_delete=models.CASCADE)
+ def get_absolute_url(self):
+ return '/'
+
class App1Title(models.Model):
pass
@@ -15,3 +18,6 @@ class App1Title(models.Model):
class App1TitleContent(models.Model):
title = models.ForeignKey(App1Title, on_delete=models.CASCADE)
+
+ def get_absolute_url(self):
+ return '/'
diff --git a/tests/utils/app_2/models.py b/tests/utils/app_2/models.py
index 692ba5a5..3d60e8ef 100644
--- a/tests/utils/app_2/models.py
+++ b/tests/utils/app_2/models.py
@@ -8,6 +8,9 @@ class App2Post(models.Model):
class App2PostContent(models.Model):
post = models.ForeignKey(App2Post, on_delete=models.CASCADE)
+ def get_absolute_url(self):
+ return '/'
+
class App2Title(models.Model):
pass
@@ -15,3 +18,6 @@ class App2Title(models.Model):
class App2TitleContent(models.Model):
title = models.ForeignKey(App2Title, on_delete=models.CASCADE)
+
+ def get_absolute_url(self):
+ return '/'
diff --git a/tests/utils/base.py b/tests/utils/base.py
index f82bbb49..170ba3bd 100644
--- a/tests/utils/base.py
+++ b/tests/utils/base.py
@@ -25,10 +25,10 @@ def setUpTestData(cls):
# create pages
cls.pg1 = create_page(title='Page 1', template='page.html', language='en',)
cls.pg2 = create_page(title='Page 2', template='page.html', language='en',)
- cls.pg3 = create_page(title='Page 3', template='page.html', language='en', published=True)
+ cls.pg3 = create_page(title='Page 3', template='page.html', language='en',)
cls.pg4 = create_page(title='Page 4', template='page.html', language='en',)
- cls.pg5 = create_page(title='Page 5', template='page.html', language='en', published=True)
- cls.pg6 = create_page(title='Page 5', template='page.html', language='en',)
+ cls.pg5 = create_page(title='Page 5', template='page.html', language='en',)
+ cls.pg6 = create_page(title='Page 6', template='page.html', language='en',)
# create users, groups and roles
cls.user = User.objects.create_superuser(
From 821ea158c19803907d741c20dffa2a59078f2612 Mon Sep 17 00:00:00 2001
From: Paulo
Date: Tue, 4 Sep 2018 16:16:39 -0400
Subject: [PATCH 016/147] Bumped version to 1.0.0
---
djangocms_moderation/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/djangocms_moderation/__init__.py b/djangocms_moderation/__init__.py
index 783097c6..c7d56bb1 100644
--- a/djangocms_moderation/__init__.py
+++ b/djangocms_moderation/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '0.0.7'
+__version__ = '1.0.0'
default_app_config = 'djangocms_moderation.apps.ModerationConfig'
From 4c7bbba7ee06538d970ede93abfcbcf9573e9d52 Mon Sep 17 00:00:00 2001
From: Noel da Costa
Date: Wed, 5 Sep 2018 13:18:52 +0100
Subject: [PATCH 017/147] Comments in collection (#48)
---
djangocms_moderation/admin.py | 200 +++++++++++++++---
djangocms_moderation/conf.py | 12 ++
djangocms_moderation/forms.py | 37 +++-
djangocms_moderation/helpers.py | 14 ++
.../migrations/0002_auto_20180905_1152.py | 52 +++++
djangocms_moderation/models.py | 30 +++
.../collectioncomment/change_list.html | 31 +++
.../requestcomment/change_list.html | 32 +++
.../moderation_request_change_list.html | 21 ++
djangocms_moderation/utils.py | 11 +-
djangocms_moderation/views.py | 2 +-
tests/test_admin.py | 27 ++-
tests/test_utils.py | 43 ++++
13 files changed, 480 insertions(+), 32 deletions(-)
create mode 100644 djangocms_moderation/migrations/0002_auto_20180905_1152.py
create mode 100644 djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
create mode 100644 djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
create mode 100644 tests/test_utils.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 525457be..3dc273b5 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -19,33 +19,44 @@
resubmit_selected,
)
from .constants import ARCHIVED, IN_REVIEW
-from .forms import WorkflowStepInlineFormSet
-from .helpers import get_form_submission_for_step
+from .forms import (
+ CollectionCommentForm,
+ RequestCommentForm,
+ WorkflowStepInlineFormSet,
+)
+from .helpers import EditAndAddOnlyFieldsMixin, get_form_submission_for_step
from .models import (
+ CollectionComment,
ConfirmationFormSubmission,
ConfirmationPage,
ModerationCollection,
ModerationRequest,
ModerationRequestAction,
+ RequestComment,
Role,
Workflow,
WorkflowStep,
)
+from . import conf # isort:skip
+from . import utils # isort:skip
from . import views # isort:skip
class ModerationRequestActionInline(admin.TabularInline):
model = ModerationRequestAction
fields = ['show_user', 'message', 'date_taken', 'form_submission']
- readonly_fields = fields
+ readonly_fields = ['show_user', 'date_taken', 'form_submission']
verbose_name = _('Action')
verbose_name_plural = _('Actions')
def has_add_permission(self, request):
return False
+ def has_delete_permission(self, request, obj=None):
+ return False
+
def show_user(self, obj):
_name = obj.get_by_user_name()
return ugettext('By {user}').format(user=_name)
@@ -79,7 +90,7 @@ class ModerationRequestAdmin(admin.ModelAdmin):
resubmit_selected,
]
inlines = [ModerationRequestActionInline]
- list_display = ['id', 'content_type', 'get_title', 'collection', 'get_preview_link', 'get_status']
+ list_filter = ['collection']
fields = ['id', 'collection', 'workflow', 'is_active', 'get_status']
readonly_fields = fields
change_list_template = 'djangocms_moderation/moderation_request_change_list.html'
@@ -91,6 +102,12 @@ def has_module_permission(self, request):
"""
return False
+ def get_list_display(self, request):
+ list_display = ['id', 'content_type', 'get_title', 'get_content_author', 'get_preview_link', 'get_status']
+ if conf.REQUEST_COMMENTS_ENABLED:
+ list_display.append('get_comments_link')
+ return list_display
+
def get_title(self, obj):
return obj.content_object
get_title.short_description = _('Title')
@@ -100,6 +117,23 @@ def get_preview_link(self, obj):
return "Link placeholder"
get_preview_link.short_description = _('Preview')
+ def get_comments_link(self, obj):
+ return format_html(
+ '{}',
+ reverse('admin:djangocms_moderation_requestcomment_changelist'),
+ obj.id,
+ _('View')
+ )
+ get_comments_link.short_description = _('Comments')
+
+ def get_content_author(self, obj):
+ """
+ This is not necessarily the same person as the RequestAction author
+ """
+ # TODO this should get the author from the version object e.g. obj.content_object.created_by
+ return "author placeholder"
+ get_content_author.short_description = _('Content author')
+
def has_add_permission(self, request):
return False
@@ -212,6 +246,114 @@ class RoleAdmin(admin.ModelAdmin):
fields = ['name', 'user', 'group', 'confirmation_page']
+class CollectionCommentAdmin(admin.ModelAdmin):
+ list_display = ['message', 'author', 'date_created']
+ fields = ['collection', 'message', 'author']
+
+ def get_changeform_initial_data(self, request):
+ # Extract the id from the URL. The id is stored in _changelsit_filters
+ # by Django so that the request knows where to return to after form submission.
+ data = {
+ 'author': request.user,
+ }
+ collection_id = utils.extract_filter_param_from_changelist_url(
+ request, '_changelist_filters', 'collection__id__exact'
+ )
+ if collection_id:
+ data['collection'] = collection_id
+ return data
+
+ def get_form(self, request, obj=None, **kwargs):
+ return CollectionCommentForm
+
+ def has_module_permission(self, request):
+ """
+ Hide the model from admin index as it depends on foreighKey
+ """
+ return False
+
+ def changelist_view(self, request, extra_context=None):
+ # If we filter by a specific collection, we want to add this collection
+ # to the context
+ collection_id = request.GET.get('collection__id__exact')
+ if collection_id:
+ try:
+ collection = ModerationCollection.objects.get(pk=int(collection_id))
+ request._collection = collection
+ except (ValueError, ModerationCollection.DoesNotExist):
+ raise Http404
+ else:
+ extra_context = dict(
+ collection=collection,
+ title=_('Collection comments')
+ )
+ else:
+ raise Http404
+
+ return super().changelist_view(request, extra_context)
+
+
+class RequestCommentAdmin(admin.ModelAdmin):
+ list_display = ['message', 'get_request_link', 'author', 'date_created']
+ fields = ['moderation_request', 'message', 'author']
+
+ def get_changeform_initial_data(self, request):
+ data = {
+ 'author': request.user,
+ }
+ moderation_request_id = utils.extract_filter_param_from_changelist_url(
+ request, '_changelist_filters', 'moderation_request__id__exact'
+ )
+ if moderation_request_id:
+ data['moderation_request'] = moderation_request_id
+ return data
+
+ def get_request_link(self, obj):
+ opts = ModerationRequest._meta
+ url = reverse(
+ 'admin:{}_{}_change'.format(opts.app_label, opts.model_name),
+ args=[obj.pk],
+ )
+ return format_html(
+ '{}',
+ url,
+ _('View')
+ )
+ get_request_link.short_description = _('Request')
+
+ def get_form(self, request, obj=None, **kwargs):
+ return RequestCommentForm
+
+ def has_module_permission(self, request):
+ """
+ Hide the model from admin index as it depends on foreighKey
+ """
+ return False
+
+ def changelist_view(self, request, extra_context=None):
+ # If we filter by a specific collection, we want to add this collection
+ # to the context
+ moderation_request_id = request.GET.get('moderation_request__id__exact')
+ if moderation_request_id:
+ try:
+ moderation_request = ModerationRequest.objects.get(pk=int(moderation_request_id))
+ collection = moderation_request.collection
+ request._collection = collection
+ except (ValueError, ModerationRequest.DoesNotExist):
+ raise Http404
+ else:
+ extra_context = dict(
+ collection=collection,
+ title=_("Request comments")
+ )
+ else:
+ # If no collection id, then don't show all requests
+ # as each collection's actions, buttons and privileges may differ
+ raise Http404
+
+ return super().changelist_view(request, extra_context)
+
+
class WorkflowStepInline(SortableInlineAdminMixin, admin.TabularInline):
formset = WorkflowStepInlineFormSet
model = WorkflowStep
@@ -234,30 +376,26 @@ class WorkflowAdmin(admin.ModelAdmin):
]
-class ModerationCollectionAdmin(admin.ModelAdmin):
+class ModerationCollectionAdmin(EditAndAddOnlyFieldsMixin, admin.ModelAdmin):
actions = None # remove `delete_selected` for now, it will be handled later
- list_display = [
- 'id',
- 'get_name_with_requests_link',
- 'job_id',
- 'get_moderator',
- 'workflow',
- 'status',
- 'date_created',
- ]
editonly_fields = ('status',) # fields editable only on EDIT
addonly_fields = ('workflow',) # fields editable only on CREATE
- def get_readonly_fields(self, request, obj=None):
- """
- Override to provide editonly_fields and addonly_fields functionality
- """
- if obj: # Editing an existing object
- return self.readonly_fields + self.addonly_fields
- else: # Adding a new object
- return self.readonly_fields + self.editonly_fields
+ def get_list_display(self, request):
+ list_display = [
+ 'id',
+ 'name',
+ 'get_moderator',
+ 'workflow',
+ 'status',
+ 'date_created',
+ 'get_requests_link'
+ ]
+ if conf.COLLECTION_COMMENTS_ENABLED:
+ list_display.append('get_comments_link')
+ return list_display
- def get_name_with_requests_link(self, obj):
+ def get_requests_link(self, obj):
"""
Name of the collection should link to the list of associated
moderation requests
@@ -266,9 +404,18 @@ def get_name_with_requests_link(self, obj):
'{}',
reverse('admin:djangocms_moderation_moderationrequest_changelist'),
obj.pk,
- obj.name,
+ _('View')
)
- get_name_with_requests_link.short_description = _('Name')
+ get_requests_link.short_description = _('Requests')
+
+ def get_comments_link(self, obj):
+ return format_html(
+ '{}',
+ reverse('admin:djangocms_moderation_collectioncomment_changelist'),
+ obj.id,
+ _('View')
+ )
+ get_comments_link.short_description = _('Comments')
def get_moderator(self, obj):
return obj.author
@@ -348,9 +495,10 @@ def form_data(self, obj):
admin.site.register(ModerationRequest, ModerationRequestAdmin)
+admin.site.register(CollectionComment, CollectionCommentAdmin)
+admin.site.register(RequestComment, RequestCommentAdmin)
admin.site.register(ModerationCollection, ModerationCollectionAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(Workflow, WorkflowAdmin)
-
admin.site.register(ConfirmationPage, ConfirmationPageAdmin)
admin.site.register(ConfirmationFormSubmission, ConfirmationFormSubmissionAdmin)
diff --git a/djangocms_moderation/conf.py b/djangocms_moderation/conf.py
index cefdd86c..9c9e6559 100644
--- a/djangocms_moderation/conf.py
+++ b/djangocms_moderation/conf.py
@@ -47,3 +47,15 @@
'CMS_MODERATION_CONFIRMATION_PAGE_TEMPLATES',
CORE_CONFIRMATION_PAGE_TEMPLATES,
)
+
+COLLECTION_COMMENTS_ENABLED = getattr(
+ settings,
+ 'CMS_MODERATION_COLLECTION_COMMENTS_ENABLED',
+ True,
+)
+
+REQUEST_COMMENTS_ENABLED = getattr(
+ settings,
+ 'CMS_MODERATION_REQUEST_COMMENTS_ENABLED',
+ True,
+)
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 4ae23c1b..11a2fcd9 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -16,7 +16,12 @@
ACTION_RESUBMITTED,
COLLECTING,
)
-from .models import ModerationCollection, ModerationRequest
+from .models import (
+ CollectionComment,
+ ModerationCollection,
+ ModerationRequest,
+ RequestComment,
+)
class WorkflowStepInlineFormSet(CustomInlineFormSet):
@@ -200,3 +205,33 @@ def save(self):
by_user=self.user,
to_user=self.cleaned_data.get('moderator'),
)
+
+
+class CollectionCommentForm(forms.ModelForm):
+ """
+ The author and moderation request should be pre-filled and non-editable.
+ NB: Hidden fields seems to be the only reliable way to do this;
+ readonly fields do not work for add, only for edit.
+ """
+ class Meta:
+ model = CollectionComment
+ fields = '__all__'
+ widgets = {
+ 'author': forms.HiddenInput(),
+ 'collection': forms.HiddenInput(),
+ }
+
+
+class RequestCommentForm(forms.ModelForm):
+ """
+ The author and moderation request should be pre-filled and non-editable.
+ NB: Hidden fields seems to be the only reliable way to do this;
+ readonly fields do not work for add, only for edit.
+ """
+ class Meta:
+ model = RequestComment
+ fields = '__all__'
+ widgets = {
+ 'author': forms.HiddenInput(),
+ 'moderation_request': forms.HiddenInput(),
+ }
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index 0dfed959..6d6b7f3d 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -50,3 +50,17 @@ def get_form_submission_for_step(active_request, current_step):
.filter(request=active_request, for_step=current_step)
)
return lookup.first()
+
+
+class EditAndAddOnlyFieldsMixin(object):
+ editonly_fields = ()
+ addonly_fields = ()
+
+ def get_readonly_fields(self, request, obj=None):
+ """
+ Override to provide editonly_fields and addonly_fields functionality
+ """
+ if obj: # Editing an existing object
+ return self.readonly_fields + self.editonly_fields
+ else: # Adding a new object
+ return self.readonly_fields + self.addonly_fields
diff --git a/djangocms_moderation/migrations/0002_auto_20180905_1152.py b/djangocms_moderation/migrations/0002_auto_20180905_1152.py
new file mode 100644
index 00000000..4c7af74f
--- /dev/null
+++ b/djangocms_moderation/migrations/0002_auto_20180905_1152.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-09-05 10:52
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('djangocms_moderation', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CollectionComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('message', models.TextField(blank=True, verbose_name='message')),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')),
+ ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.ModerationCollection')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='RequestComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('message', models.TextField(blank=True, verbose_name='message')),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AlterModelOptions(
+ name='moderationrequest',
+ options={'ordering': ['id'], 'verbose_name': 'Request', 'verbose_name_plural': 'Requests'},
+ ),
+ migrations.AddField(
+ model_name='requestcomment',
+ name='moderation_request',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.ModerationRequest'),
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index a7f17e81..6a04aa5f 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -642,6 +642,36 @@ def save(self, **kwargs):
super(ModerationRequestAction, self).save(**kwargs)
+class AbstractComment(models.Model):
+ message = models.TextField(
+ blank=True,
+ verbose_name=_('message'),
+ )
+ author = models.ForeignKey(
+ to=settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE,
+ )
+ date_created = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ abstract = True
+
+
+class CollectionComment(AbstractComment):
+ collection = models.ForeignKey(
+ to=ModerationCollection,
+ on_delete=models.CASCADE,
+ )
+
+
+class RequestComment(AbstractComment):
+ moderation_request = models.ForeignKey(
+ to=ModerationRequest,
+ on_delete=models.CASCADE,
+ )
+
+
class ConfirmationFormSubmission(models.Model):
request = models.ForeignKey(
to=ModerationRequest,
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
new file mode 100644
index 00000000..c5e5a79e
--- /dev/null
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
@@ -0,0 +1,31 @@
+{% extends "admin/change_list.html" %}
+{% load i18n %}
+
+{% block object-tools-items %}
+{{ block.super }}
+{% if submit_for_review_url %}
+ -
+ {% trans 'Submit for review' %}
+
+{% endif %}
+{% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content %}
+{{ collection }}
+{{ block.super }}
+
+{% comment %}
+ TODO This template will be overridden to provide the required functionality.
+ For now, lets just output the selected collection
+{% endcomment %}
+
+{% endblock %}
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
new file mode 100644
index 00000000..55607ab1
--- /dev/null
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
@@ -0,0 +1,32 @@
+{% extends "admin/change_list.html" %}
+{% load i18n %}
+{% load l10n %}
+
+{% block object-tools-items %}
+{{ block.super }}
+{% if submit_for_review_url %}
+ -
+ {% trans 'Submit for review' %}
+
+{% endif %}
+{% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content %}
+{{ block.super }}
+
+{% comment %}
+ TODO This template will be overridden to provide the required functionality.
+ For now, lets just output the selected collection
+{% endcomment %}
+
+{% endblock %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
index e1500e07..97a7d72b 100644
--- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -21,3 +21,24 @@ {{ collection.name }}
{% endif %}
{% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content %}
+{{ collection }}
+{{ block.super }}
+
+{% comment %}
+ TODO This template will be overridden to provide the required functionality.
+ For now, lets just output the selected collection
+{% endcomment %}
+
+{% endblock %}
+
diff --git a/djangocms_moderation/utils.py b/djangocms_moderation/utils.py
index 3b821ea1..b9bd11d4 100644
--- a/djangocms_moderation/utils.py
+++ b/djangocms_moderation/utils.py
@@ -4,7 +4,7 @@
from django.contrib.sites.models import Site
from django.utils.lru_cache import lru_cache
from django.utils.module_loading import import_string
-from django.utils.six.moves.urllib.parse import urljoin
+from django.utils.six.moves.urllib.parse import parse_qs, urljoin
from django.utils.translation import override as force_language
from cms.utils.urlutils import admin_reverse
@@ -35,3 +35,12 @@ def load_backend(path):
def generate_compliance_number(path, **kwargs):
backend = load_backend(path)
return backend(**kwargs)
+
+
+def extract_filter_param_from_changelist_url(request, keyname, parametername):
+ """
+ Searches request.GET for a given key and decodes the value for a particular parameter
+ """
+ changelist_filters = request.GET.get(keyname)
+ parameter_value = parse_qs(changelist_filters).get(parametername)
+ return parameter_value[0]
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 1de4b0a8..5ac66c96 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -145,7 +145,7 @@ def get_form_kwargs(self):
def form_valid(self, form):
form.save()
- messages.success(self.request, _("Your collection has been submitted for a review"))
+ messages.success(self.request, _("Your collection has been submitted for review"))
# Redirect back to the collection filtered moderation request change list
redirect_url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
redirect_url = "{}?collection__id__exact={}".format(
diff --git a/tests/test_admin.py b/tests/test_admin.py
index a6be7bb8..65d6ea1b 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -3,8 +3,11 @@
from cms.api import create_page
-from djangocms_moderation import constants
-from djangocms_moderation.admin import ModerationRequestAdmin
+from djangocms_moderation import conf, constants
+from djangocms_moderation.admin import (
+ ModerationCollectionAdmin,
+ ModerationRequestAdmin,
+)
from djangocms_moderation.constants import ACTION_REJECTED
from djangocms_moderation.models import (
ModerationCollection,
@@ -19,7 +22,7 @@ class MockRequest:
GET = {}
-class ModerationRequestAdminTestCase(BaseTestCase):
+class ModerationAdminTestCase(BaseTestCase):
def setUp(self):
self.wf = Workflow.objects.create(name='Workflow Test',)
self.collection = ModerationCollection.objects.create(
@@ -53,6 +56,7 @@ def setUp(self):
self.url, self.collection.pk
)
self.mra = ModerationRequestAdmin(ModerationRequest, admin.AdminSite())
+ self.mca = ModerationCollectionAdmin(ModerationCollection, admin.AdminSite())
def test_delete_selected_action_visibility(self):
mock_request = MockRequest()
@@ -149,3 +153,20 @@ def test_in_review_status_is_considered(self):
self.collection.save()
actions = self.mra.get_actions(request=mock_request)
self.assertIn('approve_selected', actions)
+
+ def test_change_list_view_should_respect_conf(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+ conf.COLLECTION_COMMENTS_ENABLED = False
+ list_display = self.mca.get_list_display(mock_request)
+ self.assertNotIn('get_comments_link', list_display)
+ conf.COLLECTION_COMMENTS_ENABLED = True
+ list_display = self.mca.get_list_display(mock_request)
+ self.assertIn('get_comments_link', list_display)
+ conf.REQUEST_COMMENTS_ENABLED = False
+ list_display = self.mra.get_list_display(mock_request)
+ self.assertNotIn('get_comments_link', list_display)
+ conf.REQUEST_COMMENTS_ENABLED = True
+ list_display = self.mra.get_list_display(mock_request)
+ self.assertIn('get_comments_link', list_display)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..c1e0919f
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,43 @@
+from django.test.client import RequestFactory
+
+from djangocms_moderation import utils
+
+from .utils.base import BaseTestCase
+
+
+class UtilsTestCase(BaseTestCase):
+ def setUp(self):
+ self.rf = RequestFactory()
+
+ def test_extract_filter_param_from_changelist_url(self):
+ mock_request = self.rf.get(
+ '/admin/djangocms_moderation/collectioncomment/add/?_changelist_filters=collection__id__exact%3D1'
+ )
+ collection_id = utils.extract_filter_param_from_changelist_url(
+ mock_request, '_changelist_filters', 'collection__id__exact'
+ )
+ self.assertEquals(collection_id, '1')
+
+ mock_request = self.rf.get(
+ '/admin/djangocms_moderation/collectioncomment/add/?_changelist_filters=collection__id__exact%3D4'
+ )
+ collection_id = utils.extract_filter_param_from_changelist_url(
+ mock_request, '_changelist_filters', 'collection__id__exact'
+ )
+ self.assertEquals(collection_id, '4')
+
+ mock_request = self.rf.get(
+ '/admin/djangocms_moderation/requestcomment/add/?_changelist_filters=moderation_request__id__exact%3D1'
+ )
+ action_id = utils.extract_filter_param_from_changelist_url(
+ mock_request, '_changelist_filters', 'moderation_request__id__exact'
+ )
+ self.assertEquals(action_id, '1')
+
+ mock_request = self.rf.get(
+ '/admin/djangocms_moderation/requestcomment/add/?_changelist_filters=moderation_request__id__exact%3D2'
+ )
+ action_id = utils.extract_filter_param_from_changelist_url(
+ mock_request, '_changelist_filters', 'moderation_request__id__exact'
+ )
+ self.assertEquals(action_id, '2')
From b850e1c25dd597b794ac67eb55c6d5903b5f7934 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Wed, 5 Sep 2018 14:57:46 +0100
Subject: [PATCH 018/147] Fix EditAndAddOnlyFieldsMixin behaviour (#50)
---
djangocms_moderation/helpers.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index 6d6b7f3d..64a08106 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -60,7 +60,7 @@ def get_readonly_fields(self, request, obj=None):
"""
Override to provide editonly_fields and addonly_fields functionality
"""
- if obj: # Editing an existing object
- return self.readonly_fields + self.editonly_fields
- else: # Adding a new object
+ if obj: # Editing an existing object, so `addonly_fields` should be readonly
return self.readonly_fields + self.addonly_fields
+ else: # Adding a new object
+ return self.readonly_fields + self.editonly_fields
From 411d00e8ec245a2673a206854ffbdc64d51dd562 Mon Sep 17 00:00:00 2001
From: Paulo
Date: Wed, 5 Sep 2018 10:27:29 -0400
Subject: [PATCH 019/147] Bumped version to 1.0.1
---
djangocms_moderation/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/djangocms_moderation/__init__.py b/djangocms_moderation/__init__.py
index c7d56bb1..b126bbfb 100644
--- a/djangocms_moderation/__init__.py
+++ b/djangocms_moderation/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '1.0.0'
+__version__ = '1.0.1'
default_app_config = 'djangocms_moderation.apps.ModerationConfig'
From ef03f5ce524ec34e1827d7479d1679af847c144c Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Wed, 5 Sep 2018 15:51:27 +0100
Subject: [PATCH 020/147] Remove collection filter from moderationrequest admin
(#51)
---
djangocms_moderation/admin.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 3dc273b5..badb970c 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -90,7 +90,6 @@ class ModerationRequestAdmin(admin.ModelAdmin):
resubmit_selected,
]
inlines = [ModerationRequestActionInline]
- list_filter = ['collection']
fields = ['id', 'collection', 'workflow', 'is_active', 'get_status']
readonly_fields = fields
change_list_template = 'djangocms_moderation/moderation_request_change_list.html'
From 262a99ce9244e1feeff2cdd0a4cf5476312f71b2 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Thu, 6 Sep 2018 12:09:30 +0100
Subject: [PATCH 021/147] Correct the get_request_link in admin (#52)
---
djangocms_moderation/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index badb970c..7f14b6d5 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -311,7 +311,7 @@ def get_request_link(self, obj):
opts = ModerationRequest._meta
url = reverse(
'admin:{}_{}_change'.format(opts.app_label, opts.model_name),
- args=[obj.pk],
+ args=[obj.moderation_request.pk],
)
return format_html(
'{}',
From 856d1c9fb903c77547378608d3cce0b175c722d6 Mon Sep 17 00:00:00 2001
From: Paulo
Date: Thu, 6 Sep 2018 10:47:41 -0400
Subject: [PATCH 022/147] Bumped version to 1.0.2
---
djangocms_moderation/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/djangocms_moderation/__init__.py b/djangocms_moderation/__init__.py
index b126bbfb..dc4c7752 100644
--- a/djangocms_moderation/__init__.py
+++ b/djangocms_moderation/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '1.0.1'
+__version__ = '1.0.2'
default_app_config = 'djangocms_moderation.apps.ModerationConfig'
From 9c180b4c1111bbd6717481b62291420d22fd9ee8 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Thu, 6 Sep 2018 15:23:45 +0100
Subject: [PATCH 023/147] Remove generic foreign key in ModerationRequest (#49)
---
djangocms_moderation/admin.py | 4 +-
djangocms_moderation/admin_actions.py | 6 +--
djangocms_moderation/cms_toolbars.py | 18 +++----
djangocms_moderation/forms.py | 31 ++++-------
djangocms_moderation/handlers.py | 51 +++++--------------
djangocms_moderation/helpers.py | 20 +-------
.../migrations/0003_auto_20180903_1206.py | 35 +++++++++++++
djangocms_moderation/models.py | 22 ++++----
.../emails/moderation-request/approved.txt | 5 +-
.../emails/moderation-request/cancelled.txt | 4 +-
.../emails/moderation-request/rejected.txt | 4 +-
.../emails/moderation-request/request.txt | 6 +--
.../item_to_collection.html | 12 ++---
djangocms_moderation/views.py | 10 ++--
tests/requirements.txt | 2 +
tests/settings.py | 16 ++++++
tests/test_admin.py | 10 ++--
tests/test_admin_actions.py | 23 ++++-----
tests/test_cms_toolbars.py | 8 +--
tests/test_forms.py | 8 +--
tests/test_handlers.py | 7 ++-
tests/test_helpers.py | 17 ++-----
tests/test_models.py | 36 +++----------
tests/test_moderation_flows.py | 4 +-
tests/test_views.py | 45 ++++++++--------
tests/utils/base.py | 30 +++++------
26 files changed, 193 insertions(+), 241 deletions(-)
create mode 100644 djangocms_moderation/migrations/0003_auto_20180903_1206.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 7f14b6d5..dc6c7bd6 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -102,13 +102,13 @@ def has_module_permission(self, request):
return False
def get_list_display(self, request):
- list_display = ['id', 'content_type', 'get_title', 'get_content_author', 'get_preview_link', 'get_status']
+ list_display = ['id', 'version', 'get_title', 'get_content_author', 'get_preview_link', 'get_status']
if conf.REQUEST_COMMENTS_ENABLED:
list_display.append('get_comments_link')
return list_display
def get_title(self, obj):
- return obj.content_object
+ return obj.version.content
get_title.short_description = _('Title')
def get_preview_link(self, obj):
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
index e6f37403..9488ade0 100644
--- a/djangocms_moderation/admin_actions.py
+++ b/djangocms_moderation/admin_actions.py
@@ -195,7 +195,7 @@ def publish_selected(modeladmin, request, queryset):
for moderation_request in queryset.all():
if moderation_request.is_approved():
num_published_requests += 1
- publish_content_object(moderation_request.content_object)
+ publish_version(moderation_request.version)
# notify the UI of the action results
messages.success(
@@ -219,6 +219,6 @@ def post_bulk_actions(collection):
collection.save(update_fields=['status'])
-def publish_content_object(content_object):
- # TODO: e.g.moderation_request.content_object.publish(request.user)
+def publish_version(version):
+ # TODO: e.g.moderation_request.version.publish(request.user)
return True
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 0f0c63da..134eb133 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
-from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool
from cms.utils.urlutils import add_url_parameters
+from djangocms_versioning.models import Version
+
from .models import ModerationRequest
from .utils import get_admin_url
@@ -18,21 +19,18 @@ class Media:
}
def post_template_populate(self):
- """
- @TODO replace page object with generic content object
- :return:
- """
- super(ModerationToolbar, self).post_template_populate()
+ super().post_template_populate()
+ # TODO replace page object with generic content object
page = self.request.current_page
if not page:
return None
try:
- content_type = ContentType.objects.get_for_model(page)
+ # TODO Make this work with the correct version
+ version = Version.objects.get(pk=9999)
moderation_request = ModerationRequest.objects.get(
- content_type=content_type,
- object_id=page.pk,
+ version=version
)
self.toolbar.add_modal_button(
name=_('In Moderation "%s"' % moderation_request.collection.name),
@@ -40,7 +38,7 @@ def post_template_populate(self):
disabled=True,
side=self.toolbar.RIGHT,
)
- except ModerationRequest.DoesNotExist:
+ except Version.DoesNotExist:
url = add_url_parameters(
get_admin_url(
name='cms_moderation_item_to_collection',
diff --git a/djangocms_moderation/forms.py b/djangocms_moderation/forms.py
index 11a2fcd9..76a21228 100644
--- a/djangocms_moderation/forms.py
+++ b/djangocms_moderation/forms.py
@@ -4,11 +4,11 @@
from django.contrib import admin
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
from django.forms.forms import NON_FIELD_ERRORS
from django.utils.translation import ugettext, ugettext_lazy as _
from adminsortable2.admin import CustomInlineFormSet
+from djangocms_versioning.models import Version
from .constants import (
ACTION_CANCELLED,
@@ -118,12 +118,11 @@ class CollectionItemForm(forms.Form):
queryset=ModerationCollection.objects.filter(status=COLLECTING),
required=True
)
- content_type = forms.ModelChoiceField(
- queryset=ContentType.objects.filter(app_label="cms", model="page"),
+ version = forms.ModelChoiceField(
+ queryset=Version.objects.all(),
required=True,
widget=forms.HiddenInput(),
)
- content_object_id = forms.IntegerField()
def set_collection_widget(self, request):
related_modeladmin = admin.site._registry.get(ModerationCollection)
@@ -148,31 +147,19 @@ def clean(self):
if self.errors:
return self.cleaned_data
- content_type = self.cleaned_data['content_type']
+ version = self.cleaned_data['version']
- try:
- content_object = content_type.get_object_for_this_type(
- pk=self.cleaned_data['content_object_id'],
- is_page_type=False,
- )
- except content_type.model_class().DoesNotExist:
- content_object = None
-
- if not content_object:
- raise forms.ValidationError(_('Invalid content_object_id, does not exist'))
-
- request_with_object_exists = ModerationRequest.objects.filter(
- content_type=content_type,
- object_id=content_object.pk,
+ request_with_version_exists = ModerationRequest.objects.filter(
+ version=version
).exists()
- if request_with_object_exists:
+ if request_with_version_exists:
raise forms.ValidationError(_(
"{} is already part of existing moderation request which is part "
- "of another active collection".format(content_object)
+ "of another active collection".format(version.content)
))
- self.cleaned_data['content_object'] = content_object
+ self.cleaned_data['version'] = version
return self.cleaned_data
diff --git a/djangocms_moderation/handlers.py b/djangocms_moderation/handlers.py
index 4a9759b1..59ba13b1 100644
--- a/djangocms_moderation/handlers.py
+++ b/djangocms_moderation/handlers.py
@@ -4,52 +4,25 @@
from django.dispatch import receiver
-from cms.operations import PUBLISH_PAGE_TRANSLATION
-from cms.signals import post_obj_operation
-
-from .constants import ACTION_FINISHED
-from .helpers import get_active_moderation_request
from .models import ConfirmationFormSubmission
from .signals import confirmation_form_submission
-@receiver(post_obj_operation)
-def close_moderation_request(sender, **kwargs):
- request = kwargs['request']
- operation_type = kwargs['operation']
- is_publish = operation_type == PUBLISH_PAGE_TRANSLATION
- publish_successful = kwargs.get('successful')
-
- if not is_publish or not publish_successful:
- return
-
- page = kwargs['obj']
- translation = kwargs['translation']
-
- active_request = get_active_moderation_request(page, translation.language)
-
- if not active_request:
- return
-
- active_request.update_status(
- action=ACTION_FINISHED,
- by_user=request.user,
- )
-
-
@receiver(confirmation_form_submission)
def moderation_confirmation_form_submission(sender, page, language, user, form_data, **kwargs):
for field_data in form_data:
if not set(('label', 'value')).issubset(field_data):
raise ValueError('Each field dict should contain label and value keys.')
- active_request = get_active_moderation_request(page, language)
- next_step = active_request.user_get_step(user)
-
- ConfirmationFormSubmission.objects.create(
- request=active_request,
- for_step=next_step,
- by_user=user,
- data=json.dumps(form_data),
- confirmation_page=next_step.role.confirmation_page,
- )
+ # TODO Confirmation pages are not used/working in 1.0.x yet
+ active_request = None # get_active_moderation_request(page, language)
+ if active_request:
+ next_step = active_request.user_get_step(user)
+
+ ConfirmationFormSubmission.objects.create(
+ request=active_request,
+ for_step=next_step,
+ by_user=user,
+ data=json.dumps(form_data),
+ confirmation_page=next_step.role.confirmation_page,
+ )
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index 64a08106..0d01e346 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
-from .models import ConfirmationFormSubmission, ModerationRequest, Workflow
+from .models import ConfirmationFormSubmission, Workflow
def get_default_workflow():
@@ -13,27 +13,11 @@ def get_default_workflow():
def get_moderation_workflow():
# TODO for now just return default, this would need to depend on the collection
- # Might be as well not needed in 4.x, leaving it here for now
+ # Might be as well not needed in 1.0.x, leaving it here for now
return get_default_workflow()
-def get_active_moderation_request(obj, language):
- content_type = ContentType.objects.get_for_model(obj)
- try:
- return ModerationRequest.objects.get(
- content_type=content_type,
- object_id=obj.pk,
- language=language,
- is_active=True,
- )
- except ModerationRequest.DoesNotExist:
- return None
-
-
def get_page_or_404(obj_id, language):
- """
- TODO is this needed in 4.x?
- """
content_type = ContentType.objects.get(app_label="cms", model="page") # how do we get this
return content_type.get_object_for_this_type(
diff --git a/djangocms_moderation/migrations/0003_auto_20180903_1206.py b/djangocms_moderation/migrations/0003_auto_20180903_1206.py
new file mode 100644
index 00000000..52bcc9e1
--- /dev/null
+++ b/djangocms_moderation/migrations/0003_auto_20180903_1206.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-09-03 11:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('djangocms_versioning', '0010_version_proxies'),
+ ('djangocms_moderation', '0002_auto_20180905_1152'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='moderationrequest',
+ name='version',
+ field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='djangocms_versioning.Version'),
+ preserve_default=False,
+ ),
+ migrations.AlterUniqueTogether(
+ name='moderationrequest',
+ unique_together=set([('collection', 'version')]),
+ ),
+ migrations.RemoveField(
+ model_name='moderationrequest',
+ name='content_type',
+ ),
+ migrations.RemoveField(
+ model_name='moderationrequest',
+ name='object_id',
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 6a04aa5f..c6f23615 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -5,8 +5,6 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
-from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models, transaction
@@ -16,6 +14,8 @@
from cms.models.fields import PlaceholderField
+from djangocms_versioning.models import Version
+
from .emails import notify_collection_moderators
from .utils import generate_compliance_number
@@ -327,16 +327,14 @@ def should_be_archived(self):
return False
return True
- def add_object(self, content_object):
+ def add_version(self, version):
"""
- Add object to the ModerationRequest in this collection.
+ Add version to the ModerationRequest in this collection.
Requires validation from .forms.CollectionItemForm
:return:
"""
- content_type = ContentType.objects.get_for_model(content_object)
return self.moderation_requests.create(
- content_type=content_type,
- object_id=content_object.pk,
+ version=version,
collection=self,
)
@@ -348,18 +346,18 @@ class ModerationRequest(models.Model):
related_name='moderation_requests',
on_delete=models.CASCADE
)
- content_type = models.ForeignKey(
- ContentType,
+ version = models.ForeignKey(
+ to=Version,
+ verbose_name=_('version'),
on_delete=models.CASCADE,
)
- object_id = models.PositiveIntegerField()
- content_object = GenericForeignKey('content_type', 'object_id')
language = models.CharField(
verbose_name=_('language'),
max_length=5,
choices=settings.LANGUAGES,
)
is_active = models.BooleanField(
+ verbose_name=_('is active'),
default=False,
db_index=True,
)
@@ -379,7 +377,7 @@ class ModerationRequest(models.Model):
class Meta:
verbose_name = _('Request')
verbose_name_plural = _('Requests')
- unique_together = ('collection', 'object_id', 'content_type')
+ unique_together = ('collection', 'version',)
ordering = ['id']
def __str__(self):
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
index 4bfcc35f..dbb52b4e 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/approved.txt
@@ -9,14 +9,13 @@ requests in the collection {{ collection_name }}.
-{% for moderation_request in moderation_requests %}
+{% for mr in moderation_requests %}
-
- {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+ {{ mr.pk }} - {{ mr.version.content }} ({{ mr.version.content_type }})
{% endfor %}
-
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
index c1f0ae98..84859fd6 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/cancelled.txt
@@ -9,9 +9,9 @@ Hello {{ collection_author }}, the following moderation requests in the collecti
-{% for moderation_request in moderation_requests %}
+{% for mr in moderation_requests %}
-
- {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+ {{ mr.pk }} - {{ mr.version.content }} ({{ mr.version.content_type }})
{% endfor %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
index 2995502b..a75bdc8e 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/rejected.txt
@@ -12,9 +12,9 @@ requests in the collection {{ collection_name }}.
-{% for moderation_request in moderation_requests %}
+{% for mr in moderation_requests %}
-
- {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+ {{ mr.pk }} - {{ mr.version.content }} ({{ mr.version.content_type }})
{% endfor %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
index f3767b0c..47138c72 100644
--- a/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
+++ b/djangocms_moderation/templates/djangocms_moderation/emails/moderation-request/request.txt
@@ -7,15 +7,15 @@ changes in the collection {{ collection_name }}.
{% trans 'Items included in this request' %}:
+
-{% for moderation_request in moderation_requests %}
+{% for mr in moderation_requests %}
-
- {{ moderation_request.pk }} - {{ moderation_request.content_object }} ({{ moderation_request.content_type }})
+ {{ mr.pk }} - {{ mr.version.content }} ({{ mr.version.content_type }})
{% endfor %}
-
{% if comment %}
{% trans 'Comment' %}:
{{ comment }}
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
index 9f752c07..1979b133 100644
--- a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -11,10 +11,8 @@ {% trans "Add to existing collection" %}
{% endif %}
@@ -23,8 +21,7 @@ {% trans "Add to existing collection" %}
- {% trans "Content Type" %} |
- {% trans "Identifier" %} |
+ {% trans "Version" %} |
{% trans "Author" %} |
{% trans "Edit Date" %} |
{% trans "Version" %} |
@@ -34,8 +31,7 @@ {% trans "Add to existing collection" %}
{% for content_object in content_object_list %}
- {{ content_object.content_type }} |
- {{ content_object.content_object }} |
+ {{ version }} |
Stub Author [versioning] |
Stub Date [versioning] |
Stub VNumber [versioning] |
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 5ac66c96..40319abc 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -1,14 +1,12 @@
from __future__ import unicode_literals
from django.contrib import admin, messages
-from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView
-from cms.models import Page
from cms.utils.urlutils import add_url_parameters
from .forms import CollectionItemForm, SubmitCollectionForModerationForm
@@ -26,10 +24,8 @@ class CollectionItemView(FormView):
def get_form_kwargs(self):
kwargs = super(CollectionItemView, self).get_form_kwargs()
- # TODO: replace page object with Version object
kwargs['initial'].update({
- 'content_object_id': self.request.GET.get('content_object_id'),
- 'content_type': ContentType.objects.get_for_model(Page).pk,
+ 'version': self.request.GET.get('version_id'),
})
collection_id = self.request.GET.get('collection_id')
@@ -38,9 +34,9 @@ def get_form_kwargs(self):
return kwargs
def form_valid(self, form):
- content_object = form.cleaned_data['content_object']
+ version = form.cleaned_data['version']
collection = form.cleaned_data['collection']
- collection.add_object(content_object)
+ collection.add_version(version)
messages.success(self.request, _('Item successfully added to moderation collection'))
return render(self.request, self.success_template_name, {})
diff --git a/tests/requirements.txt b/tests/requirements.txt
index e31ee985..1e0cfb79 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -3,6 +3,8 @@ tox
coverage
flake8
isort
+factory-boy
+freezegun
-e git+git://github.com/aldryn/aldryn-forms.git@master#egg=aldryn-forms
mock
-e git+git://github.com/divio/djangocms-versioning.git@master#egg=djangocms-versioning
diff --git a/tests/settings.py b/tests/settings.py
index 770a6acd..a6a91918 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -9,7 +9,23 @@
'aldryn_forms',
'captcha',
'emailit',
+ 'djangocms_text_ckeditor',
+ 'djangocms_versioning.test_utils.polls',
+ 'djangocms_versioning.test_utils.blogpost',
+ 'djangocms_versioning.test_utils.people',
+ 'djangocms_versioning.test_utils.text',
],
+ # As adviced, we can disable migrations in tests. This will improve
+ # test performance and removes the need for test apps to provide migrations
+ 'MIGRATION_MODULES': {
+ 'auth': None,
+ 'cms': None,
+ 'menus': None,
+ 'djangocms_versioning': None,
+ 'filer': None,
+ 'djangocms_moderation': None,
+ 'aldryn_forms': None,
+ },
}
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 65d6ea1b..42ae0c35 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.urls import reverse
-from cms.api import create_page
+from djangocms_versioning.test_utils.factories import PageVersionFactory
from djangocms_moderation import conf, constants
from djangocms_moderation.admin import (
@@ -29,11 +29,11 @@ def setUp(self):
author=self.user, name='Collection Admin Actions', workflow=self.wf, status=constants.IN_REVIEW
)
- pg1 = create_page(title='Page 1', template='page.html', language='en',)
- pg2 = create_page(title='Page 2', template='page.html', language='en',)
+ pg1_version = PageVersionFactory()
+ pg2_version = PageVersionFactory()
self.mr1 = ModerationRequest.objects.create(
- content_object=pg1, language='en', collection=self.collection, is_active=True,)
+ version=pg1_version, language='en', collection=self.collection, is_active=True,)
self.wfst = self.wf.steps.create(role=self.role2, is_required=True, order=1,)
@@ -48,7 +48,7 @@ def setUp(self):
# this moderation request is not approved
self.mr2 = ModerationRequest.objects.create(
- content_object=pg2, language='en', collection=self.collection, is_active=True,)
+ version=pg2_version, language='en', collection=self.collection, is_active=True,)
self.mr2.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
self.url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py
index 125cf6c6..e682c9bb 100644
--- a/tests/test_admin_actions.py
+++ b/tests/test_admin_actions.py
@@ -3,7 +3,7 @@
from django.contrib.admin import ACTION_CHECKBOX_NAME
from django.urls import reverse
-from cms.api import create_page
+from djangocms_versioning.test_utils.factories import PageVersionFactory
from djangocms_moderation import constants
from djangocms_moderation.admin import ModerationRequestAdmin
@@ -29,11 +29,11 @@ def setUp(self):
status=constants.IN_REVIEW,
)
- pg1 = create_page(title='Page 1', template='page.html', language='en',)
- pg2 = create_page(title='Page 2', template='page.html', language='en',)
+ pg1_version = PageVersionFactory()
+ pg2_version = PageVersionFactory()
self.mr1 = ModerationRequest.objects.create(
- content_object=pg1, language='en', collection=self.collection, is_active=True,)
+ version=pg1_version, language='en', collection=self.collection, is_active=True,)
self.wfst1 = self.wf.steps.create(role=self.role1, is_required=True, order=1,)
self.wfst2 = self.wf.steps.create(role=self.role2, is_required=True, order=1,)
@@ -45,7 +45,7 @@ def setUp(self):
# this moderation request is not approved
self.mr2 = ModerationRequest.objects.create(
- content_object=pg2, language='en', collection=self.collection, is_active=True,)
+ version=pg2_version, language='en', collection=self.collection, is_active=True,)
self.mr2.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
self.url = reverse('admin:djangocms_moderation_moderationrequest_changelist')
@@ -103,9 +103,8 @@ def test_delete_selected(self, notify_author_mock, notify_moderators_mock, mock_
self.collection.refresh_from_db()
self.assertEqual(self.collection.status, constants.ARCHIVED)
- @mock.patch('djangocms_moderation.admin_actions.publish_content_object')
- def test_publish_selected(self, mock_publish_content_object):
-
+ @mock.patch('djangocms_moderation.admin_actions.publish_version')
+ def test_publish_selected(self, mock_publish_version):
fixtures = [self.mr1, self.mr2]
data = {
'action': 'publish_selected',
@@ -113,9 +112,9 @@ def test_publish_selected(self, mock_publish_content_object):
}
self.client.post(self.url_with_filter, data, follow=True)
- assert mock_publish_content_object.called
+ assert mock_publish_version.called
# check it has been called only once, i.e. with the approved mr1
- mock_publish_content_object.assert_called_once_with(self.mr1.content_object)
+ mock_publish_version.assert_called_once_with(self.mr1.version)
@mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
@mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
@@ -237,9 +236,9 @@ def test_approve_selected_sends_correct_emails(self, notify_moderators_mock):
self.user.groups.add(self.group)
# Add one more, partially approved request
- pg3 = create_page(title='Page 3', template='page.html', language='en', )
+ pg3_version = PageVersionFactory()
self.mr3 = ModerationRequest.objects.create(
- content_object=pg3, language='en', collection=self.collection, is_active=True,)
+ version=pg3_version, language='en', collection=self.collection, is_active=True,)
self.mr3.actions.create(by_user=self.user, action=constants.ACTION_STARTED,)
self.mr3.update_status(by_user=self.user, action=constants.ACTION_APPROVED,)
self.mr3.update_status(by_user=self.user2, action=constants.ACTION_APPROVED,)
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
index b40f9b7b..9f1b1414 100644
--- a/tests/test_cms_toolbars.py
+++ b/tests/test_cms_toolbars.py
@@ -1,3 +1,5 @@
+from unittest import skip
+
from django.contrib.auth.models import AnonymousUser
from django.test.client import RequestFactory
@@ -11,8 +13,8 @@
from .utils.base import BaseTestCase
+@skip("To be fixed in the upcoming ticket")
class TestCMSToolbars(BaseTestCase):
-
def get_page_request(self, page, user, path=None, edit=False,
preview=False, structure=False, lang_code='en', disable=False):
if not path:
@@ -47,7 +49,7 @@ def get_page_request(self, page, user, path=None, edit=False,
def test_submit_for_moderation(self):
ModerationRequest.objects.all().delete()
- request = self.get_page_request(self.pg1, AnonymousUser(), '/')
+ request = self.get_page_request(self.pg1_version, AnonymousUser(), '/')
toolbar = CMSToolbar(request)
toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
toolbar.populate()
@@ -59,7 +61,7 @@ def test_submit_for_moderation(self):
)
def test_page_in_moderation(self):
- request = self.get_page_request(self.pg1, AnonymousUser(), '/')
+ request = self.get_page_request(self.pg1_version, AnonymousUser(), '/')
toolbar = CMSToolbar(request)
toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
toolbar.populate()
diff --git a/tests/test_forms.py b/tests/test_forms.py
index e7243c14..44687b0e 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -19,7 +19,7 @@ def test_form_init_approved_action(self):
form = UpdateModerationRequestForm(
action=constants.ACTION_APPROVED,
language='en',
- page=self.pg1,
+ page=self.pg1_version,
user=self.user,
workflow=self.wf1,
active_request=self.moderation_request1,
@@ -37,7 +37,7 @@ def test_form_init_cancelled_action(self):
form = UpdateModerationRequestForm(
action=constants.ACTION_CANCELLED,
language='en',
- page=self.pg1,
+ page=self.pg1_version,
user=self.user,
workflow=self.wf1,
active_request=self.moderation_request1,
@@ -50,7 +50,7 @@ def test_form_init_rejected_action(self):
form = UpdateModerationRequestForm(
action=constants.ACTION_REJECTED,
language='en',
- page=self.pg1,
+ page=self.pg1_version,
user=self.user,
workflow=self.wf1,
active_request=self.moderation_request1,
@@ -68,7 +68,7 @@ def test_form_save(self):
data,
action=constants.ACTION_APPROVED,
language='en',
- page=self.pg1,
+ page=self.pg1_version,
user=self.user,
workflow=self.wf1,
active_request=self.moderation_request1,
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index cb240cb4..ae4e1d47 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -1,3 +1,5 @@
+from unittest import skip
+
from djangocms_moderation.contrib.moderation_forms.cms_plugins import (
ModerationFormPlugin,
)
@@ -12,6 +14,7 @@
from .utils.base import BaseTestCase
+@skip("Confirmation page feature doesn't to support 1.0.x yet.")
class ModerationConfirmationFormSubmissionTest(BaseTestCase):
def setUp(self):
@@ -25,7 +28,7 @@ def test_throws_exception_when_form_data_is_invalid(self):
with self.assertRaises(ValueError) as context:
moderation_confirmation_form_submission(
sender=ModerationFormPlugin,
- page=self.pg1,
+ page=self.pg1_version,
language='en',
user=self.user,
form_data=[{'label': 'Question 1', 'answer': 'Yes'}]
@@ -35,7 +38,7 @@ def test_throws_exception_when_form_data_is_invalid(self):
def test_creates_new_form_submission_when_form_data_is_valid(self):
moderation_confirmation_form_submission(
sender=ModerationFormPlugin,
- page=self.pg1,
+ page=self.pg1_version,
language='en',
user=self.user,
form_data=[{'label': 'Question 1', 'value': 'Yes'}],
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index f7e7210e..09525805 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,7 +1,7 @@
import json
+from unittest import skip
from djangocms_moderation.helpers import (
- get_active_moderation_request,
get_form_submission_for_step,
get_page_or_404,
)
@@ -13,21 +13,10 @@
from .utils.base import BaseTestCase
-class GetCurrentModerationRequestTest(BaseTestCase):
-
- def test_existing_moderation_request(self):
- active_request = get_active_moderation_request(self.pg1, 'en')
- self.assertEqual(active_request, self.moderation_request1)
-
- def test_no_moderation_request(self):
- active_request = get_active_moderation_request(self.pg2, 'en')
- self.assertIsNone(active_request)
-
-
+@skip("Confirmation page feature doesn't to support 1.0.x yet.")
class GetPageOr404Test(BaseTestCase):
-
def test_returns_page(self):
- self.assertEqual(get_page_or_404(self.pg1.pk, 'en'), self.pg1)
+ self.assertEqual(get_page_or_404(self.pg1_version.pk, 'en'), self.pg1_version)
class GetFormSubmissions(BaseTestCase):
diff --git a/tests/test_models.py b/tests/test_models.py
index 8eecdecc..0b38713b 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -2,12 +2,9 @@
from mock import patch
from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
-from cms.api import create_page
-
from djangocms_moderation import constants
from djangocms_moderation.models import (
ConfirmationFormSubmission,
@@ -334,7 +331,7 @@ def test_compliance_number(self, mock_uuid):
mock_uuid.return_value = 'abc123'
request = ModerationRequest.objects.create(
- content_object=self.pg4,
+ version=self.pg4_version,
language='en',
is_active=True,
collection=self.collection1,
@@ -348,7 +345,7 @@ def test_compliance_number(self, mock_uuid):
def test_compliance_number_sequential_number_backend(self):
self.wf2.compliance_number_backend = 'djangocms_moderation.backends.sequential_number_backend'
request = ModerationRequest.objects.create(
- content_object=self.pg1,
+ version=self.pg1_version,
language='en',
collection=self.collection2,
)
@@ -367,7 +364,7 @@ def test_compliance_number_sequential_number_with_identifier_prefix_backend(self
self.wf2.identifier = 'SSO'
request = ModerationRequest.objects.create(
- content_object=self.pg1,
+ version=self.pg1_version,
language='en',
collection=self.collection2,
)
@@ -400,7 +397,7 @@ def test_save_when_to_user_passed(self):
def test_save_when_to_user_not_passed_and_action_started(self):
new_request = ModerationRequest.objects.create(
- content_object=self.pg2,
+ version=self.pg2_version,
language='en',
collection=self.collection1,
is_active=True,
@@ -481,9 +478,6 @@ def setUp(self):
author=self.user, name='My collection 2', workflow=self.wf1
)
- self.page1 = create_page(title='My page 1', template='page.html', language='en',)
- self.page2 = create_page(title='My page 2', template='page.html', language='en',)
-
def test_job_id(self):
self.assertEqual(str(self.collection1.pk), self.collection1.job_id)
self.assertEqual(str(self.collection2.pk), self.collection2.job_id)
@@ -503,7 +497,7 @@ def test_should_be_archived(self, is_approved_mock):
self.assertTrue(self.collection1.should_be_archived())
ModerationRequest.objects.create(
- content_object=self.pg1, collection=self.collection1, is_active=True
+ version=self.pg1_version, collection=self.collection1, is_active=True
)
is_approved_mock.return_value = False
self.assertFalse(self.collection1.should_be_archived())
@@ -518,7 +512,7 @@ def test_allow_submit_for_review(self):
self.assertFalse(self.collection1.allow_submit_for_review(user=self.user))
ModerationRequest.objects.create(
- content_object=self.pg1, collection=self.collection1, is_active=True
+ version=self.pg1_version, collection=self.collection1, is_active=True
)
self.assertTrue(self.collection1.allow_submit_for_review(user=self.user))
# Only collection author can submit
@@ -531,10 +525,10 @@ def test_allow_submit_for_review(self):
@patch('djangocms_moderation.models.notify_collection_moderators')
def test_submit_for_review(self, mock_ncm):
ModerationRequest.objects.create(
- content_object=self.pg1, language='en', collection=self.collection1
+ version=self.pg1_version, language='en', collection=self.collection1
)
ModerationRequest.objects.create(
- content_object=self.pg3, language='en', collection=self.collection1
+ version=self.pg3_version, language='en', collection=self.collection1
)
self.assertFalse(
@@ -558,17 +552,3 @@ def test_submit_for_review(self, mock_ncm):
request__collection=self.collection1, action=constants.ACTION_STARTED
).count()
)
-
- def _moderation_requests_count(self, obj, collection=None):
- """
- How many moderation requests are there [for a given collection]
- :return:
- """
- content_type = ContentType.objects.get_for_model(obj)
- queryset = ModerationRequest.objects.filter(
- content_type=content_type,
- object_id=obj.pk,
- )
- if collection:
- queryset = queryset.filter(collection=collection)
- return queryset.count()
diff --git a/tests/test_moderation_flows.py b/tests/test_moderation_flows.py
index ee5bf221..8df99f8d 100644
--- a/tests/test_moderation_flows.py
+++ b/tests/test_moderation_flows.py
@@ -69,7 +69,7 @@ def _resubmit_moderation_request(self, user, message='Test message - resubmit'):
def _cancel_moderation_request(self, user, message='Test message - cancel'):
return self._process_moderation_request(user, 'cancel', message)
- @skip('4.0 rework TBC')
+ @skip('1.0.x rework TBC')
def test_approve_moderation_workflow(self):
"""
This case tests the following workflow:
@@ -129,7 +129,7 @@ def test_approve_moderation_workflow(self):
self.assertTrue(last_action.action, constants.ACTION_FINISHED)
self.assertEqual(moderation_request.compliance_number, compliance_number)
- @skip('4.0 rework TBC')
+ @skip('1.0.x rework TBC')
def test_reject_moderation_workflow(self):
"""
This case tests the following workflow:
diff --git a/tests/test_views.py b/tests/test_views.py
index af099a19..25e3c9a6 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -7,6 +7,8 @@
from cms.utils.urlutils import add_url_parameters
+from djangocms_versioning.test_utils.factories import PageVersionFactory
+
from djangocms_moderation import constants
from djangocms_moderation.forms import CollectionItemForm
from djangocms_moderation.models import ModerationCollection, ModerationRequest
@@ -30,7 +32,8 @@ def setUp(self):
author=self.user, name='My collection 2', workflow=self.wf1
)
- self.content_type = ContentType.objects.get_for_model(self.pg1)
+ self.content_type = ContentType.objects.get_for_model(self.pg1_version)
+ self.pg_version = PageVersionFactory()
def _assert_render(self, response):
form = response.context_data['form']
@@ -50,25 +53,22 @@ def test_add_object_to_collections(self):
language='en',
args=()
), {'collection': self.collection_1.pk,
- 'content_type': self.content_type.pk,
- 'content_object_id': self.pg1.pk
+ 'version': self.pg_version.pk,
}
)
self.assertEqual(response.status_code, 200)
# self.assertContains(response, 'reloadBrowser')
- content_type = ContentType.objects.get_for_model(self.pg1)
moderation_request = ModerationRequest.objects.filter(
- content_type=content_type,
- object_id=self.pg1.pk,
+ version=self.pg_version,
)[0]
self.assertEqual(moderation_request.collection, self.collection_1)
- def test_invalid_content_already_in_collection(self):
+ def test_invalid_version_already_in_collection(self):
# add object
- self.collection_1.add_object(self.pg1)
+ self.collection_1.add_version(self.pg_version)
self.client.force_login(self.user)
@@ -78,8 +78,7 @@ def test_invalid_content_already_in_collection(self):
language='en',
args=()
), {'collection': self.collection_1.pk,
- 'content_type': self.content_type.pk,
- 'content_object_id': self.pg1.pk})
+ 'version': self.pg_version.pk})
self.assertEqual(response.status_code, 200)
self.assertIn(
@@ -87,23 +86,18 @@ def test_invalid_content_already_in_collection(self):
response.context_data['form'].errors['__all__'][0]
)
- def test_non_existing_content_object(self):
+ def test_non_existing_version(self):
self.client.force_login(self.user)
- content_type = ContentType.objects.get_for_model(self.pg1)
response = self.client.post(
get_admin_url(
name='cms_moderation_item_to_collection',
language='en',
args=()
), {'collection': self.collection_1.pk,
- 'content_type': content_type.pk,
- 'content_object_id': 9000})
+ 'version': 9000})
self.assertEqual(response.status_code, 200)
- self.assertIn(
- 'Invalid content_object_id, does not exist',
- response.context_data['form'].errors['__all__'][0]
- )
+ self.assertIn('version', response.context_data['form'].errors)
def test_prevent_locked_collections(self):
"""
@@ -118,7 +112,7 @@ def test_prevent_locked_collections(self):
name='cms_moderation_item_to_collection',
language='en',
args=()
- ), {'collection': self.collection_1.pk, 'content_object_id': self.pg1.pk})
+ ), {'collection': self.collection_1.pk, 'version': self.pg1_version.pk})
# locked collection are not part of the list
self.assertEqual(
@@ -126,11 +120,12 @@ def test_prevent_locked_collections(self):
response.context_data['form'].errors['collection'][0]
)
- def test_list_content_objects_from_collection_id_param(self):
+ def test_list_versions_from_collection_id_param(self):
ModerationRequest.objects.all().delete()
+ pg2_version = PageVersionFactory()
- self.collection_1.add_object(self.pg1)
- self.collection_2.add_object(self.pg2)
+ self.collection_1.add_version(self.pg_version)
+ self.collection_2.add_version(pg2_version)
self.client.force_login(self.user)
response = self.client.get(
@@ -148,7 +143,7 @@ def test_list_content_objects_from_collection_id_param(self):
for mod_request in moderation_requests:
self.assertTrue(mod_request in response.context_data['content_object_list'])
- def test_content_object_id_from_params(self):
+ def test_version_id_from_params(self):
self.client.force_login(self.user)
response = self.client.get(
add_url_parameters(
@@ -156,12 +151,12 @@ def test_content_object_id_from_params(self):
name='cms_moderation_item_to_collection',
language='en',
args=()
- ), content_object_id=self.pg1.pk
+ ), version_id=self.pg1_version.pk
)
)
form = response.context_data['form']
- self.assertEqual(self.pg1.pk, int(form.initial['content_object_id']))
+ self.assertEqual(self.pg1_version.pk, int(form.initial['version']))
def test_authenticated_users_only(self):
response = self.client.get(
diff --git a/tests/utils/base.py b/tests/utils/base.py
index 170ba3bd..72a01de0 100644
--- a/tests/utils/base.py
+++ b/tests/utils/base.py
@@ -1,7 +1,7 @@
from django.contrib.auth.models import Group, User
from django.test import TestCase
-from cms.api import create_page
+from djangocms_versioning.test_utils.factories import PageVersionFactory
from djangocms_moderation import constants
from djangocms_moderation.models import (
@@ -22,13 +22,13 @@ def setUpTestData(cls):
cls.wf3 = Workflow.objects.create(pk=3, name='Workflow 3',)
cls.wf4 = Workflow.objects.create(pk=4, name='Workflow 4',)
- # create pages
- cls.pg1 = create_page(title='Page 1', template='page.html', language='en',)
- cls.pg2 = create_page(title='Page 2', template='page.html', language='en',)
- cls.pg3 = create_page(title='Page 3', template='page.html', language='en',)
- cls.pg4 = create_page(title='Page 4', template='page.html', language='en',)
- cls.pg5 = create_page(title='Page 5', template='page.html', language='en',)
- cls.pg6 = create_page(title='Page 6', template='page.html', language='en',)
+ # create page versions
+ cls.pg1_version = PageVersionFactory()
+ cls.pg2_version = PageVersionFactory()
+ cls.pg3_version = PageVersionFactory()
+ cls.pg4_version = PageVersionFactory()
+ cls.pg5_version = PageVersionFactory()
+ cls.pg6_version = PageVersionFactory()
# create users, groups and roles
cls.user = User.objects.create_superuser(
@@ -74,16 +74,16 @@ def setUpTestData(cls):
)
cls.moderation_request1 = ModerationRequest.objects.create(
- content_object=cls.pg1, language='en', collection=cls.collection1, is_active=True,)
+ version=cls.pg1_version, language='en', collection=cls.collection1, is_active=True,)
cls.moderation_request1.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
ModerationRequest.objects.create(
- content_object=cls.pg3, language='en', collection=cls.collection1, is_active=False,)
+ version=cls.pg3_version, language='en', collection=cls.collection1, is_active=False,)
ModerationRequest.objects.create(
- content_object=cls.pg2, language='en', collection=cls.collection2, is_active=False,)
+ version=cls.pg2_version, language='en', collection=cls.collection2, is_active=False,)
cls.moderation_request2 = ModerationRequest.objects.create(
- content_object=cls.pg3, language='en', collection=cls.collection2, is_active=True,)
+ version=cls.pg3_version, language='en', collection=cls.collection2, is_active=True,)
cls.moderation_request2.actions.create(
by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request2.actions.create(
@@ -92,7 +92,7 @@ def setUpTestData(cls):
by_user=cls.user, action=constants.ACTION_APPROVED, step_approved=cls.wf2st2,)
cls.moderation_request3 = ModerationRequest.objects.create(
- content_object=cls.pg4, language='en', collection=cls.collection3, is_active=True,)
+ version=cls.pg4_version, language='en', collection=cls.collection3, is_active=True,)
cls.moderation_request3.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request3.actions.create(
by_user=cls.user,
@@ -102,12 +102,12 @@ def setUpTestData(cls):
)
# This request will be rejected
cls.moderation_request4 = ModerationRequest.objects.create(
- content_object=cls.pg5, language='en', collection=cls.collection3, is_active=True,)
+ version=cls.pg5_version, language='en', collection=cls.collection3, is_active=True,)
cls.moderation_request4.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
cls.moderation_request4.actions.create(by_user=cls.user2, action=constants.ACTION_REJECTED)
cls.moderation_request5 = ModerationRequest.objects.create(
- content_object=cls.pg6, language='en', collection=cls.collection4, is_active=True,)
+ version=cls.pg6_version, language='en', collection=cls.collection4, is_active=True,)
cls.moderation_request5.actions.create(by_user=cls.user, action=constants.ACTION_STARTED,)
From 3b16a3d5fba93c57b57882d9447d1404d15bf987 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 11 Sep 2018 16:16:19 +0100
Subject: [PATCH 024/147] Preview link (#56)
---
djangocms_moderation/admin.py | 20 ++++++++++++++++---
.../moderation_request_change_list.html | 12 ++++++++++-
2 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index dc6c7bd6..2d905e2a 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -8,6 +8,7 @@
from django.utils.translation import ugettext, ugettext_lazy as _
from cms.admin.placeholderadmin import PlaceholderAdminMixin
+from cms.toolbar.utils import get_object_preview_url
from adminsortable2.admin import SortableInlineAdminMixin
@@ -102,18 +103,31 @@ def has_module_permission(self, request):
return False
def get_list_display(self, request):
- list_display = ['id', 'version', 'get_title', 'get_content_author', 'get_preview_link', 'get_status']
+ list_display = [
+ 'id',
+ 'get_content_type',
+ 'get_title',
+ 'get_content_author',
+ 'get_preview_link',
+ 'get_status',
+ ]
if conf.REQUEST_COMMENTS_ENABLED:
list_display.append('get_comments_link')
return list_display
+ def get_content_type(self, obj):
+ return obj.version.content_type
+ get_content_type.short_description = _('Content type')
+
def get_title(self, obj):
return obj.version.content
get_title.short_description = _('Title')
def get_preview_link(self, obj):
- # TODO this will return Version object preview link once implemented
- return "Link placeholder"
+ return format_html(
+ '',
+ get_object_preview_url(obj.version.content),
+ )
get_preview_link.short_description = _('Preview')
def get_comments_link(self, obj):
diff --git a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
index 97a7d72b..251e7bdb 100644
--- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
+++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html
@@ -1,5 +1,15 @@
{% extends "admin/change_list.html" %}
-{% load i18n %}
+{% load i18n cms_static %}
+
+{% block extrahead %}
+ {{ block.super }}
+ {#
+ INFO: we need to add styles here instead of "extrastyle" to avoid
+ conflicts with adminstyle. We are adding cms.base.css to gain the
+ icon support, e.g. ``
+ #}
+
+{% endblock extrahead %}
{% block content_title %}
{% if collection %}
From 4e9f0d543c9fd2587e62fe81ceab212e32fd3cb0 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 11 Sep 2018 17:22:56 +0100
Subject: [PATCH 025/147] Call publish on moderation finished (#55)
---
djangocms_moderation/admin.py | 11 ++++---
djangocms_moderation/admin_actions.py | 25 +++++++++++-----
djangocms_moderation/cms_toolbars.py | 2 +-
.../migrations/0004_auto_20180907_1206.py | 26 +++++++++++++++++
djangocms_moderation/models.py | 5 +++-
tests/test_admin.py | 15 ++++++++++
tests/test_admin_actions.py | 29 +++++++++++++++----
tests/test_models.py | 6 ++++
tests/utils/base.py | 3 +-
9 files changed, 101 insertions(+), 21 deletions(-)
create mode 100644 djangocms_moderation/migrations/0004_auto_20180907_1206.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 2d905e2a..c25015be 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -177,11 +177,11 @@ def get_actions(self, request):
# `publish_selected` is possible.
_max_to_keep = 1 # publish_selected
- for mr in collection.moderation_requests.all():
+ for mr in collection.moderation_requests.all().select_related('version'):
if len(actions_to_keep) == _max_to_keep:
break # We have found all the actions, so no need to loop anymore
if 'publish_selected' not in actions_to_keep:
- if mr.is_approved() and request.user == collection.author:
+ if request.user == collection.author and mr.version_can_be_published():
actions_to_keep.append('publish_selected')
if collection.status == IN_REVIEW and 'approve_selected' not in actions_to_keep:
if mr.user_can_take_moderation_action(request.user):
@@ -230,17 +230,16 @@ def get_status(self, obj):
last_action = obj.get_last_action()
if last_action:
- if obj.is_approved():
+ if obj.version_can_be_published():
status = ugettext('Ready for publishing')
- # TODO: consider published status for version e.g.:
- # elif obj.content_object.is_published():
- # status = ugettext('Published')
elif obj.is_rejected():
status = ugettext('Pending author rework')
elif obj.is_active and obj.has_pending_step():
next_step = obj.get_next_required()
role = next_step.role.name
status = ugettext('Pending %(role)s approval') % {'role': role}
+ elif not obj.version.can_be_published():
+ status = obj.version.get_state_display()
else:
user_name = last_action.get_by_user_name()
message_data = {
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
index 9488ade0..3c900249 100644
--- a/djangocms_moderation/admin_actions.py
+++ b/djangocms_moderation/admin_actions.py
@@ -2,6 +2,8 @@
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _, ungettext
+from django_fsm import TransitionNotAllowed
+
from djangocms_moderation import constants
from djangocms_moderation.emails import (
notify_collection_author,
@@ -192,12 +194,18 @@ def publish_selected(modeladmin, request, queryset):
raise PermissionDenied
num_published_requests = 0
- for moderation_request in queryset.all():
- if moderation_request.is_approved():
- num_published_requests += 1
- publish_version(moderation_request.version)
+ for mr in queryset.all():
+ if mr.version_can_be_published():
+ if publish_version(mr.version, request.user):
+ num_published_requests += 1
+ mr.update_status(
+ action=constants.ACTION_FINISHED,
+ by_user=request.user,
+ )
+ else:
+ # TODO provide some feedback back to the user?
+ pass
- # notify the UI of the action results
messages.success(
request,
ungettext(
@@ -219,6 +227,9 @@ def post_bulk_actions(collection):
collection.save(update_fields=['status'])
-def publish_version(version):
- # TODO: e.g.moderation_request.version.publish(request.user)
+def publish_version(version, user):
+ try:
+ version.publish(user)
+ except TransitionNotAllowed:
+ return False
return True
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 134eb133..19556a2e 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -38,7 +38,7 @@ def post_template_populate(self):
disabled=True,
side=self.toolbar.RIGHT,
)
- except Version.DoesNotExist:
+ except (ModerationRequest.DoesNotExist, Version.DoesNotExist):
url = add_url_parameters(
get_admin_url(
name='cms_moderation_item_to_collection',
diff --git a/djangocms_moderation/migrations/0004_auto_20180907_1206.py b/djangocms_moderation/migrations/0004_auto_20180907_1206.py
new file mode 100644
index 00000000..afef5b6c
--- /dev/null
+++ b/djangocms_moderation/migrations/0004_auto_20180907_1206.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-09-07 11:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('djangocms_moderation', '0003_auto_20180903_1206'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='moderationrequest',
+ name='is_active',
+ field=models.BooleanField(db_index=True, default=False, verbose_name='is active'),
+ ),
+ migrations.AlterField(
+ model_name='moderationrequest',
+ name='version',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_versioning.Version', verbose_name='version'),
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index c6f23615..66f89bbc 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -383,7 +383,7 @@ class Meta:
def __str__(self):
return "{} {}".format(
self.pk,
- self.content_object.pk
+ self.version.pk
)
@cached_property
@@ -406,6 +406,9 @@ def has_required_pending_steps(self):
def is_approved(self):
return self.is_active and not self.has_required_pending_steps()
+ def version_can_be_published(self):
+ return self.is_approved() and self.version.can_be_published()
+
def is_rejected(self):
return self.get_last_action().action == constants.ACTION_REJECTED
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 42ae0c35..fbc2bd1b 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -71,6 +71,21 @@ def test_delete_selected_action_visibility(self):
actions = self.mra.get_actions(request=mock_request)
self.assertNotIn('delete_selected', actions)
+ def test_publish_selected_action_visibility_when_version_is_published(self):
+ mock_request = MockRequest()
+ mock_request.user = self.user
+ mock_request._collection = self.collection
+
+ actions = self.mra.get_actions(request=mock_request)
+ # mr1 request is approved so user can see the publish_selected action
+ self.assertIn('publish_selected', actions)
+
+ # Now, when version becomes published, they shouldn't see it
+ self.mr1.version._set_publish(self.user)
+ self.mr1.version.save()
+ actions = self.mra.get_actions(request=mock_request)
+ self.assertNotIn('publish_selected', actions)
+
def test_publish_selected_action_visibility(self):
mock_request = MockRequest()
mock_request.user = self.user
diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py
index e682c9bb..b015d0ed 100644
--- a/tests/test_admin_actions.py
+++ b/tests/test_admin_actions.py
@@ -3,6 +3,8 @@
from django.contrib.admin import ACTION_CHECKBOX_NAME
from django.urls import reverse
+from djangocms_versioning.constants import DRAFT, PUBLISHED
+from djangocms_versioning.models import Version
from djangocms_versioning.test_utils.factories import PageVersionFactory
from djangocms_moderation import constants
@@ -103,18 +105,35 @@ def test_delete_selected(self, notify_author_mock, notify_moderators_mock, mock_
self.collection.refresh_from_db()
self.assertEqual(self.collection.status, constants.ARCHIVED)
- @mock.patch('djangocms_moderation.admin_actions.publish_version')
- def test_publish_selected(self, mock_publish_version):
+ def test_publish_selected(self):
fixtures = [self.mr1, self.mr2]
+
+ # Pre-checks
+ version1 = self.mr1.version
+ version2 = self.mr2.version
+ self.assertTrue(self.mr1.is_active)
+ self.assertTrue(self.mr2.is_active)
+ self.assertEqual(version1.state, DRAFT)
+ self.assertEqual(version2.state, DRAFT)
+
data = {
'action': 'publish_selected',
ACTION_CHECKBOX_NAME: [str(f.pk) for f in fixtures]
}
self.client.post(self.url_with_filter, data, follow=True)
- assert mock_publish_version.called
- # check it has been called only once, i.e. with the approved mr1
- mock_publish_version.assert_called_once_with(self.mr1.version)
+ # After-checks
+ # We can't do refresh_from_db() for Version, as it complains about
+ # `state` field being changed directly
+ version1 = Version.objects.get(pk=version1.pk)
+ version2 = Version.objects.get(pk=version2.pk)
+ self.mr1.refresh_from_db()
+ self.mr2.refresh_from_db()
+
+ self.assertEqual(version1.state, PUBLISHED)
+ self.assertEqual(version2.state, DRAFT)
+ self.assertFalse(self.mr1.is_active)
+ self.assertTrue(self.mr2.is_active)
@mock.patch('djangocms_moderation.admin_actions.notify_collection_moderators')
@mock.patch('djangocms_moderation.admin_actions.notify_collection_author')
diff --git a/tests/test_models.py b/tests/test_models.py
index 0b38713b..ad449323 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -94,6 +94,12 @@ def test_is_approved(self):
self.assertTrue(self.moderation_request2.is_approved())
self.assertTrue(self.moderation_request3.is_approved())
+ def test_version_can_be_published(self):
+ self.assertFalse(self.moderation_request1.version_can_be_published())
+ self.assertTrue(self.moderation_request2.version_can_be_published())
+ # moderation_request3.version is already in published state
+ self.assertFalse(self.moderation_request3.version_can_be_published())
+
def test_is_rejected(self):
self.assertFalse(self.moderation_request1.is_rejected())
self.assertFalse(self.moderation_request2.is_rejected())
diff --git a/tests/utils/base.py b/tests/utils/base.py
index 72a01de0..bc75e95e 100644
--- a/tests/utils/base.py
+++ b/tests/utils/base.py
@@ -1,6 +1,7 @@
from django.contrib.auth.models import Group, User
from django.test import TestCase
+from djangocms_versioning.constants import PUBLISHED
from djangocms_versioning.test_utils.factories import PageVersionFactory
from djangocms_moderation import constants
@@ -26,7 +27,7 @@ def setUpTestData(cls):
cls.pg1_version = PageVersionFactory()
cls.pg2_version = PageVersionFactory()
cls.pg3_version = PageVersionFactory()
- cls.pg4_version = PageVersionFactory()
+ cls.pg4_version = PageVersionFactory(state=PUBLISHED)
cls.pg5_version = PageVersionFactory()
cls.pg6_version = PageVersionFactory()
From c211074dbdef51cfbe865f060441ac507a9c9911 Mon Sep 17 00:00:00 2001
From: Damilare Onajole
Date: Thu, 13 Sep 2018 12:04:00 +0100
Subject: [PATCH 026/147] Fix/add item to collection (#57)
---
djangocms_moderation/cms_toolbars.py | 79 ++++++++++---------
.../item_to_collection.html | 14 ++--
djangocms_moderation/views.py | 6 +-
tests/test_cms_toolbars.py | 51 +++++++++---
tests/test_views.py | 4 +-
5 files changed, 94 insertions(+), 60 deletions(-)
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 19556a2e..38817553 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -1,58 +1,63 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _
-from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool
from cms.utils.urlutils import add_url_parameters
+from djangocms_versioning.cms_toolbars import VersioningToolbar
from djangocms_versioning.models import Version
from .models import ModerationRequest
from .utils import get_admin_url
-class ModerationToolbar(CMSToolbar):
+class ModerationToolbar(VersioningToolbar):
class Media:
- js = ('djangocms_moderation/js/dist/bundle.moderation.min.js',)
+ js = (
+ 'djangocms_moderation/js/dist/bundle.moderation.min.js',
+ 'djangocms_versioning/js/actions.js',
+ )
css = {
'all': ('djangocms_moderation/css/moderation.css',)
}
+ def _add_publish_button(self):
+ """
+ Disable djangocms_versioning publish button
+ """
+ pass
+
def post_template_populate(self):
super().post_template_populate()
- # TODO replace page object with generic content object
- page = self.request.current_page
-
- if not page:
- return None
-
- try:
- # TODO Make this work with the correct version
- version = Version.objects.get(pk=9999)
- moderation_request = ModerationRequest.objects.get(
- version=version
- )
- self.toolbar.add_modal_button(
- name=_('In Moderation "%s"' % moderation_request.collection.name),
- url='#',
- disabled=True,
- side=self.toolbar.RIGHT,
- )
- except (ModerationRequest.DoesNotExist, Version.DoesNotExist):
- url = add_url_parameters(
- get_admin_url(
- name='cms_moderation_item_to_collection',
- language=self.current_lang,
- args=()
- ),
- content_object_id=page.pk
- )
-
- self.toolbar.add_modal_button(
- name=_('Submit for moderation'),
- url=url,
- side=self.toolbar.RIGHT,
- )
-
+ if self._is_versioned():
+ version = Version.objects.get_for_content(self.toolbar.obj)
+ try:
+ moderation_request = ModerationRequest.objects.get(
+ version=version
+ )
+ self.toolbar.add_modal_button(
+ name="%s %s" % (_('In Moderation'), moderation_request.collection.name),
+ url='#',
+ disabled=True,
+ side=self.toolbar.RIGHT,
+ )
+ except ModerationRequest.DoesNotExist:
+ url = add_url_parameters(
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language=self.current_lang,
+ args=()
+ ),
+ version_id=version.pk
+ )
+
+ self.toolbar.add_modal_button(
+ name=_('Submit for moderation'),
+ url=url,
+ side=self.toolbar.RIGHT,
+ )
+
+
+toolbar_pool.unregister(VersioningToolbar)
toolbar_pool.register(ModerationToolbar)
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
index 1979b133..65e9ae34 100644
--- a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -21,7 +21,8 @@ {% trans "Add to existing collection" %}
- {% trans "Version" %} |
+ {% trans "Content Type" %} |
+ {% trans "Identifier" %} |
{% trans "Author" %} |
{% trans "Edit Date" %} |
{% trans "Version" %} |
@@ -29,13 +30,14 @@ {% trans "Add to existing collection" %}
- {% for content_object in content_object_list %}
+ {% for mrl in moderation_request_list %}
- {{ version }} |
- Stub Author [versioning] |
- Stub Date [versioning] |
+ {{ mrl.version.content_type|title }} |
+ {{ mrl.version.content }} |
+ {{ mrl.version.created_by }} |
+ {{ mrl.version.created }} |
Stub VNumber [versioning] |
- Stub Published [versioning] |
+ {{ mrl.version.get_state_display }} |
{% endfor %}
diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py
index 40319abc..53186af5 100644
--- a/djangocms_moderation/views.py
+++ b/djangocms_moderation/views.py
@@ -58,13 +58,13 @@ def get_context_data(self, **kwargs):
if collection_id:
collection = ModerationCollection.objects.get(pk=collection_id)
- content_object_list = collection.moderation_requests.all()
+ moderation_request_list = collection.moderation_requests.all()
else:
- content_object_list = []
+ moderation_request_list = []
model_admin = admin.site._registry[ModerationCollection]
context.update({
- 'content_object_list': content_object_list,
+ 'moderation_request_list': moderation_request_list,
'opts': opts_meta,
'title': _('Add to collection'),
'form': self.get_form(),
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
index 9f1b1414..3dd3382e 100644
--- a/tests/test_cms_toolbars.py
+++ b/tests/test_cms_toolbars.py
@@ -1,20 +1,22 @@
-from unittest import skip
-
-from django.contrib.auth.models import AnonymousUser
from django.test.client import RequestFactory
from cms.middleware.toolbar import ToolbarMiddleware
from cms.toolbar.toolbar import CMSToolbar
from cms.utils.conf import get_cms_setting
+from djangocms_versioning.test_utils.factories import (
+ PageVersionFactory,
+ UserFactory,
+)
+
from djangocms_moderation.cms_toolbars import ModerationToolbar
from djangocms_moderation.models import ModerationRequest
from .utils.base import BaseTestCase
-@skip("To be fixed in the upcoming ticket")
class TestCMSToolbars(BaseTestCase):
+
def get_page_request(self, page, user, path=None, edit=False,
preview=False, structure=False, lang_code='en', disable=False):
if not path:
@@ -46,12 +48,34 @@ def get_page_request(self, page, user, path=None, edit=False,
request.toolbar.populate()
return request
+ def _get_toolbar(self, content_obj, **kwargs):
+ """Helper method to set up the toolbar
+ """
+ request = RequestFactory().get('/')
+ request.user = UserFactory()
+ request.session = {}
+ cms_toolbar = CMSToolbar(request)
+ toolbar = ModerationToolbar(
+ request, toolbar=cms_toolbar, is_current_app=True, app_path='/')
+ toolbar.toolbar.obj = content_obj
+ if kwargs.get('edit_mode', False):
+ toolbar.toolbar.edit_mode_active = True
+ toolbar.toolbar.content_mode_active = False
+ toolbar.toolbar.structure_mode_active = False
+ elif kwargs.get('preview_mode', False):
+ toolbar.toolbar.edit_mode_active = False
+ toolbar.toolbar.content_mode_active = True
+ toolbar.toolbar.structure_mode_active = False
+ elif kwargs.get('structure_mode', False):
+ toolbar.toolbar.edit_mode_active = False
+ toolbar.toolbar.content_mode_active = False
+ toolbar.toolbar.structure_mode_active = True
+ return toolbar
+
def test_submit_for_moderation(self):
ModerationRequest.objects.all().delete()
-
- request = self.get_page_request(self.pg1_version, AnonymousUser(), '/')
- toolbar = CMSToolbar(request)
- toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
+ version = PageVersionFactory()
+ toolbar = self._get_toolbar(version.content, edit_mode=True)
toolbar.populate()
toolbar.post_template_populate()
@@ -61,13 +85,16 @@ def test_submit_for_moderation(self):
)
def test_page_in_moderation(self):
- request = self.get_page_request(self.pg1_version, AnonymousUser(), '/')
- toolbar = CMSToolbar(request)
- toolbar = ModerationToolbar(request, toolbar=toolbar, is_current_app=True, app_path='/')
+ ModerationRequest.objects.all().delete()
+ version = PageVersionFactory()
+ self.collection1.add_version(
+ version=version
+ )
+ toolbar = self._get_toolbar(version.content, edit_mode=True)
toolbar.populate()
toolbar.post_template_populate()
self.assertEquals(
toolbar.toolbar.get_right_items()[0].buttons[0].name,
- 'In Moderation "%s"' % self.collection1.name
+ 'In Moderation %s' % self.collection1.name
)
diff --git a/tests/test_views.py b/tests/test_views.py
index 25e3c9a6..59f4e07c 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -44,7 +44,7 @@ def _assert_render(self, response):
self.assertEqual(response.context_data['title'], _('Add to collection'))
- def test_add_object_to_collections(self):
+ def test_version_object_to_collections(self):
ModerationRequest.objects.all().delete()
self.client.force_login(self.user)
response = self.client.post(
@@ -141,7 +141,7 @@ def test_list_versions_from_collection_id_param(self):
moderation_requests = ModerationRequest.objects.filter(collection=self.collection_2)
# moderation request is content_object
for mod_request in moderation_requests:
- self.assertTrue(mod_request in response.context_data['content_object_list'])
+ self.assertTrue(mod_request in response.context_data['moderation_request_list'])
def test_version_id_from_params(self):
self.client.force_login(self.user)
From 2af3b9319482a9e948200dc6b98291d92e084e6e Mon Sep 17 00:00:00 2001
From: Rizwan
Date: Fri, 14 Sep 2018 12:09:32 +0100
Subject: [PATCH 027/147] Added filter for moderation collection admin UI (#58)
---
djangocms_moderation/admin.py | 11 ++++++-----
djangocms_moderation/models.py | 2 +-
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index c25015be..91014981 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -392,12 +392,17 @@ class ModerationCollectionAdmin(EditAndAddOnlyFieldsMixin, admin.ModelAdmin):
actions = None # remove `delete_selected` for now, it will be handled later
editonly_fields = ('status',) # fields editable only on EDIT
addonly_fields = ('workflow',) # fields editable only on CREATE
+ list_filter = [
+ 'author',
+ 'status',
+ 'date_created',
+ ]
def get_list_display(self, request):
list_display = [
'id',
'name',
- 'get_moderator',
+ 'author',
'workflow',
'status',
'date_created',
@@ -429,10 +434,6 @@ def get_comments_link(self, obj):
)
get_comments_link.short_description = _('Comments')
- def get_moderator(self, obj):
- return obj.author
- get_moderator.short_description = _('Moderator')
-
def get_urls(self):
def _url(regex, fn, name, **kwargs):
return url(regex, self.admin_site.admin_view(fn), kwargs=kwargs, name=name)
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 66f89bbc..4055b12a 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -252,7 +252,7 @@ class ModerationCollection(models.Model):
name = models.CharField(verbose_name=_('name'), max_length=128)
author = models.ForeignKey(
to=settings.AUTH_USER_MODEL,
- verbose_name=_('author'),
+ verbose_name=_('moderator'),
related_name='+',
on_delete=models.CASCADE,
)
From fa32f92ceacd37d6a18f0f54dbd98e6f7c512edd Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Fri, 14 Sep 2018 12:26:33 +0100
Subject: [PATCH 028/147] Add version # to the list (#59)
---
.../templates/djangocms_moderation/item_to_collection.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
index 65e9ae34..c50efeda 100644
--- a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -25,7 +25,7 @@ {% trans "Add to existing collection" %}
{% trans "Identifier" %} |
{% trans "Author" %} |
{% trans "Edit Date" %} |
- {% trans "Version" %} |
+ {% trans "Version #" %} |
{% trans "Status" %} |
@@ -36,7 +36,7 @@ {% trans "Add to existing collection" %}
{{ mrl.version.content }} |
{{ mrl.version.created_by }} |
{{ mrl.version.created }} |
- Stub VNumber [versioning] |
+ {{ mrl.version.number }} |
{{ mrl.version.get_state_display }} |
{% endfor %}
From a527a4b6e595d791e32f0b7dca70d01a78934a09 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Fri, 14 Sep 2018 14:50:30 +0100
Subject: [PATCH 029/147] Review lock - FE (#60)
---
djangocms_moderation/cms_toolbars.py | 38 +++++++++++---
djangocms_moderation/models.py | 6 ++-
djangocms_moderation/utils.py | 27 ++++++++++
tests/test_cms_toolbars.py | 78 ++++++++++++++--------------
tests/test_handlers.py | 2 +-
tests/test_helpers.py | 2 +-
tests/test_utils.py | 34 ++++++++++++
7 files changed, 138 insertions(+), 49 deletions(-)
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 38817553..6512b865 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -8,14 +8,16 @@
from djangocms_versioning.models import Version
from .models import ModerationRequest
-from .utils import get_admin_url
+from .utils import get_admin_url, is_obj_review_locked
class ModerationToolbar(VersioningToolbar):
class Media:
+ # Media keeps all the settings from the parent class, so we only need
+ # to add moderation media here
+ # https://docs.djangoproject.com/en/2.1/topics/forms/media/#extend
js = (
'djangocms_moderation/js/dist/bundle.moderation.min.js',
- 'djangocms_versioning/js/actions.js',
)
css = {
'all': ('djangocms_moderation/css/moderation.css',)
@@ -23,21 +25,39 @@ class Media:
def _add_publish_button(self):
"""
- Disable djangocms_versioning publish button
+ Disable djangocms_versioning publish button as it needs to go through
+ the moderation first
"""
pass
- def post_template_populate(self):
- super().post_template_populate()
+ def _add_edit_button(self):
+ """
+ We need to check if the object is not 'Review locked', and only allow
+ Edit button if it isn't
+ """
+ if is_obj_review_locked(self.toolbar.obj, self.request.user):
+ # Don't display edit button as the item is Review locked
+ # TODO alternatively we could add the edit button using super
+ # and mark it as disabled, instead of adding another -disabled one
+ self.toolbar.add_modal_button(
+ _('Edit'),
+ url='#',
+ disabled=True,
+ side=self.toolbar.RIGHT,
+ )
+ return super()._add_edit_button()
- if self._is_versioned():
+ def _add_moderation_buttons(self):
+ if self._is_versioned() and self.toolbar.edit_mode_active:
version = Version.objects.get_for_content(self.toolbar.obj)
try:
moderation_request = ModerationRequest.objects.get(
version=version
)
self.toolbar.add_modal_button(
- name="%s %s" % (_('In Moderation'), moderation_request.collection.name),
+ name=_('In Moderation "%(collection_name)s"') % {
+ 'collection_name': moderation_request.collection.name
+ },
url='#',
disabled=True,
side=self.toolbar.RIGHT,
@@ -58,6 +78,10 @@ def post_template_populate(self):
side=self.toolbar.RIGHT,
)
+ def post_template_populate(self):
+ super().post_template_populate()
+ self._add_moderation_buttons()
+
toolbar_pool.unregister(VersioningToolbar)
toolbar_pool.register(ModerationToolbar)
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index 4055b12a..c0ed4875 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -391,7 +391,8 @@ def author(self):
"""
Author of this request is the user who created the first action
"""
- return self.get_first_action().by_user
+ first_action = self.get_first_action()
+ return first_action.by_user if first_action else None
@cached_property
def workflow(self):
@@ -410,7 +411,8 @@ def version_can_be_published(self):
return self.is_approved() and self.version.can_be_published()
def is_rejected(self):
- return self.get_last_action().action == constants.ACTION_REJECTED
+ last_action = self.get_last_action()
+ return last_action and last_action.action == constants.ACTION_REJECTED
@transaction.atomic
def update_status(self, action, by_user, message='', to_user=None):
diff --git a/djangocms_moderation/utils.py b/djangocms_moderation/utils.py
index b9bd11d4..26d0d3e6 100644
--- a/djangocms_moderation/utils.py
+++ b/djangocms_moderation/utils.py
@@ -9,6 +9,8 @@
from cms.utils.urlutils import admin_reverse
+from djangocms_versioning.models import Version
+
def get_absolute_url(location, site=None):
if not site:
@@ -44,3 +46,28 @@ def extract_filter_param_from_changelist_url(request, keyname, parametername):
changelist_filters = request.GET.get(keyname)
parameter_value = parse_qs(changelist_filters).get(parametername)
return parameter_value[0]
+
+
+def is_obj_review_locked(obj, user):
+ """
+ Util function which determines if the `obj` is Review locked.
+ It is the equivalent of "Can `user` edit the version of object `obj`"?
+ """
+ version = Version.objects.get_for_content(obj)
+
+ try:
+ from djangocms_moderation.models import ModerationRequest # noqa
+ moderation_request = ModerationRequest.objects.get(
+ version=version
+ )
+ except ModerationRequest.DoesNotExist:
+ # If there is no moderation request with this version yet, then
+ # the `obj` is not Review locked
+ return False
+
+ # If `user` can resubmit the moderation request, it means they can edit
+ # the version to submit the changes. Review lock should be lifted for them
+ if moderation_request.user_can_resubmit(user):
+ return False
+
+ return True
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
index 3dd3382e..a236e22e 100644
--- a/tests/test_cms_toolbars.py
+++ b/tests/test_cms_toolbars.py
@@ -2,7 +2,6 @@
from cms.middleware.toolbar import ToolbarMiddleware
from cms.toolbar.toolbar import CMSToolbar
-from cms.utils.conf import get_cms_setting
from djangocms_versioning.test_utils.factories import (
PageVersionFactory,
@@ -16,31 +15,10 @@
class TestCMSToolbars(BaseTestCase):
-
- def get_page_request(self, page, user, path=None, edit=False,
- preview=False, structure=False, lang_code='en', disable=False):
- if not path:
- path = page.get_absolute_url()
-
- if edit:
- path += '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
-
- if structure:
- path += '?%s' % get_cms_setting('CMS_TOOLBAR_URL__BUILD')
-
- if preview:
- path += '?preview'
-
- request = RequestFactory().get(path)
+ def _get_page_request(self, page, user):
+ request = RequestFactory().get('/')
request.session = {}
request.user = user
- request.LANGUAGE_CODE = lang_code
- if edit:
- request.GET = {'edit': None}
- else:
- request.GET = {'edit_off': None}
- if disable:
- request.GET[get_cms_setting('CMS_TOOLBAR_URL__DISABLE')] = None
request.current_page = page
mid = ToolbarMiddleware()
mid.process_request(request)
@@ -48,28 +26,21 @@ def get_page_request(self, page, user, path=None, edit=False,
request.toolbar.populate()
return request
- def _get_toolbar(self, content_obj, **kwargs):
+ def _get_toolbar(self, content_obj, edit_mode=False):
"""Helper method to set up the toolbar
"""
- request = RequestFactory().get('/')
- request.user = UserFactory()
- request.session = {}
+ page = PageVersionFactory().content.page
+ request = self._get_page_request(
+ page=page, user=UserFactory(is_staff=True)
+ )
cms_toolbar = CMSToolbar(request)
toolbar = ModerationToolbar(
request, toolbar=cms_toolbar, is_current_app=True, app_path='/')
toolbar.toolbar.obj = content_obj
- if kwargs.get('edit_mode', False):
+ if edit_mode:
toolbar.toolbar.edit_mode_active = True
toolbar.toolbar.content_mode_active = False
toolbar.toolbar.structure_mode_active = False
- elif kwargs.get('preview_mode', False):
- toolbar.toolbar.edit_mode_active = False
- toolbar.toolbar.content_mode_active = True
- toolbar.toolbar.structure_mode_active = False
- elif kwargs.get('structure_mode', False):
- toolbar.toolbar.edit_mode_active = False
- toolbar.toolbar.content_mode_active = False
- toolbar.toolbar.structure_mode_active = True
return toolbar
def test_submit_for_moderation(self):
@@ -93,8 +64,39 @@ def test_page_in_moderation(self):
toolbar = self._get_toolbar(version.content, edit_mode=True)
toolbar.populate()
toolbar.post_template_populate()
+ self.assertEquals(
+ toolbar.toolbar.get_right_items()[1].buttons[0].name,
+ 'In Moderation "%s"' % self.collection1.name
+ )
+
+ def test_add_edit_button(self):
+ ModerationRequest.objects.all().delete()
+ version = PageVersionFactory()
+ toolbar = self._get_toolbar(version.content)
+ toolbar.populate()
+ toolbar.post_template_populate()
+ # We can see the Edit button, as the version hasn't been submitted
+ # to the moderation (collection) yet
+ self.assertEquals(
+ toolbar.toolbar.get_right_items()[0].buttons[0].name,
+ 'Edit',
+ )
+ self.assertFalse(
+ toolbar.toolbar.get_right_items()[0].buttons[0].disabled
+ )
+ # Lets add the version to moderation, the Edit should no longer be
+ # clickable
+ self.collection1.add_version(
+ version=version
+ )
+ toolbar = self._get_toolbar(version.content)
+ toolbar.populate()
+ toolbar.post_template_populate()
self.assertEquals(
toolbar.toolbar.get_right_items()[0].buttons[0].name,
- 'In Moderation %s' % self.collection1.name
+ 'Edit',
+ )
+ self.assertTrue(
+ toolbar.toolbar.get_right_items()[0].buttons[0].disabled
)
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index ae4e1d47..2f6d0e68 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -14,7 +14,7 @@
from .utils.base import BaseTestCase
-@skip("Confirmation page feature doesn't to support 1.0.x yet.")
+@skip("Confirmation page feature doesn't support 1.0.x yet")
class ModerationConfirmationFormSubmissionTest(BaseTestCase):
def setUp(self):
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 09525805..54db4d0d 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -13,7 +13,7 @@
from .utils.base import BaseTestCase
-@skip("Confirmation page feature doesn't to support 1.0.x yet.")
+@skip("Confirmation page feature doesn't support 1.0.x yet")
class GetPageOr404Test(BaseTestCase):
def test_returns_page(self):
self.assertEqual(get_page_or_404(self.pg1_version.pk, 'en'), self.pg1_version)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index c1e0919f..b92312be 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,11 @@
from django.test.client import RequestFactory
+from djangocms_versioning.test_utils.factories import PageVersionFactory
+
from djangocms_moderation import utils
+from djangocms_moderation.constants import ACTION_REJECTED, ACTION_STARTED
+from djangocms_moderation.models import ModerationCollection, ModerationRequest
+from djangocms_moderation.utils import is_obj_review_locked
from .utils.base import BaseTestCase
@@ -41,3 +46,32 @@ def test_extract_filter_param_from_changelist_url(self):
mock_request, '_changelist_filters', 'moderation_request__id__exact'
)
self.assertEquals(action_id, '2')
+
+
+class TestReviewLock(BaseTestCase):
+ def test_is_obj_review_locked(self):
+ page_version = PageVersionFactory()
+
+ page_content = page_version.content
+ self.assertFalse(is_obj_review_locked(page_content, self.user))
+ self.assertFalse(is_obj_review_locked(page_content, self.user2))
+ self.assertFalse(is_obj_review_locked(page_content, self.user3))
+
+ collection = ModerationCollection.objects.create(
+ author=self.user, name='My collection 1', workflow=self.wf1
+ )
+ collection.add_version(page_version)
+ # Now the version is part of the collection so it is review locked
+ self.assertTrue(is_obj_review_locked(page_content, self.user))
+ self.assertTrue(is_obj_review_locked(page_content, self.user2))
+ self.assertTrue(is_obj_review_locked(page_content, self.user3))
+
+ mr = ModerationRequest.objects.get(collection=collection)
+ mr.actions.create(by_user=self.user, action=ACTION_STARTED,)
+
+ # Now we reject the moderation request, which means that `user` can
+ # resubmit the changes, the review lock is lifted for them
+ mr.actions.create(by_user=self.user2, action=ACTION_REJECTED)
+ self.assertFalse(is_obj_review_locked(page_content, self.user))
+ self.assertTrue(is_obj_review_locked(page_content, self.user2))
+ self.assertTrue(is_obj_review_locked(page_content, self.user3))
From c26a5f87fe2a7d21a4a4e177db45f4c1512808c5 Mon Sep 17 00:00:00 2001
From: Mateusz Kamycki
Date: Mon, 17 Sep 2018 13:43:20 +0200
Subject: [PATCH 030/147] Fix `add_edit_button` in toolbar (#62)
---
djangocms_moderation/cms_toolbars.py | 2 +-
tests/test_cms_toolbars.py | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 6512b865..d3178a6b 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -35,7 +35,7 @@ def _add_edit_button(self):
We need to check if the object is not 'Review locked', and only allow
Edit button if it isn't
"""
- if is_obj_review_locked(self.toolbar.obj, self.request.user):
+ if self.toolbar.obj and is_obj_review_locked(self.toolbar.obj, self.request.user):
# Don't display edit button as the item is Review locked
# TODO alternatively we could add the edit button using super
# and mark it as disabled, instead of adding another -disabled one
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
index a236e22e..c6c892b6 100644
--- a/tests/test_cms_toolbars.py
+++ b/tests/test_cms_toolbars.py
@@ -100,3 +100,12 @@ def test_add_edit_button(self):
self.assertTrue(
toolbar.toolbar.get_right_items()[0].buttons[0].disabled
)
+
+ def test_add_edit_button_without_toolbar_object(self):
+ ModerationRequest.objects.all().delete()
+ toolbar = self._get_toolbar(None)
+ toolbar.populate()
+ toolbar.post_template_populate()
+ # We shouldnt see Edit button when there is no toolbar object set.
+ # Some of the custom views in some apps dont have toolbar.obj
+ self.assertEquals(toolbar.toolbar.get_right_items(), [])
From 71f9c6975ece310cf14dcecd0f49e14cfa8e7dbd Mon Sep 17 00:00:00 2001
From: Rizwan
Date: Mon, 17 Sep 2018 15:08:40 +0100
Subject: [PATCH 031/147] Added logic so user should not be able to change
other user's comments (#61)
---
djangocms_moderation/admin.py | 38 ++++++++++++++++++-
djangocms_moderation/templatetags/__init__.py | 0
.../templatetags/admin_modify.py | 23 +++++++++++
3 files changed, 59 insertions(+), 2 deletions(-)
create mode 100644 djangocms_moderation/templatetags/__init__.py
create mode 100644 djangocms_moderation/templatetags/admin_modify.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 91014981..ff70c951 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -4,6 +4,7 @@
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.http import Http404
+from django.shortcuts import get_object_or_404
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext, ugettext_lazy as _
@@ -48,7 +49,6 @@
class ModerationRequestActionInline(admin.TabularInline):
model = ModerationRequestAction
fields = ['show_user', 'message', 'date_taken', 'form_submission']
- readonly_fields = ['show_user', 'date_taken', 'form_submission']
verbose_name = _('Action')
verbose_name_plural = _('Actions')
@@ -81,6 +81,11 @@ def form_submission(self, obj):
)
form_submission.short_description = _('Form Submission')
+ def get_readonly_fields(self, request, obj=None):
+ if obj and request.user == obj.author:
+ return ['show_user', 'date_taken', 'form_submission']
+ return self.fields
+
class ModerationRequestAdmin(admin.ModelAdmin):
actions = [ # filtered out in `self.get_actions`
@@ -304,6 +309,21 @@ def changelist_view(self, request, extra_context=None):
return super().changelist_view(request, extra_context)
+ def change_view(self, request, object_id, form_url='', extra_context=None):
+ extra_context = extra_context or {}
+ collection_comment = get_object_or_404(CollectionComment, pk=int(object_id))
+ if request.user != collection_comment.author:
+ extra_context['readonly'] = True
+ return super().change_view(request, object_id,
+ form_url, extra_context=extra_context)
+
+ def has_delete_permission(self, request, obj=None):
+ return request.user == getattr(obj, 'author', None)
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj and request.user != obj.author:
+ return self.list_display
+
class RequestCommentAdmin(admin.ModelAdmin):
list_display = ['message', 'get_request_link', 'author', 'date_created']
@@ -362,9 +382,23 @@ def changelist_view(self, request, extra_context=None):
# If no collection id, then don't show all requests
# as each collection's actions, buttons and privileges may differ
raise Http404
-
return super().changelist_view(request, extra_context)
+ def change_view(self, request, object_id, form_url='', extra_context=None):
+ extra_context = extra_context or {}
+ request_comment = get_object_or_404(RequestComment, pk=int(object_id))
+ if request.user != request_comment.author:
+ extra_context['readonly'] = True
+ return super().change_view(request, object_id,
+ form_url, extra_context=extra_context)
+
+ def has_delete_permission(self, request, obj=None):
+ return request.user == getattr(obj, 'author', None)
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj and request.user != obj.author:
+ return self.list_display
+
class WorkflowStepInline(SortableInlineAdminMixin, admin.TabularInline):
formset = WorkflowStepInlineFormSet
diff --git a/djangocms_moderation/templatetags/__init__.py b/djangocms_moderation/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangocms_moderation/templatetags/admin_modify.py b/djangocms_moderation/templatetags/admin_modify.py
new file mode 100644
index 00000000..9971977d
--- /dev/null
+++ b/djangocms_moderation/templatetags/admin_modify.py
@@ -0,0 +1,23 @@
+from django import template
+from django.contrib.admin.templatetags.admin_modify import (
+ prepopulated_fields_js as original_prepopulated_fields_js,
+ submit_row as original_submit_row,
+)
+
+
+register = template.Library()
+
+
+@register.inclusion_tag('admin/prepopulated_fields_js.html', takes_context=True)
+def prepopulated_fields_js(context):
+ return original_prepopulated_fields_js(context)
+
+
+@register.inclusion_tag('admin/submit_line.html', takes_context=True)
+def submit_row(context):
+ """
+ Hide the row of buttons for delete and save if readonly otherwise display.
+ """
+ if context.get('readonly'):
+ return dict()
+ return original_submit_row(context)
From f982a9d03d8b3bed4f8508f71ea242edd2a50904 Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 18 Sep 2018 17:16:42 +0100
Subject: [PATCH 032/147] Don't add normal edit button if in moderation (#66)
---
djangocms_moderation/cms_toolbars.py | 3 ++-
tests/test_cms_toolbars.py | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index d3178a6b..9664ce50 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -45,7 +45,8 @@ def _add_edit_button(self):
disabled=True,
side=self.toolbar.RIGHT,
)
- return super()._add_edit_button()
+ else:
+ return super()._add_edit_button()
def _add_moderation_buttons(self):
if self._is_versioned() and self.toolbar.edit_mode_active:
diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py
index c6c892b6..b77711cf 100644
--- a/tests/test_cms_toolbars.py
+++ b/tests/test_cms_toolbars.py
@@ -93,6 +93,7 @@ def test_add_edit_button(self):
toolbar = self._get_toolbar(version.content)
toolbar.populate()
toolbar.post_template_populate()
+ self.assertEqual(1, len(toolbar.toolbar.get_right_items()))
self.assertEquals(
toolbar.toolbar.get_right_items()[0].buttons[0].name,
'Edit',
From ca5479dfd4f5ea909ed85c5d79c650aacbfcee2c Mon Sep 17 00:00:00 2001
From: Noel da Costa
Date: Wed, 19 Sep 2018 12:22:14 +0100
Subject: [PATCH 033/147] Feature/fix comments breadcrumb trails (#64)
---
.gitignore | 2 +
djangocms_moderation/admin.py | 56 ++++++++++++++-----
.../collectioncomment/change_form.html | 17 ++++++
.../requestcomment/change_form.html | 18 ++++++
.../templatetags/admin_modify.py | 23 --------
.../templatetags/comments_extras.py | 25 +++++++++
6 files changed, 103 insertions(+), 38 deletions(-)
create mode 100644 djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_form.html
create mode 100644 djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_form.html
delete mode 100644 djangocms_moderation/templatetags/admin_modify.py
create mode 100644 djangocms_moderation/templatetags/comments_extras.py
diff --git a/.gitignore b/.gitignore
index 1274a3dc..c3b1fe07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ build/
*.sqlite
.coverage
.python-version
+node_modules/
+yarn.lock
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index ff70c951..5f0308a9 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -306,16 +306,28 @@ def changelist_view(self, request, extra_context=None):
)
else:
raise Http404
-
return super().changelist_view(request, extra_context)
- def change_view(self, request, object_id, form_url='', extra_context=None):
- extra_context = extra_context or {}
- collection_comment = get_object_or_404(CollectionComment, pk=int(object_id))
- if request.user != collection_comment.author:
- extra_context['readonly'] = True
- return super().change_view(request, object_id,
- form_url, extra_context=extra_context)
+ def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
+ # get the collection for the breadcrumb trail
+ collection_id = utils.extract_filter_param_from_changelist_url(
+ request, '_changelist_filters', 'collection__id__exact'
+ )
+ extra_context = extra_context or dict(
+ show_save_and_add_another=False,
+ show_save_and_continue=False,
+ )
+ if object_id:
+ try:
+ collection_comment = get_object_or_404(CollectionComment, pk=int(object_id))
+ except ValueError:
+ raise Http404
+ if request.user != collection_comment.author:
+ extra_context['readonly'] = True
+
+ if collection_id:
+ extra_context['collection_id'] = collection_id
+ return super().changeform_view(request, object_id, form_url, extra_context)
def has_delete_permission(self, request, obj=None):
return request.user == getattr(obj, 'author', None)
@@ -384,13 +396,27 @@ def changelist_view(self, request, extra_context=None):
raise Http404
return super().changelist_view(request, extra_context)
- def change_view(self, request, object_id, form_url='', extra_context=None):
- extra_context = extra_context or {}
- request_comment = get_object_or_404(RequestComment, pk=int(object_id))
- if request.user != request_comment.author:
- extra_context['readonly'] = True
- return super().change_view(request, object_id,
- form_url, extra_context=extra_context)
+ def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
+ extra_context = extra_context or dict(
+ show_save_and_add_another=False,
+ show_save_and_continue=False,
+ )
+ if object_id:
+ try:
+ request_comment = get_object_or_404(RequestComment, pk=int(object_id))
+ except ValueError:
+ raise Http404
+ if request.user != request_comment.author:
+ extra_context['readonly'] = True
+
+ # for breadcrumb trail
+ moderation_request_id = utils.extract_filter_param_from_changelist_url(
+ request, '_changelist_filters', 'moderation_request__id__exact'
+ )
+ if moderation_request_id:
+ extra_context['moderation_request_id'] = moderation_request_id
+
+ return super().changeform_view(request, object_id, form_url, extra_context)
def has_delete_permission(self, request, obj=None):
return request.user == getattr(obj, 'author', None)
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_form.html b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_form.html
new file mode 100644
index 00000000..6882c08e
--- /dev/null
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_form.html
@@ -0,0 +1,17 @@
+{% extends "admin/change_form.html" %}
+{% load i18n admin_urls static admin_modify comments_extras l10n %}
+
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+
+{% block submit_buttons_bottom %}{% comments_submit_row %}{% endblock %}
+
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_form.html b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_form.html
new file mode 100644
index 00000000..c6f48582
--- /dev/null
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_form.html
@@ -0,0 +1,18 @@
+{% extends "admin/change_form.html" %}
+{% load i18n admin_urls static admin_modify comments_extras l10n %}
+
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+
+{% block submit_buttons_bottom %}{% comments_submit_row %}{% endblock %}
+
diff --git a/djangocms_moderation/templatetags/admin_modify.py b/djangocms_moderation/templatetags/admin_modify.py
deleted file mode 100644
index 9971977d..00000000
--- a/djangocms_moderation/templatetags/admin_modify.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from django import template
-from django.contrib.admin.templatetags.admin_modify import (
- prepopulated_fields_js as original_prepopulated_fields_js,
- submit_row as original_submit_row,
-)
-
-
-register = template.Library()
-
-
-@register.inclusion_tag('admin/prepopulated_fields_js.html', takes_context=True)
-def prepopulated_fields_js(context):
- return original_prepopulated_fields_js(context)
-
-
-@register.inclusion_tag('admin/submit_line.html', takes_context=True)
-def submit_row(context):
- """
- Hide the row of buttons for delete and save if readonly otherwise display.
- """
- if context.get('readonly'):
- return dict()
- return original_submit_row(context)
diff --git a/djangocms_moderation/templatetags/comments_extras.py b/djangocms_moderation/templatetags/comments_extras.py
new file mode 100644
index 00000000..c4cc47f7
--- /dev/null
+++ b/djangocms_moderation/templatetags/comments_extras.py
@@ -0,0 +1,25 @@
+from django import template
+from django.contrib.admin.templatetags.admin_modify import (
+ submit_row as original_submit_row,
+)
+
+
+register = template.Library()
+
+
+@register.inclusion_tag('admin/submit_line.html', takes_context=True)
+def comments_submit_row(context):
+ """
+ Displays the row of buttons for delete and save.
+ """
+ if context.get('readonly'):
+ return dict()
+ ctx = original_submit_row(context)
+ ctx.update({
+ 'show_save_and_add_another': (
+ context.get('show_save_and_add_another', True) and
+ context['has_add_permission'] and not context['is_popup'] and
+ (not context['save_as'] or context['add'])
+ ),
+ })
+ return ctx
From e13838772df0e580c83702ac3d9c45a5fff56385 Mon Sep 17 00:00:00 2001
From: Vadim Sikora
Date: Mon, 24 Sep 2018 13:03:58 +0200
Subject: [PATCH 034/147] Bugfix for sideframe (#68)
---
.eslintrc.js | 1 +
djangocms_moderation/admin.py | 7 +-
.../static/djangocms_moderation/js/actions.js | 14 +
package-lock.json | 8459 +++++++++++++++++
tests/test_moderation_flows.py | 9 +-
5 files changed, 8485 insertions(+), 5 deletions(-)
create mode 100644 djangocms_moderation/static/djangocms_moderation/js/actions.js
create mode 100644 package-lock.json
diff --git a/.eslintrc.js b/.eslintrc.js
index 19dbd3dc..e2014bfb 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -8,6 +8,7 @@ module.exports = {
},
"globals": {
"CMS": true,
+ "django": true,
"Promise": true
},
"root": true,
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 5f0308a9..3746666d 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -88,6 +88,9 @@ def get_readonly_fields(self, request, obj=None):
class ModerationRequestAdmin(admin.ModelAdmin):
+ class Media:
+ js = ('djangocms_moderation/js/actions.js',)
+
actions = [ # filtered out in `self.get_actions`
delete_selected,
publish_selected,
@@ -130,7 +133,9 @@ def get_title(self, obj):
def get_preview_link(self, obj):
return format_html(
- '',
+ ''
+ ''
+ '',
get_object_preview_url(obj.version.content),
)
get_preview_link.short_description = _('Preview')
diff --git a/djangocms_moderation/static/djangocms_moderation/js/actions.js b/djangocms_moderation/static/djangocms_moderation/js/actions.js
new file mode 100644
index 00000000..8441cea8
--- /dev/null
+++ b/djangocms_moderation/static/djangocms_moderation/js/actions.js
@@ -0,0 +1,14 @@
+(function($) {
+ if (!$) {
+ return;
+ }
+
+ $(function() {
+ $('.js-moderation-close-sideframe').on('click', function () {
+ try {
+ window.top.CMS.API.Sideframe.close();
+ } catch (e) {}
+ });
+ });
+
+})((typeof django !== 'undefined' && django.jQuery) || (typeof CMS !== 'undefined' && CMS.$) || false);
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..a811c70a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,8459 @@
+{
+ "name": "djangoCMS",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/helper-module-imports": {
+ "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"
+ }
+ },
+ "@babel/types": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0.tgz",
+ "integrity": "sha512-5tPDap4bGKTLPtci2SUl/B7Gv8RnuJFuQoWx26RJobS0fFrz4reUA3JnwIM+HVHEmWE0C1mzKhDtTp8NsWY02Q==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2",
+ "lodash": "^4.17.10",
+ "to-fast-properties": "^2.0.0"
+ },
+ "dependencies": {
+ "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
+ }
+ }
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+ "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+ "dev": true
+ },
+ "acorn-dynamic-import": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz",
+ "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=",
+ "dev": true,
+ "requires": {
+ "acorn": "^4.0.3"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+ "dev": true
+ }
+ }
+ },
+ "acorn-jsx": {
+ "version": "3.0.1",
+ "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+ "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+ "dev": true,
+ "requires": {
+ "acorn": "^3.0.4"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "3.3.0",
+ "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+ "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+ "dev": true
+ }
+ }
+ },
+ "ajv": {
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "dev": true,
+ "requires": {
+ "co": "^4.6.0",
+ "fast-deep-equal": "^1.0.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.3.0"
+ }
+ },
+ "ajv-keywords": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
+ "dev": true
+ },
+ "align-text": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2",
+ "longest": "^1.0.1",
+ "repeat-string": "^1.5.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "dev": true
+ },
+ "ansi-colors": {
+ "version": "1.1.0",
+ "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
+ "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "^0.1.0"
+ }
+ },
+ "ansi-cyan": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz",
+ "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "0.1.0"
+ }
+ },
+ "ansi-escapes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
+ "dev": true
+ },
+ "ansi-gray": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
+ "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "0.1.0"
+ }
+ },
+ "ansi-red": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz",
+ "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "0.1.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "ansi-wrap": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
+ "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
+ "dev": true
+ },
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ }
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "dev": true
+ },
+ "archy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
+ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
+ "dev": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "dev": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true
+ },
+ "arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "dev": true
+ },
+ "arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "dev": true
+ },
+ "array-differ": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+ "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
+ "dev": true
+ },
+ "array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
+ "dev": true
+ },
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+ "dev": true
+ },
+ "array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true
+ },
+ "array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "dev": true,
+ "requires": {
+ "array-uniq": "^1.0.1"
+ }
+ },
+ "array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+ "dev": true
+ },
+ "array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "assert": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+ "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+ "dev": true,
+ "requires": {
+ "util": "0.10.3"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+ "dev": true
+ },
+ "util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+ "dev": true,
+ "requires": {
+ "inherits": "2.0.1"
+ }
+ }
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true
+ },
+ "assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true
+ },
+ "async": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+ "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.10"
+ }
+ },
+ "async-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+ "dev": true
+ },
+ "async-foreach": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+ "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+ "dev": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true
+ },
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "dev": true
+ },
+ "autoprefixer": {
+ "version": "6.7.7",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
+ "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=",
+ "dev": true,
+ "requires": {
+ "browserslist": "^1.7.6",
+ "caniuse-db": "^1.0.30000634",
+ "normalize-range": "^0.1.2",
+ "num2fraction": "^1.2.2",
+ "postcss": "^5.2.16",
+ "postcss-value-parser": "^3.2.3"
+ }
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "dev": true
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+ "dev": true
+ },
+ "babel-code-frame": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "esutils": "^2.0.2",
+ "js-tokens": "^3.0.2"
+ }
+ },
+ "babel-core": {
+ "version": "6.26.3",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "^6.26.0",
+ "babel-generator": "^6.26.0",
+ "babel-helpers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "convert-source-map": "^1.5.1",
+ "debug": "^2.6.9",
+ "json5": "^0.5.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.4",
+ "path-is-absolute": "^1.0.1",
+ "private": "^0.1.8",
+ "slash": "^1.0.0",
+ "source-map": "^0.5.7"
+ }
+ },
+ "babel-eslint": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz",
+ "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "^6.22.0",
+ "babel-traverse": "^6.23.1",
+ "babel-types": "^6.23.0",
+ "babylon": "^6.17.0"
+ }
+ },
+ "babel-generator": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+ "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+ "dev": true,
+ "requires": {
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "detect-indent": "^4.0.0",
+ "jsesc": "^1.3.0",
+ "lodash": "^4.17.4",
+ "source-map": "^0.5.7",
+ "trim-right": "^1.0.1"
+ }
+ },
+ "babel-helper-builder-binary-assignment-operator-visitor": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz",
+ "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=",
+ "dev": true,
+ "requires": {
+ "babel-helper-explode-assignable-expression": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-call-delegate": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
+ "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
+ "dev": true,
+ "requires": {
+ "babel-helper-hoist-variables": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-define-map": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz",
+ "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=",
+ "dev": true,
+ "requires": {
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
+ }
+ },
+ "babel-helper-explode-assignable-expression": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz",
+ "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-function-name": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz",
+ "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
+ "dev": true,
+ "requires": {
+ "babel-helper-get-function-arity": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-get-function-arity": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz",
+ "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-hoist-variables": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
+ "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-optimise-call-expression": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz",
+ "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-regex": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz",
+ "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
+ }
+ },
+ "babel-helper-remap-async-to-generator": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz",
+ "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=",
+ "dev": true,
+ "requires": {
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-replace-supers": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz",
+ "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=",
+ "dev": true,
+ "requires": {
+ "babel-helper-optimise-call-expression": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helpers": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+ "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-loader": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.5.tgz",
+ "integrity": "sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw==",
+ "dev": true,
+ "requires": {
+ "find-cache-dir": "^1.0.0",
+ "loader-utils": "^1.0.2",
+ "mkdirp": "^0.5.1"
+ }
+ },
+ "babel-messages": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-check-es2015-constants": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
+ "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-lodash": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz",
+ "integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0-beta.49",
+ "@babel/types": "^7.0.0-beta.49",
+ "glob": "^7.1.1",
+ "lodash": "^4.17.10",
+ "require-package-name": "^2.0.1"
+ }
+ },
+ "babel-plugin-rewire": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz",
+ "integrity": "sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==",
+ "dev": true
+ },
+ "babel-plugin-syntax-async-functions": {
+ "version": "6.13.0",
+ "resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
+ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=",
+ "dev": true
+ },
+ "babel-plugin-syntax-dynamic-import": {
+ "version": "6.18.0",
+ "resolved": "http://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz",
+ "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo="
+ },
+ "babel-plugin-syntax-exponentiation-operator": {
+ "version": "6.13.0",
+ "resolved": "http://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz",
+ "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=",
+ "dev": true
+ },
+ "babel-plugin-syntax-trailing-function-commas": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz",
+ "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=",
+ "dev": true
+ },
+ "babel-plugin-transform-async-to-generator": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz",
+ "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=",
+ "dev": true,
+ "requires": {
+ "babel-helper-remap-async-to-generator": "^6.24.1",
+ "babel-plugin-syntax-async-functions": "^6.8.0",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-arrow-functions": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
+ "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-block-scoped-functions": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
+ "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-block-scoping": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
+ "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
+ }
+ },
+ "babel-plugin-transform-es2015-classes": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz",
+ "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=",
+ "dev": true,
+ "requires": {
+ "babel-helper-define-map": "^6.24.1",
+ "babel-helper-function-name": "^6.24.1",
+ "babel-helper-optimise-call-expression": "^6.24.1",
+ "babel-helper-replace-supers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-computed-properties": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
+ "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-destructuring": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
+ "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-duplicate-keys": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
+ "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-for-of": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
+ "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-function-name": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
+ "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
+ "dev": true,
+ "requires": {
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-literals": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz",
+ "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-amd": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz",
+ "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-commonjs": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz",
+ "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-strict-mode": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-types": "^6.26.0"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-systemjs": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
+ "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
+ "dev": true,
+ "requires": {
+ "babel-helper-hoist-variables": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-umd": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz",
+ "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-object-super": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
+ "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
+ "dev": true,
+ "requires": {
+ "babel-helper-replace-supers": "^6.24.1",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-parameters": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
+ "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
+ "dev": true,
+ "requires": {
+ "babel-helper-call-delegate": "^6.24.1",
+ "babel-helper-get-function-arity": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-shorthand-properties": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
+ "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-spread": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz",
+ "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-sticky-regex": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
+ "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
+ "dev": true,
+ "requires": {
+ "babel-helper-regex": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-template-literals": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
+ "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-typeof-symbol": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
+ "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-unicode-regex": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
+ "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
+ "dev": true,
+ "requires": {
+ "babel-helper-regex": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "regexpu-core": "^2.0.0"
+ }
+ },
+ "babel-plugin-transform-exponentiation-operator": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz",
+ "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=",
+ "dev": true,
+ "requires": {
+ "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1",
+ "babel-plugin-syntax-exponentiation-operator": "^6.8.0",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-regenerator": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
+ "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
+ "dev": true,
+ "requires": {
+ "regenerator-transform": "^0.10.0"
+ }
+ },
+ "babel-plugin-transform-runtime": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz",
+ "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-strict-mode": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
+ "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-preset-env": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz",
+ "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==",
+ "dev": true,
+ "requires": {
+ "babel-plugin-check-es2015-constants": "^6.22.0",
+ "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
+ "babel-plugin-transform-async-to-generator": "^6.22.0",
+ "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
+ "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0",
+ "babel-plugin-transform-es2015-block-scoping": "^6.23.0",
+ "babel-plugin-transform-es2015-classes": "^6.23.0",
+ "babel-plugin-transform-es2015-computed-properties": "^6.22.0",
+ "babel-plugin-transform-es2015-destructuring": "^6.23.0",
+ "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0",
+ "babel-plugin-transform-es2015-for-of": "^6.23.0",
+ "babel-plugin-transform-es2015-function-name": "^6.22.0",
+ "babel-plugin-transform-es2015-literals": "^6.22.0",
+ "babel-plugin-transform-es2015-modules-amd": "^6.22.0",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0",
+ "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0",
+ "babel-plugin-transform-es2015-modules-umd": "^6.23.0",
+ "babel-plugin-transform-es2015-object-super": "^6.22.0",
+ "babel-plugin-transform-es2015-parameters": "^6.23.0",
+ "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0",
+ "babel-plugin-transform-es2015-spread": "^6.22.0",
+ "babel-plugin-transform-es2015-sticky-regex": "^6.22.0",
+ "babel-plugin-transform-es2015-template-literals": "^6.22.0",
+ "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0",
+ "babel-plugin-transform-es2015-unicode-regex": "^6.22.0",
+ "babel-plugin-transform-exponentiation-operator": "^6.22.0",
+ "babel-plugin-transform-regenerator": "^6.22.0",
+ "browserslist": "^3.2.6",
+ "invariant": "^2.2.2",
+ "semver": "^5.3.0"
+ },
+ "dependencies": {
+ "browserslist": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+ "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30000844",
+ "electron-to-chromium": "^1.3.47"
+ }
+ }
+ }
+ },
+ "babel-register": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+ "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+ "dev": true,
+ "requires": {
+ "babel-core": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "core-js": "^2.5.0",
+ "home-or-tmp": "^2.0.0",
+ "lodash": "^4.17.4",
+ "mkdirp": "^0.5.1",
+ "source-map-support": "^0.4.15"
+ }
+ },
+ "babel-runtime": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+ "requires": {
+ "core-js": "^2.4.0",
+ "regenerator-runtime": "^0.11.0"
+ }
+ },
+ "babel-template": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+ "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "lodash": "^4.17.4"
+ }
+ },
+ "babel-traverse": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "^6.26.0",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "debug": "^2.6.8",
+ "globals": "^9.18.0",
+ "invariant": "^2.2.2",
+ "lodash": "^4.17.4"
+ }
+ },
+ "babel-types": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "esutils": "^2.0.2",
+ "lodash": "^4.17.4",
+ "to-fast-properties": "^1.0.3"
+ }
+ },
+ "babylon": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "dev": true,
+ "requires": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "base64-js": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+ "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+ "dev": true
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "beeper": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz",
+ "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=",
+ "dev": true
+ },
+ "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
+ },
+ "binary-extensions": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
+ "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
+ "dev": true
+ },
+ "block-stream": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+ "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+ "dev": true,
+ "requires": {
+ "inherits": "~2.0.0"
+ }
+ },
+ "bn.js": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+ "dev": true
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "dev": true,
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "dev": true,
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.0.1",
+ "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "browserify-sign": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+ "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.1",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.2",
+ "elliptic": "^6.0.0",
+ "inherits": "^2.0.1",
+ "parse-asn1": "^5.0.0"
+ }
+ },
+ "browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "dev": true,
+ "requires": {
+ "pako": "~1.0.5"
+ }
+ },
+ "browserslist": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+ "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+ "dev": true,
+ "requires": {
+ "caniuse-db": "^1.0.30000639",
+ "electron-to-chromium": "^1.2.7"
+ }
+ },
+ "browserslist-saucelabs": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/browserslist-saucelabs/-/browserslist-saucelabs-0.2.5.tgz",
+ "integrity": "sha1-C1GkMVq37yqudyEFiT6IUZp+gio=",
+ "dev": true,
+ "requires": {
+ "browserslist": "^1.0.1",
+ "lodash": "^3.10.1"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "3.10.1",
+ "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
+ "dev": true
+ }
+ }
+ },
+ "buffer": {
+ "version": "4.9.1",
+ "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+ "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+ "dev": true,
+ "requires": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+ "dev": true
+ },
+ "bufferstreams": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.1.3.tgz",
+ "integrity": "sha512-HaJnVuslRF4g2kSDeyl++AaVizoitCpL9PglzCYwy0uHHyvWerfvEb8jWmYbF1z4kiVFolGomnxSGl+GUQp2jg==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+ "dev": true
+ },
+ "cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "requires": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ }
+ },
+ "caller-path": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+ "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+ "dev": true,
+ "requires": {
+ "callsites": "^0.2.0"
+ }
+ },
+ "callsites": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+ "dev": true
+ },
+ "camelcase-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^2.0.0",
+ "map-obj": "^1.0.0"
+ }
+ },
+ "caniuse-db": {
+ "version": "1.0.30000887",
+ "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000887.tgz",
+ "integrity": "sha512-yOScC1WJ6ihxxPNeWSqYc2nKHqeHzXMY382yvC0mZdi+kWBrlEdCFeR/T1s5Abe5n51HoD6IA/Gho2T8vnRT2g==",
+ "dev": true
+ },
+ "caniuse-lite": {
+ "version": "1.0.30000887",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000887.tgz",
+ "integrity": "sha512-AHpONWuGFWO8yY9igdXH94tikM6ERS84286r0cAMAXYFtJBk76lhiMhtCxBJNBZsD6hzlvpWZ2AtbVFEkf4JQA==",
+ "dev": true
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "dev": true
+ },
+ "center-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+ "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+ "dev": true,
+ "requires": {
+ "align-text": "^0.1.3",
+ "lazy-cache": "^1.0.3"
+ }
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
+ },
+ "chardet": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
+ "dev": true
+ },
+ "chokidar": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
+ "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+ "dev": true,
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.0",
+ "braces": "^2.3.0",
+ "fsevents": "^1.2.2",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.1",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "lodash.debounce": "^4.0.8",
+ "normalize-path": "^2.1.1",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.0.0",
+ "upath": "^1.0.5"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+ "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ }
+ }
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "circular-json": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+ "dev": true
+ },
+ "class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "clean-css": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+ "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^2.0.0"
+ }
+ },
+ "cli-width": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "dev": true
+ },
+ "cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "clone": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz",
+ "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=",
+ "dev": true
+ },
+ "clone-stats": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz",
+ "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=",
+ "dev": true
+ },
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+ "dev": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true
+ },
+ "collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "requires": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "dev": true
+ },
+ "combined-stream": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+ "dev": true
+ },
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "console-browserify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+ "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+ "dev": true,
+ "requires": {
+ "date-now": "^0.1.4"
+ }
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+ "dev": true
+ },
+ "constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+ "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true
+ },
+ "core-js": {
+ "version": "2.5.7",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+ "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
+ },
+ "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=",
+ "dev": true
+ },
+ "cosmiconfig": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz",
+ "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==",
+ "dev": true,
+ "requires": {
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.4.3",
+ "minimist": "^1.2.0",
+ "object-assign": "^4.1.0",
+ "os-homedir": "^1.0.1",
+ "parse-json": "^2.2.0",
+ "require-from-string": "^1.1.0"
+ }
+ },
+ "create-ecdh": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+ "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.0.0"
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "css": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+ "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.5.2",
+ "urix": "^0.1.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "dev": true,
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "d": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
+ "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+ "dev": true,
+ "requires": {
+ "es5-ext": "^0.10.9"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "date-now": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+ "dev": true
+ },
+ "dateformat": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+ "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+ "dev": true,
+ "requires": {
+ "get-stdin": "^4.0.1",
+ "meow": "^3.3.0"
+ }
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "debug-fabulous": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.0.4.tgz",
+ "integrity": "sha1-+gccXYdIRoVCSAdCHKSxawsaB2M=",
+ "dev": true,
+ "requires": {
+ "debug": "2.X",
+ "lazy-debug-legacy": "0.0.X",
+ "object-assign": "4.1.0"
+ },
+ "dependencies": {
+ "object-assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
+ "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=",
+ "dev": true
+ }
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "defaults": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+ "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+ "dev": true,
+ "requires": {
+ "clone": "^1.0.2"
+ },
+ "dependencies": {
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "dev": true
+ }
+ }
+ },
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "dependencies": {
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "del": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+ "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+ "dev": true,
+ "requires": {
+ "globby": "^5.0.0",
+ "is-path-cwd": "^1.0.0",
+ "is-path-in-cwd": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "rimraf": "^2.2.8"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+ "dev": true
+ },
+ "deprecated": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz",
+ "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=",
+ "dev": true
+ },
+ "des.js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+ "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+ "dev": true
+ },
+ "detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "detect-newline": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
+ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
+ "dev": true
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "domain-browser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+ "dev": true
+ },
+ "duplexer2": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+ "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~1.1.9"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
+ }
+ }
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "electron-to-chromium": {
+ "version": "1.3.70",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.70.tgz",
+ "integrity": "sha512-WYMjqCnPVS5JA+XvwEnpwucJpVi2+q9cdCFpbhxgWGsCtforFBEkuP9+nCyy/wnU/0SyLcLRIeZct9ayMGcXoQ==",
+ "dev": true
+ },
+ "elliptic": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+ "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.4.0",
+ "brorand": "^1.0.1",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.0"
+ }
+ },
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz",
+ "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=",
+ "dev": true,
+ "requires": {
+ "once": "~1.3.0"
+ },
+ "dependencies": {
+ "once": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+ "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ }
+ }
+ },
+ "enhanced-resolve": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz",
+ "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "memory-fs": "^0.4.0",
+ "object-assign": "^4.0.1",
+ "tapable": "^0.2.7"
+ }
+ },
+ "errno": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+ "dev": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es5-ext": {
+ "version": "0.10.46",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz",
+ "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==",
+ "dev": true,
+ "requires": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.1",
+ "next-tick": "1"
+ }
+ },
+ "es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "es6-map": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
+ "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14",
+ "es6-iterator": "~2.0.1",
+ "es6-set": "~0.1.5",
+ "es6-symbol": "~3.1.1",
+ "event-emitter": "~0.3.5"
+ }
+ },
+ "es6-set": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
+ "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14",
+ "es6-iterator": "~2.0.1",
+ "es6-symbol": "3.1.1",
+ "event-emitter": "~0.3.5"
+ }
+ },
+ "es6-symbol": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+ "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "es6-weak-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz",
+ "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.14",
+ "es6-iterator": "^2.0.1",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "escope": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz",
+ "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=",
+ "dev": true,
+ "requires": {
+ "es6-map": "^0.1.3",
+ "es6-weak-map": "^2.0.1",
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint": {
+ "version": "4.19.1",
+ "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+ "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
+ "dev": true,
+ "requires": {
+ "ajv": "^5.3.0",
+ "babel-code-frame": "^6.22.0",
+ "chalk": "^2.1.0",
+ "concat-stream": "^1.6.0",
+ "cross-spawn": "^5.1.0",
+ "debug": "^3.1.0",
+ "doctrine": "^2.1.0",
+ "eslint-scope": "^3.7.1",
+ "eslint-visitor-keys": "^1.0.0",
+ "espree": "^3.5.4",
+ "esquery": "^1.0.0",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^2.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob": "^7.1.2",
+ "globals": "^11.0.1",
+ "ignore": "^3.3.3",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^3.0.6",
+ "is-resolvable": "^1.0.0",
+ "js-yaml": "^3.9.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.2",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.2",
+ "path-is-inside": "^1.0.2",
+ "pluralize": "^7.0.0",
+ "progress": "^2.0.0",
+ "regexpp": "^1.0.1",
+ "require-uncached": "^1.0.3",
+ "semver": "^5.3.0",
+ "strip-ansi": "^4.0.0",
+ "strip-json-comments": "~2.0.1",
+ "table": "4.0.2",
+ "text-table": "~0.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "debug": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
+ "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "globals": {
+ "version": "11.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+ "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "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
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "eslint-plugin-compat": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-1.0.4.tgz",
+ "integrity": "sha512-16yjDdjrivRQT7/Kov+3O6DMvfg8WYC1JKPAsvf/UNtdLBeMXVYATohAM4nOak1ynGP69mKUlOjw7nroUqY9Sg==",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.23.0",
+ "browserslist": "2.1.4",
+ "caniuse-db": "1.0.30000671",
+ "requireindex": "^1.1.0"
+ },
+ "dependencies": {
+ "browserslist": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.1.4.tgz",
+ "integrity": "sha1-zFJq9KExK30uBWU+VtDIq3DA4FM=",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30000670",
+ "electron-to-chromium": "^1.3.11"
+ }
+ },
+ "caniuse-db": {
+ "version": "1.0.30000671",
+ "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000671.tgz",
+ "integrity": "sha1-nwcbvHuWmUY4zLr0eCnVihV3qO0=",
+ "dev": true
+ }
+ }
+ },
+ "eslint-scope": {
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+ "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
+ },
+ "espree": {
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+ "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+ "dev": true,
+ "requires": {
+ "acorn": "^5.5.0",
+ "acorn-jsx": "^3.0.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.0.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "events": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+ "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+ "dev": true
+ },
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dev": true,
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "execa": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^5.0.1",
+ "get-stream": "^3.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "exit-hook": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
+ "dev": true
+ },
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "requires": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "exports-loader": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.4.tgz",
+ "integrity": "sha1-1w/GEhl1s1/BKDDPUnVL4nQPyIY=",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^1.0.2",
+ "source-map": "0.5.x"
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "external-editor": {
+ "version": "2.2.0",
+ "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+ "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.4.0",
+ "iconv-lite": "^0.4.17",
+ "tmp": "^0.0.33"
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "requires": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "dev": true
+ },
+ "fancy-log": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz",
+ "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=",
+ "dev": true,
+ "requires": {
+ "ansi-gray": "^0.1.1",
+ "color-support": "^1.1.3",
+ "time-stamp": "^1.0.0"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^1.2.1",
+ "object-assign": "^4.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "find-cache-dir": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
+ "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+ "dev": true,
+ "requires": {
+ "commondir": "^1.0.1",
+ "make-dir": "^1.0.0",
+ "pkg-dir": "^2.0.0"
+ }
+ },
+ "find-index": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz",
+ "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=",
+ "dev": true
+ },
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ },
+ "findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ }
+ },
+ "fined": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz",
+ "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ }
+ },
+ "first-chunk-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz",
+ "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=",
+ "dev": true
+ },
+ "flagged-respawn": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz",
+ "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=",
+ "dev": true
+ },
+ "flat-cache": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz",
+ "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=",
+ "dev": true,
+ "requires": {
+ "circular-json": "^0.3.1",
+ "del": "^2.0.2",
+ "graceful-fs": "^4.1.2",
+ "write": "^0.2.1"
+ }
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true
+ },
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "dev": true
+ },
+ "fork-stream": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz",
+ "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=",
+ "dev": true
+ },
+ "form-data": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+ "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "dependencies": {
+ "combined-stream": {
+ "version": "1.0.6",
+ "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+ "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ }
+ }
+ },
+ "fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "requires": {
+ "map-cache": "^0.2.2"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
+ "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "nan": "^2.9.2",
+ "node-pre-gyp": "^0.10.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "dev": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "2.6.9",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "deep-extend": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.21",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true,
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "bundled": true,
+ "dev": true
+ },
+ "minipass": {
+ "version": "2.2.4",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.1",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^2.1.2",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.10.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.0",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.1.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
+ }
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.1.10",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.7",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.5.1",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.0.5"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.1",
+ "bundled": true,
+ "dev": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.5.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "chownr": "^1.0.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.2.4",
+ "minizlib": "^1.1.0",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.1",
+ "yallist": "^3.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "string-width": "^1.0.2"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true
+ },
+ "yallist": {
+ "version": "3.0.2",
+ "bundled": true,
+ "dev": true
+ }
+ }
+ },
+ "fstream": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+ "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ }
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "dev": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "gaze": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz",
+ "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=",
+ "dev": true,
+ "requires": {
+ "globule": "~0.1.0"
+ }
+ },
+ "generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "dev": true,
+ "requires": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "generate-object-property": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+ "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+ "dev": true,
+ "requires": {
+ "is-property": "^1.0.0"
+ }
+ },
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
+ "get-stdin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+ "dev": true
+ },
+ "get-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+ "dev": true
+ },
+ "get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+ "dev": true
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "glob-stream": {
+ "version": "3.1.18",
+ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz",
+ "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=",
+ "dev": true,
+ "requires": {
+ "glob": "^4.3.1",
+ "glob2base": "^0.0.12",
+ "minimatch": "^2.0.1",
+ "ordered-read-streams": "^0.1.0",
+ "through2": "^0.6.1",
+ "unique-stream": "^1.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz",
+ "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=",
+ "dev": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^2.0.1",
+ "once": "^1.3.0"
+ }
+ },
+ "minimatch": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz",
+ "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.0.0"
+ }
+ }
+ }
+ },
+ "glob-watcher": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz",
+ "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=",
+ "dev": true,
+ "requires": {
+ "gaze": "^0.5.1"
+ }
+ },
+ "glob2base": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz",
+ "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=",
+ "dev": true,
+ "requires": {
+ "find-index": "^0.1.1"
+ }
+ },
+ "global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "requires": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ }
+ },
+ "global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ }
+ },
+ "globals": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+ "dev": true
+ },
+ "globby": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+ "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+ "dev": true,
+ "requires": {
+ "array-union": "^1.0.1",
+ "arrify": "^1.0.0",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "globule": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz",
+ "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=",
+ "dev": true,
+ "requires": {
+ "glob": "~3.1.21",
+ "lodash": "~1.0.1",
+ "minimatch": "~0.2.11"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "3.1.21",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
+ "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "~1.2.0",
+ "inherits": "1",
+ "minimatch": "~0.2.11"
+ }
+ },
+ "graceful-fs": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz",
+ "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=",
+ "dev": true
+ },
+ "inherits": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz",
+ "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=",
+ "dev": true
+ },
+ "lodash": {
+ "version": "1.0.2",
+ "resolved": "http://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz",
+ "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+ "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "2",
+ "sigmund": "~1.0.0"
+ }
+ }
+ }
+ },
+ "glogg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz",
+ "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==",
+ "dev": true,
+ "requires": {
+ "sparkles": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+ "dev": true
+ },
+ "gulp": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz",
+ "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=",
+ "dev": true,
+ "requires": {
+ "archy": "^1.0.0",
+ "chalk": "^1.0.0",
+ "deprecated": "^0.0.1",
+ "gulp-util": "^3.0.0",
+ "interpret": "^1.0.0",
+ "liftoff": "^2.1.0",
+ "minimist": "^1.1.0",
+ "orchestrator": "^0.3.0",
+ "pretty-hrtime": "^1.0.0",
+ "semver": "^4.1.0",
+ "tildify": "^1.0.0",
+ "v8flags": "^2.0.2",
+ "vinyl-fs": "^0.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
+ "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+ "dev": true
+ }
+ }
+ },
+ "gulp-clean-css": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-3.10.0.tgz",
+ "integrity": "sha512-7Isf9Y690o/Q5MVjEylH1H7L8WeZ89woW7DnhD5unTintOdZb67KdOayRgp9trUFo+f9UyJtuatV42e/+kghPg==",
+ "dev": true,
+ "requires": {
+ "clean-css": "4.2.1",
+ "plugin-error": "1.0.1",
+ "through2": "2.0.3",
+ "vinyl-sourcemaps-apply": "0.2.1"
+ },
+ "dependencies": {
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ }
+ }
+ },
+ "gulp-eslint": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/gulp-eslint/-/gulp-eslint-3.0.1.tgz",
+ "integrity": "sha1-BOV+PhjGl0JnwSz2hV3HF9SjE70=",
+ "dev": true,
+ "requires": {
+ "bufferstreams": "^1.1.1",
+ "eslint": "^3.0.0",
+ "gulp-util": "^3.0.6"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+ "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+ "dev": true,
+ "requires": {
+ "co": "^4.6.0",
+ "json-stable-stringify": "^1.0.1"
+ }
+ },
+ "ajv-keywords": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz",
+ "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=",
+ "dev": true
+ },
+ "ansi-escapes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+ "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+ "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^1.0.1"
+ }
+ },
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "dev": true
+ },
+ "dateformat": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz",
+ "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "^6.16.0",
+ "chalk": "^1.1.3",
+ "concat-stream": "^1.5.2",
+ "debug": "^2.1.1",
+ "doctrine": "^2.0.0",
+ "escope": "^3.6.0",
+ "espree": "^3.4.0",
+ "esquery": "^1.0.0",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^2.0.0",
+ "glob": "^7.0.3",
+ "globals": "^9.14.0",
+ "ignore": "^3.2.0",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^0.12.0",
+ "is-my-json-valid": "^2.10.0",
+ "is-resolvable": "^1.0.0",
+ "js-yaml": "^3.5.1",
+ "json-stable-stringify": "^1.0.0",
+ "levn": "^0.3.0",
+ "lodash": "^4.0.0",
+ "mkdirp": "^0.5.0",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.2",
+ "path-is-inside": "^1.0.1",
+ "pluralize": "^1.2.1",
+ "progress": "^1.1.8",
+ "require-uncached": "^1.0.2",
+ "shelljs": "^0.7.5",
+ "strip-bom": "^3.0.0",
+ "strip-json-comments": "~2.0.1",
+ "table": "^3.7.8",
+ "text-table": "~0.2.0",
+ "user-home": "^2.0.0"
+ }
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "gulp-util": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
+ "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=",
+ "dev": true,
+ "requires": {
+ "array-differ": "^1.0.0",
+ "array-uniq": "^1.0.2",
+ "beeper": "^1.0.0",
+ "chalk": "^1.0.0",
+ "dateformat": "^2.0.0",
+ "fancy-log": "^1.1.0",
+ "gulplog": "^1.0.0",
+ "has-gulplog": "^0.1.0",
+ "lodash._reescape": "^3.0.0",
+ "lodash._reevaluate": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.template": "^3.0.0",
+ "minimist": "^1.1.0",
+ "multipipe": "^0.1.2",
+ "object-assign": "^3.0.0",
+ "replace-ext": "0.0.1",
+ "through2": "^2.0.0",
+ "vinyl": "^0.5.0"
+ },
+ "dependencies": {
+ "object-assign": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+ "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=",
+ "dev": true
+ }
+ }
+ },
+ "inquirer": {
+ "version": "0.12.0",
+ "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
+ "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^1.1.0",
+ "ansi-regex": "^2.0.0",
+ "chalk": "^1.0.0",
+ "cli-cursor": "^1.0.1",
+ "cli-width": "^2.0.0",
+ "figures": "^1.3.5",
+ "lodash": "^4.3.0",
+ "readline2": "^1.0.1",
+ "run-async": "^0.1.0",
+ "rx-lite": "^3.1.2",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.0",
+ "through": "^2.3.6"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "onetime": {
+ "version": "1.1.0",
+ "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+ "dev": true
+ },
+ "pluralize": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz",
+ "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=",
+ "dev": true
+ },
+ "progress": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+ "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+ "dev": true,
+ "requires": {
+ "exit-hook": "^1.0.0",
+ "onetime": "^1.0.0"
+ }
+ },
+ "run-async": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
+ "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0"
+ }
+ },
+ "rx-lite": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
+ "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ },
+ "table": {
+ "version": "3.8.3",
+ "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz",
+ "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
+ "dev": true,
+ "requires": {
+ "ajv": "^4.7.0",
+ "ajv-keywords": "^1.0.0",
+ "chalk": "^1.1.1",
+ "lodash": "^4.0.0",
+ "slice-ansi": "0.0.4",
+ "string-width": "^2.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ },
+ "user-home": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz",
+ "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0"
+ }
+ },
+ "vinyl": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
+ "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+ "dev": true,
+ "requires": {
+ "clone": "^1.0.0",
+ "clone-stats": "^0.0.1",
+ "replace-ext": "0.0.1"
+ }
+ }
+ }
+ },
+ "gulp-if": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-1.2.5.tgz",
+ "integrity": "sha1-m9nBYDLswo4BVL+wWCjSMxZvLak=",
+ "dev": true,
+ "requires": {
+ "gulp-match": "~0.2.1",
+ "ternary-stream": "^1.2.0",
+ "through2": "~0.6.2"
+ }
+ },
+ "gulp-match": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-0.2.1.tgz",
+ "integrity": "sha1-C+0I2ovW6JaG+J/7AEM3+LrQbSI=",
+ "dev": true,
+ "requires": {
+ "minimatch": "^1.0.0"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-1.0.0.tgz",
+ "integrity": "sha1-4N0hILSeG3JM6NcUxSCCKpQ4V20=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "2",
+ "sigmund": "~1.0.0"
+ }
+ }
+ }
+ },
+ "gulp-plumber": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.0.tgz",
+ "integrity": "sha512-L/LJftsbKoHbVj6dN5pvMsyJn9jYI0wT0nMg3G6VZhDac4NesezecYTi8/48rHi+yEic3sUpw6jlSc7qNWh32A==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "fancy-log": "^1.3.2",
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.3"
+ },
+ "dependencies": {
+ "arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ }
+ },
+ "arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=",
+ "dev": true
+ },
+ "array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+ "dev": true
+ },
+ "extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^1.1.0"
+ }
+ },
+ "kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=",
+ "dev": true
+ },
+ "plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=",
+ "dev": true,
+ "requires": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ }
+ },
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ }
+ }
+ },
+ "gulp-postcss": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-6.4.0.tgz",
+ "integrity": "sha1-eKMuPIeqbNzsWuHJBeGW1HjoxdU=",
+ "dev": true,
+ "requires": {
+ "gulp-util": "^3.0.8",
+ "postcss": "^5.2.12",
+ "postcss-load-config": "^1.2.0",
+ "vinyl-sourcemaps-apply": "^0.2.1"
+ },
+ "dependencies": {
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "dev": true
+ },
+ "dateformat": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
+ "dev": true
+ },
+ "gulp-util": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
+ "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=",
+ "dev": true,
+ "requires": {
+ "array-differ": "^1.0.0",
+ "array-uniq": "^1.0.2",
+ "beeper": "^1.0.0",
+ "chalk": "^1.0.0",
+ "dateformat": "^2.0.0",
+ "fancy-log": "^1.1.0",
+ "gulplog": "^1.0.0",
+ "has-gulplog": "^0.1.0",
+ "lodash._reescape": "^3.0.0",
+ "lodash._reevaluate": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.template": "^3.0.0",
+ "minimist": "^1.1.0",
+ "multipipe": "^0.1.2",
+ "object-assign": "^3.0.0",
+ "replace-ext": "0.0.1",
+ "through2": "^2.0.0",
+ "vinyl": "^0.5.0"
+ }
+ },
+ "object-assign": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+ "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=",
+ "dev": true
+ },
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ },
+ "vinyl": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
+ "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+ "dev": true,
+ "requires": {
+ "clone": "^1.0.0",
+ "clone-stats": "^0.0.1",
+ "replace-ext": "0.0.1"
+ }
+ }
+ }
+ },
+ "gulp-sass": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-3.1.0.tgz",
+ "integrity": "sha1-U9xLaKH13f5EJKtMJHZVJpqLdLc=",
+ "dev": true,
+ "requires": {
+ "gulp-util": "^3.0",
+ "lodash.clonedeep": "^4.3.2",
+ "node-sass": "^4.2.0",
+ "through2": "^2.0.0",
+ "vinyl-sourcemaps-apply": "^0.2.0"
+ },
+ "dependencies": {
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ }
+ }
+ },
+ "gulp-sourcemaps": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.4.1.tgz",
+ "integrity": "sha1-j2XcXA0Hsv1ciLxg7H8T5WcWv3Q=",
+ "dev": true,
+ "requires": {
+ "acorn": "4.X",
+ "convert-source-map": "1.X",
+ "css": "2.X",
+ "debug-fabulous": "0.0.X",
+ "detect-newline": "2.X",
+ "graceful-fs": "4.X",
+ "source-map": "0.X",
+ "strip-bom": "3.X",
+ "through2": "2.X",
+ "vinyl": "1.X"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+ "dev": true
+ },
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "dev": true
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ },
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ },
+ "vinyl": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz",
+ "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=",
+ "dev": true,
+ "requires": {
+ "clone": "^1.0.0",
+ "clone-stats": "^0.0.1",
+ "replace-ext": "0.0.1"
+ }
+ }
+ }
+ },
+ "gulp-util": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.5.tgz",
+ "integrity": "sha1-LJ7qP/WGs/UXc8AtdMiCcTR9oCk=",
+ "dev": true,
+ "requires": {
+ "array-differ": "^1.0.0",
+ "array-uniq": "^1.0.2",
+ "beeper": "^1.0.0",
+ "chalk": "^1.0.0",
+ "dateformat": "^1.0.11",
+ "lodash._reescape": "^3.0.0",
+ "lodash._reevaluate": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.template": "^3.0.0",
+ "minimist": "^1.1.0",
+ "multipipe": "^0.1.2",
+ "object-assign": "^2.0.0",
+ "replace-ext": "0.0.1",
+ "through2": "^0.6.3",
+ "vinyl": "^0.4.3"
+ },
+ "dependencies": {
+ "object-assign": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz",
+ "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=",
+ "dev": true
+ }
+ }
+ },
+ "gulplog": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz",
+ "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=",
+ "dev": true,
+ "requires": {
+ "glogg": "^1.0.0"
+ }
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+ "dev": true
+ },
+ "har-validator": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+ "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+ "dev": true,
+ "requires": {
+ "ajv": "^5.1.0",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+ "dev": true
+ },
+ "has-gulplog": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz",
+ "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=",
+ "dev": true,
+ "requires": {
+ "sparkles": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+ "dev": true
+ },
+ "has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "hash-base": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+ "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "hash.js": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
+ "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "dev": true,
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "home-or-tmp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+ "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.1"
+ }
+ },
+ "homedir-polyfill": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
+ "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
+ "dev": true,
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "hosted-git-info": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+ "dev": true
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ieee754": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+ "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+ "dev": true
+ },
+ "ignore": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+ "dev": true
+ },
+ "imports-loader": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.7.1.tgz",
+ "integrity": "sha1-8gS180cCoywdt9SNidXoZ6BEElM=",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^1.0.2",
+ "source-map": "^0.5.6"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "in-publish": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+ "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+ "dev": true
+ },
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+ "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^3.0.0",
+ "chalk": "^2.0.0",
+ "cli-cursor": "^2.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^2.0.4",
+ "figures": "^2.0.0",
+ "lodash": "^4.3.0",
+ "mute-stream": "0.0.7",
+ "run-async": "^2.2.0",
+ "rx-lite": "^4.0.8",
+ "rx-lite-aggregates": "^4.0.8",
+ "string-width": "^2.1.0",
+ "strip-ansi": "^4.0.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+ "dev": true
+ },
+ "invariant": {
+ "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"
+ }
+ },
+ "invert-kv": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+ "dev": true
+ },
+ "is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "requires": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "is-builtin-module": {
+ "version": "1.0.0",
+ "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+ "dev": true,
+ "requires": {
+ "builtin-modules": "^1.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "is-directory": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+ "dev": true
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ },
+ "is-my-ip-valid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
+ "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
+ "dev": true
+ },
+ "is-my-json-valid": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz",
+ "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==",
+ "dev": true,
+ "requires": {
+ "generate-function": "^2.0.0",
+ "generate-object-property": "^1.1.0",
+ "is-my-ip-valid": "^1.0.0",
+ "jsonpointer": "^4.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-path-cwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+ "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+ "dev": true
+ },
+ "is-path-in-cwd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "dev": true,
+ "requires": {
+ "is-path-inside": "^1.0.0"
+ }
+ },
+ "is-path-inside": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+ "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+ "dev": true,
+ "requires": {
+ "path-is-inside": "^1.0.1"
+ }
+ },
+ "is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "dev": true
+ },
+ "is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+ "dev": true
+ },
+ "is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "requires": {
+ "is-unc-path": "^1.0.0"
+ }
+ },
+ "is-resolvable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "dev": true
+ },
+ "is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "requires": {
+ "unc-path-regex": "^0.1.2"
+ }
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "dev": true
+ },
+ "is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "dev": true
+ },
+ "js-base64": {
+ "version": "2.4.9",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz",
+ "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+ "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "dev": true,
+ "optional": true
+ },
+ "jsesc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+ "dev": true
+ },
+ "json-loader": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
+ "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==",
+ "dev": true
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "dev": true
+ },
+ "json-schema-traverse": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+ "dev": true
+ },
+ "json-stable-stringify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+ "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+ "dev": true,
+ "requires": {
+ "jsonify": "~0.0.0"
+ }
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "dev": true
+ },
+ "json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true
+ },
+ "jsonify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+ "dev": true
+ },
+ "jsonpointer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
+ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
+ "dev": true
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "karma-sourcemap-loader": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz",
+ "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "dev": true
+ },
+ "lazy-cache": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
+ "dev": true
+ },
+ "lazy-debug-legacy": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz",
+ "integrity": "sha1-U3cWwHduTPeePtG2IfdljCkRsbE=",
+ "dev": true
+ },
+ "lcid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+ "dev": true,
+ "requires": {
+ "invert-kv": "^1.0.0"
+ }
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "liftoff": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
+ "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=",
+ "dev": true,
+ "requires": {
+ "extend": "^3.0.0",
+ "findup-sync": "^2.0.0",
+ "fined": "^1.0.1",
+ "flagged-respawn": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.0",
+ "rechoir": "^0.6.2",
+ "resolve": "^1.1.7"
+ }
+ },
+ "load-json-file": {
+ "version": "1.1.0",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "loader-runner": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz",
+ "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==",
+ "dev": true
+ },
+ "loader-utils": {
+ "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",
+ "json5": "^0.5.0"
+ }
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "dev": true
+ },
+ "lodash._basecopy": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+ "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+ "dev": true
+ },
+ "lodash._basetostring": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
+ "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=",
+ "dev": true
+ },
+ "lodash._basevalues": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
+ "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=",
+ "dev": true
+ },
+ "lodash._getnative": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+ "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+ "dev": true
+ },
+ "lodash._isiterateecall": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+ "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+ "dev": true
+ },
+ "lodash._reescape": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
+ "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=",
+ "dev": true
+ },
+ "lodash._reevaluate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
+ "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=",
+ "dev": true
+ },
+ "lodash._reinterpolate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
+ "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
+ "dev": true
+ },
+ "lodash._root": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
+ "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
+ "dev": true
+ },
+ "lodash.assign": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+ "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+ "dev": true
+ },
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+ "dev": true
+ },
+ "lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+ "dev": true
+ },
+ "lodash.escape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
+ "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=",
+ "dev": true,
+ "requires": {
+ "lodash._root": "^3.0.0"
+ }
+ },
+ "lodash.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+ "dev": true
+ },
+ "lodash.isarray": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+ "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+ "dev": true
+ },
+ "lodash.keys": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+ "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+ "dev": true,
+ "requires": {
+ "lodash._getnative": "^3.0.0",
+ "lodash.isarguments": "^3.0.0",
+ "lodash.isarray": "^3.0.0"
+ }
+ },
+ "lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
+ },
+ "lodash.mergewith": {
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+ "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+ "dev": true
+ },
+ "lodash.restparam": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
+ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
+ "dev": true
+ },
+ "lodash.template": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
+ "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=",
+ "dev": true,
+ "requires": {
+ "lodash._basecopy": "^3.0.0",
+ "lodash._basetostring": "^3.0.0",
+ "lodash._basevalues": "^3.0.0",
+ "lodash._isiterateecall": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.escape": "^3.0.0",
+ "lodash.keys": "^3.0.0",
+ "lodash.restparam": "^3.0.0",
+ "lodash.templatesettings": "^3.0.0"
+ }
+ },
+ "lodash.templatesettings": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
+ "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=",
+ "dev": true,
+ "requires": {
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.escape": "^3.0.0"
+ }
+ },
+ "longest": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+ "dev": true
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "dev": true,
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+ "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+ "dev": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "make-dir": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+ "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+ "dev": true,
+ "requires": {
+ "pify": "^3.0.0"
+ }
+ },
+ "make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
+ "map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+ "dev": true
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "dev": true
+ },
+ "map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "dev": true,
+ "requires": {
+ "object-visit": "^1.0.0"
+ }
+ },
+ "md5.js": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+ "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "mem": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+ "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "memory-fs": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+ "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+ "dev": true,
+ "requires": {
+ "errno": "^0.1.3",
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "meow": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "dev": true,
+ "requires": {
+ "camelcase-keys": "^2.0.0",
+ "decamelize": "^1.1.2",
+ "loud-rejection": "^1.0.0",
+ "map-obj": "^1.0.1",
+ "minimist": "^1.1.3",
+ "normalize-package-data": "^2.3.4",
+ "object-assign": "^4.0.1",
+ "read-pkg-up": "^1.0.1",
+ "redent": "^1.0.0",
+ "trim-newlines": "^1.0.0"
+ }
+ },
+ "merge-stream": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-0.1.8.tgz",
+ "integrity": "sha1-SKB7O0oSHXSj7b/c20sIrb8CQLE=",
+ "dev": true,
+ "requires": {
+ "through2": "^0.6.1"
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ }
+ },
+ "mime-db": {
+ "version": "1.36.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
+ "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.20",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
+ "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+ "dev": true,
+ "requires": {
+ "mime-db": "~1.36.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "dev": true
+ },
+ "mixin-deep": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+ "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "multipipe": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
+ "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=",
+ "dev": true,
+ "requires": {
+ "duplexer2": "0.0.2"
+ }
+ },
+ "mute-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+ "dev": true
+ },
+ "nan": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
+ "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==",
+ "dev": true
+ },
+ "nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ }
+ },
+ "natives": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.5.tgz",
+ "integrity": "sha512-1pJ+02gl2KJgCPFtpZGtuD4lGSJnIZvvFHCQTOeDRMSXjfu2GmYWuhI8NFMA4W2I5NNFRbfy/YCiVt4CgNpP8A==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "neo-async": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
+ "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==",
+ "dev": true
+ },
+ "next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
+ "dev": true
+ },
+ "node-gyp": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+ "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+ "dev": true,
+ "requires": {
+ "fstream": "^1.0.0",
+ "glob": "^7.0.3",
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "^0.5.0",
+ "nopt": "2 || 3",
+ "npmlog": "0 || 1 || 2 || 3 || 4",
+ "osenv": "0",
+ "request": "^2.87.0",
+ "rimraf": "2",
+ "semver": "~5.3.0",
+ "tar": "^2.0.0",
+ "which": "1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+ "dev": true
+ }
+ }
+ },
+ "node-libs-browser": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
+ "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==",
+ "dev": true,
+ "requires": {
+ "assert": "^1.1.1",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^4.3.0",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "^1.0.0",
+ "crypto-browserify": "^3.11.0",
+ "domain-browser": "^1.1.1",
+ "events": "^1.0.0",
+ "https-browserify": "^1.0.0",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "0.0.0",
+ "process": "^0.11.10",
+ "punycode": "^1.2.4",
+ "querystring-es3": "^0.2.0",
+ "readable-stream": "^2.3.3",
+ "stream-browserify": "^2.0.1",
+ "stream-http": "^2.7.2",
+ "string_decoder": "^1.0.0",
+ "timers-browserify": "^2.0.4",
+ "tty-browserify": "0.0.0",
+ "url": "^0.11.0",
+ "util": "^0.10.3",
+ "vm-browserify": "0.0.4"
+ }
+ },
+ "node-sass": {
+ "version": "4.9.3",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz",
+ "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==",
+ "dev": true,
+ "requires": {
+ "async-foreach": "^0.1.3",
+ "chalk": "^1.1.1",
+ "cross-spawn": "^3.0.0",
+ "gaze": "^1.0.0",
+ "get-stdin": "^4.0.1",
+ "glob": "^7.0.3",
+ "in-publish": "^2.0.0",
+ "lodash.assign": "^4.2.0",
+ "lodash.clonedeep": "^4.3.2",
+ "lodash.mergewith": "^4.6.0",
+ "meow": "^3.7.0",
+ "mkdirp": "^0.5.1",
+ "nan": "^2.10.0",
+ "node-gyp": "^3.8.0",
+ "npmlog": "^4.0.0",
+ "request": "2.87.0",
+ "sass-graph": "^2.2.4",
+ "stdout-stream": "^1.4.0",
+ "true-case-path": "^1.0.2"
+ },
+ "dependencies": {
+ "cross-spawn": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+ "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "which": "^1.2.9"
+ }
+ },
+ "gaze": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+ "dev": true,
+ "requires": {
+ "globule": "^1.0.0"
+ }
+ },
+ "globule": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+ "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+ "dev": true,
+ "requires": {
+ "glob": "~7.1.1",
+ "lodash": "~4.17.10",
+ "minimatch": "~3.0.2"
+ }
+ }
+ }
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "is-builtin-module": "^1.0.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ },
+ "normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dev": true,
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "dev": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "num2fraction": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+ "dev": true
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "oauth-sign": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+ "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "dev": true,
+ "requires": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+ "dev": true,
+ "requires": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+ "dev": true,
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.4",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "wordwrap": "~1.0.0"
+ }
+ },
+ "orchestrator": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz",
+ "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "~0.1.5",
+ "sequencify": "~0.0.7",
+ "stream-consume": "~0.1.0"
+ }
+ },
+ "ordered-read-streams": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz",
+ "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=",
+ "dev": true
+ },
+ "os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+ "dev": true
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true
+ },
+ "os-locale": {
+ "version": "1.4.0",
+ "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+ "dev": true,
+ "requires": {
+ "lcid": "^1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true
+ },
+ "pako": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+ "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+ "dev": true
+ },
+ "parse-asn1": {
+ "version": "5.1.1",
+ "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+ "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+ "dev": true,
+ "requires": {
+ "asn1.js": "^4.0.0",
+ "browserify-aes": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3"
+ }
+ },
+ "parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+ "dev": true,
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ }
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+ "dev": true
+ },
+ "pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true
+ },
+ "path-browserify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+ "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+ "dev": true
+ },
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+ "dev": true,
+ "requires": {
+ "path-root-regex": "^0.1.0"
+ }
+ },
+ "path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=",
+ "dev": true
+ },
+ "path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "pbkdf2": {
+ "version": "3.0.16",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",
+ "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==",
+ "dev": true,
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+ "dev": true
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+ "dev": true
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "dev": true,
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "pkg-dir": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+ "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+ "dev": true,
+ "requires": {
+ "find-up": "^2.1.0"
+ }
+ },
+ "plugin-error": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
+ "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^1.0.1",
+ "arr-diff": "^4.0.0",
+ "arr-union": "^3.1.0",
+ "extend-shallow": "^3.0.2"
+ }
+ },
+ "pluralize": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+ "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
+ "dev": true
+ },
+ "posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "5.2.18",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+ "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "js-base64": "^2.1.9",
+ "source-map": "^0.5.6",
+ "supports-color": "^3.2.3"
+ }
+ },
+ "postcss-load-config": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz",
+ "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=",
+ "dev": true,
+ "requires": {
+ "cosmiconfig": "^2.1.0",
+ "object-assign": "^4.1.0",
+ "postcss-load-options": "^1.2.0",
+ "postcss-load-plugins": "^2.3.0"
+ }
+ },
+ "postcss-load-options": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz",
+ "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=",
+ "dev": true,
+ "requires": {
+ "cosmiconfig": "^2.1.0",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "postcss-load-plugins": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz",
+ "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=",
+ "dev": true,
+ "requires": {
+ "cosmiconfig": "^2.1.1",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz",
+ "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+ "dev": true
+ },
+ "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
+ },
+ "process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+ "dev": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+ "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
+ "dev": true
+ },
+ "prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+ "dev": true
+ },
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+ "dev": true
+ },
+ "public-encrypt": {
+ "version": "4.0.2",
+ "resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
+ "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "dev": true
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true
+ },
+ "querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+ "dev": true
+ },
+ "querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+ "dev": true
+ },
+ "randombytes": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+ "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "raw-loader": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
+ "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao="
+ },
+ "read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+ "dev": true,
+ "requires": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "dev": true,
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "dev": true,
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "readline2": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz",
+ "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "mute-stream": "0.0.5"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "mute-stream": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
+ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=",
+ "dev": true
+ }
+ }
+ },
+ "rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+ "dev": true,
+ "requires": {
+ "resolve": "^1.1.6"
+ }
+ },
+ "redent": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "dev": true,
+ "requires": {
+ "indent-string": "^2.1.0",
+ "strip-indent": "^1.0.1"
+ }
+ },
+ "regenerate": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+ "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+ "dev": true
+ },
+ "regenerator-runtime": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+ },
+ "regenerator-transform": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
+ "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.18.0",
+ "babel-types": "^6.19.0",
+ "private": "^0.1.6"
+ }
+ },
+ "regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "regexpp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+ "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==",
+ "dev": true
+ },
+ "regexpu-core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
+ "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
+ "dev": true,
+ "requires": {
+ "regenerate": "^1.2.1",
+ "regjsgen": "^0.2.0",
+ "regjsparser": "^0.1.4"
+ }
+ },
+ "regjsgen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+ "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+ "dev": true
+ },
+ "regjsparser": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+ "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+ "dev": true,
+ "requires": {
+ "jsesc": "~0.5.0"
+ },
+ "dependencies": {
+ "jsesc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+ "dev": true
+ }
+ }
+ },
+ "remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+ "dev": true
+ },
+ "repeat-element": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+ "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+ "dev": true
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "replace-ext": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz",
+ "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=",
+ "dev": true
+ },
+ "request": {
+ "version": "2.87.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
+ "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
+ "dev": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.6.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.5",
+ "extend": "~3.0.1",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.1",
+ "har-validator": "~5.0.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.17",
+ "oauth-sign": "~0.8.2",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.1",
+ "safe-buffer": "^5.1.1",
+ "tough-cookie": "~2.3.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.1.0"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-from-string": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz",
+ "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "dev": true
+ },
+ "require-package-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
+ "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=",
+ "dev": true
+ },
+ "require-uncached": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+ "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+ "dev": true,
+ "requires": {
+ "caller-path": "^0.1.0",
+ "resolve-from": "^1.0.0"
+ }
+ },
+ "requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+ "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.5"
+ }
+ },
+ "resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
+ "dev": true
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "dev": true,
+ "requires": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true
+ },
+ "right-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+ "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+ "dev": true,
+ "requires": {
+ "align-text": "^0.1.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.5"
+ }
+ },
+ "ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "run-async": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "dev": true,
+ "requires": {
+ "is-promise": "^2.1.0"
+ }
+ },
+ "rx-lite": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+ "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=",
+ "dev": true
+ },
+ "rx-lite-aggregates": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+ "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+ "dev": true,
+ "requires": {
+ "rx-lite": "*"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "dev": true,
+ "requires": {
+ "ret": "~0.1.10"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "sass-graph": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+ "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.0",
+ "lodash": "^4.0.0",
+ "scss-tokenizer": "^0.2.3",
+ "yargs": "^7.0.0"
+ }
+ },
+ "scss-tokenizer": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+ "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+ "dev": true,
+ "requires": {
+ "js-base64": "^2.1.8",
+ "source-map": "^0.4.2"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+ "dev": true,
+ "requires": {
+ "amdefine": ">=0.0.4"
+ }
+ }
+ }
+ },
+ "semver": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+ "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+ "dev": true
+ },
+ "sequencify": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz",
+ "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=",
+ "dev": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "set-value": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+ "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+ "dev": true
+ },
+ "sha.js": {
+ "version": "2.4.11",
+ "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "shelljs": {
+ "version": "0.7.8",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
+ "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ }
+ },
+ "sigmund": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
+ "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+ "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
+ "snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "dev": true,
+ "requires": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.2.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "source-list-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
+ "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "source-map-resolve": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+ "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+ "dev": true,
+ "requires": {
+ "atob": "^2.1.1",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "source-map-support": {
+ "version": "0.4.18",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+ "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.5.6"
+ }
+ },
+ "source-map-url": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+ "dev": true
+ },
+ "sparkles": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
+ "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==",
+ "dev": true
+ },
+ "spdx-correct": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
+ "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
+ "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz",
+ "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==",
+ "dev": true
+ },
+ "split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "srcdoc-polyfill": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/srcdoc-polyfill/-/srcdoc-polyfill-1.0.0.tgz",
+ "integrity": "sha1-gbbXkTHzMjHqDyBckja+kOmspxg="
+ },
+ "sshpk": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+ "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+ "dev": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "dev": true,
+ "requires": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "stdout-stream": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+ "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "stream-browserify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+ "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+ "dev": true,
+ "requires": {
+ "inherits": "~2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "stream-consume": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz",
+ "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==",
+ "dev": true
+ },
+ "stream-http": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+ "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+ "dev": true,
+ "requires": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.3.6",
+ "to-arraybuffer": "^1.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "dev": true,
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "dev": true
+ },
+ "strip-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "dev": true,
+ "requires": {
+ "get-stdin": "^4.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^1.0.0"
+ }
+ },
+ "table": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
+ "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
+ "dev": true,
+ "requires": {
+ "ajv": "^5.2.3",
+ "ajv-keywords": "^2.1.0",
+ "chalk": "^2.1.0",
+ "lodash": "^4.17.4",
+ "slice-ansi": "1.0.0",
+ "string-width": "^2.1.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "tapable": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
+ "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=",
+ "dev": true
+ },
+ "tar": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+ "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+ "dev": true,
+ "requires": {
+ "block-stream": "*",
+ "fstream": "^1.0.2",
+ "inherits": "2"
+ }
+ },
+ "ternary-stream": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-1.2.3.tgz",
+ "integrity": "sha1-8Zafg4R/lkImG8FC4X7iAKrag/0=",
+ "dev": true,
+ "requires": {
+ "duplexer2": "~0.0.2",
+ "fork-stream": "~0.0.4",
+ "merge-stream": "~0.1.6",
+ "through2": "~0.6.3"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "through2": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
+ "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
+ "dev": true,
+ "requires": {
+ "readable-stream": ">=1.0.33-1 <1.1.0-0",
+ "xtend": ">=4.0.0 <4.1.0-0"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
+ }
+ }
+ },
+ "tildify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz",
+ "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0"
+ }
+ },
+ "time-stamp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
+ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=",
+ "dev": true
+ },
+ "timers-browserify": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
+ "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
+ "dev": true,
+ "requires": {
+ "setimmediate": "^1.0.4"
+ }
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "to-arraybuffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+ "dev": true
+ },
+ "to-fast-properties": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+ "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+ "dev": true
+ },
+ "to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ },
+ "tough-cookie": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+ "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+ "dev": true,
+ "requires": {
+ "punycode": "^1.4.1"
+ }
+ },
+ "trim-newlines": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+ "dev": true
+ },
+ "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
+ },
+ "true-case-path": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+ "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.2"
+ }
+ },
+ "tty-browserify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+ "dev": true
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true,
+ "optional": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+ "dev": true
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.5.1",
+ "uglify-to-browserify": "~1.0.0",
+ "yargs": "~3.10.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "dev": true
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "dev": true,
+ "requires": {
+ "center-align": "^0.1.1",
+ "right-align": "^0.1.1",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^1.0.2",
+ "cliui": "^2.1.0",
+ "decamelize": "^1.0.0",
+ "window-size": "0.1.0"
+ }
+ }
+ }
+ },
+ "uglify-to-browserify": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+ "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+ "dev": true,
+ "optional": true
+ },
+ "uglifyjs-webpack-plugin": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
+ "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.5.6",
+ "uglify-js": "^2.8.29",
+ "webpack-sources": "^1.0.1"
+ }
+ },
+ "unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+ "dev": true
+ },
+ "union-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+ "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^0.4.3"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "set-value": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+ "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.1",
+ "to-object-path": "^0.3.0"
+ }
+ }
+ }
+ },
+ "unique-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz",
+ "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=",
+ "dev": true
+ },
+ "unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "dev": true,
+ "requires": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "dependencies": {
+ "has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "dependencies": {
+ "isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "requires": {
+ "isarray": "1.0.0"
+ }
+ }
+ }
+ },
+ "has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+ "dev": true
+ }
+ }
+ },
+ "upath": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
+ "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+ "dev": true
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ }
+ }
+ },
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "dev": true
+ },
+ "url": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+ "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+ "dev": true,
+ "requires": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+ "dev": true
+ }
+ }
+ },
+ "use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "dev": true
+ },
+ "user-home": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
+ "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
+ "dev": true
+ },
+ "util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "dev": true,
+ "requires": {
+ "inherits": "2.0.3"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+ "dev": true
+ },
+ "v8flags": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
+ "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
+ "dev": true,
+ "requires": {
+ "user-home": "^1.1.1"
+ }
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "vinyl": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz",
+ "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=",
+ "dev": true,
+ "requires": {
+ "clone": "^0.2.0",
+ "clone-stats": "^0.0.1"
+ }
+ },
+ "vinyl-fs": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz",
+ "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=",
+ "dev": true,
+ "requires": {
+ "defaults": "^1.0.0",
+ "glob-stream": "^3.1.5",
+ "glob-watcher": "^0.0.6",
+ "graceful-fs": "^3.0.0",
+ "mkdirp": "^0.5.0",
+ "strip-bom": "^1.0.0",
+ "through2": "^0.6.1",
+ "vinyl": "^0.4.0"
+ },
+ "dependencies": {
+ "graceful-fs": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz",
+ "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=",
+ "dev": true,
+ "requires": {
+ "natives": "^1.1.0"
+ }
+ },
+ "strip-bom": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz",
+ "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=",
+ "dev": true,
+ "requires": {
+ "first-chunk-stream": "^1.0.0",
+ "is-utf8": "^0.2.0"
+ }
+ }
+ }
+ },
+ "vinyl-sourcemaps-apply": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz",
+ "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.5.1"
+ }
+ },
+ "vm-browserify": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+ "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+ "dev": true,
+ "requires": {
+ "indexof": "0.0.1"
+ }
+ },
+ "watchpack": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+ "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^2.0.2",
+ "graceful-fs": "^4.1.2",
+ "neo-async": "^2.5.0"
+ }
+ },
+ "webpack": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.12.0.tgz",
+ "integrity": "sha512-Sw7MdIIOv/nkzPzee4o0EdvCuPmxT98+vVpIvwtcwcF1Q4SDSNp92vwcKc4REe7NItH9f1S4ra9FuQ7yuYZ8bQ==",
+ "dev": true,
+ "requires": {
+ "acorn": "^5.0.0",
+ "acorn-dynamic-import": "^2.0.0",
+ "ajv": "^6.1.0",
+ "ajv-keywords": "^3.1.0",
+ "async": "^2.1.2",
+ "enhanced-resolve": "^3.4.0",
+ "escope": "^3.6.0",
+ "interpret": "^1.0.0",
+ "json-loader": "^0.5.4",
+ "json5": "^0.5.1",
+ "loader-runner": "^2.3.0",
+ "loader-utils": "^1.1.0",
+ "memory-fs": "~0.4.1",
+ "mkdirp": "~0.5.0",
+ "node-libs-browser": "^2.0.0",
+ "source-map": "^0.5.3",
+ "supports-color": "^4.2.1",
+ "tapable": "^0.2.7",
+ "uglifyjs-webpack-plugin": "^0.4.6",
+ "watchpack": "^1.4.0",
+ "webpack-sources": "^1.0.1",
+ "yargs": "^8.0.2"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
+ "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ajv-keywords": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+ "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "dev": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "load-json-file": {
+ "version": "2.0.0",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "os-locale": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+ "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
+ "dev": true,
+ "requires": {
+ "execa": "^0.7.0",
+ "lcid": "^1.0.0",
+ "mem": "^1.1.0"
+ }
+ },
+ "path-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+ "dev": true,
+ "requires": {
+ "pify": "^2.0.0"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ },
+ "read-pkg": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^2.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^2.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+ "dev": true,
+ "requires": {
+ "find-up": "^2.0.0",
+ "read-pkg": "^2.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+ "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
+ "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^2.0.0",
+ "read-pkg-up": "^2.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^7.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
+ "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0"
+ }
+ }
+ }
+ },
+ "webpack-sources": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
+ "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
+ "dev": true,
+ "requires": {
+ "source-list-map": "^2.0.0",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "window-size": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+ "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+ "dev": true
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "write": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+ "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+ "dev": true
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+ "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^3.0.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^1.4.0",
+ "read-pkg-up": "^1.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^1.0.2",
+ "which-module": "^1.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^5.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+ "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^3.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+ "dev": true
+ }
+ }
+ }
+ }
+}
diff --git a/tests/test_moderation_flows.py b/tests/test_moderation_flows.py
index 8df99f8d..cf2517a2 100644
--- a/tests/test_moderation_flows.py
+++ b/tests/test_moderation_flows.py
@@ -16,6 +16,7 @@
from djangocms_moderation.utils import get_admin_url
+@skip('1.0.x rework TBC')
class ModerationFlowsTestCase(TestCase):
@classmethod
def setUpTestData(cls):
@@ -23,8 +24,6 @@ def setUpTestData(cls):
name='Workflow 1', is_default=True, requires_compliance_number=True
)
- cls.page = create_page(title='Page 1', template='page.html', language='en', )
-
# create users, groups and roles
cls.author = User.objects.create_superuser(
username='test1', email='test1@test1.com', password='test1',
@@ -36,6 +35,10 @@ def setUpTestData(cls):
username='test3', email='test3@test.com', password='test3',
)
+ cls.page = create_page(
+ title='Page 1', template='page.html', language='en', created_by=cls.author
+ )
+
cls.role1 = Role.objects.create(name='Role 1', user=cls.moderator_1)
cls.role2 = Role.objects.create(name='Role 2', user=cls.moderator_2)
@@ -69,7 +72,6 @@ def _resubmit_moderation_request(self, user, message='Test message - resubmit'):
def _cancel_moderation_request(self, user, message='Test message - cancel'):
return self._process_moderation_request(user, 'cancel', message)
- @skip('1.0.x rework TBC')
def test_approve_moderation_workflow(self):
"""
This case tests the following workflow:
@@ -129,7 +131,6 @@ def test_approve_moderation_workflow(self):
self.assertTrue(last_action.action, constants.ACTION_FINISHED)
self.assertEqual(moderation_request.compliance_number, compliance_number)
- @skip('1.0.x rework TBC')
def test_reject_moderation_workflow(self):
"""
This case tests the following workflow:
From bd9823d1ce982b4accc49c119419d40a812a6e72 Mon Sep 17 00:00:00 2001
From: Rizwan
Date: Tue, 25 Sep 2018 13:47:16 +0100
Subject: [PATCH 035/147] Prepopulate moderation collection author (#65)
---
djangocms_moderation/admin.py | 26 ++++++++++++++++++++------
djangocms_moderation/helpers.py | 14 --------------
2 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 3746666d..5f250de0 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+from django import forms
from django.conf.urls import url
from django.contrib import admin
from django.core.urlresolvers import reverse
@@ -26,7 +27,7 @@
RequestCommentForm,
WorkflowStepInlineFormSet,
)
-from .helpers import EditAndAddOnlyFieldsMixin, get_form_submission_for_step
+from .helpers import get_form_submission_for_step
from .models import (
CollectionComment,
ConfirmationFormSubmission,
@@ -273,11 +274,11 @@ class CollectionCommentAdmin(admin.ModelAdmin):
fields = ['collection', 'message', 'author']
def get_changeform_initial_data(self, request):
- # Extract the id from the URL. The id is stored in _changelsit_filters
- # by Django so that the request knows where to return to after form submission.
data = {
'author': request.user,
}
+ # Extract the id from the URL. The id is stored in _changelist_filters
+ # by Django so that the request knows where to return to after form submission.
collection_id = utils.extract_filter_param_from_changelist_url(
request, '_changelist_filters', 'collection__id__exact'
)
@@ -453,10 +454,8 @@ class WorkflowAdmin(admin.ModelAdmin):
]
-class ModerationCollectionAdmin(EditAndAddOnlyFieldsMixin, admin.ModelAdmin):
+class ModerationCollectionAdmin(admin.ModelAdmin):
actions = None # remove `delete_selected` for now, it will be handled later
- editonly_fields = ('status',) # fields editable only on EDIT
- addonly_fields = ('workflow',) # fields editable only on CREATE
list_filter = [
'author',
'status',
@@ -517,6 +516,21 @@ def _url(regex, fn, name, **kwargs):
]
return url_patterns + super().get_urls()
+ def get_changeform_initial_data(self, request):
+ return {'author': request.user}
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj:
+ return ['author', 'workflow']
+ else:
+ return ['status']
+
+ def get_form(self, request, obj=None, **kwargs):
+ form = super().get_form(request, obj, **kwargs)
+ if obj and 'author' in self.readonly_fields:
+ form.base_fields['author'].widget = forms.HiddenInput()
+ return form
+
class ConfirmationPageAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
view_on_site = True
diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py
index 0d01e346..82cb8156 100644
--- a/djangocms_moderation/helpers.py
+++ b/djangocms_moderation/helpers.py
@@ -34,17 +34,3 @@ def get_form_submission_for_step(active_request, current_step):
.filter(request=active_request, for_step=current_step)
)
return lookup.first()
-
-
-class EditAndAddOnlyFieldsMixin(object):
- editonly_fields = ()
- addonly_fields = ()
-
- def get_readonly_fields(self, request, obj=None):
- """
- Override to provide editonly_fields and addonly_fields functionality
- """
- if obj: # Editing an existing object, so `addonly_fields` should be readonly
- return self.readonly_fields + self.addonly_fields
- else: # Adding a new object
- return self.readonly_fields + self.editonly_fields
From 7690bd77cf634cfd8b7357e1682132c0123af63e Mon Sep 17 00:00:00 2001
From: Matus Moravcik
Date: Tue, 25 Sep 2018 13:47:32 +0100
Subject: [PATCH 036/147] Override version admin list view (#67)
---
djangocms_moderation/admin.py | 12 ++--
djangocms_moderation/apps.py | 3 +-
djangocms_moderation/cms_toolbars.py | 21 +++---
.../migrations/0005_auto_20180919_1348.py | 27 ++++++++
djangocms_moderation/models.py | 27 +-------
djangocms_moderation/monkeypatch.py | 68 +++++++++++++++++++
.../collectioncomment/change_list.html | 26 +++----
.../requestcomment/change_list.html | 19 ------
.../item_to_collection.html | 31 ++++++---
.../moderation_request_change_list.html | 12 ----
djangocms_moderation/utils.py | 27 +++++---
djangocms_moderation/views.py | 43 ++++++++++--
tests/test_admin.py | 6 +-
tests/test_monkeypatch.py | 61 +++++++++++++++++
tests/test_utils.py | 19 +++++-
tests/test_views.py | 56 +++++++++++++--
tests/utils/base.py | 4 ++
17 files changed, 332 insertions(+), 130 deletions(-)
create mode 100644 djangocms_moderation/migrations/0005_auto_20180919_1348.py
create mode 100644 djangocms_moderation/monkeypatch.py
create mode 100644 tests/test_monkeypatch.py
diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 5f250de0..fd6054dd 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -116,7 +116,7 @@ def get_list_display(self, request):
'id',
'get_content_type',
'get_title',
- 'get_content_author',
+ 'get_version_author',
'get_preview_link',
'get_status',
]
@@ -150,13 +150,9 @@ def get_comments_link(self, obj):
)
get_comments_link.short_description = _('Comments')
- def get_content_author(self, obj):
- """
- This is not necessarily the same person as the RequestAction author
- """
- # TODO this should get the author from the version object e.g. obj.content_object.created_by
- return "author placeholder"
- get_content_author.short_description = _('Content author')
+ def get_version_author(self, obj):
+ return obj.version.created_by
+ get_version_author.short_description = _('Version author')
def has_add_permission(self, request):
return False
diff --git a/djangocms_moderation/apps.py b/djangocms_moderation/apps.py
index 1e6fedd2..1ac2c50b 100644
--- a/djangocms_moderation/apps.py
+++ b/djangocms_moderation/apps.py
@@ -9,5 +9,6 @@ class ModerationConfig(AppConfig):
verbose_name = _('django CMS Moderation')
def ready(self):
- import djangocms_moderation.handlers
+ import djangocms_moderation.handlers # noqa: F401
import djangocms_moderation.signals # noqa: F401
+ import djangocms_moderation.monkeypatch # noqa: F401
diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py
index 9664ce50..9943bd08 100644
--- a/djangocms_moderation/cms_toolbars.py
+++ b/djangocms_moderation/cms_toolbars.py
@@ -7,8 +7,11 @@
from djangocms_versioning.cms_toolbars import VersioningToolbar
from djangocms_versioning.models import Version
-from .models import ModerationRequest
-from .utils import get_admin_url, is_obj_review_locked
+from .utils import (
+ get_active_moderation_request,
+ get_admin_url,
+ is_obj_review_locked,
+)
class ModerationToolbar(VersioningToolbar):
@@ -50,11 +53,8 @@ def _add_edit_button(self):
def _add_moderation_buttons(self):
if self._is_versioned() and self.toolbar.edit_mode_active:
- version = Version.objects.get_for_content(self.toolbar.obj)
- try:
- moderation_request = ModerationRequest.objects.get(
- version=version
- )
+ moderation_request = get_active_moderation_request(self.toolbar.obj)
+ if moderation_request:
self.toolbar.add_modal_button(
name=_('In Moderation "%(collection_name)s"') % {
'collection_name': moderation_request.collection.name
@@ -63,14 +63,17 @@ def _add_moderation_buttons(self):
disabled=True,
side=self.toolbar.RIGHT,
)
- except ModerationRequest.DoesNotExist:
+ else:
+ version = Version.objects.get_for_content(self.toolbar.obj)
url = add_url_parameters(
get_admin_url(
name='cms_moderation_item_to_collection',
language=self.current_lang,
args=()
),
- version_id=version.pk
+ version_id=version.pk,
+ # Indicate to the view that we opened the view as a modal
+ _modal=1,
)
self.toolbar.add_modal_button(
diff --git a/djangocms_moderation/migrations/0005_auto_20180919_1348.py b/djangocms_moderation/migrations/0005_auto_20180919_1348.py
new file mode 100644
index 00000000..29829e31
--- /dev/null
+++ b/djangocms_moderation/migrations/0005_auto_20180919_1348.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-09-19 12:48
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('djangocms_moderation', '0004_auto_20180907_1206'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='moderationcollection',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='moderator'),
+ ),
+ migrations.AlterField(
+ model_name='moderationrequest',
+ name='is_active',
+ field=models.BooleanField(db_index=True, default=True, verbose_name='is active'),
+ ),
+ ]
diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py
index c0ed4875..78077393 100644
--- a/djangocms_moderation/models.py
+++ b/djangocms_moderation/models.py
@@ -174,31 +174,6 @@ def clean(self):
def first_step(self):
return self.steps.first()
- def _lookup_active_request(self, page, language):
- lookup = (
- self
- .requests
- .filter(
- page=page,
- language=language,
- is_active=True,
- )
- )
- return lookup
-
- def get_active_request(self, page, language):
- lookup = self._lookup_active_request(page, language)
-
- try:
- active_request = lookup.get()
- except ModerationRequest.DoesNotExist:
- active_request = None
- return active_request
-
- def has_active_request(self, page, language):
- lookup = self._lookup_active_request(page, language)
- return lookup.exists()
-
@python_2_unicode_compatible
class WorkflowStep(models.Model):
@@ -358,7 +333,7 @@ class ModerationRequest(models.Model):
)
is_active = models.BooleanField(
verbose_name=_('is active'),
- default=False,
+ default=True,
db_index=True,
)
date_sent = models.DateTimeField(
diff --git a/djangocms_moderation/monkeypatch.py b/djangocms_moderation/monkeypatch.py
new file mode 100644
index 00000000..fb566b85
--- /dev/null
+++ b/djangocms_moderation/monkeypatch.py
@@ -0,0 +1,68 @@
+from django.utils.html import format_html
+from django.utils.translation import ugettext_lazy as _
+
+from cms.utils.urlutils import add_url_parameters
+
+from djangocms_versioning.admin import VersionAdmin
+from djangocms_versioning.constants import DRAFT
+
+from .utils import (
+ get_active_moderation_request,
+ get_admin_url,
+ is_obj_review_locked,
+)
+
+
+def get_state_actions(func):
+ """
+ Monkey patch VersionAdmin's get_state_actions to remove publish link,
+ as we don't want publishing CMSToolbar button in moderation.
+ + Add moderation link
+ """
+ def inner(self):
+ links = func(self)
+ links = [link for link in links if link != self._get_publish_link]
+ return links + [self._get_moderation_link]
+ return inner
+
+
+def _get_moderation_link(self, version, request):
+ if version.state != DRAFT:
+ return ''
+ moderation_request = get_active_moderation_request(version.content)
+ if moderation_request:
+ return _('In Moderation "%(collection_name)s"') % {
+ 'collection_name': moderation_request.collection.name
+ }
+ else:
+ url = add_url_parameters(
+ get_admin_url(
+ name='cms_moderation_item_to_collection',
+ language='en',
+ args=()
+ ),
+ version_id=version.pk
+ )
+ # TODO use a fancy icon as for the rest of the actions?
+ return format_html(
+ '{}',
+ url,
+ _('Submit for moderation')
+ )
+
+
+def _get_edit_link(func):
+ """
+ Don't display edit link if the object is review locked
+ """
+ def inner(self, version, request):
+ if is_obj_review_locked(version.content, request.user):
+ return ''
+ return func(self, version, request)
+
+ return inner
+
+
+VersionAdmin.get_state_actions = get_state_actions(VersionAdmin.get_state_actions)
+VersionAdmin._get_edit_link = _get_edit_link(VersionAdmin._get_edit_link)
+VersionAdmin._get_moderation_link = _get_moderation_link
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
index c5e5a79e..9e67890f 100644
--- a/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/collectioncomment/change_list.html
@@ -1,13 +1,14 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
-{% block object-tools-items %}
-{{ block.super }}
-{% if submit_for_review_url %}
-
- {% trans 'Submit for review' %}
-
-{% endif %}
+{% block content_title %}
+ {% if collection %}
+
+ {% blocktrans with collection.name|capfirst as collection_name %}
+ {{ collection_name }} comments
+ {% endblocktrans %}
+
+ {% endif %}
{% endblock %}
{% block breadcrumbs %}
@@ -18,14 +19,3 @@
› {% trans "Comments" %}
{% endblock %}
-
-{% block content %}
-{{ collection }}
-{{ block.super }}
-
-{% comment %}
- TODO This template will be overridden to provide the required functionality.
- For now, lets just output the selected collection
-{% endcomment %}
-
-{% endblock %}
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
index 55607ab1..3de2b183 100644
--- a/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/requestcomment/change_list.html
@@ -2,15 +2,6 @@
{% load i18n %}
{% load l10n %}
-{% block object-tools-items %}
-{{ block.super }}
-{% if submit_for_review_url %}
-
- {% trans 'Submit for review' %}
-
-{% endif %}
-{% endblock %}
-
{% block breadcrumbs %}
{% endblock %}
-
-{% block content %}
-{{ block.super }}
-
-{% comment %}
- TODO This template will be overridden to provide the required functionality.
- For now, lets just output the selected collection
-{% endcomment %}
-
-{% endblock %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
index c50efeda..49633f98 100644
--- a/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/item_to_collection.html
@@ -5,7 +5,11 @@
+{% endblock %}
+
+{% block extrahead %}
+{{ block.super }}
-{% endblock %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/items_to_collection.html b/djangocms_moderation/templates/djangocms_moderation/items_to_collection.html
index a252b278..b44ec140 100644
--- a/djangocms_moderation/templates/djangocms_moderation/items_to_collection.html
+++ b/djangocms_moderation/templates/djangocms_moderation/items_to_collection.html
@@ -1,19 +1,99 @@
-{% extends "djangocms_moderation/item_to_collection.html" %}
+{% extends "admin/change_form.html" %}
{% load i18n static %}
-{% block item_to_collection_header %}
+
+{% block content %}
+
+{% endblock %}
+
+{% block extrahead %}
+{{ block.super }}
+
{% endblock %}
diff --git a/djangocms_moderation/templates/djangocms_moderation/request_form.html b/djangocms_moderation/templates/djangocms_moderation/request_form.html
index 4fc0a38a..9abc8c70 100644
--- a/djangocms_moderation/templates/djangocms_moderation/request_form.html
+++ b/djangocms_moderation/templates/djangocms_moderation/request_form.html
@@ -8,7 +8,7 @@
{{ adminform.non_field_errors }}
{% endif %}
-