Esempio n. 1
0
class RevisionDashboardForm(forms.Form):
    locale = forms.ChoiceField(
        choices=LANG_CHOICES,
        # Required for non-translations, which is
        # enforced in Document.clean().
        required=False,
        label=_(u'Locale:'))
    user = StrippedCharField(
        min_length=1, max_length=255,
        required=False,
        label=_(u'User:'******'Topic:'))
    start_date = forms.DateField(
        required=False, label=_(u'Start Date:'),
        input_formats=['%m/%d/%Y'],
        widget=forms.TextInput(attrs={'pattern': '\d{1,2}/\d{1,2}/\d{4}'}))
    end_date = forms.DateField(
        required=False, label=_(u'End Date:'),
        input_formats=['%m/%d/%Y'],
        widget=forms.TextInput(attrs={'pattern': '\d{1,2}/\d{1,2}/\d{4}'}))
    preceding_period = forms.ChoiceField(
        choices=PERIOD_CHOICES,
        required=False,
        label=_(u'Preceding Period:'))
Esempio n. 2
0
class TreeMoveForm(forms.Form):
    title = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={"placeholder": TITLE_PLACEHOLDER}),
        label=_("Title:"),
        help_text=_("Title of article"),
        error_messages={
            "required": TITLE_REQUIRED,
            "min_length": TITLE_SHORT,
            "max_length": TITLE_LONG,
        },
    )
    slug = StrippedCharField(
        min_length=1,
        max_length=255,
        widget=forms.TextInput(),
        label=_("New slug:"),
        help_text=_("New article URL"),
        error_messages={
            "required": SLUG_REQUIRED,
            "min_length": SLUG_SHORT,
            "max_length": SLUG_LONG,
        },
    )
    locale = StrippedCharField(min_length=2,
                               max_length=5,
                               widget=forms.HiddenInput())

    def clean_slug(self):
        slug = self.cleaned_data["slug"]
        # We only want the slug here; inputting a full URL would lead
        # to disaster.
        if "://" in slug:
            raise forms.ValidationError("Please enter only the slug to move "
                                        "to, not the full URL.")

        # Removes leading slash and {locale/docs/} if necessary
        # IMPORTANT: This exact same regex is used on the client side, so
        # update both if doing so
        slug = SLUG_CLEANSING_RE.sub("", slug)

        # Remove the trailing slash if one is present, because it
        # will screw up the page move, which doesn't expect one.
        return slug.rstrip("/")

    def clean(self):
        cleaned_data = super(TreeMoveForm, self).clean()
        if set(["slug", "locale"]).issubset(cleaned_data):
            slug, locale = cleaned_data["slug"], cleaned_data["locale"]
            try:
                valid_slug_parent(slug, locale)
            except Exception as e:
                raise forms.ValidationError(e.args[0])
        return cleaned_data
Esempio n. 3
0
class TreeMoveForm(forms.Form):
    title = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': TITLE_PLACEHOLDER}),
        label=_lazy(u'Title:'),
        help_text=_lazy(u'Title of article'),
        error_messages={
            'required': TITLE_REQUIRED,
            'min_length': TITLE_SHORT,
            'max_length': TITLE_LONG
        })
    slug = StrippedCharField(min_length=1,
                             max_length=255,
                             widget=forms.TextInput(),
                             label=_lazy(u'New slug:'),
                             help_text=_lazy(u'New article URL'),
                             error_messages={
                                 'required': SLUG_REQUIRED,
                                 'min_length': SLUG_SHORT,
                                 'max_length': SLUG_LONG
                             })
    locale = StrippedCharField(min_length=2,
                               max_length=5,
                               widget=forms.HiddenInput())

    def clean_slug(self):
        # We only want the slug here; inputting a full URL would lead
        # to disaster.
        if '://' in self.cleaned_data['slug']:
            raise forms.ValidationError('Please enter only the slug to move '
                                        'to, not the full URL.')

        # Removes leading slash and {locale/docs/} if necessary
        # IMPORTANT: This exact same regex is used on the client side, so
        # update both if doing so
        self.cleaned_data['slug'] = SLUG_CLEANSING_RE.sub(
            '', self.cleaned_data['slug'])

        # Remove the trailing slash if one is present, because it
        # will screw up the page move, which doesn't expect one.
        self.cleaned_data['slug'] = self.cleaned_data['slug'].rstrip('/')

        return self.cleaned_data['slug']

    def clean(self):
        cleaned_data = super(TreeMoveForm, self).clean()
        if set(['slug', 'locale']).issubset(cleaned_data):
            slug, locale = cleaned_data['slug'], cleaned_data['locale']
            try:
                valid_slug_parent(slug, locale)
            except Exception, e:
                raise forms.ValidationError(e.args[0])
        return cleaned_data
Esempio n. 4
0
class RevisionDashboardForm(forms.Form):
    ALL_AUTHORS = 0
    KNOWN_AUTHORS = 1
    UNKNOWN_AUTHORS = 2
    AUTHOR_CHOICES = [
        (ALL_AUTHORS, _("All Authors")),
        (KNOWN_AUTHORS, _("Known Authors")),
        (UNKNOWN_AUTHORS, _("Unknown Authors")),
    ]

    locale = forms.ChoiceField(
        choices=LANG_CHOICES,
        # Required for non-translations, which is
        # enforced in Document.clean().
        required=False,
        label=_("Locale:"),
    )
    user = StrippedCharField(min_length=1,
                             max_length=255,
                             required=False,
                             label=_("User:"******"Topic:"))
    start_date = forms.DateField(
        required=False,
        label=_("Start Date:"),
        input_formats=["%m/%d/%Y"],
        widget=forms.TextInput(attrs={"pattern": r"\d{1,2}/\d{1,2}/\d{4}"}),
    )
    end_date = forms.DateField(
        required=False,
        label=_("End Date:"),
        input_formats=["%m/%d/%Y"],
        widget=forms.TextInput(attrs={"pattern": r"\d{1,2}/\d{1,2}/\d{4}"}),
    )
    preceding_period = forms.ChoiceField(choices=PERIOD_CHOICES,
                                         required=False,
                                         label=_("Preceding Period:"))
    authors = forms.ChoiceField(choices=AUTHOR_CHOICES,
                                required=False,
                                label=_("Authors"))
Esempio n. 5
0
class RevisionDashboardForm(forms.Form):
    ALL_AUTHORS = 0
    KNOWN_AUTHORS = 1
    UNKNOWN_AUTHORS = 2
    AUTHOR_CHOICES = [
        (ALL_AUTHORS, _('All Authors')),
        (KNOWN_AUTHORS, _('Known Authors')),
        (UNKNOWN_AUTHORS, _('Unknown Authors')),
    ]

    locale = forms.ChoiceField(
        choices=LANG_CHOICES,
        # Required for non-translations, which is
        # enforced in Document.clean().
        required=False,
        label=_('Locale:'))
    user = StrippedCharField(min_length=1,
                             max_length=255,
                             required=False,
                             label=_('User:'******'Topic:'))
    start_date = forms.DateField(
        required=False,
        label=_('Start Date:'),
        input_formats=['%m/%d/%Y'],
        widget=forms.TextInput(attrs={'pattern': r'\d{1,2}/\d{1,2}/\d{4}'}))
    end_date = forms.DateField(
        required=False,
        label=_('End Date:'),
        input_formats=['%m/%d/%Y'],
        widget=forms.TextInput(attrs={'pattern': r'\d{1,2}/\d{1,2}/\d{4}'}))
    preceding_period = forms.ChoiceField(choices=PERIOD_CHOICES,
                                         required=False,
                                         label=_('Preceding Period:'))
    authors = forms.ChoiceField(choices=AUTHOR_CHOICES,
                                required=False,
                                label=_('Authors'))
class RevisionForm(AkismetCheckFormMixin, forms.ModelForm):
    """
    Form to create new revisions.
    """
    title = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': TITLE_PLACEHOLDER}),
        label=_(u'Title:'),
        help_text=_(u'Title of article'),
        error_messages={
            'required': TITLE_REQUIRED,
            'min_length': TITLE_SHORT,
            'max_length': TITLE_LONG,
        }
    )

    slug = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(),
        label=_(u'Slug:'),
        help_text=_(u'Article URL'),
        error_messages={
            'required': SLUG_REQUIRED,
            'min_length': SLUG_SHORT,
            'max_length': SLUG_LONG,
        }
    )

    tags = StrippedCharField(
        required=False,
        max_length=255,
        label=_(u'Tags:'),
        error_messages={
            'max_length': TAGS_LONG,
        }
    )

    keywords = StrippedCharField(
        required=False,
        label=_(u'Keywords:'),
        help_text=_(u'Affects search results'),
    )

    summary = StrippedCharField(
        required=False,
        min_length=5,
        max_length=1000,
        widget=forms.Textarea(),
        label=_(u'Search result summary:'),
        help_text=_(u'Only displayed on search results page'),
        error_messages={
            'required': SUMMARY_REQUIRED,
            'min_length': SUMMARY_SHORT,
            'max_length': SUMMARY_LONG
        },
    )

    content = StrippedCharField(
        min_length=5,
        max_length=300000,
        label=_(u'Content:'),
        widget=forms.Textarea(),
        error_messages={
            'required': CONTENT_REQUIRED,
            'min_length': CONTENT_SHORT,
            'max_length': CONTENT_LONG,
        }
    )

    comment = StrippedCharField(
        max_length=255,
        required=False,
        label=_(u'Comment:')
    )

    review_tags = forms.MultipleChoiceField(
        label=ugettext("Tag this revision for review?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=REVIEW_FLAG_TAGS,
    )

    localization_tags = forms.MultipleChoiceField(
        label=ugettext("Tag this revision for localization?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=LOCALIZATION_FLAG_TAGS,
    )

    current_rev = forms.CharField(
        required=False,
        widget=forms.HiddenInput(),
    )

    class Meta(object):
        model = Revision
        fields = ('title', 'slug', 'tags', 'keywords', 'summary', 'content',
                  'comment', 'based_on', 'toc_depth',
                  'render_max_age')

    def __init__(self, *args, **kwargs):
        self.section_id = kwargs.pop('section_id', None)
        self.is_async_submit = kwargs.pop('is_async_submit', None)

        # when creating a new document with a parent, this will be set
        self.parent_slug = kwargs.pop('parent_slug', None)

        super(RevisionForm, self).__init__(*args, **kwargs)

        self.fields['based_on'].widget = forms.HiddenInput()

        if self.instance and self.instance.pk:
            # Ensure both title and slug are populated from parent document,
            # if last revision didn't have them
            if not self.instance.title:
                self.initial['title'] = self.instance.document.title
            if not self.instance.slug:
                self.initial['slug'] = self.instance.document.slug

            content = self.instance.content
            parsed_content = kuma.wiki.content.parse(content)
            parsed_content.injectSectionIDs()
            if self.section_id:
                parsed_content.extractSection(self.section_id)
            parsed_content.filterEditorSafety()
            content = parsed_content.serialize()
            self.initial['content'] = content

            self.initial['review_tags'] = list(self.instance
                                                   .review_tags
                                                   .names())
            self.initial['localization_tags'] = list(self.instance
                                                         .localization_tags
                                                         .names())

        if self.section_id:
            self.fields['toc_depth'].required = False

    def clean_slug(self):
        # Since this form can change the URL of the page on which the editing
        # happens, changes to the slug are ignored for an iframe submissions
        if self.is_async_submit:
            return self.instance.document.slug

        # Get the cleaned slug
        slug = self.cleaned_data['slug']

        # first check if the given slug doesn't contain slashes and other
        # characters not allowed in a revision slug component (without parent)
        if slug and INVALID_REV_SLUG_CHARS_RE.search(slug):
            raise forms.ValidationError(SLUG_INVALID)

        # edits can come in without a slug, so default to the current doc slug
        if not slug:
            try:
                slug = self.instance.slug = self.instance.document.slug
            except ObjectDoesNotExist:
                pass

        # then if there is a parent document we prefix the slug with its slug
        if self.parent_slug:
            slug = u'/'.join([self.parent_slug, slug])

        try:
            doc = Document.objects.get(locale=self.instance.document.locale,
                                       slug=slug)
            if self.instance and self.instance.document:
                if (not doc.get_redirect_url() and
                        doc.pk != self.instance.document.pk):
                    # There's another document with this value,
                    # and we're not a revision of it.
                    raise forms.ValidationError(SLUG_COLLIDES)
            else:
                # This document-and-revision doesn't exist yet, so there
                # shouldn't be any collisions at all.
                raise forms.ValidationError(SLUG_COLLIDES)

        except Document.DoesNotExist:
            # No existing document for this value, so we're good here.
            pass

        return slug

    def clean_tags(self):
        """
        Validate the tags ensuring we have no case-sensitive duplicates.
        """
        tags = self.cleaned_data['tags']
        cleaned_tags = []

        if tags:
            for tag in parse_tags(tags):
                # Note: The exact match query doesn't work correctly with
                # MySQL with regards to case-sensitivity. If we move to
                # Postgresql in the future this code may need to change.
                doc_tag = (DocumentTag.objects.filter(name__exact=tag)
                                              .values_list('name', flat=True))

                # Write a log we can grep to help find pre-existing duplicate
                # document tags for cleanup.
                if len(doc_tag) > 1:
                    log.warn('Found duplicate document tags: %s' % doc_tag)

                if doc_tag:
                    if doc_tag[0] != tag and doc_tag[0].lower() == tag.lower():
                        # The tag differs only by case. Do not add a new one,
                        # add the existing one.
                        cleaned_tags.append(doc_tag[0])
                        continue

                cleaned_tags.append(tag)

        return ' '.join([u'"%s"' % t for t in cleaned_tags])

    def clean_content(self):
        """
        Validate the content, performing any section editing if necessary
        """
        content = self.cleaned_data['content']

        # If we're editing a section, we need to replace the section content
        # from the current revision.
        if self.section_id and self.instance and self.instance.document:
            # Make sure we start with content form the latest revision.
            full_content = self.instance.document.current_revision.content
            # Replace the section content with the form content.
            parsed_content = kuma.wiki.content.parse(full_content)
            parsed_content.replaceSection(self.section_id, content)
            content = parsed_content.serialize()

        return content

    def clean_current_rev(self):
        """
        If a current revision is supplied in the form, compare it against
        what the document claims is the current revision. If there's a
        difference, then an edit has occurred since the form was constructed
        and we treat it as a mid-air collision.
        """
        current_rev = self.cleaned_data.get('current_rev', None)

        if not current_rev:
            # If there's no current_rev, just bail.
            return current_rev

        try:
            doc_current_rev = self.instance.document.current_revision.id
            if unicode(current_rev) != unicode(doc_current_rev):

                if (self.section_id and self.instance and
                        self.instance.document):
                    # This is a section edit. So, even though the revision has
                    # changed, it still might not be a collision if the section
                    # in particular hasn't changed.
                    orig_ct = (Revision.objects
                                       .get(pk=current_rev)
                                       .get_section_content(self.section_id))
                    curr_ct = (self.instance
                                   .document.current_revision
                                   .get_section_content(self.section_id))
                    if orig_ct != curr_ct:
                        # Oops. Looks like the section did actually get
                        # changed, so yeah this is a collision.
                        url = reverse(
                            'wiki.document_revisions',
                            kwargs={'document_path': self.instance.document.slug}
                        )
                        raise forms.ValidationError(MIDAIR_COLLISION % {'url': url})

                    return current_rev

                else:
                    # No section edit, so this is a flat-out collision.
                    url = reverse(
                        'wiki.document_revisions',
                        kwargs={'document_path': self.instance.document.slug}
                    )
                    raise forms.ValidationError(MIDAIR_COLLISION % {'url': url})

        except Document.DoesNotExist:
            # If there's no document yet, just bail.
            return current_rev

    def akismet_enabled(self):
        """
        Disables Akismet for users with SPAM_EXEMPTED_FLAG
        """
        client_ready = super(RevisionForm, self).akismet_enabled()
        user_exempted = waffle.flag_is_active(self.request, SPAM_EXEMPTED_FLAG)
        return client_ready and not user_exempted

    @property
    def akismet_error_message(self):
        request = getattr(self, 'request', None)
        user = request and request.user
        return mark_safe(render_to_string('wiki/includes/spam_error.html',
                                          {'user': user}))

    def akismet_error(self, parameters, exception=None):
        """
        Upon errors from the Akismet API records the user, document
        and date of the attempt for further analysis. Then call the
        parent class' error handler.
        """
        try:
            document = self.instance.document
        except ObjectDoesNotExist:
            document = None

        if exception and isinstance(exception, AkismetError):
            # For Akismet errors, save the submission and exception details
            dsa_params = parameters.copy()
            dsa_params['akismet_status_code'] = exception.status_code
            dsa_params['akismet_debug_help'] = exception.debug_help
            dsa_params['akismet_response'] = exception.response.content
            review = DocumentSpamAttempt.AKISMET_ERROR
        else:
            # For detected spam, save the details for review
            dsa_params = parameters
            review = DocumentSpamAttempt.NEEDS_REVIEW

        # Wrapping this in a try/finally to make sure that even if
        # creating a spam attempt object fails we call the parent
        # method that raises a ValidationError
        try:
            DocumentSpamAttempt.objects.create(
                title=self.cleaned_data['title'],
                slug=self.cleaned_data['slug'],
                user=self.request.user,
                document=document,
                data=json.dumps(dsa_params, indent=2, sort_keys=True),
                review=review
            )
        finally:
            if not waffle.flag_is_active(self.request, SPAM_TRAINING_FLAG):
                super(RevisionForm, self).akismet_error(parameters, exception)

    def akismet_parameters(self):
        """
        Returns the parameters for Akismet's check-comment API endpoint.

        The form cleaning also saves the data into the instance, which will
        cause future calls to return different data. The results during the
        initial form cleaning are cached in ._akismet_data, and returned for
        future calls, such as the unit tests.
        """
        if not getattr(self, '_akismet_data', None):
            try:
                document = self.instance.document
            except ObjectDoesNotExist:
                self._akismet_data = AkismetNewDocumentData(
                    self.request, self.cleaned_data, self.data.get('locale'))
            else:
                if document.current_revision:
                    self._akismet_data = AkismetEditDocumentData(
                        self.request, self.cleaned_data, document)
                else:
                    # New translation, compare to English document
                    based_on = self.cleaned_data.get('based_on')
                    assert based_on, 'Expected a new translation.'
                    document = based_on.document
                    self._akismet_data = AkismetNewTranslationData(
                        self.request, self.cleaned_data, document,
                        self.data.get('locale'))

        parameters = self._akismet_data.parameters.copy()
        parameters.update(self.akismet_parameter_overrides())
        return parameters

    def akismet_call(self, parameters):
        """
        Skip Akismet check if the content is the same.

        This happens if the edit is to a non-content field, such as
        setting or clearing the technical review flag.
        """
        if not parameters['comment_content']:
            return False  # No content change, not spam
        return super(RevisionForm, self).akismet_call(parameters)

    def save(self, document, **kwargs):
        """
        Persists the revision and returns it.
        Takes the view request and document of the revision.
        Does some specific things when the revision is fully saved.
        """
        # have to check for first edit before we save
        is_first_edit = not self.request.user.wiki_revisions().exists()

        # Making sure we don't commit the saving right away since we
        # want to do other things here.
        kwargs['commit'] = False

        if self.section_id and self.instance and self.instance.document:
            # The logic to save a section is slightly different and may
            # need to evolve over time; a section edit doesn't submit
            # all the fields, and we need to account for that when we
            # construct the new Revision.
            doc = Document.objects.get(pk=self.instance.document.id)
            old_rev = doc.current_revision
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = self.request.user
            new_rev.toc_depth = old_rev.toc_depth
            new_rev.save()
            new_rev.review_tags.set(*list(old_rev.review_tags.names()))

        else:
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = self.request.user
            new_rev.toc_depth = self.cleaned_data['toc_depth']
            new_rev.save()
            new_rev.review_tags.set(*self.cleaned_data['review_tags'])
            new_rev.localization_tags.set(*self.cleaned_data['localization_tags'])

            # when enabled store the user's IP address
            if waffle.switch_is_active('store_revision_ips'):
                RevisionIP.objects.log(
                    revision=new_rev,
                    headers=self.request.META,
                    data=json.dumps(self.akismet_parameters(),
                                    indent=2, sort_keys=True)
                )

            # send first edit emails
            if is_first_edit:
                send_first_edit_email.delay(new_rev.pk)

            # schedule a document rendering
            document.schedule_rendering('max-age=0')

            # schedule event notifications
            EditDocumentEvent(new_rev).fire(exclude=new_rev.creator)

        return new_rev
class DocumentForm(forms.ModelForm):
    """
    Used for managing the wiki document data model that houses general
    data of a wiki page.
    """
    title = StrippedCharField(min_length=1,
                              max_length=255,
                              widget=forms.TextInput(
                                  attrs={'placeholder': TITLE_PLACEHOLDER}),
                              label=_(u'Title:'),
                              help_text=_(u'Title of article'),
                              error_messages={'required': TITLE_REQUIRED,
                                              'min_length': TITLE_SHORT,
                                              'max_length': TITLE_LONG})

    slug = StrippedCharField(min_length=1,
                             max_length=255,
                             widget=forms.TextInput(),
                             label=_(u'Slug:'),
                             help_text=_(u'Article URL'),
                             error_messages={'required': SLUG_REQUIRED,
                                             'min_length': SLUG_SHORT,
                                             'max_length': SLUG_LONG})

    parent_topic = forms.ModelChoiceField(queryset=Document.objects.all(),
                                          required=False,
                                          label=_(u'Parent:'))

    locale = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = Document
        fields = ('title', 'slug', 'locale')

    def __init__(self, *args, **kwargs):
        # when creating a new document with a parent, this will be set
        self.parent_slug = kwargs.pop('parent_slug', None)
        super(DocumentForm, self).__init__(*args, **kwargs)

    def clean_slug(self):
        slug = self.cleaned_data['slug']
        if slug == '':
            # Default to the title, if missing.
            slug = self.cleaned_data['title']
        elif self.parent_slug:
            # Prepend parent slug if given from view
            slug = self.parent_slug + '/' + slug
        # check both for disallowed characters and match for the allowed
        if (INVALID_DOC_SLUG_CHARS_RE.search(slug) or
                not DOCUMENT_PATH_RE.search(slug)):
            raise forms.ValidationError(SLUG_INVALID)
        # Guard against slugs that match urlpatterns
        for pattern in RESERVED_SLUGS_RES:
            if pattern.match(slug):
                raise forms.ValidationError(SLUG_INVALID)
        return slug

    def save(self, parent=None, *args, **kwargs):
        """Persist the Document form, and return the saved Document."""
        doc = super(DocumentForm, self).save(commit=False, *args, **kwargs)
        doc.parent = parent
        if 'parent_topic' in self.cleaned_data:
            doc.parent_topic = self.cleaned_data['parent_topic']
        doc.save()
        # not strictly necessary since we didn't change
        # any m2m data since we instantiated the doc
        self.save_m2m()
        return doc
Esempio n. 8
0
class ContributionForm(forms.Form):
    name = StrippedCharField(
        min_length=1,
        max_length=255,
        label=_('Your full name'),
        widget=forms.TextInput(
            attrs={
                'class': 'form-input form-input-email',
                'placeholder': _('Your full name'),
                'data-error-message': _('Required')
            }
        )
    )
    email = forms.EmailField(
        label=_('Your email'),
        widget=forms.EmailInput(
            attrs={
                'class': 'form-input form-input-email',
                'placeholder': _('*****@*****.**'),
                'data-error-message': _('Must be a valid email'),
                'title': _('Why do you need my email address? This is so we'
                           ' can send you a receipt of your contribution. This'
                           ' is handy if you would like a refund.')
            }
        )
    )
    donation_choices = forms.TypedChoiceField(
        required=False,
        choices=DONATION_CHOICES,
        label=_('Contribution choices'),
        empty_value=0,
        coerce=int,
        initial=DONATION_CHOICES[1][0],
        widget=forms.RadioSelect(
            attrs={
                'class': 'form-radios form-radios-donation-choices',
                'data-dynamic-choice-selector': ''
            }
        )
    )
    donation_amount = forms.DecimalField(
        required=False,
        label='$',
        max_digits=10,
        decimal_places=2,
        widget=forms.TextInput(
            attrs={
                'type': 'number',
                'step': '0.01',
                'class': 'form-input form-input-amount',
                'placeholder': _('Other'),
                'data-error-message': _('Must be more than $1')
            }
        ),
        validators=[MinValueValidator(1)]
    )
    stripe_token = forms.CharField(
        label=u'',
        required=False,
        widget=forms.HiddenInput(),
        max_length=255
    )
    stripe_public_key = forms.CharField(
        label=u'',
        required=False,
        widget=forms.HiddenInput(),
        max_length=255
    )

    accept_checkbox = forms.BooleanField(
        label=u'',
        required=False,
        widget=forms.CheckboxInput(
            attrs={
                'class': 'required checkbox form-control',
                'data-error-message': _('You must agree to the terms to continue')
            },
        ),
    )

    def clean(self):
        """Validate that either an amount or set choice was made."""
        d = self.cleaned_data
        donation_choice = d.get('donation_choices', False)
        donation_amount = d.get('donation_amount', False)

        no_selection = not (donation_amount or donation_choice)
        both_selections = donation_amount and donation_choice
        if no_selection or both_selections:
            raise forms.ValidationError(_('Please enter an amount or choose'
                                          ' from the pre-selected options'))
        return d

    def __init__(self, *args, **kwargs):
        super(ContributionForm, self).__init__(*args, **kwargs)
        self.fields['stripe_public_key'].initial = settings.STRIPE_PUBLIC_KEY

    def get_amount(self):
        amount = self.cleaned_data['donation_amount'] or self.cleaned_data['donation_choices']
        if isinstance(amount, Decimal):
            amount = amount * Decimal('100')
            amount = amount.quantize(Decimal('0'))
        else:
            amount = amount * 100
        return amount

    def get_token(self):
        token = self.cleaned_data.get('stripe_token', '')
        if not token:
            log.error(
                'Stripe error!, something went wrong, cant find STRIPE_TOKEN for {} [{}]'.format(
                    self.cleaned_data['name'],
                    self.cleaned_data['email']
                )
            )
        return token

    def make_charge(self):
        """Make a charge using the Stripe API and validated form."""
        amount = self.get_amount()
        token = self.get_token()
        if token and amount:
            try:
                stripe.Charge.create(
                    amount=amount,
                    currency='usd',
                    source=token,
                    description='Support MDN Web Docs',
                    receipt_email=self.cleaned_data['email'],
                    metadata={'name': self.cleaned_data['name']}
                )
                return True
            except stripe.error.CardError as e:
                body = e.json_body
                err = body.get('error', {})
                log.error("""
Status is: {http_status}
Type is: {type}
Code is: {code}
Param is: {param}
Message is: {message}
User name: {name}
User email: {email}""".format(**{
                    'http_status': e.http_status,
                    'type': err.get('type'),
                    'code': err.get('code'),
                    'param': err.get('param'),
                    'message': err.get('message'),
                    'name': self.cleaned_data['name'],
                    'email': self.cleaned_data['email']
                }))
            except stripe.error.RateLimitError as e:
                log.error(
                    'Stripe: Too many requests made to the API too quickly: {} [{}] {}'.format(
                        self.cleaned_data['name'],
                        self.cleaned_data['email'],
                        e
                    )
                )
            except stripe.error.InvalidRequestError as e:
                log.error(
                    'Stripe: Invalid parameters were supplied to Stripe API: {} [{}] {}'.format(
                        self.cleaned_data['name'],
                        self.cleaned_data['email'],
                        e
                    )
                )
            except stripe.error.AuthenticationError as e:
                log.error(
                    'Stripe: Authentication with Stripe API failed (maybe you changed API keys recently)): ' +
                    '{} [{}] {}'.format(
                        self.cleaned_data['name'],
                        self.cleaned_data['email'],
                        e
                    )
                )
            except stripe.error.APIConnectionError as e:
                log.error(
                    'Stripe: Network communication with Stripe failed: {} [{}] {}'.format(
                        self.cleaned_data['name'],
                        self.cleaned_data['email'],
                        e
                    )
                )
            except Exception as e:
                log.error(
                    'Stripe charge, something went wrong: {} [{}] {}'.format(
                        self.cleaned_data['name'],
                        self.cleaned_data['email'],
                        e
                    )
                )
        return False
Esempio n. 9
0
class DocumentForm(forms.ModelForm):
    """Form to create/edit a document."""

    title = StrippedCharField(min_length=1, max_length=255,
                              widget=forms.TextInput(
                                  attrs={'placeholder': TITLE_PLACEHOLDER}),
                              label=_lazy(u'Title:'),
                              help_text=_lazy(u'Title of article'),
                              error_messages={'required': TITLE_REQUIRED,
                                              'min_length': TITLE_SHORT,
                                              'max_length': TITLE_LONG})

    slug = StrippedCharField(min_length=1, max_length=255,
                             widget=forms.TextInput(),
                             label=_lazy(u'Slug:'),
                             help_text=_lazy(u'Article URL'),
                             error_messages={'required': SLUG_REQUIRED,
                                             'min_length': SLUG_SHORT,
                                             'max_length': SLUG_LONG})

    category = forms.ChoiceField(choices=Document.CATEGORIES,
                                 initial=10,
                                 # Required for non-translations, which is
                                 # enforced in Document.clean().
                                 required=False,
                                 label=_lazy(u'Category:'),
                                 help_text=_lazy(u'Type of article'),
                                 widget=forms.HiddenInput())

    parent_topic = forms.ModelChoiceField(queryset=Document.objects.all(),
                                          required=False,
                                          label=_lazy(u'Parent:'))

    locale = forms.CharField(widget=forms.HiddenInput())

    def clean_slug(self):
        slug = self.cleaned_data['slug']
        if slug == '':
            # Default to the title, if missing.
            slug = self.cleaned_data['title']
        # "?", " ", quote disallowed in slugs altogether
        if '?' in slug or ' ' in slug or '"' in slug or "'" in slug:
            raise forms.ValidationError(SLUG_INVALID)
        # Pattern copied from urls.py
        if not re.compile(r'^[^\$]+$').match(slug):
            raise forms.ValidationError(SLUG_INVALID)
        # Guard against slugs that match urlpatterns
        for pat in RESERVED_SLUGS:
            if re.compile(pat).match(slug):
                raise forms.ValidationError(SLUG_INVALID)
        return slug

    class Meta:
        model = Document
        fields = ('title', 'slug', 'category', 'locale')

    def save(self, parent_doc, **kwargs):
        """Persist the Document form, and return the saved Document."""
        doc = super(DocumentForm, self).save(commit=False, **kwargs)
        doc.parent = parent_doc
        if 'parent_topic' in self.cleaned_data:
            doc.parent_topic = self.cleaned_data['parent_topic']
        doc.save()
        # not strictly necessary since we didn't change
        # any m2m data since we instantiated the doc
        self.save_m2m()
        return doc
Esempio n. 10
0
class RevisionForm(forms.ModelForm):
    """Form to create new revisions."""

    title = StrippedCharField(min_length=1, max_length=255,
                              required=False,
                              widget=forms.TextInput(
                                  attrs={'placeholder': TITLE_PLACEHOLDER}),
                              label=_lazy(u'Title:'),
                              help_text=_lazy(u'Title of article'),
                              error_messages={'required': TITLE_REQUIRED,
                                              'min_length': TITLE_SHORT,
                                              'max_length': TITLE_LONG})
    slug = StrippedCharField(min_length=1, max_length=255,
                             required=False,
                             widget=forms.TextInput(),
                             label=_lazy(u'Slug:'),
                             help_text=_lazy(u'Article URL'),
                             error_messages={'required': SLUG_REQUIRED,
                                             'min_length': SLUG_SHORT,
                                             'max_length': SLUG_LONG})

    tags = StrippedCharField(required=False,
                             label=_lazy(u'Tags:'))

    keywords = StrippedCharField(required=False,
                                 label=_lazy(u'Keywords:'),
                                 help_text=_lazy(u'Affects search results'))

    summary = StrippedCharField(
        required=False,
        min_length=5, max_length=1000,
        widget=forms.Textarea(),
        label=_lazy(u'Search result summary:'),
        help_text=_lazy(u'Only displayed on search results page'),
        error_messages={'required': SUMMARY_REQUIRED,
                        'min_length': SUMMARY_SHORT,
                        'max_length': SUMMARY_LONG})

    content = StrippedCharField(
        min_length=5, max_length=300000,
        label=_lazy(u'Content:'),
        widget=forms.Textarea(),
        error_messages={'required': CONTENT_REQUIRED,
                        'min_length': CONTENT_SHORT,
                        'max_length': CONTENT_LONG})

    comment = StrippedCharField(required=False, label=_lazy(u'Comment:'))

    review_tags = forms.MultipleChoiceField(
        label=_("Tag this revision for review?"),
        widget=CheckboxSelectMultiple, required=False,
        choices=REVIEW_FLAG_TAGS)

    localization_tags = forms.MultipleChoiceField(
        label=_("Tag this revision for localization?"),
        widget=CheckboxSelectMultiple, required=False,
        choices=LOCALIZATION_FLAG_TAGS)

    current_rev = forms.CharField(required=False,
                                  widget=forms.HiddenInput())

    class Meta(object):
        model = Revision
        fields = ('title', 'slug', 'tags', 'keywords', 'summary', 'content',
                  'comment', 'based_on', 'toc_depth',
                  'render_max_age')

    def __init__(self, *args, **kwargs):

        # Snag some optional kwargs and delete them before calling
        # super-constructor.
        for n in ('section_id', 'is_iframe_target'):
            if n not in kwargs:
                setattr(self, n, None)
            else:
                setattr(self, n, kwargs[n])
                del kwargs[n]

        super(RevisionForm, self).__init__(*args, **kwargs)
        self.fields['based_on'].widget = forms.HiddenInput()

        if self.instance and self.instance.pk:

            # Ensure both title and slug are populated from parent document, if
            # last revision didn't have them
            if not self.instance.title:
                self.initial['title'] = self.instance.document.title
            if not self.instance.slug:
                self.initial['slug'] = self.instance.document.slug

            content = self.instance.content
            if not self.instance.document.is_template:
                tool = kuma.wiki.content.parse(content)
                tool.injectSectionIDs()
                if self.section_id:
                    tool.extractSection(self.section_id)
                tool.filterEditorSafety()
                content = tool.serialize()
            self.initial['content'] = content

            self.initial['review_tags'] = list(self.instance.review_tags
                                                            .values_list('name',
                                                                         flat=True))
            self.initial['localization_tags'] = list(self.instance
                                                         .localization_tags
                                                         .values_list('name',
                                                                      flat=True))

        if self.section_id:
            self.fields['toc_depth'].required = False

    def _clean_collidable(self, name):
        value = self.cleaned_data[name]

        if self.is_iframe_target:
            # Since these collidables can change the URL of the page, changes
            # to them are ignored for an iframe submission
            return getattr(self.instance.document, name)

        error_message = {'slug': SLUG_COLLIDES}.get(name, OTHER_COLLIDES)
        try:
            existing_doc = Document.objects.get(
                locale=self.instance.document.locale,
                **{name: value})
            if self.instance and self.instance.document:
                if (not existing_doc.redirect_url() and
                        existing_doc.pk != self.instance.document.pk):
                    # There's another document with this value,
                    # and we're not a revision of it.
                    raise forms.ValidationError(error_message)
            else:
                # This document-and-revision doesn't exist yet, so there
                # shouldn't be any collisions at all.
                raise forms.ValidationError(error_message)

        except Document.DoesNotExist:
            # No existing document for this value, so we're good here.
            pass

        return value

    def clean_slug(self):
        # TODO: move this check somewhere else?
        # edits can come in without a slug, so default to the current doc slug
        if not self.cleaned_data['slug']:
            existing_slug = self.instance.document.slug
            self.cleaned_data['slug'] = self.instance.slug = existing_slug
        cleaned_slug = self._clean_collidable('slug')
        return cleaned_slug

    def clean_content(self):
        """Validate the content, performing any section editing if necessary"""
        content = self.cleaned_data['content']

        # If we're editing a section, we need to replace the section content
        # from the current revision.
        if self.section_id and self.instance and self.instance.document:
            # Make sure we start with content form the latest revision.
            full_content = self.instance.document.current_revision.content
            # Replace the section content with the form content.
            tool = kuma.wiki.content.parse(full_content)
            tool.replaceSection(self.section_id, content)
            content = tool.serialize()

        return content

    def clean_current_rev(self):
        """If a current revision is supplied in the form, compare it against
        what the document claims is the current revision. If there's a
        difference, then an edit has occurred since the form was constructed
        and we treat it as a mid-air collision."""
        current_rev = self.cleaned_data.get('current_rev', None)

        if not current_rev:
            # If there's no current_rev, just bail.
            return current_rev

        try:
            doc_current_rev = self.instance.document.current_revision.id
            if unicode(current_rev) != unicode(doc_current_rev):

                if (self.section_id and self.instance and
                        self.instance.document):
                    # This is a section edit. So, even though the revision has
                    # changed, it still might not be a collision if the section
                    # in particular hasn't changed.
                    orig_ct = (Revision.objects.get(pk=current_rev)
                               .get_section_content(self.section_id))
                    curr_ct = (self.instance.document.current_revision
                               .get_section_content(self.section_id))
                    if orig_ct != curr_ct:
                        # Oops. Looks like the section did actually get
                        # changed, so yeah this is a collision.
                        raise forms.ValidationError(MIDAIR_COLLISION)

                    return current_rev

                else:
                    # No section edit, so this is a flat-out collision.
                    raise forms.ValidationError(MIDAIR_COLLISION)

        except Document.DoesNotExist:
            # If there's no document yet, just bail.
            return current_rev

    def save_section(self, creator, document, **kwargs):
        """Save a section edit."""
        # This is separate because the logic is slightly different and
        # may need to evolve over time; a section edit doesn't submit
        # all the fields, and we need to account for that when we
        # construct the new Revision.

        old_rev = Document.objects.get(pk=self.instance.document.id).current_revision
        new_rev = super(RevisionForm, self).save(commit=False, **kwargs)
        new_rev.document = document
        new_rev.creator = creator
        new_rev.toc_depth = old_rev.toc_depth
        new_rev.save()
        new_rev.review_tags.set(*list(old_rev.review_tags
                                             .values_list('name', flat=True)))
        return new_rev

    def save(self, creator, document, **kwargs):
        """Persist me, and return the saved Revision.

        Take several other necessary pieces of data that aren't from the
        form.

        """
        if (self.section_id and self.instance and
                self.instance.document):
            return self.save_section(creator, document, **kwargs)
        # Throws a TypeError if somebody passes in a commit kwarg:
        new_rev = super(RevisionForm, self).save(commit=False, **kwargs)

        new_rev.document = document
        new_rev.creator = creator
        new_rev.toc_depth = self.cleaned_data['toc_depth']
        new_rev.save()
        new_rev.review_tags.set(*self.cleaned_data['review_tags'])
        new_rev.localization_tags.set(*self.cleaned_data['localization_tags'])
        return new_rev
Esempio n. 11
0
class RevisionForm(AkismetCheckFormMixin, forms.ModelForm):
    """
    Form to create new revisions.
    """
    title = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': TITLE_PLACEHOLDER}),
        label=_(u'Title:'),
        help_text=_(u'Title of article'),
        error_messages={
            'required': TITLE_REQUIRED,
            'min_length': TITLE_SHORT,
            'max_length': TITLE_LONG,
        })

    slug = StrippedCharField(min_length=1,
                             max_length=255,
                             required=False,
                             widget=forms.TextInput(),
                             label=_(u'Slug:'),
                             help_text=_(u'Article URL'),
                             error_messages={
                                 'required': SLUG_REQUIRED,
                                 'min_length': SLUG_SHORT,
                                 'max_length': SLUG_LONG,
                             })

    tags = StrippedCharField(
        required=False,
        label=_(u'Tags:'),
    )

    keywords = StrippedCharField(
        required=False,
        label=_(u'Keywords:'),
        help_text=_(u'Affects search results'),
    )

    summary = StrippedCharField(
        required=False,
        min_length=5,
        max_length=1000,
        widget=forms.Textarea(),
        label=_(u'Search result summary:'),
        help_text=_(u'Only displayed on search results page'),
        error_messages={
            'required': SUMMARY_REQUIRED,
            'min_length': SUMMARY_SHORT,
            'max_length': SUMMARY_LONG
        },
    )

    content = StrippedCharField(min_length=5,
                                max_length=300000,
                                label=_(u'Content:'),
                                widget=forms.Textarea(),
                                error_messages={
                                    'required': CONTENT_REQUIRED,
                                    'min_length': CONTENT_SHORT,
                                    'max_length': CONTENT_LONG,
                                })

    comment = StrippedCharField(required=False, label=_(u'Comment:'))

    review_tags = forms.MultipleChoiceField(
        label=ugettext("Tag this revision for review?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=REVIEW_FLAG_TAGS,
    )

    localization_tags = forms.MultipleChoiceField(
        label=ugettext("Tag this revision for localization?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=LOCALIZATION_FLAG_TAGS,
    )

    current_rev = forms.CharField(
        required=False,
        widget=forms.HiddenInput(),
    )

    class Meta(object):
        model = Revision
        fields = ('title', 'slug', 'tags', 'keywords', 'summary', 'content',
                  'comment', 'based_on', 'toc_depth', 'render_max_age')

    def __init__(self, *args, **kwargs):
        self.section_id = kwargs.pop('section_id', None)
        self.is_iframe_target = kwargs.pop('is_iframe_target', None)

        # when creating a new document with a parent, this will be set
        self.parent_slug = kwargs.pop('parent_slug', None)

        super(RevisionForm, self).__init__(*args, **kwargs)

        self.fields['based_on'].widget = forms.HiddenInput()

        if self.instance and self.instance.pk:
            # Ensure both title and slug are populated from parent document,
            # if last revision didn't have them
            if not self.instance.title:
                self.initial['title'] = self.instance.document.title
            if not self.instance.slug:
                self.initial['slug'] = self.instance.document.slug

            content = self.instance.content
            if not self.instance.document.is_template:
                parsed_content = kuma.wiki.content.parse(content)
                parsed_content.injectSectionIDs()
                if self.section_id:
                    parsed_content.extractSection(self.section_id)
                parsed_content.filterEditorSafety()
                content = parsed_content.serialize()
            self.initial['content'] = content

            self.initial['review_tags'] = list(
                self.instance.review_tags.names())
            self.initial['localization_tags'] = list(
                self.instance.localization_tags.names())

        if self.section_id:
            self.fields['toc_depth'].required = False

    def clean_slug(self):
        # Since this form can change the URL of the page on which the editing
        # happens, changes to the slug are ignored for an iframe submissions
        if self.is_iframe_target:
            return self.instance.document.slug

        # Get the cleaned slug
        slug = self.cleaned_data['slug']

        # first check if the given slug doesn't contain slashes and other
        # characters not allowed in a revision slug component (without parent)
        if slug and INVALID_REV_SLUG_CHARS_RE.search(slug):
            raise forms.ValidationError(SLUG_INVALID)

        # edits can come in without a slug, so default to the current doc slug
        if not slug:
            try:
                slug = self.instance.slug = self.instance.document.slug
            except ObjectDoesNotExist:
                pass

        # then if there is a parent document we prefix the slug with its slug
        if self.parent_slug:
            slug = u'/'.join([self.parent_slug, slug])

        try:
            doc = Document.objects.get(locale=self.instance.document.locale,
                                       slug=slug)
            if self.instance and self.instance.document:
                if (not doc.get_redirect_url()
                        and doc.pk != self.instance.document.pk):
                    # There's another document with this value,
                    # and we're not a revision of it.
                    raise forms.ValidationError(SLUG_COLLIDES)
            else:
                # This document-and-revision doesn't exist yet, so there
                # shouldn't be any collisions at all.
                raise forms.ValidationError(SLUG_COLLIDES)

        except Document.DoesNotExist:
            # No existing document for this value, so we're good here.
            pass

        return slug

    def clean_tags(self):
        """
        Validate the tags ensuring we have no case-sensitive duplicates.
        """
        tags = self.cleaned_data['tags']
        cleaned_tags = []

        if tags:
            for tag in parse_tags(tags):
                # Note: The exact match query doesn't work correctly with
                # MySQL with regards to case-sensitivity. If we move to
                # Postgresql in the future this code may need to change.
                doc_tag = (DocumentTag.objects.filter(
                    name__exact=tag).values_list('name', flat=True))

                # Write a log we can grep to help find pre-existing duplicate
                # document tags for cleanup.
                if len(doc_tag) > 1:
                    log.warn(
                        'Found duplicate document tags: {0!s}'.format(doc_tag))

                if doc_tag:
                    if doc_tag[0] != tag and doc_tag[0].lower() == tag.lower():
                        # The tag differs only by case. Do not add a new one,
                        # add the existing one.
                        cleaned_tags.append(doc_tag[0])
                        continue

                cleaned_tags.append(tag)

        return ' '.join([u'"{0!s}"'.format(t) for t in cleaned_tags])

    def clean_content(self):
        """
        Validate the content, performing any section editing if necessary
        """
        content = self.cleaned_data['content']

        # If we're editing a section, we need to replace the section content
        # from the current revision.
        if self.section_id and self.instance and self.instance.document:
            # Make sure we start with content form the latest revision.
            full_content = self.instance.document.current_revision.content
            # Replace the section content with the form content.
            parsed_content = kuma.wiki.content.parse(full_content)
            parsed_content.replaceSection(self.section_id, content)
            content = parsed_content.serialize()

        return content

    def clean_current_rev(self):
        """
        If a current revision is supplied in the form, compare it against
        what the document claims is the current revision. If there's a
        difference, then an edit has occurred since the form was constructed
        and we treat it as a mid-air collision.
        """
        current_rev = self.cleaned_data.get('current_rev', None)

        if not current_rev:
            # If there's no current_rev, just bail.
            return current_rev

        try:
            doc_current_rev = self.instance.document.current_revision.id
            if unicode(current_rev) != unicode(doc_current_rev):

                if (self.section_id and self.instance
                        and self.instance.document):
                    # This is a section edit. So, even though the revision has
                    # changed, it still might not be a collision if the section
                    # in particular hasn't changed.
                    orig_ct = (Revision.objects.get(
                        pk=current_rev).get_section_content(self.section_id))
                    curr_ct = (self.instance.document.current_revision.
                               get_section_content(self.section_id))
                    if orig_ct != curr_ct:
                        # Oops. Looks like the section did actually get
                        # changed, so yeah this is a collision.
                        raise forms.ValidationError(MIDAIR_COLLISION)

                    return current_rev

                else:
                    # No section edit, so this is a flat-out collision.
                    raise forms.ValidationError(MIDAIR_COLLISION)

        except Document.DoesNotExist:
            # If there's no document yet, just bail.
            return current_rev

    def akismet_enabled(self):
        """
        Makes sure that users that have been granted the
        'wiki_akismet_exempted' waffle flag are exempted from spam checks.
        """
        client_ready = super(RevisionForm, self).akismet_enabled()
        user_exempted = waffle.flag_is_active(self.request, SPAM_EXEMPTED_FLAG)
        return client_ready and not user_exempted

    @property
    def akismet_error_message(self):
        return mark_safe(render_to_string('wiki/includes/spam_error.html', {}))

    def akismet_error(self):
        """
        Upon errors from the Akismet API records the user, document
        and date of the attempt for further analysis. Then call the
        parent class' error handler.
        """
        try:
            document = self.instance.document
        except ObjectDoesNotExist:
            document = None
        # wrapping this in a try/finally to make sure that even if
        # creating a spam attempt object fails we call the parent
        # method that raises a ValidationError
        try:
            DocumentSpamAttempt.objects.create(
                title=self.cleaned_data['title'],
                slug=self.cleaned_data['slug'],
                user=self.request.user,
                document=document,
            )
        finally:
            super(RevisionForm, self).akismet_error()

    def akismet_parameters(self):
        """
        Returns a dict of parameters to pass to Akismet's submission
        API endpoints. Uses the given form and form parameters to
        build the dict.

        Must follow the data used for retrieving a dict of this data
        for a model instance in
        ``RevisionAkismetSubmissionAdminForm.akismet_parameters`` method!
        """
        language = self.cleaned_data.get('locale',
                                         settings.WIKI_DEFAULT_LANGUAGE)

        content = u'\n'.join([
            self.cleaned_data.get(field, '')
            for field in SPAM_SUBMISSION_REVISION_FIELDS
        ])

        return {
            # 'en-US' -> 'en_us'
            'blog_lang': translation.to_locale(language).lower(),
            'blog_charset': 'UTF-8',
            'comment_author': author_from_user(self.request.user),
            'comment_author_email': author_email_from_user(self.request.user),
            'comment_content': content,
            'comment_type': 'wiki-revision',
            'user_ip': self.request.META.get('REMOTE_ADDR', ''),
            'user_agent': self.request.META.get('HTTP_USER_AGENT', ''),
            'referrer': self.request.META.get('HTTP_REFERER', ''),
        }

    def save(self, document, **kwargs):
        """
        Persists the revision and returns it.
        Takes the view request and document of the revision.
        Does some specific things when the revision is fully saved.
        """
        # have to check for first edit before we save
        is_first_edit = self.request.user.wiki_revisions().count() == 0

        # Making sure we don't commit the saving right away since we
        # want to do other things here.
        kwargs['commit'] = False

        if self.section_id and self.instance and self.instance.document:
            # The logic to save a section is slightly different and may
            # need to evolve over time; a section edit doesn't submit
            # all the fields, and we need to account for that when we
            # construct the new Revision.
            doc = Document.objects.get(pk=self.instance.document.id)
            old_rev = doc.current_revision
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = self.request.user
            new_rev.toc_depth = old_rev.toc_depth
            new_rev.save()
            new_rev.review_tags.set(*list(old_rev.review_tags.names()))

        else:
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = self.request.user
            new_rev.toc_depth = self.cleaned_data['toc_depth']
            new_rev.save()
            new_rev.review_tags.set(*self.cleaned_data['review_tags'])
            new_rev.localization_tags.set(
                *self.cleaned_data['localization_tags'])

            # when enabled store the user's IP address
            if waffle.switch_is_active('store_revision_ips'):
                RevisionIP.objects.log(
                    revision=new_rev,
                    headers=self.request.META,
                )

            # send first edit emails
            if is_first_edit:
                send_first_edit_email.delay(new_rev.pk)

            # schedule a document rendering
            document.schedule_rendering('max-age=0')

            # schedule event notifications
            EditDocumentEvent(new_rev).fire(exclude=new_rev.creator)

        return new_rev
Esempio n. 12
0
class ContributionForm(forms.Form):
    name = StrippedCharField(min_length=1,
                             max_length=255,
                             label=_('Your full name'),
                             widget=forms.TextInput(
                                 attrs={
                                     'class': 'form-input form-input-email',
                                     'placeholder': _('Your full name'),
                                     'data-error-message': _('Required')
                                 }))
    email = forms.EmailField(
        label=_('Your email'),
        widget=forms.EmailInput(
            attrs={
                'class':
                'form-input form-input-email',
                'placeholder':
                _('*****@*****.**'),
                'data-error-message':
                _('Must be a valid email'),
                'title':
                _('Why do you need my email address? This is so we'
                  ' can send you a receipt of your contribution. This'
                  ' is handy if you would like a refund.')
            }))
    donation_choices = forms.TypedChoiceField(
        required=False,
        choices=DONATION_CHOICES,
        label=_('Contribution choices'),
        empty_value=0,
        coerce=int,
        initial=DONATION_CHOICES[1][0],
        widget=forms.RadioSelect(
            attrs={'class': 'form-radios form-radios-donation-choices'}))
    donation_amount = forms.DecimalField(
        required=False,
        label='$',
        max_digits=10,
        decimal_places=2,
        widget=forms.TextInput(
            attrs={
                'class': 'form-input form-input-amount',
                'placeholder': _('Other amount'),
                'data-error-message': _('Must be more than $1')
            }),
        validators=[MinValueValidator(1)])
    stripe_token = forms.CharField(label=u'',
                                   required=False,
                                   widget=forms.HiddenInput(),
                                   max_length=255)
    stripe_public_key = forms.CharField(label=u'',
                                        required=False,
                                        widget=forms.HiddenInput(),
                                        max_length=255)

    def clean(self):
        """Validate that either an amount or set choice was made."""
        d = self.cleaned_data
        donation_choice = d.get('donation_choices', False)
        donation_amount = d.get('donation_amount', False)

        no_selection = not (donation_amount or donation_choice)
        both_selections = donation_amount and donation_choice
        if no_selection or both_selections:
            raise forms.ValidationError(
                _('Please select donation amount or'
                  ' choose from pre-selected choices'))
        return d

    def __init__(self, *args, **kwargs):
        super(ContributionForm, self).__init__(*args, **kwargs)
        self.fields['stripe_public_key'].initial = settings.STRIPE_PUBLIC_KEY

    def make_charge(self):
        """Make a charge using the Stripe API and validated form."""
        amount = self.cleaned_data['donation_amount'] or self.cleaned_data[
            'donation_choices']
        if isinstance(amount, Decimal):
            amount = amount * Decimal('100')
            amount = amount.quantize(Decimal('0'))
        else:
            amount = amount * 100
        token = self.cleaned_data.get('stripe_token', '')
        if token and amount:
            try:
                stripe.Charge.create(
                    amount=amount,
                    currency='usd',
                    source=token,
                    description='Support MDN Web Docs',
                    receipt_email=self.cleaned_data['email'],
                    metadata={'name': self.cleaned_data['name']})
                return True
            except Exception as e:
                log.error(
                    'Stripe charge, something went wrong: {} [{}] {}'.format(
                        self.cleaned_data['name'], self.cleaned_data['email'],
                        e))
        return False
Esempio n. 13
0
class DocumentForm(forms.ModelForm):
    """
    Used for managing the wiki document data model that houses general
    data of a wiki page.
    """

    title = StrippedCharField(
        min_length=1,
        max_length=255,
        widget=forms.TextInput(attrs={"placeholder": TITLE_PLACEHOLDER}),
        label=_("Title:"),
        help_text=_("Title of article"),
        error_messages={
            "required": TITLE_REQUIRED,
            "min_length": TITLE_SHORT,
            "max_length": TITLE_LONG,
        },
    )

    slug = StrippedCharField(
        min_length=1,
        max_length=255,
        widget=forms.TextInput(),
        label=_("Slug:"),
        help_text=_("Article URL"),
        error_messages={
            "required": SLUG_REQUIRED,
            "min_length": SLUG_SHORT,
            "max_length": SLUG_LONG,
        },
    )

    parent_topic = forms.ModelChoiceField(queryset=Document.objects.all(),
                                          required=False,
                                          label=_("Parent:"))

    locale = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = Document
        fields = ("title", "slug", "locale")

    def __init__(self, *args, **kwargs):
        # when creating a new document with a parent, this will be set
        self.parent_slug = kwargs.pop("parent_slug", None)
        super(DocumentForm, self).__init__(*args, **kwargs)

    def clean_slug(self):
        from kuma.wiki.urls import non_document_patterns

        slug = self.cleaned_data["slug"]
        if slug == "":
            # Default to the title, if missing.
            slug = self.cleaned_data["title"]
        elif self.parent_slug:
            # Prepend parent slug if given from view
            slug = self.parent_slug + "/" + slug

        # Convert to NFKC, required for URLs (bug 1357416)
        # http://www.unicode.org/faq/normalization.html
        slug = unicodedata.normalize("NFKC", slug)

        # check both for disallowed characters and match for the allowed
        if INVALID_DOC_SLUG_CHARS_RE.search(
                slug) or not DOCUMENT_PATH_RE.search(slug):
            raise forms.ValidationError(SLUG_INVALID)
        # Guard against slugs that match reserved URL patterns.
        for url_pattern in non_document_patterns:
            if url_pattern.resolve(slug):
                raise forms.ValidationError(SLUG_INVALID)
        return slug

    def save(self, parent=None, *args, **kwargs):
        """Persist the Document form, and return the saved Document."""
        doc = super(DocumentForm, self).save(commit=False, *args, **kwargs)
        doc.parent = parent
        if "parent_topic" in self.cleaned_data:
            doc.parent_topic = self.cleaned_data["parent_topic"]
        doc.save()
        # not strictly necessary since we didn't change
        # any m2m data since we instantiated the doc
        self.save_m2m()
        return doc
Esempio n. 14
0
class RevisionForm(forms.ModelForm):
    """
    Form to create new revisions.
    """
    title = StrippedCharField(
        min_length=1,
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': TITLE_PLACEHOLDER}),
        label=_lazy(u'Title:'),
        help_text=_lazy(u'Title of article'),
        error_messages={
            'required': TITLE_REQUIRED,
            'min_length': TITLE_SHORT,
            'max_length': TITLE_LONG
        })
    slug = StrippedCharField(min_length=1,
                             max_length=255,
                             required=False,
                             widget=forms.TextInput(),
                             label=_lazy(u'Slug:'),
                             help_text=_lazy(u'Article URL'),
                             error_messages={
                                 'required': SLUG_REQUIRED,
                                 'min_length': SLUG_SHORT,
                                 'max_length': SLUG_LONG
                             })

    tags = StrippedCharField(required=False, label=_lazy(u'Tags:'))

    keywords = StrippedCharField(required=False,
                                 label=_lazy(u'Keywords:'),
                                 help_text=_lazy(u'Affects search results'))

    summary = StrippedCharField(
        required=False,
        min_length=5,
        max_length=1000,
        widget=forms.Textarea(),
        label=_lazy(u'Search result summary:'),
        help_text=_lazy(u'Only displayed on search results page'),
        error_messages={
            'required': SUMMARY_REQUIRED,
            'min_length': SUMMARY_SHORT,
            'max_length': SUMMARY_LONG
        })

    content = StrippedCharField(min_length=5,
                                max_length=300000,
                                label=_lazy(u'Content:'),
                                widget=forms.Textarea(),
                                error_messages={
                                    'required': CONTENT_REQUIRED,
                                    'min_length': CONTENT_SHORT,
                                    'max_length': CONTENT_LONG
                                })

    comment = StrippedCharField(required=False, label=_lazy(u'Comment:'))

    review_tags = forms.MultipleChoiceField(
        label=_("Tag this revision for review?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=REVIEW_FLAG_TAGS)

    localization_tags = forms.MultipleChoiceField(
        label=_("Tag this revision for localization?"),
        widget=CheckboxSelectMultiple,
        required=False,
        choices=LOCALIZATION_FLAG_TAGS)

    current_rev = forms.CharField(required=False, widget=forms.HiddenInput())

    class Meta(object):
        model = Revision
        fields = ('title', 'slug', 'tags', 'keywords', 'summary', 'content',
                  'comment', 'based_on', 'toc_depth', 'render_max_age')

    def __init__(self, *args, **kwargs):
        self.section_id = kwargs.pop('section_id', None)
        self.is_iframe_target = kwargs.pop('is_iframe_target', None)

        # when creating a new document with a parent, this will be set
        self.parent_slug = kwargs.pop('parent_slug', None)

        super(RevisionForm, self).__init__(*args, **kwargs)

        self.fields['based_on'].widget = forms.HiddenInput()

        if self.instance and self.instance.pk:
            # Ensure both title and slug are populated from parent document,
            # if last revision didn't have them
            if not self.instance.title:
                self.initial['title'] = self.instance.document.title
            if not self.instance.slug:
                self.initial['slug'] = self.instance.document.slug

            content = self.instance.content
            if not self.instance.document.is_template:
                parsed_content = kuma.wiki.content.parse(content)
                parsed_content.injectSectionIDs()
                if self.section_id:
                    parsed_content.extractSection(self.section_id)
                parsed_content.filterEditorSafety()
                content = parsed_content.serialize()
            self.initial['content'] = content

            self.initial['review_tags'] = list(
                self.instance.review_tags.values_list('name', flat=True))
            self.initial['localization_tags'] = list(
                self.instance.localization_tags.values_list('name', flat=True))

        if self.section_id:
            self.fields['toc_depth'].required = False

    def clean_slug(self):
        # Since this form can change the URL of the page on which the editing
        # happens, changes to the slug are ignored for an iframe submissions
        if self.is_iframe_target:
            return self.instance.document.slug

        # Get the cleaned slug
        slug = self.cleaned_data['slug']

        # first check if the given slug doesn't contain slashes and other
        # characters not allowed in a revision slug component (without parent)
        if slug and INVALID_REV_SLUG_CHARS_RE.search(slug):
            raise forms.ValidationError(SLUG_INVALID)

        # edits can come in without a slug, so default to the current doc slug
        if not slug:
            try:
                slug = self.instance.slug = self.instance.document.slug
            except ObjectDoesNotExist:
                pass

        # then if there is a parent document we prefix the slug with its slug
        if self.parent_slug:
            slug = u'/'.join([self.parent_slug, slug])

        try:
            doc = Document.objects.get(locale=self.instance.document.locale,
                                       slug=slug)
            if self.instance and self.instance.document:
                if (not doc.get_redirect_url()
                        and doc.pk != self.instance.document.pk):
                    # There's another document with this value,
                    # and we're not a revision of it.
                    raise forms.ValidationError(SLUG_COLLIDES)
            else:
                # This document-and-revision doesn't exist yet, so there
                # shouldn't be any collisions at all.
                raise forms.ValidationError(SLUG_COLLIDES)

        except Document.DoesNotExist:
            # No existing document for this value, so we're good here.
            pass

        return slug

    def clean_content(self):
        """
        Validate the content, performing any section editing if necessary
        """
        content = self.cleaned_data['content']

        # If we're editing a section, we need to replace the section content
        # from the current revision.
        if self.section_id and self.instance and self.instance.document:
            # Make sure we start with content form the latest revision.
            full_content = self.instance.document.current_revision.content
            # Replace the section content with the form content.
            parsed_content = kuma.wiki.content.parse(full_content)
            parsed_content.replaceSection(self.section_id, content)
            content = parsed_content.serialize()

        return content

    def clean_current_rev(self):
        """
        If a current revision is supplied in the form, compare it against
        what the document claims is the current revision. If there's a
        difference, then an edit has occurred since the form was constructed
        and we treat it as a mid-air collision.
        """
        current_rev = self.cleaned_data.get('current_rev', None)

        if not current_rev:
            # If there's no current_rev, just bail.
            return current_rev

        try:
            doc_current_rev = self.instance.document.current_revision.id
            if unicode(current_rev) != unicode(doc_current_rev):

                if (self.section_id and self.instance
                        and self.instance.document):
                    # This is a section edit. So, even though the revision has
                    # changed, it still might not be a collision if the section
                    # in particular hasn't changed.
                    orig_ct = (Revision.objects.get(
                        pk=current_rev).get_section_content(self.section_id))
                    curr_ct = (self.instance.document.current_revision.
                               get_section_content(self.section_id))
                    if orig_ct != curr_ct:
                        # Oops. Looks like the section did actually get
                        # changed, so yeah this is a collision.
                        raise forms.ValidationError(MIDAIR_COLLISION)

                    return current_rev

                else:
                    # No section edit, so this is a flat-out collision.
                    raise forms.ValidationError(MIDAIR_COLLISION)

        except Document.DoesNotExist:
            # If there's no document yet, just bail.
            return current_rev

    def save(self, request, document, **kwargs):
        """
        Persists the revision and returns it.
        Takes the view request and document of the revision.
        Does some specific things when the revision is fully saved.
        """
        # have to check for first edit before we save
        is_first_edit = request.user.wiki_revisions().count() == 0

        # Making sure we don't commit the saving right away since we
        # want to do other things here.
        kwargs['commit'] = False

        if self.section_id and self.instance and self.instance.document:
            # The logic to save a section is slightly different and may
            # need to evolve over time; a section edit doesn't submit
            # all the fields, and we need to account for that when we
            # construct the new Revision.
            old_rev = Document.objects.get(
                pk=self.instance.document.id).current_revision
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = request.user
            new_rev.toc_depth = old_rev.toc_depth
            new_rev.save()
            new_rev.review_tags.set(
                *list(old_rev.review_tags.values_list('name', flat=True)))

        else:
            new_rev = super(RevisionForm, self).save(**kwargs)
            new_rev.document = document
            new_rev.creator = request.user
            new_rev.toc_depth = self.cleaned_data['toc_depth']
            new_rev.save()
            new_rev.review_tags.set(*self.cleaned_data['review_tags'])
            new_rev.localization_tags.set(
                *self.cleaned_data['localization_tags'])

            # when enabled store the user's IP address
            if waffle.switch_is_active('store_revision_ips'):
                ip = request.META.get('REMOTE_ADDR')
                RevisionIP.objects.create(revision=new_rev, ip=ip)

            # send first edit emails
            if is_first_edit:
                send_first_edit_email.delay(new_rev.pk)

            # schedule a document rendering
            document.schedule_rendering('max-age=0')

            # schedule event notifications
            EditDocumentEvent(new_rev).fire(exclude=new_rev.creator)

        return new_rev