class SoftwareProductPageSerializer(ProductSerializer):
    handles_recordings_how = serializers.CharField(required=False,
                                                   max_length=5000)
    recording_alert = ExtendedYesNoField(default='CD')
    recording_alert_helptext = serializers.CharField(required=False,
                                                     max_length=5000)
    medical_privacy_compliant = serializers.BooleanField(default=False)
    medical_privacy_compliant_helptext = serializers.CharField(required=False,
                                                               max_length=5000)
    host_controls = serializers.CharField(required=False, max_length=5000)
    easy_to_learn_and_use = serializers.BooleanField(default=False)
    easy_to_learn_and_use_helptext = serializers.CharField(required=False,
                                                           max_length=5000)
예제 #2
0
class Product(ClusterableModel):
    """
    A product that may not have privacy included.
    """
    @property
    def specific(self):
        for Type in registered_product_types:
            try:
                return Type.objects.get(slug=self.slug)
            except ObjectDoesNotExist:
                pass
        return self

    draft = models.BooleanField(
        help_text=
        'When checked, this product will only show for CMS-authenticated users',
        default=True,
    )

    privacy_ding = models.BooleanField(
        help_text='Tick this box if privacy is not included for this product',
        default=False,
    )

    adult_content = models.BooleanField(
        help_text=
        'When checked, product thumbnail will appear blurred as well as have an 18+ badge on it',
        default=False,
    )

    uses_wifi = models.BooleanField(
        help_text='Does this product rely on WiFi connectivity?',
        default=False,
    )

    uses_bluetooth = models.BooleanField(
        help_text='Does this product rely on Bluetooth connectivity?',
        default=False,
    )

    review_date = models.DateField(help_text='Review date of this product', )

    name = models.CharField(
        max_length=100,
        help_text='Name of Product',
        blank=True,
    )

    slug = models.CharField(max_length=256,
                            help_text='slug used in urls',
                            blank=True,
                            default=None,
                            editable=False)

    company = models.CharField(
        max_length=100,
        help_text='Name of Company',
        blank=True,
    )

    product_category = models.ManyToManyField(
        'buyersguide.BuyersGuideProductCategory',
        related_name='pniproduct',
        blank=True)

    blurb = models.TextField(max_length=5000,
                             help_text='Description of the product',
                             blank=True)

    url = models.URLField(
        max_length=2048,
        help_text='Link to this product page',
        blank=True,
    )

    price = models.CharField(
        max_length=100,
        help_text='Price',
        blank=True,
    )

    image = models.FileField(
        max_length=2048,
        help_text='Image representing this product',
        upload_to=get_product_image_upload_path,
        blank=True,
    )

    cloudinary_image = CloudinaryField(
        help_text='Image representing this product - hosted on Cloudinary',
        blank=True,
        verbose_name='image',
        folder='foundationsite/buyersguide',
        use_filename=True)

    worst_case = models.CharField(
        max_length=5000,
        help_text=
        "What's the worst thing that could happen by using this product?",
        blank=True,
    )

    # What is required to sign up?

    signup_requires_email = ExtendedYesNoField(
        help_text=
        'Does this product requires providing an email address in order to sign up?'
    )

    signup_requires_phone = ExtendedYesNoField(
        help_text=
        'Does this product requires providing a phone number in order to sign up?'
    )

    signup_requires_third_party_account = ExtendedYesNoField(
        help_text=
        'Does this product require a third party account in order to sign up?')

    signup_requirement_explanation = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe the particulars around sign-up requirements here.')

    # How does it use this data?

    how_does_it_use_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this product use the data collected?')

    data_collection_policy_is_bad = models.BooleanField(
        default=False, verbose_name='Privacy ding')

    # Privacy policy

    user_friendly_privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a user-friendly privacy policy?')

    # Minimum security standards

    show_ding_for_minimum_security_standards = models.BooleanField(
        default=False, verbose_name="Privacy ding")

    meets_minimum_security_standards = models.BooleanField(
        null=True,
        help_text='Does this product meet our minimum security standards?',
    )

    uses_encryption = ExtendedYesNoField(
        help_text='Does the product use encryption?', )

    uses_encryption_helptext = models.TextField(max_length=5000, blank=True)

    security_updates = ExtendedYesNoField(help_text='Security updates?', )

    security_updates_helptext = models.TextField(max_length=5000, blank=True)

    strong_password = ExtendedYesNoField()

    strong_password_helptext = models.TextField(max_length=5000, blank=True)

    manage_vulnerabilities = ExtendedYesNoField(
        help_text='Manages security vulnerabilities?', )

    manage_vulnerabilities_helptext = models.TextField(max_length=5000,
                                                       blank=True)

    privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a privacy policy?')

    privacy_policy_helptext = models.TextField(  # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION
        max_length=5000,
        blank=True)
    """
    privacy_policy_links = one to many, defined in PrivacyPolicyLink
    """

    # How to contact the company

    phone_number = models.CharField(
        max_length=100,
        help_text='Phone Number',
        blank=True,
    )

    live_chat = models.CharField(
        max_length=100,
        help_text='Live Chat',
        blank=True,
    )

    email = models.CharField(
        max_length=100,
        help_text='Email',
        blank=True,
    )

    twitter = models.CharField(
        max_length=100,
        help_text='Twitter username',
        blank=True,
    )

    updates = models.ManyToManyField('buyersguide.Update',
                                     related_name='pniproduct',
                                     blank=True)

    def get_product_updates(self):
        """
        This function is used by our custom ProductUpdatesFieldPanel, to make sure
        updates are alphabetically listed. Eventually we want to replace this with
        "that, but also nothing older than 2 years". We can't do that yet, though,
        as product updates currently do not record their creation_date.
        """
        return ProductUpdate.objects.all().order_by('title')

    # comments are not a model field, but are "injected" on the product page instead

    related_products = models.ManyToManyField(
        'self',
        related_name='related_pniproduct',
        blank=True,
        symmetrical=False)

    def get_related_products(self):
        """
        This function is used by our custom RelatedProductFieldPanel, to make sure
        we don't list every single PNI product ever entered into the system, but only
        products added in recent iterations of PNI. For PNI v4 this has been set to
        "any product entered after 2019".
        """
        return Product.objects.filter(
            review_date__gte=date(2020, 1, 1)).order_by('name')

    # List of fields to show in admin to hide the image/cloudinary_image field. There's probably a better way to do
    # this using `_meta.get_fields()`. To be refactored in the future.
    panels = product_panels

    @property
    def is_current(self):
        d = self.review_date
        review = datetime(d.year, d.month, d.day)
        cutoff = datetime(2019, 6, 1)
        return cutoff < review

    @property
    def votes(self):
        return get_product_vote_information(self)

    def to_dict(self):
        """
        Rather than rendering products based on the instance object,
        we serialize the product to a dictionary, and instead render
        the template based on that.

        NOTE: if you add indirect fields like Foreign/ParentalKey or
              @property definitions, those needs to be added!
        """
        model_dict = model_to_dict(self)
        model_dict['votes'] = self.votes
        model_dict['slug'] = self.slug

        # model_to_dict does NOT capture related fields or @properties!
        model_dict['privacy_policy_links'] = list(
            self.privacy_policy_links.all())
        model_dict['is_current'] = self.is_current

        return model_dict

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        models.Model.save(self, *args, **kwargs)

    def __str__(self):
        return f'{self.name} ({self.company})'

    class Meta:
        # use oldest-first ordering
        ordering = ['id']
예제 #3
0
class Product(ClusterableModel):
    """
    A thing you can buy in stores and our review of it
    """

    draft = models.BooleanField(
        help_text=
        'When checked, this product will only show for CMS-authenticated users',
        default=True,
    )

    adult_content = models.BooleanField(
        help_text=
        'When checked, product thumbnail will appear blurred as well as have an 18+ badge on it',
        default=False,
    )

    review_date = models.DateField(help_text='Review date of this product', )

    @property
    def is_current(self):
        d = self.review_date
        review = datetime(d.year, d.month, d.day)
        cutoff = datetime(2019, 6, 1)
        return cutoff < review

    name = models.CharField(
        max_length=100,
        help_text='Name of Product',
        blank=True,
    )

    slug = models.CharField(max_length=256,
                            help_text='slug used in urls',
                            blank=True,
                            default=None,
                            editable=False)

    company = models.CharField(
        max_length=100,
        help_text='Name of Company',
        blank=True,
    )

    product_category = models.ManyToManyField(BuyersGuideProductCategory,
                                              related_name='product',
                                              blank=True)

    blurb = models.TextField(max_length=5000,
                             help_text='Description of the product',
                             blank=True)

    url = models.URLField(
        max_length=2048,
        help_text='Link to this product page',
        blank=True,
    )

    price = models.CharField(
        max_length=100,
        help_text='Price',
        blank=True,
    )

    image = models.FileField(
        max_length=2048,
        help_text='Image representing this product',
        upload_to=get_product_image_upload_path,
        blank=True,
    )

    cloudinary_image = CloudinaryImageField(
        help_text='Image representing this product - hosted on Cloudinary',
        blank=True,
        verbose_name='image',
    )

    meets_minimum_security_standards = models.BooleanField(
        null=True,
        help_text='Does this product meet minimum security standards?',
    )

    # Minimum security standards (stars)

    uses_encryption = ExtendedYesNoField(
        help_text='Does the product use encryption?', )

    uses_encryption_helptext = models.TextField(max_length=5000, blank=True)

    security_updates = ExtendedYesNoField(help_text='Security updates?', )

    security_updates_helptext = models.TextField(max_length=5000, blank=True)

    strong_password = ExtendedYesNoField()

    strong_password_helptext = models.TextField(max_length=5000, blank=True)

    manage_vulnerabilities = ExtendedYesNoField(
        help_text='Manages security vulnerabilities?', )

    manage_vulnerabilities_helptext = models.TextField(max_length=5000,
                                                       blank=True)

    privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a privacy policy?')

    privacy_policy_helptext = models.TextField(  # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION
        max_length=5000,
        blank=True)

    share_data = models.BooleanField(  # TO BE REMOVED
        null=True,
        help_text='Does the maker share data with other companies?',
    )

    share_data_helptext = models.TextField(  # TO BE REMOVED
        max_length=5000, blank=True)

    # It uses your...

    camera_device = ExtendedYesNoField(
        help_text='Does this device have or access a camera?', )

    camera_app = ExtendedYesNoField(
        help_text='Does the app have or access a camera?', )

    microphone_device = ExtendedYesNoField(
        help_text='Does this Device have or access a microphone?', )

    microphone_app = ExtendedYesNoField(
        help_text='Does this app have or access a microphone?', )

    location_device = ExtendedYesNoField(
        help_text='Does this product access your location?', )

    location_app = ExtendedYesNoField(
        help_text='Does this app access your location?', )

    # How it handles privacy

    how_does_it_share = models.CharField(
        max_length=5000,
        help_text='How does this product handle data?',
        blank=True)

    delete_data = models.BooleanField(  # TO BE REMOVED
        null=True,
        help_text='Can you request data be deleted?',
    )

    delete_data_helptext = models.TextField(  # TO BE REMOVED
        max_length=5000, blank=True)

    parental_controls = ExtendedYesNoField(
        null=True,
        help_text='Are there rules for children?',
    )

    child_rules_helptext = models.TextField(  # TO BE REMOVED
        max_length=5000, blank=True)

    collects_biometrics = ExtendedYesNoField(
        help_text='Does this product collect biometric data?', )

    collects_biometrics_helptext = models.TextField(max_length=5000,
                                                    blank=True)

    user_friendly_privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a user-friendly privacy policy?')

    user_friendly_privacy_policy_helptext = models.TextField(max_length=5000,
                                                             blank=True)
    """
    privacy_policy_links =  one to many, defined in PrivacyPolicyLink
    """

    worst_case = models.CharField(
        max_length=5000,
        help_text=
        "What's the worst thing that could happen by using this product?",
        blank=True,
    )

    PP_CHOICES = (  # TO BE REMOVED
        ('0', 'Can\'t Determine'),
        ('7', 'Grade 7'),
        ('8', 'Grade 8'),
        ('9', 'Grade 9'),
        ('10', 'Grade 10'),
        ('11', 'Grade 11'),
        ('12', 'Grade 12'),
        ('13', 'Grade 13'),
        ('14', 'Grade 14'),
        ('15', 'Grade 15'),
        ('16', 'Grade 16'),
        ('17', 'Grade 17'),
        ('18', 'Grade 18'),
        ('19', 'Grade 19'),
    )

    privacy_policy_reading_level = models.CharField(  # TO BE REMOVED IN FAVOUR OF USER_FRIENDLY_PRIVACY_POLICY
        choices=PP_CHOICES,
        default='0',
        max_length=2,
    )

    privacy_policy_url = models.URLField(  # TO BE REMOVED IN FAVOUR OF PRIVACY_POLICY_LINKS
        max_length=2048,
        help_text='Link to privacy policy',
        blank=True)

    privacy_policy_reading_level_url = models.URLField(  # TO BE REMOVED
        max_length=2048,
        help_text='Link to privacy policy reading level',
        blank=True)

    # How to contact the company

    phone_number = models.CharField(
        max_length=100,
        help_text='Phone Number',
        blank=True,
    )

    live_chat = models.CharField(
        max_length=100,
        help_text='Live Chat',
        blank=True,
    )

    email = models.CharField(
        max_length=100,
        help_text='Email',
        blank=True,
    )

    twitter = models.CharField(
        max_length=100,
        help_text='Twitter username',
        blank=True,
    )

    updates = models.ManyToManyField(Update,
                                     related_name='products',
                                     blank=True)

    # comments are not a model field, but are "injected" on the product page instead

    related_products = models.ManyToManyField('self',
                                              related_name='rps',
                                              blank=True,
                                              symmetrical=False)

    # ---

    if settings.USE_CLOUDINARY:
        image_field = FieldPanel('cloudinary_image')
    else:
        image_field = FieldPanel('image')

    # List of fields to show in admin to hide the image/cloudinary_image field. There's probably a better way to do
    # this using `_meta.get_fields()`. To be refactored in the future.
    panels = [
        MultiFieldPanel([
            FieldPanel('draft'),
        ],
                        heading="Publication status",
                        classname="collapsible"),
        MultiFieldPanel([
            FieldPanel('adult_content'),
            FieldPanel('review_date'),
            FieldPanel('name'),
            FieldPanel('company'),
            FieldPanel('product_category'),
            FieldPanel('blurb'),
            FieldPanel('url'),
            FieldPanel('price'), image_field,
            FieldPanel('meets_minimum_security_standards')
        ],
                        heading="General Product Details",
                        classname="collapsible"),
        MultiFieldPanel(
            [
                FieldPanel('uses_encryption'),
                FieldPanel('uses_encryption_helptext'),
                FieldPanel('security_updates'),
                FieldPanel('security_updates_helptext'),
                FieldPanel('strong_password'),
                FieldPanel('strong_password_helptext'),
                FieldPanel('manage_vulnerabilities'),
                FieldPanel('manage_vulnerabilities_helptext'),
                FieldPanel('privacy_policy'),
                FieldPanel(
                    'privacy_policy_helptext'),  # NEED A "clear" MIGRATION
                FieldPanel('share_data'),
                FieldPanel('share_data_helptext'),

                # DEPRECATED AND WILL BE REMOVED
                FieldPanel('privacy_policy_url'),
                FieldPanel('privacy_policy_reading_level'),
                FieldPanel('privacy_policy_reading_level_url'),
            ],
            heading="Minimum Security Standards",
            classname="collapsible"),
        MultiFieldPanel([
            FieldPanel('camera_device'),
            FieldPanel('camera_app'),
            FieldPanel('microphone_device'),
            FieldPanel('microphone_app'),
            FieldPanel('location_device'),
            FieldPanel('location_app'),
        ],
                        heading="Can it snoop?",
                        classname="collapsible"),
        MultiFieldPanel([
            FieldPanel('how_does_it_share'),
            FieldPanel('delete_data'),
            FieldPanel('delete_data_helptext'),
            FieldPanel('parental_controls'),
            FieldPanel('collects_biometrics'),
            FieldPanel('collects_biometrics_helptext'),
            FieldPanel('user_friendly_privacy_policy'),
            FieldPanel('user_friendly_privacy_policy_helptext'),
            FieldPanel('worst_case'),
        ],
                        heading="How does it handle privacy",
                        classname="collapsible"),
        MultiFieldPanel([
            InlinePanel(
                'privacy_policy_links',
                label='link',
                min_num=1,
                max_num=3,
            ),
        ],
                        heading="Privacy policy links",
                        classname="collapsible"),
        MultiFieldPanel([
            FieldPanel('phone_number'),
            FieldPanel('live_chat'),
            FieldPanel('email'),
            FieldPanel('twitter'),
        ],
                        heading="Ways to contact the company",
                        classname="collapsible"),
        FieldPanel('updates'),
        FieldPanel('related_products'),
    ]

    @property
    def votes(self):
        votes = {}
        confidence_vote_breakdown = {}
        creepiness = {'vote_breakdown': {}}

        try:
            # Get vote QuerySets
            creepiness_votes = self.range_product_votes.get(
                attribute='creepiness')
            confidence_votes = self.boolean_product_votes.get(
                attribute='confidence')

            # Aggregate the Creepiness votes
            creepiness['average'] = creepiness_votes.average
            for vote_breakdown in creepiness_votes.rangevotebreakdown_set.all(
            ):
                creepiness['vote_breakdown'][str(
                    vote_breakdown.bucket)] = vote_breakdown.count

            # Aggregate the confidence votes
            for boolean_vote_breakdown in confidence_votes.booleanvotebreakdown_set.all(
            ):
                confidence_vote_breakdown[str(boolean_vote_breakdown.bucket
                                              )] = boolean_vote_breakdown.count

            # Build + return the votes dict
            votes['creepiness'] = creepiness
            votes['confidence'] = confidence_vote_breakdown
            votes['total'] = BooleanVote.objects.filter(product=self).count()
            return votes

        except ObjectDoesNotExist:
            # There's no aggregate data available yet, return None
            return None

    @property
    def numeric_reading_grade(self):
        try:
            return int(self.privacy_policy_reading_level)
        except ValueError:
            return 0

    @property
    def reading_grade(self):
        val = self.numeric_reading_grade
        if val == 0:
            return 0
        if val <= 8:
            return 'Middle school'
        if val <= 12:
            return 'High school'
        if val <= 16:
            return 'College'
        return 'Grad school'

    def to_dict(self):
        """
        Rather than rendering products based on the instance object,
        we serialize the product to a dictionary, and instead render
        the template based on that.

        NOTE: if you add indirect fields like Foreign/ParentalKey or
              @property definitions, those needs to be added!
        """
        model_dict = model_to_dict(self)

        model_dict['votes'] = self.votes
        model_dict['slug'] = self.slug
        model_dict['delete_data'] = tri_to_quad(self.delete_data)

        # TODO: remove these two entries
        model_dict['numeric_reading_grade'] = self.numeric_reading_grade
        model_dict['reading_grade'] = self.reading_grade

        # model_to_dict does NOT capture related fields or @properties!
        model_dict['privacy_policy_links'] = list(
            self.privacy_policy_links.all())
        model_dict['is_current'] = self.is_current

        return model_dict

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        models.Model.save(self, *args, **kwargs)

    def __str__(self):
        return str(self.name)
예제 #4
0
class ProductPage(FoundationMetadataPageMixin, Page):
    """
    ProductPage is the superclass that SoftwareProductPage and
    GeneralProductPage inherit from. This should not be an abstract
    model as we need it to connect the two page types together.
    """

    privacy_ding = models.BooleanField(
        help_text='Tick this box if privacy is not included for this product',
        default=False,
    )
    adult_content = models.BooleanField(
        help_text='When checked, product thumbnail will appear blurred as well as have an 18+ badge on it',
        default=False,
    )
    uses_wifi = models.BooleanField(
        help_text='Does this product rely on WiFi connectivity?',
        default=False,
    )
    uses_bluetooth = models.BooleanField(
        help_text='Does this product rely on Bluetooth connectivity?',
        default=False,
    )
    review_date = models.DateField(
        help_text='Review date of this product',
        auto_now_add=True,
    )
    company = models.CharField(
        max_length=100,
        help_text='Name of Company',
        blank=True,
    )
    blurb = models.TextField(
        max_length=5000,
        help_text='Description of the product',
        blank=True
    )
    # TODO: We'll need to update this URL in the template
    product_url = models.URLField(
        max_length=2048,
        help_text='Link to this product page',
        blank=True,
    )
    price = models.CharField(
        max_length=100,
        help_text='Price',
        blank=True,
    )
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Image representing this product',
    )
    cloudinary_image = CloudinaryField(
        help_text='Image representing this product - hosted on Cloudinary',
        blank=True,
        verbose_name='image',
        folder='foundationsite/buyersguide',
        use_filename=True
    )
    worst_case = models.CharField(
        max_length=5000,
        help_text="What's the worst thing that could happen by using this product?",
        blank=True,
    )

    """
    product_privacy_policy_links = Orderable, defined in ProductPagePrivacyPolicyLink
    Other "magic" relations that use InlinePanels will follow the same pattern of
    using Wagtail Orderables.
    """

    # What is required to sign up?
    signup_requires_email = ExtendedYesNoField(
        help_text='Does this product requires providing an email address in order to sign up?'
    )
    signup_requires_phone = ExtendedYesNoField(
        help_text='Does this product requires providing a phone number in order to sign up?'
    )
    signup_requires_third_party_account = ExtendedYesNoField(
        help_text='Does this product require a third party account in order to sign up?'
    )
    signup_requirement_explanation = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe the particulars around sign-up requirements here.'
    )

    # How does it use this data?
    how_does_it_use_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this product use the data collected?'
    )
    data_collection_policy_is_bad = models.BooleanField(
        default=False,
        verbose_name='Privacy ding'
    )

    # Privacy policy
    user_friendly_privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a user-friendly privacy policy?'
    )

    # Minimum security standards
    show_ding_for_minimum_security_standards = models.BooleanField(
        default=False,
        verbose_name="Privacy ding"
    )
    meets_minimum_security_standards = models.BooleanField(
        null=True,
        blank=True,
        help_text='Does this product meet our minimum security standards?',
    )
    uses_encryption = ExtendedYesNoField(
        help_text='Does the product use encryption?',
    )
    uses_encryption_helptext = models.TextField(
        max_length=5000,
        blank=True
    )
    security_updates = ExtendedYesNoField(
        help_text='Security updates?',
    )
    security_updates_helptext = models.TextField(
        max_length=5000,
        blank=True
    )
    strong_password = ExtendedYesNoField()
    strong_password_helptext = models.TextField(
        max_length=5000,
        blank=True
    )
    manage_vulnerabilities = ExtendedYesNoField(
        help_text='Manages security vulnerabilities?',
    )
    manage_vulnerabilities_helptext = models.TextField(
        max_length=5000,
        blank=True
    )
    privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a privacy policy?'
    )
    privacy_policy_helptext = models.TextField(  # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION
        max_length=5000,
        blank=True
    )

    # How to contact the company
    phone_number = models.CharField(
        max_length=100,
        help_text='Phone Number',
        blank=True,
    )
    live_chat = models.CharField(
        max_length=100,
        help_text='Live Chat',
        blank=True,
    )
    email = models.CharField(
        max_length=100,
        help_text='Email',
        blank=True,
    )
    twitter = models.CharField(
        max_length=100,
        help_text='Twitter username',
        blank=True,
    )

    content_panels = Page.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel('privacy_ding'),
                FieldPanel('adult_content'),
                FieldPanel('company'),
                FieldPanel('product_url'),
                FieldPanel('price'),
                FieldPanel('uses_wifi'),
                FieldPanel('uses_bluetooth'),
                FieldPanel('blurb'),
                image_field,
                FieldPanel('worst_case'),
            ],
            heading='General Product Details',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [
                InlinePanel('product_categories', label='Category'),
            ],
            heading='Product Categories',
            classname='collapsible',
        ),
        MultiFieldPanel(
            [
                FieldPanel('signup_requires_email'),
                FieldPanel('signup_requires_phone'),
                FieldPanel('signup_requires_third_party_account'),
                FieldPanel('signup_requirement_explanation'),
            ],
            heading='What is required to sign up',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [

                FieldPanel('how_does_it_use_data_collected'),
                FieldPanel('data_collection_policy_is_bad'),
            ],
            heading='How does it use this data',
            classname='collapsible',
        ),
        MultiFieldPanel(
            [
                FieldPanel('user_friendly_privacy_policy'),
            ],
            heading='Privacy policy',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [
                InlinePanel(
                    'product_privacy_policy_links',
                    label='link',
                    min_num=1,
                    max_num=3,
                ),
            ],
            heading='Privacy policy links',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [
                FieldPanel('show_ding_for_minimum_security_standards'),
                FieldPanel('meets_minimum_security_standards'),
                FieldPanel('uses_encryption'),
                FieldPanel('uses_encryption_helptext'),
                FieldPanel('security_updates'),
                FieldPanel('security_updates_helptext'),
                FieldPanel('strong_password'),
                FieldPanel('strong_password_helptext'),
                FieldPanel('manage_vulnerabilities'),
                FieldPanel('manage_vulnerabilities_helptext'),
                FieldPanel('privacy_policy'),
                FieldPanel('privacy_policy_helptext'),
            ],
            heading='Security',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [
                FieldPanel('phone_number'),
                FieldPanel('live_chat'),
                FieldPanel('email'),
                FieldPanel('twitter'),
            ],
            heading='Ways to contact the company',
            classname='collapsible'
        ),
        MultiFieldPanel(
            [
                InlinePanel('product_updates', label='Update')
            ],
            heading='Product Updates',
        ),
        MultiFieldPanel(
            [
                InlinePanel('related_product_pages', label='Product')
            ],
            heading='Related Products',
        ),
    ]

    class Meta:
        verbose_name = "Product Page"
예제 #5
0
class SoftwareProduct(Product):
    """
    A thing you can install on your computer and our review of it
    """

    # How does it handle privacy?

    handles_recordings_how = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this software handle your recordings')

    recording_alert = ExtendedYesNoField(
        null=True,
        help_text='Alerts when calls are being recorded?',
    )

    recording_alert_helptext = models.TextField(max_length=5000, blank=True)

    signup_with_email = models.BooleanField(
        null=True,
        help_text='Email required to sign up?',
    )

    signup_with_phone = models.BooleanField(
        null=True,
        help_text='Phone number required to sign up?',
    )

    signup_with_third_party = models.BooleanField(
        null=True,
        help_text='Third Party account required to sign up?',
    )

    signup_methods_helptext = models.TextField(
        max_length=5000,
        blank=True,
        help_text=
        'Describe the kind of contact information requirements for signing up for this product'
    )

    medical_privacy_compliant = models.BooleanField(
        null=True, help_text='Compliant with US medical privacy laws?')

    medical_privacy_compliant_helptext = models.TextField(max_length=5000,
                                                          blank=True)

    # Can I control it?

    host_controls = models.TextField(max_length=5000, blank=True)

    easy_to_learn_and_use = models.BooleanField(
        null=True,
        help_text='Is it easy to learn & use the features?',
    )

    easy_to_learn_and_use_helptext = models.TextField(max_length=5000,
                                                      blank=True)

    # administrative panels

    panels = Product.panels.copy()

    panels = insert_panels_after(
        panels,
        'Minimum Security Standards for general products',
        [
            MultiFieldPanel([
                FieldPanel('signup_with_email'),
                FieldPanel('signup_with_phone'),
                FieldPanel('signup_with_third_party'),
                FieldPanel('signup_methods_helptext'),
            ],
                            heading='How does it handle signup?',
                            classname='collapsible'),
        ],
    )

    panels = insert_panels_after(
        panels,
        'How does it handle data sharing',
        [
            MultiFieldPanel([
                FieldPanel('handles_recordings_how'),
                FieldPanel('recording_alert'),
                FieldPanel('recording_alert_helptext'),
                FieldPanel('medical_privacy_compliant'),
                FieldPanel('medical_privacy_compliant_helptext'),
            ],
                            heading='How does it handle privacy?',
                            classname='collapsible'),
        ],
    )

    panels = insert_panels_after(
        panels,
        'How does it handle privacy?',
        [
            MultiFieldPanel([
                FieldPanel('host_controls'),
                FieldPanel('easy_to_learn_and_use'),
                FieldPanel('easy_to_learn_and_use_helptext'),
            ],
                            heading='Can I control it',
                            classname='collapsible'),
        ],
    )

    def to_dict(self):
        model_dict = super().to_dict()
        model_dict['product_type'] = 'software'
        return model_dict
예제 #6
0
class GeneralProductPage(ProductPage):
    template = 'buyersguide/product_page.html'

    camera_device = ExtendedYesNoField(
        help_text='Does this device have or access a camera?', )

    camera_app = ExtendedYesNoField(
        help_text='Does the app have or access a camera?', )

    microphone_device = ExtendedYesNoField(
        help_text='Does this Device have or access a microphone?', )

    microphone_app = ExtendedYesNoField(
        help_text='Does this app have or access a microphone?', )

    location_device = ExtendedYesNoField(
        help_text='Does this product access your location?', )

    location_app = ExtendedYesNoField(
        help_text='Does this app access your location?', )

    # What data does it collect?

    personal_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of personal data does this product collect?')

    biometric_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of biometric data does this product collect?')

    social_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of social data does this product collect?')

    # How can you control your data

    how_can_you_control_your_data = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this product let you control your data?')

    data_control_policy_is_bad = models.BooleanField(
        default=False, verbose_name='Privacy ding')

    # Company track record

    company_track_record = models.CharField(
        choices=TRACK_RECORD_CHOICES,
        default='Average',
        help_text='This company has a ... track record',
        max_length=20)

    track_record_is_bad = models.BooleanField(default=False,
                                              verbose_name='Privacy ding')

    track_record_details = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe the track record of this company here.')

    # Offline use

    offline_capable = ExtendedYesNoField(
        help_text='Can this product be used offline?', )

    offline_use_description = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe how this product can be used offline.')

    # Artificial Intelligence

    uses_ai = ExtendedYesNoField(help_text='Does the product use AI?')

    ai_uses_personal_data = ExtendedYesNoField(
        help_text=
        'Does the AI use your personal data to make decisions about you?')

    ai_is_transparent = ExtendedYesNoField(
        help_text='Does the company allow users to see how the AI works?')

    ai_helptext = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Helpful text around AI to show on the product page',
    )

    @classmethod
    def map_import_fields(cls):
        generic_product_import_fields = super().map_import_fields()
        general_product_mappings = {
            "Has camera device": "camera_device",
            "Has camera app": "camera_app",
            "Has microphone device": "microphone_device",
            "Has microphone app": "microphone_app",
            "Has location device": "location_device",
            "Has location app": "location_app",
            "Personal data collected": "personal_data_collected",
            "Biometric data collected": "biometric_data_collected",
            "Social data collected": "social_data_collected",
            "How you can control your data": "how_can_you_control_your_data",
            "Company track record": "company_track_record",
            "Show company track record privacy ding": "track_record_is_bad",
            "Offline capable": "offline_capable",
            "Offline use": "offline_use_description",
            "Uses AI": "uses_ai",
            "AI uses personal data": "ai_uses_personal_data",
            "AI help text": "ai_helptext",
            "AI is transparent": "ai_is_transparent",
        }
        # Return the merged fields
        return {**generic_product_import_fields, **general_product_mappings}

    def get_export_fields(self):
        """
        This should be a dictionary of the fields to send to Airtable.
        Keys are the Column Names in Airtable. Values are the Wagtail values we want to send.
        """
        generic_product_data = super().get_export_fields()
        general_product_data = {
            "Has camera device": self.camera_device,
            "Has camera app": self.camera_app,
            "Has microphone device": self.microphone_device,
            "Has microphone app": self.microphone_app,
            "Has location device": self.location_device,
            "Has location app": self.location_app,
            "Personal data collected": self.personal_data_collected,
            "Biometric data collected": self.biometric_data_collected,
            "Social data collected": self.social_data_collected,
            "How you can control your data":
            self.how_can_you_control_your_data,
            "Company track record": self.company_track_record,
            "Show company track record privacy ding": self.track_record_is_bad,
            "Offline capable": self.offline_capable,
            "Offline use": self.offline_use_description,
            "Uses AI": self.uses_ai,
            "AI uses personal data": self.ai_uses_personal_data,
            "AI is transparent": self.ai_uses_personal_data,
            "AI help text": self.ai_helptext,
        }
        # Merge the two dicts together.
        data = {**generic_product_data, **general_product_data}
        return data

    # administrative panels
    content_panels = ProductPage.content_panels.copy()
    content_panels = insert_panels_after(
        content_panels,
        'Product Categories',
        [
            MultiFieldPanel([
                FieldPanel('camera_device'),
                FieldPanel('camera_app'),
                FieldPanel('microphone_device'),
                FieldPanel('microphone_app'),
                FieldPanel('location_device'),
                FieldPanel('location_app'),
            ],
                            heading='Can it snoop?',
                            classname='collapsible'),
        ],
    )

    content_panels = insert_panels_after(
        content_panels, 'What is required to sign up', [
            MultiFieldPanel(
                [
                    FieldPanel('personal_data_collected'),
                    FieldPanel('biometric_data_collected'),
                    FieldPanel('social_data_collected'),
                ],
                heading='What data does it collect',
                classname='collapsible',
            ),
        ])

    content_panels = insert_panels_after(
        content_panels,
        'How does it use this data',
        [
            MultiFieldPanel(
                [
                    FieldPanel('how_can_you_control_your_data'),
                    FieldPanel('data_control_policy_is_bad'),
                ],
                heading='How can you control your data',
                classname='collapsible',
            ),
            MultiFieldPanel([
                FieldPanel('company_track_record'),
                FieldPanel('track_record_is_bad'),
                FieldPanel('track_record_details'),
            ],
                            heading='Company track record',
                            classname='collapsible'),
            MultiFieldPanel([
                FieldPanel('offline_capable'),
                FieldPanel('offline_use_description'),
            ],
                            heading='Offline use',
                            classname='collapsible'),
        ],
    )

    content_panels = insert_panels_after(
        content_panels,
        'Security',
        [
            MultiFieldPanel([
                FieldPanel('uses_ai'),
                FieldPanel('ai_uses_personal_data'),
                FieldPanel('ai_is_transparent'),
                FieldPanel('ai_helptext'),
            ],
                            heading='Artificial Intelligence',
                            classname='collapsible'),
        ],
    )

    @property
    def product_type(self):
        return "general"

    class Meta:
        verbose_name = "General Product Page"
예제 #7
0
class SoftwareProductPage(ProductPage):
    template = 'buyersguide/product_page.html'

    handles_recordings_how = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this software handle your recordings')

    recording_alert = ExtendedYesNoField(
        null=True,
        help_text='Alerts when calls are being recorded?',
    )

    recording_alert_helptext = models.TextField(max_length=5000, blank=True)
    # NullBooleanField is deprecated as of Django 3.1.
    # We're using it here primarily for a data migration, but we should
    # move to BooleanField as soon as it's safe to do so with the content we have
    medical_privacy_compliant = models.NullBooleanField(
        null=True, help_text='Compliant with US medical privacy laws?')

    medical_privacy_compliant_helptext = models.TextField(max_length=5000,
                                                          blank=True)

    # Can I control it?

    host_controls = models.TextField(max_length=5000, blank=True)
    # NullBooleanField is deprecated as of Django 3.1.
    # We're using it here primarily for a data migration, but we should
    # move to BooleanField as soon as it's safe to do so with the content we have
    easy_to_learn_and_use = models.NullBooleanField(
        null=True,
        help_text='Is it easy to learn & use the features?',
    )

    easy_to_learn_and_use_helptext = models.TextField(max_length=5000,
                                                      blank=True)

    @classmethod
    def map_import_fields(cls):
        generic_product_import_fields = super().map_import_fields()
        software_product_mappings = {
            "How it handles recording": "handles_recordings_how",
            "Recording alert": "recording_alert",
            "Recording alert help text": "recording_alert_helptext",
            "Medical privacy compliant": "medical_privacy_compliant",
            "Medical privacy compliant help text":
            "medical_privacy_compliant_helptext",
            "Host controls": "host_controls",
            "Easy to learn and use": "easy_to_learn_and_use",
            "Easy to learn and use help text":
            "easy_to_learn_and_use_helptext",
        }
        # Return the merged fields
        return {**generic_product_import_fields, **software_product_mappings}

    def get_export_fields(self):
        """
        This should be a dictionary of the fields to send to Airtable.
        Keys are the Column Names in Airtable. Values are the Wagtail values we want to send.
        """
        generic_product_data = super().get_export_fields()
        software_product_data = {
            "How it handles recording":
            self.handles_recordings_how,
            "Recording alert":
            self.recording_alert,
            "Recording alert help text":
            self.recording_alert_helptext,
            "Medical privacy compliant":
            True if self.medical_privacy_compliant else False,
            "Medical privacy compliant help text":
            self.medical_privacy_compliant_helptext,
            "Host controls":
            self.host_controls,
            "Easy to learn and use":
            True if self.easy_to_learn_and_use else False,
            "Easy to learn and use help text":
            self.easy_to_learn_and_use_helptext,
        }

        data = {**generic_product_data, **software_product_data}
        return data

    content_panels = ProductPage.content_panels.copy()
    content_panels = insert_panels_after(
        content_panels,
        'Product Categories',
        [
            MultiFieldPanel([
                FieldPanel('handles_recordings_how'),
                FieldPanel('recording_alert'),
                FieldPanel('recording_alert_helptext'),
                FieldPanel('medical_privacy_compliant'),
                FieldPanel('medical_privacy_compliant_helptext'),
            ],
                            heading='How does it handle privacy?',
                            classname='collapsible'),
            MultiFieldPanel([
                FieldPanel('host_controls'),
                FieldPanel('easy_to_learn_and_use'),
                FieldPanel('easy_to_learn_and_use_helptext'),
            ],
                            heading='Can I control it',
                            classname='collapsible'),
        ],
    )

    @property
    def product_type(self):
        return "software"

    class Meta:
        verbose_name = "Software Product Page"
예제 #8
0
class ProductPage(AirtableMixin, FoundationMetadataPageMixin, Page):
    """
    ProductPage is the superclass that SoftwareProductPage and
    GeneralProductPage inherit from. This should not be an abstract
    model as we need it to connect the two page types together.
    """

    template = 'buyersguide/product_page.html'

    privacy_ding = models.BooleanField(
        help_text='Tick this box if privacy is not included for this product',
        default=False,
    )
    adult_content = models.BooleanField(
        help_text=
        'When checked, product thumbnail will appear blurred as well as have an 18+ badge on it',
        default=False,
    )
    uses_wifi = models.BooleanField(
        help_text='Does this product rely on WiFi connectivity?',
        default=False,
    )
    uses_bluetooth = models.BooleanField(
        help_text='Does this product rely on Bluetooth connectivity?',
        default=False,
    )
    review_date = models.DateField(
        help_text='Review date of this product',
        default=timezone.now,
    )
    company = models.CharField(
        max_length=100,
        help_text='Name of Company',
        blank=True,
    )
    blurb = models.TextField(max_length=5000,
                             help_text='Description of the product',
                             blank=True)
    product_url = models.URLField(
        max_length=2048,
        help_text='Link to this product page',
        blank=True,
    )
    price = models.CharField(
        max_length=100,
        help_text='Price',
        blank=True,
    )
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Image representing this product',
    )
    cloudinary_image = CloudinaryField(
        help_text='Image representing this product - hosted on Cloudinary',
        blank=True,
        verbose_name='image',
        folder='foundationsite/buyersguide',
        use_filename=True)
    worst_case = models.TextField(
        max_length=5000,
        help_text=
        "What's the worst thing that could happen by using this product?",
        blank=True,
    )
    """
    privacy_policy_links = Orderable, defined in ProductPagePrivacyPolicyLink
    Other "magic" relations that use InlinePanels will follow the same pattern of
    using Wagtail Orderables.
    """

    # What is required to sign up?
    signup_requires_email = ExtendedYesNoField(
        help_text=
        'Does this product requires providing an email address in order to sign up?'
    )
    signup_requires_phone = ExtendedYesNoField(
        help_text=
        'Does this product requires providing a phone number in order to sign up?'
    )
    signup_requires_third_party_account = ExtendedYesNoField(
        help_text=
        'Does this product require a third party account in order to sign up?')
    signup_requirement_explanation = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe the particulars around sign-up requirements here.')

    # How does it use this data?
    how_does_it_use_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this product use the data collected?')
    data_collection_policy_is_bad = models.BooleanField(
        default=False, verbose_name='Privacy ding')

    # Privacy policy
    user_friendly_privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a user-friendly privacy policy?')

    user_friendly_privacy_policy_helptext = models.TextField(max_length=5000,
                                                             blank=True)

    # Minimum security standards
    show_ding_for_minimum_security_standards = models.BooleanField(
        default=False, verbose_name="Privacy ding")
    meets_minimum_security_standards = models.BooleanField(
        null=True,
        blank=True,
        help_text='Does this product meet our minimum security standards?',
    )
    uses_encryption = ExtendedYesNoField(
        help_text='Does the product use encryption?', )
    uses_encryption_helptext = models.TextField(max_length=5000, blank=True)
    security_updates = ExtendedYesNoField(help_text='Security updates?', )
    security_updates_helptext = models.TextField(max_length=5000, blank=True)
    strong_password = ExtendedYesNoField()
    strong_password_helptext = models.TextField(max_length=5000, blank=True)
    manage_vulnerabilities = ExtendedYesNoField(
        help_text='Manages security vulnerabilities?', )
    manage_vulnerabilities_helptext = models.TextField(max_length=5000,
                                                       blank=True)
    privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a privacy policy?')
    privacy_policy_helptext = models.TextField(  # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION
        max_length=5000,
        blank=True)

    # How to contact the company
    phone_number = models.CharField(
        max_length=100,
        help_text='Phone Number',
        blank=True,
    )
    live_chat = models.CharField(
        max_length=100,
        help_text='Live Chat',
        blank=True,
    )
    email = models.CharField(
        max_length=100,
        help_text='Email',
        blank=True,
    )
    twitter = models.CharField(
        max_length=100,
        help_text='Twitter username',
        blank=True,
    )

    # Un-editable voting fields. Don't add these to the content_panels.
    creepiness_value = models.IntegerField(
        default=0)  # The total points for creepiness
    votes = models.ForeignKey(
        ProductPageVotes,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='votes',
    )

    @classmethod
    def map_import_fields(cls):
        mappings = {
            "Title": "title",
            "Wagtail Page ID": "pk",
            "Slug": "slug",
            "Show privacy ding": "privacy_ding",
            "Has adult content": "adult_content",
            "Uses wifi": "uses_wifi",
            "Uses Bluetooth": "uses_bluetooth",
            "Review date": "review_date",
            "Company": "company",
            "Blurb": "blurb",
            "Product link": "product_url",
            "Price": "price",
            "Worst case": "worst_case",
            "Signup requires email": "signup_requires_email",
            "Signup requires phone number": "signup_requires_phone",
            "Signup requires 3rd party account":
            "signup_requires_third_party_account",
            "Signup explanation": "signup_requirement_explanation",
            "How it collects data": "how_does_it_use_data_collected",
            "Data collection privacy ding": "data_collection_policy_is_bad",
            "User friendly privacy policy": "user_friendly_privacy_policy",
            "User friendly privacy policy help text":
            "user_friendly_privacy_policy_helptext",
            "Meets MSS": "meets_minimum_security_standards",
            "Meets MSS privacy policy ding":
            "show_ding_for_minimum_security_standards",
            "Uses encryption": "uses_encryption",
            "Encryption help text": "uses_encryption_helptext",
            "Has security updates": "security_updates",
            "Security updates help text": "security_updates_helptext",
            "Strong password": "******",
            "Strong password help text": "strong_password_helptext",
            "Manages security vulnerabilities": "manage_vulnerabilities",
            "Manages security help text": "manage_vulnerabilities_helptext",
            "Has privacy policy": "privacy_policy",
            "Privacy policy help text": "privacy_policy_helptext",
            "Phone number": "phone_number",
            "Live chat": "live_chat",
            "Email address": "email",
            "Twitter": "twitter",
        }
        return mappings

    def get_export_fields(self):
        """
        This should be a dictionary of the fields to send to Airtable.
        Keys are the Column Names in Airtable. Values are the Wagtail values we want to send.
        """
        return {
            "Title":
            self.title,
            "Slug":
            self.slug,
            "Wagtail Page ID":
            self.pk if hasattr(self, 'pk') else 0,
            "Last Updated":
            str(self.last_published_at)
            if self.last_published_at else str(timezone.now().isoformat()),
            "Status":
            self.get_status_for_airtable(),
            "Show privacy ding":
            self.privacy_ding,
            "Has adult content":
            self.adult_content,
            "Uses wifi":
            self.uses_wifi,
            "Uses Bluetooth":
            self.uses_bluetooth,
            "Review date":
            str(self.review_date),
            "Company":
            self.company,
            "Blurb":
            self.blurb,
            "Product link":
            self.product_url if self.product_url else '',
            "Price":
            self.price,
            "Worst case":
            self.worst_case,
            "Signup requires email":
            self.signup_requires_email,
            "Signup requires phone number":
            self.signup_requires_phone,
            "Signup requires 3rd party account":
            self.signup_requires_third_party_account,
            "Signup explanation":
            self.signup_requirement_explanation,
            "How it collects data":
            self.how_does_it_use_data_collected,
            "Data collection privacy ding":
            self.data_collection_policy_is_bad,
            "User friendly privacy policy":
            self.user_friendly_privacy_policy,
            "User friendly privacy policy help text":
            self.user_friendly_privacy_policy_helptext,
            "Meets MSS":
            self.meets_minimum_security_standards,
            "Meets MSS privacy policy ding":
            self.show_ding_for_minimum_security_standards,
            "Uses encryption":
            self.uses_encryption,
            "Encryption help text":
            self.uses_encryption_helptext,
            "Has security updates":
            self.security_updates,
            "Security updates help text":
            self.security_updates_helptext,
            "Strong password":
            self.strong_password,
            "Strong password help text":
            self.strong_password_helptext,
            "Manages security vulnerabilities":
            self.manage_vulnerabilities,
            "Manages security help text":
            self.manage_vulnerabilities_helptext,
            "Has privacy policy":
            self.privacy_policy,
            "Privacy policy help text":
            self.privacy_policy_helptext,
            "Phone number":
            self.phone_number,
            "Live chat":
            self.live_chat,
            "Email address":
            self.email,
            "Twitter":
            self.twitter if self.twitter else ''
        }

    def get_status_for_airtable(self):
        if self.live:
            if self.has_unpublished_changes:
                return "Live + Draft"
            return "Live"
        return "Draft"

    @property
    def total_vote_count(self):
        return sum(self.get_or_create_votes())

    @property
    def creepiness(self):
        try:
            average = self.creepiness_value / self.total_vote_count
        except ZeroDivisionError:
            average = 50
        return average

    @property
    def get_voting_json(self):
        """
        Return a dictionary as a string with the relevant data needed for the frontend:
        """
        votes = self.votes.get_votes()
        data = {
            'creepiness': {
                'vote_breakdown': {k: v
                                   for (k, v) in enumerate(votes)},
                'average': self.creepiness
            },
            'total': self.total_vote_count
        }
        return json.dumps(data)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('review_date'),
            FieldPanel('privacy_ding'),
            FieldPanel('adult_content'),
            FieldPanel('company'),
            FieldPanel('product_url'),
            FieldPanel('price'),
            FieldPanel('uses_wifi'),
            FieldPanel('uses_bluetooth'),
            FieldPanel('blurb'),
            image_field,
            FieldPanel('worst_case'),
        ],
                        heading='General Product Details',
                        classname='collapsible'),
        MultiFieldPanel(
            [
                InlinePanel('product_categories', label='Category'),
            ],
            heading='Product Categories',
            classname='collapsible',
        ),
        MultiFieldPanel([
            FieldPanel('signup_requires_email'),
            FieldPanel('signup_requires_phone'),
            FieldPanel('signup_requires_third_party_account'),
            FieldPanel('signup_requirement_explanation'),
        ],
                        heading='What is required to sign up',
                        classname='collapsible'),
        MultiFieldPanel(
            [
                FieldPanel('how_does_it_use_data_collected'),
                FieldPanel('data_collection_policy_is_bad'),
            ],
            heading='How does it use this data',
            classname='collapsible',
        ),
        MultiFieldPanel([
            FieldPanel('user_friendly_privacy_policy'),
            FieldPanel('user_friendly_privacy_policy_helptext'),
        ],
                        heading='Privacy policy',
                        classname='collapsible'),
        MultiFieldPanel([
            InlinePanel(
                'privacy_policy_links',
                label='link',
                min_num=1,
                max_num=3,
            ),
        ],
                        heading='Privacy policy links',
                        classname='collapsible'),
        MultiFieldPanel([
            FieldPanel('show_ding_for_minimum_security_standards'),
            FieldPanel('meets_minimum_security_standards'),
            FieldPanel('uses_encryption'),
            FieldPanel('uses_encryption_helptext'),
            FieldPanel('security_updates'),
            FieldPanel('security_updates_helptext'),
            FieldPanel('strong_password'),
            FieldPanel('strong_password_helptext'),
            FieldPanel('manage_vulnerabilities'),
            FieldPanel('manage_vulnerabilities_helptext'),
            FieldPanel('privacy_policy'),
            FieldPanel('privacy_policy_helptext'),
        ],
                        heading='Security',
                        classname='collapsible'),
        MultiFieldPanel([
            FieldPanel('phone_number'),
            FieldPanel('live_chat'),
            FieldPanel('email'),
            FieldPanel('twitter'),
        ],
                        heading='Ways to contact the company',
                        classname='collapsible'),
        MultiFieldPanel(
            [InlinePanel('updates', label='Update')],
            heading='Product Updates',
        ),
        MultiFieldPanel(
            [InlinePanel('related_product_pages', label='Product')],
            heading='Related Products',
        ),
    ]

    @property
    def product_type(self):
        return "unknown"

    def get_or_create_votes(self):
        """
        If a page doesn't have a ProductPageVotes objects, create it.
        Regardless of whether or not its created, return the parsed votes.
        """
        if not self.votes:
            votes = ProductPageVotes()
            votes.save()
            self.votes = votes
            self.save()
        return self.votes.get_votes()

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['product'] = self
        context['categories'] = BuyersGuideProductCategory.objects.filter(
            hidden=False)
        context[
            'mediaUrl'] = settings.CLOUDINARY_URL if settings.USE_CLOUDINARY else settings.MEDIA_URL
        context['use_commento'] = settings.USE_COMMENTO
        context['pageTitle'] = f'{self.title} | ' + gettext(
            "Privacy & security guide") + ' | Mozilla Foundation'
        context['about_page'] = BuyersGuidePage.objects.first()
        return context

    def serve(self, request, *args, **kwargs):
        # In Wagtail we use the serve() method to detect POST submissions.
        # Alternatively, this could be a routable view.
        # For more on this, see the docs here:
        # https://docs.wagtail.io/en/stable/reference/pages/model_recipes.html#overriding-the-serve-method
        if request.body and request.method == "POST":
            # If the request is POST. Parse the body.
            data = json.loads(request.body)
            # If the POST body has a productID and value, it's someone voting on the product
            if data.get("value"):
                # Product ID and Value can both be zero. It's impossible to get a Page with ID of zero.
                try:
                    value = int(data["value"])  # ie. 0 to 100
                except ValueError:
                    return HttpResponseNotAllowed(
                        'Product ID or value is invalid')

                if value < 0 or value > 100:
                    return HttpResponseNotAllowed('Cannot save vote')

                try:
                    product = ProductPage.objects.get(pk=self.id)
                    # If the product exists but isn't live and the user isn't logged in..
                    if (not product.live and
                            not request.user.is_authenticated) or not product:
                        return HttpResponseNotFound("Product does not exist")

                    # Save the new voting totals
                    product.creepiness_value = product.creepiness_value + value

                    # Add the vote to the vote bin
                    if not product.votes:
                        # If there is no vote bin attached to this product yet, create one now.
                        votes = ProductPageVotes()
                        votes.save()
                        product.votes = votes

                    # Add the vote to the proper "vote bin"
                    votes = product.votes.get_votes()
                    index = int((value - 1) / 20)
                    votes[index] += 1
                    product.votes.set_votes(votes)

                    # Don't save this as a revision with .save_revision() as to not spam the Audit log
                    # And don't make this live with .publish(). The Page model will have the proper
                    # data stored on it already, and the revision history won't be spammed by votes.
                    product.save()
                    return HttpResponse('Vote recorded',
                                        content_type='text/plain')
                except ProductPage.DoesNotExist:
                    return HttpResponseNotFound('Missing page')
                except ValidationError as ex:
                    return HttpResponseNotAllowed(
                        f'Payload validation failed: {ex}')
                except Error as ex:
                    print(
                        f'Internal Server Error (500) for ProductPage: {ex.message} ({type(ex)})'
                    )
                    return HttpResponseServerError()

        self.get_or_create_votes()

        return super().serve(request, *args, **kwargs)

    def save(self, *args, **kwargs):
        # When a new ProductPage is created, ensure a vote bin always exists.
        # We can use save() or a post-save Wagtail hook.
        save = super().save(*args, **kwargs)
        self.get_or_create_votes()
        return save

    class Meta:
        verbose_name = "Product Page"
예제 #9
0
class Product(ClusterableModel):
    """
    A product that may not have privacy included.
    """
    @property
    def specific(self):
        for Type in registered_product_types:
            try:
                return Type.objects.get(slug=self.slug)
            except ObjectDoesNotExist:
                pass
        return self

    draft = models.BooleanField(
        help_text=
        'When checked, this product will only show for CMS-authenticated users',
        default=True,
    )

    adult_content = models.BooleanField(
        help_text=
        'When checked, product thumbnail will appear blurred as well as have an 18+ badge on it',
        default=False,
    )

    review_date = models.DateField(help_text='Review date of this product', )

    name = models.CharField(
        max_length=100,
        help_text='Name of Product',
        blank=True,
    )

    slug = models.CharField(max_length=256,
                            help_text='slug used in urls',
                            blank=True,
                            default=None,
                            editable=False)

    company = models.CharField(
        max_length=100,
        help_text='Name of Company',
        blank=True,
    )

    product_category = models.ManyToManyField(
        'buyersguide.BuyersGuideProductCategory',
        related_name='pniproduct',
        blank=True)

    blurb = models.TextField(max_length=5000,
                             help_text='Description of the product',
                             blank=True)

    url = models.URLField(
        max_length=2048,
        help_text='Link to this product page',
        blank=True,
    )

    price = models.CharField(
        max_length=100,
        help_text='Price',
        blank=True,
    )

    image = models.FileField(
        max_length=2048,
        help_text='Image representing this product',
        upload_to=get_product_image_upload_path,
        blank=True,
    )

    cloudinary_image = CloudinaryField(
        help_text='Image representing this product - hosted on Cloudinary',
        blank=True,
        verbose_name='image',
        folder='foundationsite/buyersguide',
        use_filename=True)

    meets_minimum_security_standards = models.BooleanField(
        null=True,
        help_text='Does this product meet minimum security standards?',
    )

    # Minimum security standards (stars)

    uses_encryption = ExtendedYesNoField(
        help_text='Does the product use encryption?', )

    uses_encryption_helptext = models.TextField(max_length=5000, blank=True)

    security_updates = ExtendedYesNoField(help_text='Security updates?', )

    security_updates_helptext = models.TextField(max_length=5000, blank=True)

    strong_password = ExtendedYesNoField()

    strong_password_helptext = models.TextField(max_length=5000, blank=True)

    manage_vulnerabilities = ExtendedYesNoField(
        help_text='Manages security vulnerabilities?', )

    manage_vulnerabilities_helptext = models.TextField(max_length=5000,
                                                       blank=True)

    privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a privacy policy?')

    privacy_policy_helptext = models.TextField(  # REPURPOSED: WILL REQUIRE A 'clear' MIGRATION
        max_length=5000,
        blank=True)

    # How it handles privacy

    share_data = models.BooleanField(  # TO BE REMOVED?
        null=True,
        help_text='Does the maker share data with other companies?',
    )

    share_data_helptext = models.TextField(  # TO BE REMOVED?
        max_length=5000, blank=True)

    how_does_it_share = models.CharField(
        max_length=5000,
        help_text='How does this product handle data?',
        blank=True)

    user_friendly_privacy_policy = ExtendedYesNoField(
        help_text='Does this product have a user-friendly privacy policy?')

    user_friendly_privacy_policy_helptext = models.TextField(max_length=5000,
                                                             blank=True)
    """
    privacy_policy_links =  one to many, defined in PrivacyPolicyLink
    """

    worst_case = models.CharField(
        max_length=5000,
        help_text=
        "What's the worst thing that could happen by using this product?",
        blank=True,
    )

    # How to contact the company

    phone_number = models.CharField(
        max_length=100,
        help_text='Phone Number',
        blank=True,
    )

    live_chat = models.CharField(
        max_length=100,
        help_text='Live Chat',
        blank=True,
    )

    email = models.CharField(
        max_length=100,
        help_text='Email',
        blank=True,
    )

    twitter = models.CharField(
        max_length=100,
        help_text='Twitter username',
        blank=True,
    )

    updates = models.ManyToManyField('buyersguide.Update',
                                     related_name='pniproduct',
                                     blank=True)

    # comments are not a model field, but are "injected" on the product page instead

    related_products = models.ManyToManyField(
        'self',
        related_name='related_pniproduct',
        blank=True,
        symmetrical=False)

    # List of fields to show in admin to hide the image/cloudinary_image field. There's probably a better way to do
    # this using `_meta.get_fields()`. To be refactored in the future.
    panels = product_panels

    @property
    def is_current(self):
        d = self.review_date
        review = datetime(d.year, d.month, d.day)
        cutoff = datetime(2019, 6, 1)
        return cutoff < review

    @property
    def votes(self):
        return get_product_vote_information(self)

    def to_dict(self):
        """
        Rather than rendering products based on the instance object,
        we serialize the product to a dictionary, and instead render
        the template based on that.

        NOTE: if you add indirect fields like Foreign/ParentalKey or
              @property definitions, those needs to be added!
        """
        model_dict = model_to_dict(self)
        model_dict['votes'] = self.votes
        model_dict['slug'] = self.slug

        # model_to_dict does NOT capture related fields or @properties!
        model_dict['privacy_policy_links'] = list(
            self.privacy_policy_links.all())
        model_dict['is_current'] = self.is_current

        return model_dict

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        models.Model.save(self, *args, **kwargs)

    def __str__(self):
        return f'{self.name} ({self.company})'

    class Meta:
        # use oldest-first ordering
        ordering = ['id']
class GeneralProduct(Product):
    """
    A thing you can buy in stores and our review of it
    """

    # Can it snoop on me

    camera_device = ExtendedYesNoField(
        help_text='Does this device have or access a camera?', )

    camera_app = ExtendedYesNoField(
        help_text='Does the app have or access a camera?', )

    microphone_device = ExtendedYesNoField(
        help_text='Does this Device have or access a microphone?', )

    microphone_app = ExtendedYesNoField(
        help_text='Does this app have or access a microphone?', )

    location_device = ExtendedYesNoField(
        help_text='Does this product access your location?', )

    location_app = ExtendedYesNoField(
        help_text='Does this app access your location?', )

    # What data does it collect?

    personal_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of personal data does this product collect?')

    biometric_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of biometric data does this product collect?')

    social_data_collected = models.TextField(
        max_length=5000,
        blank=True,
        help_text='What kind of social data does this product collect?')

    # How can you control your data

    how_can_you_control_your_data = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this product let you control your data?')

    data_control_policy_is_bad = models.BooleanField(
        default=False, verbose_name='Privacy ding')

    # Company track record

    track_record_choices = [('Great', 'Great'), ('Average', 'Average'),
                            ('Needs Improvement', 'Needs Improvement'),
                            ('Bad', 'Bad')]

    company_track_record = models.CharField(
        choices=track_record_choices,
        default='Average',
        help_text='This company has a ... track record',
        max_length=20)

    track_record_is_bad = models.BooleanField(default=False,
                                              verbose_name='Privacy ding')

    track_record_details = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe the track record of this company here.')

    # Offline use

    offline_capable = ExtendedYesNoField(
        help_text='Can this product be used offline?', )

    offline_use_description = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Describe how this product can be used offline.')

    # Artificial Intelligence

    uses_ai = ExtendedYesNoField(help_text='Does the product use AI?')

    ai_uses_personal_data = ExtendedYesNoField(
        help_text=
        'Does the AI use your personal data to make decisions about you?')

    ai_is_transparent = ExtendedYesNoField(
        help_text='Does the company allow users to see how the AI works?')

    ai_helptext = models.TextField(
        max_length=5000,
        blank=True,
        help_text='Helpful text around AI to show on the product page',
    )

    # administrative panels

    panels = Product.panels.copy()

    panels = insert_panels_after(
        panels,
        'General Product Details',
        [
            MultiFieldPanel([
                FieldPanel('camera_device'),
                FieldPanel('camera_app'),
                FieldPanel('microphone_device'),
                FieldPanel('microphone_app'),
                FieldPanel('location_device'),
                FieldPanel('location_app'),
            ],
                            heading='Can it snoop?',
                            classname='collapsible'),
        ],
    )

    panels = insert_panels_after(panels, 'What is required to sign up', [
        MultiFieldPanel(
            [
                FieldPanel('personal_data_collected'),
                FieldPanel('biometric_data_collected'),
                FieldPanel('social_data_collected'),
            ],
            heading='What data does it collect',
            classname='collapsible',
        ),
    ])

    panels = insert_panels_after(
        panels,
        'How does it use this data',
        [
            MultiFieldPanel(
                [
                    FieldPanel('how_can_you_control_your_data'),
                    FieldPanel('data_control_policy_is_bad'),
                ],
                heading='How can you control your data',
                classname='collapsible',
            ),
            MultiFieldPanel([
                FieldPanel('company_track_record'),
                FieldPanel('track_record_is_bad'),
                FieldPanel('track_record_details'),
            ],
                            heading='Company track record',
                            classname='collapsible'),
            MultiFieldPanel([
                FieldPanel('offline_capable'),
                FieldPanel('offline_use_description'),
            ],
                            heading='Offline use',
                            classname='collapsible'),
        ],
    )

    panels = insert_panels_after(
        panels,
        'Security',
        [
            MultiFieldPanel([
                FieldPanel('uses_ai'),
                FieldPanel('ai_uses_personal_data'),
                FieldPanel('ai_is_transparent'),
                FieldPanel('ai_helptext'),
            ],
                            heading='Artificial Intelligence',
                            classname='collapsible'),
        ],
    )

    def to_dict(self):
        model_dict = super().to_dict()
        model_dict['product_type'] = 'general'
        return model_dict
예제 #11
0
class GeneralProduct(Product):
    """
    A thing you can buy in stores and our review of it
    """

    # It uses your...

    camera_device = ExtendedYesNoField(
        help_text='Does this device have or access a camera?', )

    camera_app = ExtendedYesNoField(
        help_text='Does the app have or access a camera?', )

    microphone_device = ExtendedYesNoField(
        help_text='Does this Device have or access a microphone?', )

    microphone_app = ExtendedYesNoField(
        help_text='Does this app have or access a microphone?', )

    location_device = ExtendedYesNoField(
        help_text='Does this product access your location?', )

    location_app = ExtendedYesNoField(
        help_text='Does this app access your location?', )

    # how it handles privacy

    delete_data = models.BooleanField(  # TO BE REMOVED?
        null=True,
        help_text='Can you request data be deleted?',
    )

    delete_data_helptext = models.TextField(  # TO BE REMOVED?
        max_length=5000, blank=True)

    parental_controls = ExtendedYesNoField(
        null=True,
        help_text='Are there rules for children?',
    )

    child_rules_helptext = models.TextField(  # TO BE REMOVED?
        max_length=5000, blank=True)

    collects_biometrics = ExtendedYesNoField(
        help_text='Does this product collect biometric data?', )

    collects_biometrics_helptext = models.TextField(max_length=5000,
                                                    blank=True)

    # administrative panels

    panels = Product.panels.copy()

    panels = insert_panels_after(
        panels,
        'Minimum Security Standards for general products',
        [
            MultiFieldPanel([
                FieldPanel('camera_device'),
                FieldPanel('camera_app'),
                FieldPanel('microphone_device'),
                FieldPanel('microphone_app'),
                FieldPanel('location_device'),
                FieldPanel('location_app'),
            ],
                            heading='Can it snoop?',
                            classname='collapsible'),
        ],
    )

    panels = insert_panels_after(
        panels,
        'How does it handle data sharing',
        [
            MultiFieldPanel([
                FieldPanel('delete_data'),
                FieldPanel('delete_data_helptext'),
                FieldPanel('parental_controls'),
                FieldPanel('collects_biometrics'),
                FieldPanel('collects_biometrics_helptext'),
                FieldPanel('user_friendly_privacy_policy'),
                FieldPanel('user_friendly_privacy_policy_helptext'),
            ],
                            heading='How does it handle privacy',
                            classname='collapsible'),
        ],
    )

    def to_dict(self):
        model_dict = super().to_dict()
        model_dict['delete_data'] = tri_to_quad(self.delete_data)
        model_dict['product_type'] = 'general'
        return model_dict
예제 #12
0
class SoftwareProduct(Product):
    """
    A thing you can install on your computer and our review of it
    """

    # How does it handle privacy?

    handles_recordings_how = models.TextField(
        max_length=5000,
        blank=True,
        help_text='How does this software handle your recordings')

    recording_alert = ExtendedYesNoField(
        null=True,
        help_text='Alerts when calls are being recorded?',
    )

    recording_alert_helptext = models.TextField(max_length=5000, blank=True)

    medical_privacy_compliant = models.BooleanField(
        null=True, help_text='Compliant with US medical privacy laws?')

    medical_privacy_compliant_helptext = models.TextField(max_length=5000,
                                                          blank=True)

    # Can I control it?

    host_controls = models.TextField(max_length=5000, blank=True)

    easy_to_learn_and_use = models.BooleanField(
        null=True,
        help_text='Is it easy to learn & use the features?',
    )

    easy_to_learn_and_use_helptext = models.TextField(max_length=5000,
                                                      blank=True)

    # administrative panels

    panels = Product.panels.copy()

    panels = insert_panels_after(
        panels,
        'General Product Details',
        [
            MultiFieldPanel([
                FieldPanel('handles_recordings_how'),
                FieldPanel('recording_alert'),
                FieldPanel('recording_alert_helptext'),
                FieldPanel('medical_privacy_compliant'),
                FieldPanel('medical_privacy_compliant_helptext'),
            ],
                            heading='How does it handle privacy?',
                            classname='collapsible'),
            MultiFieldPanel([
                FieldPanel('host_controls'),
                FieldPanel('easy_to_learn_and_use'),
                FieldPanel('easy_to_learn_and_use_helptext'),
            ],
                            heading='Can I control it',
                            classname='collapsible'),
        ],
    )

    def to_dict(self):
        model_dict = super().to_dict()
        model_dict['product_type'] = 'software'
        return model_dict