Beispiel #1
0
 def test_deconstruct_kwargs_kept(self):
     instance = TaggableManager(through=OfficialThroughModel, to="dummy.To")
     name, path, args, kwargs = instance.deconstruct()
     new_instance = TaggableManager(*args, **kwargs)
     self.assertEqual(
         "tests.OfficialThroughModel", new_instance.remote_field.through
     )
     self.assertEqual("dummy.To", new_instance.remote_field.model)
Beispiel #2
0
 def test_formfield(self):
     tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
     ff = tm.formfield()
     self.assertEqual(ff.label, 'categories')
     self.assertEqual(ff.help_text, u'Add some categories')
     self.assertEqual(ff.required, False)
     
     self.assertEqual(ff.clean(""), [])
     
     tm = TaggableManager()
     ff = tm.formfield()
     self.assertRaises(ValidationError, ff.clean, "")
Beispiel #3
0
    def test_formfield(self):
        tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
        ff = tm.formfield()
        self.assertEqual(ff.label, 'Categories')
        self.assertEqual(ff.help_text, 'Add some categories')
        self.assertEqual(ff.required, False)
        self.assertEqual(list(ff.queryset), list(self.food_model.tags.all()))

        self.assertEqual(list(ff.clean("")), [])

        tm = TaggableManager()
        ff = tm.formfield()
        self.assertRaises(ValidationError, ff.clean, "")
Beispiel #4
0
class Show(models.Model):
    """
    A podcast show, which has many episodes.
    """
    EXPLICIT_CHOICES = (
        (1, _("yes")),
        (2, _("no")),
        (3, _("clean")),
    )
    uuid = UUIDField(_("id"), unique=True)

    created = models.DateTimeField(_("created"),
                                   auto_now_add=True,
                                   editable=False)
    updated = models.DateTimeField(_("updated"), auto_now=True, editable=False)
    published = models.DateTimeField(_("published"),
                                     null=True,
                                     blank=True,
                                     editable=False)

    sites = models.ManyToManyField(Site, verbose_name=_('Sites'))

    ttl = models.PositiveIntegerField(
        _("ttl"),
        default=1440,
        help_text=_("""``Time to Live,`` the number of minutes a channel can be
        cached before refreshing."""))

    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="podcast_shows",
        verbose_name=_("owner"),
        on_delete=models.PROTECT,
        help_text=_(
            """Make certain the user account has a name and e-mail address.""")
    )

    editor_email = models.EmailField(
        _("editor email"),
        blank=True,
        help_text=_(
            "Email address of the person responsible for the feed's content."))
    webmaster_email = models.EmailField(
        _("webmaster email"),
        blank=True,
        help_text=_(
            "Email address of the person responsible for channel publishing."))

    if 'licenses' in settings.INSTALLED_APPS:
        license = models.ForeignKey(License, verbose_name=_("license"))
    else:
        license = models.CharField(
            _("license"),
            max_length=255,
            help_text=
            _("To publish a podcast to iTunes it is required to set a license type."
              ))

    organization = models.CharField(
        _("organization"),
        max_length=255,
        help_text=
        _("Name of the organization, company or Web site producing the podcast."
          ))
    link = models.URLField(_("link"),
                           help_text=_("""URL of either the main website or the
        podcast section of the main website."""))

    enable_comments = models.BooleanField(default=True)

    author_text = models.CharField(_("author text"),
                                   max_length=255,
                                   help_text=_("""
            This tag contains the name of the person or company that is most
            widely attributed to publishing the Podcast and will be
            displayed immediately underneath the title of the Podcast.
            The suggested format is: '[email protected] (Full Name)'
            but 'Full Name' only, is acceptable. Multiple authors
            should be comma separated."""))

    title = models.CharField(_("title"), max_length=255)
    slug = AutoSlugField(_("slug"), populate_from="title", unique="True")

    subtitle = models.CharField(
        _("subtitle"),
        max_length=255,
        help_text=_("Looks best if only a few words, like a tagline."))

    # If the show is not on iTunes, many fields may be ignored in your user forms
    on_itunes = models.BooleanField(
        _("iTunes"),
        default=True,
        help_text=_("Checked if the podcast is submitted to iTunes"))

    description_pretty = models.TextField(
        _("pretty description"),
        blank=True,
        help_text=
        "May be longer than 4000 characters and contain HTML tags and styling."
    )

    description = models.TextField(_("description"),
                                   max_length=4000,
                                   help_text=_("""
            This is your chance to tell potential subscribers all about your
            podcast. Describe your subject matter, media format,
            episode schedule, and other relevant info so that they
            know what they'll be getting when they subscribe. In
            addition, make a list of the most relevant search terms
            that you want yourp podcast to match, then build them into
            your description. Note that iTunes removes podcasts that
            include lists of irrelevant words in the itunes:summary,
            description, or itunes:keywords tags. This field can be up
            to 4000 characters."""))

    if 'photologue' in settings.INSTALLED_APPS:
        original_image = models.ForeignKey(Photo,
                                           verbose_name=_("image"),
                                           default=None,
                                           null=True,
                                           blank=True,
                                           on_delete=models.SET_NULL,
                                           help_text=_("""
                A podcast must have 1400 x 1400 pixel cover art in JPG or PNG
                format using RGB color space. See our technical spec for
                details. To be eligible for featuring on iTunes Stores,
                choose an attractive, original, and square JPEG (.jpg) or
                PNG (.png) image at a size of 1400x1400 pixels. The image
                will be scaled down to 50x50 pixels at smallest in iTunes.
                For reference see the <a
                href="http://www.apple.com/itunes/podcasts/specs.html#metadata">iTunes
                Podcast specs</a>.<br /><br /> For episode artwork to
                display in iTunes, image must be <a
                href="http://answers.yahoo.com/question/index?qid=20080501164348AAjvBvQ">
                saved to file's <strong>metadata</strong></a> before
                enclosure uploading!"""))
    else:
        original_image = ImageField(_("image"),
                                    upload_to=get_show_upload_folder,
                                    blank=True,
                                    help_text=_("""
                A podcast must have 1400 x 1400 pixel cover art in JPG or PNG
                format using RGB color space. See our technical spec for
                details. To be eligible for featuring on iTunes Stores,
                choose an attractive, original, and square JPEG (.jpg) or
                PNG (.png) image at a size of 1400x1400 pixels. The image
                will be scaled down to 50x50 pixels at smallest in iTunes.
                For reference see the <a
                href="http://www.apple.com/itunes/podcasts/specs.html#metadata">iTunes
                Podcast specs</a>.<br /><br /> For episode artwork to
                display in iTunes, image must be <a
                href="http://answers.yahoo.com/question/index?qid=20080501164348AAjvBvQ">
                saved to file's <strong>metadata</strong></a> before
                enclosure uploading!"""))

    if ResizeToFill:
        admin_thumb_sm = ImageSpecField(source="original_image",
                                        processors=[ResizeToFill(50, 50)],
                                        options={"quality": 100})
        admin_thumb_lg = ImageSpecField(source="original_image",
                                        processors=[ResizeToFill(450, 450)],
                                        options={"quality": 100})
        img_show_sm = ImageSpecField(source="original_image",
                                     processors=[ResizeToFill(120, 120)],
                                     options={"quality": 100})
        img_show_lg = ImageSpecField(source="original_image",
                                     processors=[ResizeToFill(550, 550)],
                                     options={"quality": 100})
        img_itunes_sm = ImageSpecField(source="original_image",
                                       processors=[ResizeToFill(144, 144)],
                                       options={"quality": 100})
        img_itunes_lg = ImageSpecField(source="original_image",
                                       processors=[ResizeToFill(1400, 1400)],
                                       options={"quality": 100})

    feedburner = models.URLField(
        _("feedburner url"),
        blank=True,
        help_text=_("""Fill this out after saving this show and at least one
            episode. URL should look like "http://feeds.feedburner.com/TitleOfShow".
            See <a href="http://code.google.com/p/django-podcast/">documentation</a>
            for more. <a href="http://www.feedburner.com/fb/a/ping">Manually ping</a>"""
                    ))

    # iTunes specific fields
    explicit = models.PositiveSmallIntegerField(
        _("explicit"),
        default=1,
        choices=EXPLICIT_CHOICES,
        help_text=_("``Clean`` will put the clean iTunes graphic by it."))
    redirect = models.URLField(
        _("redirect"),
        blank=True,
        help_text=_("""The show's new URL feed if changing
            the URL of the current show feed. Must continue old feed for at least
            two weeks and write a 301 redirect for old feed."""))
    keywords = models.CharField(
        _("keywords"),
        max_length=255,
        blank=True,
        help_text=_("""A comma-demlimitedlist of up to 12 words for iTunes
            searches. Perhaps include misspellings of the title."""))
    itunes = models.URLField(
        _("itunes store url"),
        blank=True,
        help_text=_("""Fill this out after saving this show and at least one
            episode. URL should look like:
            "http://phobos.apple.com/WebObjects/MZStore.woa/wa/viewPodcast?id=000000000".
            See <a href="http://code.google.com/p/django-podcast/">documentation</a> for more."""
                    ))

    twitter_tweet_prefix = models.CharField(
        _("Twitter tweet prefix"),
        max_length=80,
        help_text=_(
            "Enter a short ``tweet_text`` prefix for new episodes on this show."
        ),
        blank=True)

    objects = ShowQuerySet.as_manager()
    tags = TaggableManager(blank=True)

    class Meta:
        verbose_name = _("Show")
        verbose_name_plural = _("Shows")
        ordering = ("organization", "slug")

    def __str__(self):
        return self.title

    def get_share_url(self):
        return "http://{0}{1}".format(Site.objects.get_current(),
                                      self.get_absolute_url())

    def get_absolute_url(self):
        return reverse("podcasting_show_detail", kwargs={"slug": self.slug})

    @property
    def current_episode(self):
        try:
            return self.episode_set.published().order_by("-published")[0]
        except IndexError:
            return None
Beispiel #5
0
class Batch(models.Model):
    '''A batch has one or more images for some number of patients, each of which
    is associated with a Study or Session. A batch maps cleanly to a folder that is
    dropped into data for processing, and the application moves through tasks based
    on batches.
    '''
    uid = models.CharField(max_length=200, null=False, unique=True)

    status = models.CharField(choices=BATCH_STATUS,
                              default="NEW",
                              max_length=250)

    add_date = models.DateTimeField('date added', auto_now_add=True)
    has_error = models.BooleanField(choices=ERROR_STATUS,
                                    default=False,
                                    verbose_name="HasError")

    qa = JSONField(default=dict())
    logs = JSONField(default=dict())
    modify_date = models.DateTimeField('date modified', auto_now=True)
    tags = TaggableManager()

    def change_images_status(self, status):
        '''change all images to have the same status'''
        for dcm in self.image_set.all():
            dcm.status = status
            dcm.save()

    def get_image_paths(self):
        '''return file paths for all images associated
        with a batch'''
        image_files = []
        for dcm in self.image_set.all():
            try:
                if hasattr(dcm.image, 'file'):
                    dicom_file = dcm.image.path
                    if os.path.exists(dicom_file):
                        image_files.append(dicom_file)

            # Image object has no file associated with it
            except ValueError:
                pass

        return image_files

    def get_finished(self):
        '''return file paths that aren't in PHI folder'''
        return [x for x in self.get_image_paths() if "/PHI/" not in x]

    def get_path(self):
        return "%s/%s" % (MEDIA_ROOT, self.id)

    def get_absolute_url(self):
        return reverse('batch_details', args=[str(self.id)])

    def __str__(self):
        return "%s-%s" % (self.id, self.uid)

    def __unicode__(self):
        return "%s-%s" % (self.id, self.uid)

    def get_label(self):
        return "batch"

    class Meta:
        app_label = 'main'
Beispiel #6
0
class FOIARequest(models.Model):
    """A Freedom of Information Act request"""
    # pylint: disable=too-many-public-methods
    # pylint: disable=too-many-instance-attributes

    user = models.ForeignKey(User)
    title = models.CharField(max_length=255, db_index=True)
    slug = models.SlugField(max_length=255)
    status = models.CharField(max_length=10, choices=STATUS, db_index=True)
    jurisdiction = models.ForeignKey('jurisdiction.Jurisdiction')
    agency = models.ForeignKey('agency.Agency', blank=True, null=True)
    date_submitted = models.DateField(blank=True, null=True, db_index=True)
    date_updated = models.DateField(blank=True, null=True, db_index=True)
    date_done = models.DateField(blank=True,
                                 null=True,
                                 verbose_name='Date response received')
    date_due = models.DateField(blank=True, null=True, db_index=True)
    days_until_due = models.IntegerField(blank=True, null=True)
    date_followup = models.DateField(blank=True, null=True)
    date_estimate = models.DateField(blank=True,
                                     null=True,
                                     verbose_name='Estimated Date Completed')
    date_processing = models.DateField(blank=True, null=True)
    embargo = models.BooleanField(default=False)
    permanent_embargo = models.BooleanField(default=False)
    date_embargo = models.DateField(blank=True, null=True)
    price = models.DecimalField(max_digits=14,
                                decimal_places=2,
                                default='0.00')
    requested_docs = models.TextField(blank=True)
    description = models.TextField(blank=True)
    featured = models.BooleanField(default=False)
    tracker = models.BooleanField(default=False)
    sidebar_html = models.TextField(blank=True)
    tracking_id = models.CharField(blank=True, max_length=255)
    mail_id = models.CharField(blank=True, max_length=255, editable=False)
    updated = models.BooleanField(default=False)
    email = models.CharField(blank=True, max_length=254)
    other_emails = fields.EmailsListField(blank=True, max_length=255)
    times_viewed = models.IntegerField(default=0)
    disable_autofollowups = models.BooleanField(default=False)
    missing_proxy = models.BooleanField(
        default=False,
        help_text='This request requires a proxy to file, but no such '
        'proxy was avilable upon draft creation.')
    parent = models.ForeignKey('self',
                               blank=True,
                               null=True,
                               on_delete=models.SET_NULL)
    block_incoming = models.BooleanField(
        default=False,
        help_text=('Block emails incoming to this request from '
                   'automatically being posted on the site'))
    crowdfund = models.OneToOneField('crowdfund.Crowdfund',
                                     related_name='foia',
                                     blank=True,
                                     null=True)
    multirequest = models.ForeignKey(
        'foia.FOIAMultiRequest',
        blank=True,
        null=True,
    )

    read_collaborators = models.ManyToManyField(
        User,
        related_name='read_access',
        blank=True,
    )
    edit_collaborators = models.ManyToManyField(
        User,
        related_name='edit_access',
        blank=True,
    )
    access_key = models.CharField(blank=True, max_length=255)

    objects = FOIARequestQuerySet.as_manager()
    tags = TaggableManager(through=TaggedItemBase, blank=True)

    foia_type = 'foia'

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        """The url for this object"""
        return reverse('foia-detail',
                       kwargs={
                           'jurisdiction': self.jurisdiction.slug,
                           'jidx': self.jurisdiction.pk,
                           'slug': self.slug,
                           'idx': self.pk,
                       })

    def save(self, *args, **kwargs):
        """Normalize fields before saving and set the embargo expiration if necessary"""
        self.slug = slugify(self.slug)
        self.title = self.title.strip()
        if self.embargo:
            if self.status in END_STATUS:
                default_date = date.today() + timedelta(30)
                existing_date = self.date_embargo
                self.date_embargo = default_date if not existing_date else existing_date
            else:
                self.date_embargo = None
        if self.status == 'submitted' and self.date_processing is None:
            self.date_processing = date.today()

        # add a reversion comment if possible
        if 'comment' in kwargs:
            comment = kwargs.pop('comment')
            if reversion.revision_context_manager.is_active():
                reversion.set_comment(comment)
        super(FOIARequest, self).save(*args, **kwargs)

    def is_editable(self):
        """Can this request be updated?"""
        return self.status == 'started'

    def has_crowdfund(self):
        """Does this request have crowdfunding enabled?"""
        return bool(self.crowdfund)

    def is_payable(self):
        """Can this request be payed for by the user?"""
        has_open_crowdfund = self.has_crowdfund(
        ) and not self.crowdfund.expired()
        has_payment_status = self.status == 'payment'
        return has_payment_status and not has_open_crowdfund

    def get_stripe_amount(self):
        """Output a Stripe Checkout formatted price"""
        return int(self.price * 100)

    def is_public(self):
        """Is this document viewable to everyone"""
        return self.has_perm(AnonymousUser(), 'view')

    # Request Sharing and Permissions

    def has_perm(self, user, perm):
        """Short cut for checking a FOIA permission"""
        return user.has_perm('foia.%s_foiarequest' % perm, self)

    ## Creator

    def created_by(self, user):
        """Did this user create this request?"""
        return self.user == user

    ## Editors

    def has_editor(self, user):
        """Checks whether the given user is an editor."""
        user_is_editor = False
        if self.edit_collaborators.filter(pk=user.pk).exists():
            user_is_editor = True
        return user_is_editor

    def add_editor(self, user):
        """Grants the user permission to edit this request."""
        if not self.has_viewer(user) and not self.has_editor(
                user) and not self.created_by(user):
            self.edit_collaborators.add(user)
            self.save()
            logger.info('%s granted edit access to %s', user, self)
        return

    def remove_editor(self, user):
        """Revokes the user's permission to edit this request."""
        if self.has_editor(user):
            self.edit_collaborators.remove(user)
            self.save()
            logger.info('%s revoked edit access from %s', user, self)
        return

    def demote_editor(self, user):
        """Reduces the editor's access to that of a viewer."""
        self.remove_editor(user)
        self.add_viewer(user)
        return

    ## Viewers

    def has_viewer(self, user):
        """Checks whether the given user is a viewer."""
        user_is_viewer = False
        if self.read_collaborators.filter(pk=user.pk).exists():
            user_is_viewer = True
        return user_is_viewer

    def add_viewer(self, user):
        """Grants the user permission to view this request."""
        if not self.has_viewer(user) and not self.has_editor(
                user) and not self.created_by(user):
            self.read_collaborators.add(user)
            self.save()
            logger.info('%s granted view access to %s', user, self)
        return

    def remove_viewer(self, user):
        """Revokes the user's permission to view this request."""
        if self.has_viewer(user):
            self.read_collaborators.remove(user)
            logger.info('%s revoked view access from %s', user, self)
            self.save()
        return

    def promote_viewer(self, user):
        """Enhances the viewer's access to that of an editor."""
        self.remove_viewer(user)
        self.add_editor(user)
        return

    ## Access key

    def generate_access_key(self):
        """Generates a random key for accessing the request when it is private."""
        key = utils.generate_key(24)
        self.access_key = key
        self.save()
        logger.info('New access key generated for %s', self)
        return key

    def public_documents(self):
        """Get a list of public documents attached to this request"""
        return self.files.filter(access='public')

    def first_request(self):
        """Return the first request text"""
        try:
            return self.communications.all()[0].communication
        except IndexError:
            return ''

    def last_comm(self):
        """Return the last communication"""
        return self.communications.last()

    def last_response(self):
        """Return the most recent response"""
        return self.communications.filter(
            response=True).order_by('-date').first()

    def set_mail_id(self):
        """Set the mail id, which is the unique identifier for the auto mailer system"""
        # use raw sql here in order to avoid race conditions
        uid = int(
            md5(self.title.encode('utf8') +
                datetime.now().isoformat()).hexdigest(), 16) % 10**8
        mail_id = '%s-%08d' % (self.pk, uid)
        cursor = connection.cursor()
        cursor.execute(
            "UPDATE foia_foiarequest "
            "SET mail_id = CASE WHEN mail_id='' THEN %s ELSE mail_id END "
            "WHERE id = %s", [mail_id, self.pk])
        # set object's mail id to what is in the database
        self.mail_id = FOIARequest.objects.get(pk=self.pk).mail_id

    def get_mail_id(self):
        """Get the mail id - generate it if it doesn't exist"""
        if not self.mail_id:
            self.set_mail_id()
        return self.mail_id

    def get_other_emails(self):
        """Get the other emails for this request as a list"""
        # Adding blank emails here breaks mailgun backend
        return [
            e for e in fields.email_separator_re.split(self.other_emails) if e
        ]

    def get_to_who(self):
        """Who communications are to"""
        if self.agency:
            return self.agency.name
        else:
            return ''

    def get_saved(self):
        """Get the old model that is saved in the db"""
        try:
            return FOIARequest.objects.get(pk=self.pk)
        except FOIARequest.DoesNotExist:
            return None

    def latest_response(self):
        """How many days since the last response"""
        response = self.last_response()
        if response:
            return (date.today() - response.date.date()).days

    def processing_length(self):
        """How many days since the request was set as processing"""
        days_since = 0
        if self.date_processing:
            days_since = (date.today() - self.date_processing).days
        return days_since

    def update(self, anchor=None):
        """Various actions whenever the request has been updated"""
        # pylint: disable=unused-argument
        # Do something with anchor
        self.updated = True
        self.save()
        self.update_dates()

    def notify(self, action):
        """
        Notify the owner of the request.
        Notify followers if the request is not under embargo.
        Mark any existing notifications with the same message as read,
        to avoid notifying users with duplicated information.
        """
        identical_notifications = (
            Notification.objects.for_object(self).get_unread().filter(
                action__actor_object_id=action.actor_object_id,
                action__verb=action.verb))
        for notification in identical_notifications:
            notification.mark_read()
        utils.notify(self.user, action)
        if self.is_public():
            utils.notify(followers(self), action)

    def submit(self, appeal=False, snail=False, thanks=False):
        """
        The request has been submitted.
        Notify admin and try to auto submit.
        There is functionally no difference between appeals and other submissions
        besides the receiving agency.
        The only difference between a thanks andother submissions is that we do
        not set the request status, unless the request requires a proxy.
        """

        # can email appeal if the agency has an appeal agency which has an email address
        # and can accept emailed appeals
        can_email_appeal = (appeal and self.agency
                            and self.agency.appeal_agency
                            and self.agency.appeal_agency.email
                            and self.agency.appeal_agency.can_email_appeals)
        # update email addresses for the request
        if can_email_appeal:
            self.email = self.agency.appeal_agency.get_email()
            self.other_emails = self.agency.appeal_agency.other_emails
        elif not self.email and self.agency:
            self.email = self.agency.get_email()
            self.other_emails = self.agency.other_emails
        # if agency isnt approved, do not email or snail mail
        # it will be handled after agency is approved
        approved_agency = self.agency and self.agency.status == 'approved'
        can_email = self.email and not appeal and not self.missing_proxy
        comm = self.last_comm()
        # if the request can be emailed, email it, otherwise send a notice to the admin
        # if this is a thanks, send it as normal but do not change the status
        if not snail and approved_agency and (can_email or can_email_appeal):
            if appeal and not thanks:
                self.status = 'appealing'
            elif self.has_ack() and not thanks:
                self.status = 'processed'
            elif not thanks:
                self.status = 'ack'
            self._send_msg()
            self.update_dates()
        elif self.missing_proxy:
            # flag for proxy re-submitting
            self.status = 'submitted'
            self.date_processing = date.today()
            task.models.FlaggedTask.objects.create(
                foia=self,
                text='This request was filed for an agency requiring a '
                'proxy, but no proxy was available.  Please add a suitable '
                'proxy for the state and refile it with a note that the '
                'request is being filed by a state citizen. Make sure the '
                'new request is associated with the original user\'s '
                'account. To add someone as a proxy, change their user type '
                'to "Proxy" and make sure they properly have their state '
                'set on the backend.  This message should only appear when '
                'a suitable proxy does not exist.')
        elif approved_agency:
            # snail mail it
            if not thanks:
                self.status = 'submitted'
                self.date_processing = date.today()
            notice = 'n' if self.communications.count() == 1 else 'u'
            notice = 'a' if appeal else notice
            comm.delivered = 'mail'
            comm.save()
            task.models.SnailMailTask.objects.create(category=notice,
                                                     communication=comm)
        elif not thanks:
            # there should never be a thanks to an unapproved agency
            # not an approved agency, all we do is mark as submitted
            self.status = 'submitted'
            self.date_processing = date.today()
        self.save()

    def process_attachments(self, user):
        """Attach all outbound attachments to the last communication"""
        attachments = self.pending_attachments.filter(
            user=user,
            sent=False,
        )
        comm = self.last_comm()
        access = 'private' if self.embargo else 'public'
        for attachment in attachments:
            file_ = comm.files.create(
                foia=self,
                title=os.path.basename(attachment.ffile.name),
                date=comm.date,
                source=user.get_full_name(),
                access=access,
            )
            file_.ffile.name = attachment.ffile.name
            file_.save()
        attachments.update(sent=True)

    def followup(self, automatic=False, show_all_comms=True):
        """Send a follow up email for this request"""
        from muckrock.foia.models.communication import FOIACommunication

        if self.date_estimate and date.today() < self.date_estimate:
            estimate = 'future'
        elif self.date_estimate:
            estimate = 'past'
        else:
            estimate = 'none'

        comm = FOIACommunication.objects.create(foia=self,
                                                from_who='MuckRock.com',
                                                to_who=self.get_to_who(),
                                                date=datetime.now(),
                                                response=False,
                                                full_html=False,
                                                autogenerated=automatic,
                                                communication=render_to_string(
                                                    'text/foia/followup.txt', {
                                                        'request': self,
                                                        'estimate': estimate
                                                    }))

        if not self.email and self.agency:
            self.email = self.agency.get_email()
            self.other_emails = self.agency.other_emails
            self.save()

        if self.email:
            self._send_msg(show_all_comms)
        else:
            self.status = 'submitted'
            self.date_processing = date.today()
            self.save()
            comm.delivered = 'mail'
            comm.save()
            task.models.SnailMailTask.objects.create(category='f',
                                                     communication=comm)

        # Do not self.update() here for now to avoid excessive emails
        self.update_dates()

    def appeal(self, appeal_message, user):
        """Send a followup to the agency or its appeal agency."""
        from muckrock.foia.models.communication import FOIACommunication
        communication = FOIACommunication.objects.create(
            foia=self,
            from_who=self.user.get_full_name(),
            to_who=self.get_to_who(),
            date=datetime.now(),
            communication=appeal_message,
            response=False,
            full_html=False,
            autogenerated=False)
        self.process_attachments(user)
        self.submit(appeal=True)
        return communication

    def pay(self, user, amount):
        """
        Users can make payments for request fees.
        Upon payment, we create a snail mail task and we set the request to a processing status.
        Payments are always snail mail, because we need to mail the check to the agency.
        Since collaborators may make payments, we do not assume the user is the request creator.
        Returns the communication that was generated.
        """
        from muckrock.foia.models.communication import FOIACommunication
        from muckrock.task.models import SnailMailTask
        # We mark the request as processing
        self.status = 'submitted'
        self.date_processing = date.today()
        self.save()
        # We create the payment communication and a snail mail task for it.
        payable_to = self.agency.payable_to if self.agency else None
        comm = FOIACommunication.objects.create(
            foia=self,
            from_who='MuckRock.com',
            to_who=self.get_to_who(),
            date=datetime.now(),
            delivered='mail',
            response=False,
            full_html=False,
            autogenerated=False,
            communication=render_to_string('message/communication/payment.txt',
                                           {
                                               'amount': amount,
                                               'payable_to': payable_to
                                           }))
        SnailMailTask.objects.create(communication=comm,
                                     category='p',
                                     user=user,
                                     amount=amount)
        # We perform some logging and activity generation
        logger.info('%s has paid %0.2f for request %s', user.username, amount,
                    self.title)
        utils.new_action(user, 'paid fees', target=self)
        # We return the communication we generated, in case the caller wants to do anything with it
        return comm

    def _send_msg(self, show_all_comms=True):
        """Send a message for this request as an email or fax"""
        # self.email should be set before calling this method

        # get last comm to set delivered and raw_email
        comm = self.communications.last()
        subject = comm.subject or self.default_subject()
        subject = subject[:255]

        # pylint:disable=attribute-defined-outside-init
        self.reverse_communications = self.communications.reverse()
        is_email = not all(c.isdigit() for c in self.email)
        context = {'request': self, 'show_all_comms': show_all_comms}
        if is_email:
            context['reply_link'] = self.get_agency_reply_link(self.email)
        body = render_to_string(
            'text/foia/request_email.txt',
            context,
        )

        # send the msg
        if is_email:
            self._send_email(subject, body, comm)
        else:
            self._send_fax(subject, body, comm)

        comm.subject = subject
        comm.save()

        # unblock incoming messages if we send one out
        self.block_incoming = False
        self.save()

    def get_agency_reply_link(self, email):
        """Get the link for the agency user to log in"""
        agency = self.agency
        agency_user_profile = agency.get_user().profile
        return agency_user_profile.wrap_url(
            reverse(
                'acct-agency-redirect-login',
                kwargs={
                    'agency_slug': agency.slug,
                    'agency_idx': agency.pk,
                    'foia_slug': self.slug,
                    'foia_idx': self.pk,
                },
            ),
            email=email,
        )

    def _send_email(self, subject, body, comm):
        """Send the message as an email"""

        from_addr = self.get_mail_id()
        cc_addrs = self.get_other_emails()
        from_email = '%s@%s' % (from_addr, settings.MAILGUN_SERVER_NAME)
        msg = EmailMultiAlternatives(subject=subject,
                                     body=body,
                                     from_email=from_email,
                                     to=[self.email],
                                     bcc=cc_addrs +
                                     ['*****@*****.**'],
                                     headers={
                                         'Cc': ','.join(cc_addrs),
                                         'X-Mailgun-Variables': {
                                             'comm_id': comm.pk
                                         }
                                     })
        msg.attach_alternative(linebreaks(escape(body)), 'text/html')
        # atach all files from the latest communication
        for file_ in comm.files.all():
            name = file_.name()
            content = file_.ffile.read()
            mimetype, _ = mimetypes.guess_type(name)
            if mimetype and mimetype.startswith('text/'):
                enc = chardet.detect(content)['encoding']
                content = content.decode(enc)
            msg.attach(name, content)

        msg.send(fail_silently=False)

        # update communication
        comm.set_raw_email(msg.message())
        comm.delivered = 'email'

    def _send_fax(self, subject, body, comm):
        """Send the message as a fax"""
        # pylint: disable=no-self-use
        from muckrock.foia.tasks import send_fax
        send_fax.apply_async(args=[comm.pk, subject, body])

    def update_dates(self):
        """Set the due date, follow up date and days until due attributes"""
        cal = self.jurisdiction.get_calendar()
        # first submit
        if not self.date_submitted:
            self.date_submitted = date.today()
            days = self.jurisdiction.get_days()
            if days:
                self.date_due = cal.business_days_from(date.today(), days)
        # updated from mailgun without setting status or submitted
        if self.status in ['ack', 'processed']:
            # unpause the count down
            if self.days_until_due is not None:
                self.date_due = cal.business_days_from(date.today(),
                                                       self.days_until_due)
                self.days_until_due = None
            self._update_followup_date()
        # if we are no longer waiting on the agency, do not follow up
        if self.status not in ['ack', 'processed'] and self.date_followup:
            self.date_followup = None
        # if we need to respond, pause the count down until we do
        if self.status in ['fix', 'payment'] and self.date_due:
            last_datetime = self.last_comm().date
            if not last_datetime:
                last_datetime = datetime.now()
            self.days_until_due = cal.business_days_between(
                last_datetime.date(), self.date_due)
            self.date_due = None
        self.save()

    def _update_followup_date(self):
        """Update the follow up date"""
        try:
            new_date = self.last_comm().date.date() + timedelta(
                self._followup_days())
            if self.date_due and self.date_due > new_date:
                new_date = self.date_due

            if not self.date_followup or self.date_followup < new_date:
                self.date_followup = new_date

        except IndexError:
            # This request has no communications at the moment, cannot asign a follow up date
            pass

    def _followup_days(self):
        """How many days do we wait until we follow up?"""
        if self.status == 'ack' and self.jurisdiction:
            # if we have not at least been acknowledged yet, set the days
            # to the period required by law
            jurisdiction_days = self.jurisdiction.get_days()
            if jurisdiction_days is not None:
                return jurisdiction_days
        if self.date_estimate and date.today() < self.date_estimate:
            # return the days until the estimated date
            date_difference = self.date_estimate - date.today()
            return date_difference.days
        if self.jurisdiction and self.jurisdiction.level == 'f':
            return 30
        else:
            return 15

    def update_tags(self, tags):
        """Update the requests tags"""
        tag_set = set()
        for tag in parse_tags(tags):
            new_tag, _ = Tag.objects.get_or_create(name=tag)
            tag_set.add(new_tag)
        self.tags.set(*tag_set)

    def user_actions(self, user):
        '''Provides action interfaces for users'''
        is_owner = self.created_by(user)
        is_agency_user = (user.is_authenticated()
                          and user.profile.acct_type == 'agency')
        can_follow = (user.is_authenticated() and not is_owner
                      and not is_agency_user)
        is_following = user.is_authenticated() and user in followers(self)
        is_admin = user.is_staff
        kwargs = {
            'jurisdiction': self.jurisdiction.slug,
            'jidx': self.jurisdiction.pk,
            'idx': self.pk,
            'slug': self.slug
        }
        return [
            Action(test=not is_agency_user,
                   link=reverse('foia-clone', kwargs=kwargs),
                   title='Clone',
                   desc='Start a new request using this one as a base',
                   class_name='primary'),
            Action(test=can_follow,
                   link=reverse('foia-follow', kwargs=kwargs),
                   title=('Unfollow' if is_following else 'Follow'),
                   class_name=('default' if is_following else 'primary')),
            Action(
                test=self.has_perm(user, 'flag'),
                title='Get Help',
                action='flag',
                desc=
                u'Something broken, buggy, or off?  Let us know and we’ll fix it',
                class_name='failure modal'),
            Action(test=is_admin,
                   title='Contact User',
                   action='contact_user',
                   desc=u'Send this request\'s owner an email',
                   class_name='modal'),
        ]

    def contextual_request_actions(self, user, can_edit):
        '''Provides context-sensitive action interfaces for requests'''
        can_follow_up = can_edit and self.status != 'started'
        can_appeal = self.has_perm(user, 'appeal')
        kwargs = {
            'jurisdiction': self.jurisdiction.slug,
            'jidx': self.jurisdiction.pk,
            'idx': self.pk,
            'slug': self.slug
        }
        return [
            Action(test=user.is_staff,
                   link=reverse('foia-admin-fix', kwargs=kwargs),
                   title='Admin Fix',
                   desc='Open the admin fix form',
                   class_name='default'),
            Action(
                test=can_edit,
                title='Get Advice',
                action='question',
                desc=
                u'Get your questions answered by Muckrock’s community of FOIA experts',
                class_name='modal'),
            Action(test=can_follow_up,
                   title='Follow Up',
                   action='follow_up',
                   desc='Send a message directly to the agency',
                   class_name='reply'),
            Action(test=can_appeal,
                   title='Appeal',
                   action='appeal',
                   desc=u'Appeal an agency’s decision',
                   class_name='reply'),
        ]

    def total_pages(self):
        """Get the total number of pages for this request"""
        pages = self.files.aggregate(Sum('pages'))['pages__sum']
        if pages is None:
            return 0
        return pages

    def has_ack(self):
        """Has this request been acknowledged?"""
        return self.communications.filter(response=True).exists()

    def proxy_reject(self):
        """Mark this request as being rejected due to a proxy being required"""
        from muckrock.task.models import FlaggedTask
        # mark the agency as requiring a proxy going forward
        self.agency.requires_proxy = True
        self.agency.save()
        # mark to re-file with a proxy
        FlaggedTask.objects.create(
            foia=self,
            text='This request was rejected as requiring a proxy; please refile'
            ' it with one of our volunteers names and a note that the request is'
            ' being filed by a state citizen. Make sure the new request is'
            ' associated with the original user\'s account. To add someone as'
            ' a proxy, change their user type to "Proxy" and make sure they'
            ' properly have their state set on the backend. This message should'
            ' only appear the first time an agency rejects a request for being'
            ' from an out-of-state resident.')
        self.notes.create(
            author=User.objects.get(username='******'),
            note='The request has been rejected with the agency stating that '
            'you must be a resident of the state. MuckRock is working with our '
            'in-state volunteers to refile this request, and it should appear '
            'in your account within a few days.',
        )

    def default_subject(self):
        """Make a subject line for a communication for this request"""
        law_name = self.jurisdiction.get_law_name()
        if self.tracking_id:
            return 'RE: %s Request #%s' % (law_name, self.tracking_id)
        elif self.communications.count() > 1:
            return 'RE: %s Request: %s' % (law_name, self.title)
        else:
            return '%s Request: %s' % (law_name, self.title)

    class Meta:
        # pylint: disable=too-few-public-methods
        ordering = ['title']
        verbose_name = 'FOIA Request'
        app_label = 'foia'
        permissions = (
            ('view_foiarequest', 'Can view this request'),
            ('embargo_foiarequest', 'Can embargo request to make it private'),
            ('embargo_perm_foiarequest',
             'Can embargo a request permananently'),
            ('crowdfund_foiarequest',
             'Can start a crowdfund campaign for the request'),
            ('appeal_foiarequest', 'Can appeal the requests decision'),
            ('thank_foiarequest', 'Can thank the FOI officer for their help'),
            ('flag_foiarequest', 'Can flag the request for staff attention'),
            ('followup_foiarequest', 'Can send a manual follow up'),
            ('agency_reply_foiarequest', 'Can send a direct reply'),
            ('upload_attachment_foiarequest', 'Can upload an attachment'),
        )
Beispiel #7
0
class NewsPost(models.Model):
    # user = UserForeignKey(
    #     auto_user_add=True, on_delete=models.CASCADE)
    author = models.ForeignKey(Profile,
                               null=True,
                               blank=True,
                               on_delete=models.CASCADE)
    title = models.CharField(verbose_name='title', max_length=100)
    over_view = models.TextField(blank=True, null=True)
    content = models.TextField(verbose_name='')
    tags = TaggableManager()
    comment_count = models.IntegerField(default=0)
    thumbnail = models.ImageField(
        default='/News_Pictures/element5-digital-WCPg9ROZbM0-unsplash.jpg',
        upload_to='News_Pictures',
        null=True,
        blank=True)
    # categories = models.ManyToManyField(Category)
    categories = models.ManyToManyField(Categories)
    featured = models.BooleanField(default=True)
    previous_post = models.ForeignKey('self',
                                      on_delete=models.SET_NULL,
                                      related_name='previous',
                                      null=True,
                                      blank=True)
    next_post = models.ForeignKey('self',
                                  on_delete=models.SET_NULL,
                                  related_name='next',
                                  null=True,
                                  blank=True)
    liked = models.ManyToManyField(Profile,
                                   related_name='like',
                                   default=None,
                                   blank=True)
    publish = models.DateField(auto_now=False, auto_now_add=False)
    words_count = models.IntegerField(default=0)
    read_time = models.TimeField(null=True, blank=True)
    update = models.DateTimeField(auto_now=True, auto_now_add=False)
    date_created = models.DateTimeField(auto_now=False, auto_now_add=True)
    time_stamp = models.DateTimeField(auto_now_add=True)

    objects = PostManager()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("news:post-details", kwargs={'newspost_id': self.id})

    def get_over_view_mark_down(self):
        over_view = self.over_view
        markdown_text = markdown(over_view)
        return mark_safe(markdown_text)

    def get_mark_down(self):
        content = self.content
        markdown_text = markdown(content)
        return mark_safe(markdown_text)

    def get_tag_url(self):
        return reverse("news:post-by-tag", kwargs={'newspost_id': self.id})

    def get_like_url(self):
        return reverse("news:like", kwargs={'newspost_id': self.id})

    def get_update_url(self):
        return reverse("news:update-post", kwargs={'newspost_id': self.id})

    def get_delete_url(self):
        return reverse("news:delete-post", kwargs={'newspost_id': self.id})

    @property
    def get_comments(self):
        return self.comments.all().order_by('-time_stamp')

    @property
    def num_liked(self):
        return self.liked.all().count()

    @property
    def view_count(self):
        return PostView.objects.filter(post=self).count()
Beispiel #8
0
class FOIAComposer(models.Model):
    """A FOIA request composer"""

    # pylint: disable=too-many-instance-attributes

    user = models.ForeignKey(User,
                             on_delete=models.PROTECT,
                             related_name="composers")
    # only null for initial migration
    organization = models.ForeignKey(
        "organization.Organization",
        on_delete=models.PROTECT,
        related_name="composers",
        null=True,
    )
    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    status = models.CharField(max_length=10, choices=STATUS, default="started")
    agencies = models.ManyToManyField("agency.Agency",
                                      related_name="composers")
    requested_docs = models.TextField(blank=True)
    edited_boilerplate = models.BooleanField(default=False)
    datetime_created = models.DateTimeField(default=timezone.now,
                                            db_index=True)
    datetime_submitted = models.DateTimeField(blank=True,
                                              null=True,
                                              db_index=True)
    embargo = models.BooleanField(default=False)
    permanent_embargo = models.BooleanField(default=False)
    parent = models.ForeignKey(
        "self",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        help_text="The composer this was cloned from, if cloned",
    )

    # for refunding requests if necessary
    num_monthly_requests = models.PositiveSmallIntegerField(default=0)
    num_reg_requests = models.PositiveSmallIntegerField(default=0)

    # for delayed submission
    delayed_id = models.CharField(blank=True, max_length=255)

    objects = FOIAComposerQuerySet.as_manager()
    tags = TaggableManager(through=TaggedItemBase, blank=True)

    class Meta:
        verbose_name = "FOIA Composer"

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        """Set title and slug on save"""
        # pylint: disable=signature-differs
        self.title = self.title.strip() or "Untitled"
        self.slug = slugify(self.title) or "untitled"
        super(FOIAComposer, self).save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        """Resolve any pending new agency tasks"""
        # pylint: disable=signature-differs
        for agency in self.agencies.filter(status="pending"):
            if agency.composers.count() == 1:
                agency.delete()
        super(FOIAComposer, self).delete(*args, **kwargs)

    def get_absolute_url(self):
        """The url for this object"""
        return reverse("foia-composer-detail",
                       kwargs={
                           "slug": self.slug,
                           "idx": self.pk
                       })

    def submit(self, contact_info=None, no_proxy=False):
        """Submit a composer to create the requests"""
        # pylint: disable=import-outside-toplevel
        from muckrock.foia.tasks import composer_create_foias, composer_delayed_submit

        num_requests = self.agencies.count()
        request_count = self.organization.make_requests(num_requests)
        self.num_reg_requests = request_count["regular"]
        self.num_monthly_requests = request_count["monthly"]
        self.status = "submitted"
        self.datetime_submitted = timezone.now()
        self.save()

        if num_requests == 1:
            # if only one request, create it immediately so we can redirect there
            composer_create_foias(self.pk, contact_info, no_proxy)
        else:
            # otherwise do it delayed so the page doesn't risk timing out
            composer_create_foias.delay(self.pk, contact_info, no_proxy)

        # if num_requests is less than the multi-review amount, we will approve
        # the request right away, other wise we create a multirequest task
        # if the request contains a moderated keyword, it will also not be
        # approved
        approve = (num_requests < settings.MULTI_REVIEW_AMOUNT
                   and not self.needs_moderation())
        result = composer_delayed_submit.apply_async(
            args=(self.pk, approve, contact_info),
            countdown=COMPOSER_SUBMIT_DELAY)
        self.delayed_id = result.id
        self.save()

    def needs_moderation(self):
        """Check for moderated keywords"""
        for keyword in config.MODERATION_KEYWORDS.split("\n"):
            if keyword in self.title or keyword in self.requested_docs:
                return True
        return False

    def approved(self, contact_info=None):
        """A pending composer is approved for sending to the agencies"""
        for foia in self.foias.all():
            foia.submit(contact_info=contact_info)
        self.status = "filed"
        self.save()

    def has_perm(self, user, perm):
        """Short cut for checking a FOIA composer permission"""
        return user.has_perm("foia.%s_foiacomposer" % perm, self)

    def return_requests(self, num_requests=None):
        """Return requests to the composer's author"""
        if num_requests is None:
            # if no num_requests passed in, refund all requests
            return_amts = {
                "regular": self.num_reg_requests,
                "monthly": self.num_monthly_requests,
            }
        else:
            return_amts = self._calc_return_requests(num_requests)

        self._return_requests(return_amts)

    @transaction.atomic
    def _return_requests(self, return_amts):
        """Helper method for return requests

        Does the actually returning
        """
        self.num_reg_requests = F("num_reg_requests") - Least(
            return_amts["regular"], F("num_reg_requests"))
        self.num_monthly_requests = F("num_monthly_requests") - Least(
            return_amts["monthly"], F("num_monthly_requests"))
        self.save()

        self.organization.return_requests(return_amts)

    def _calc_return_requests(self, num_requests):
        """Determine how many of each type of request to return"""
        used = [self.num_reg_requests, self.num_monthly_requests]
        ret = []
        while num_requests:
            try:
                num_used = used.pop(0)
            except IndexError:
                ret.append(num_requests)
                break
            else:
                num_ret = min(num_used, num_requests)
                num_requests -= num_ret
                ret.append(num_ret)
        ret_dict = dict(
            zip_longest(["regular", "monthly", "extra"], ret, fillvalue=0))
        ret_dict["regular"] += ret_dict.pop("extra")
        return ret_dict

    def revokable(self):
        """Is this composer revokable?"""
        return (self.delayed_id != "" and self.datetime_submitted >
                timezone.now() - timedelta(seconds=COMPOSER_EDIT_DELAY)
                and self.status == "submitted")

    def revoke(self):
        """Revoke a submitted composer"""
        # pylint: disable=import-outside-toplevel
        from muckrock.foia.signals import foia_file_delete_s3

        current_app.control.revoke(self.delayed_id)
        self.status = "started"
        self.delayed_id = ""
        self.datetime_submitted = None
        disconnect_kwargs = {
            "signal": post_delete,
            "receiver": foia_file_delete_s3,
            "sender": FOIAFile,
            "dispatch_uid": "muckrock.foia.signals.file_delete_s3",
        }
        with TempDisconnectSignal(**disconnect_kwargs):
            self.foias.all().delete()
        self.pending_attachments.update(sent=False)
        self.return_requests()
        self.save()

    def attachments_over_size_limit(self, user):
        """Are the pending attachments for this composer over the size limit?"""
        total_size = sum(
            a.ffile.size
            for a in self.pending_attachments.filter(user=user, sent=False))
        return total_size > settings.MAX_ATTACHMENT_TOTAL_SIZE
Beispiel #9
0
    def setUp(self):
        tags = TaggableManager()
        settings.SIGNALS = 'off'
        dc2 = DcFactory.create(name='Tokyo', symbol="dc2")
        dc1 = DcFactory.create(name="Bilbao", symbol="dc1")
        cluster1 = LogicalClusterFactory.create(id=1,
                                                name='cluster1_siteA_test')
        cluster2 = LogicalClusterFactory.create(id=2,
                                                name='cluster2_siteB_test')
        time_profile = TimeProfile.objects.create(name='generic',
                                                  max_connections=1,
                                                  connect_timeout=0.5,
                                                  first_byte_timeout=0.1,
                                                  between_bytes_timeout=1)
        non_active_active_routed_by_path = DirectorFactory.create(
            name='first_service',
            route_expression='/first',
            active_active=False,
            mode='round-robin',
            protocol='https',
            remove_path=False,
            time_profile=time_profile)

        active_active_remove_path = DirectorFactory.create(
            name='second_service',
            mode='random',
            route_expression='/second',
            remove_path=True)
        active_active_routed_by_domain = DirectorFactory.create(
            name='third_service',
            mode='random',
            router='req.http.host',
            route_expression='third.service.org')
        active_active_with_too_long_name = DirectorFactory.create(
            name='fourth_director_which_has_a_ridiculously_long_name',
            mode='random',
            router='req.http.host',
            route_expression='unusual.name.org')
        active_active_absent_in_second_cluster = DirectorFactory.create(
            name='fifth_director_only_cluster1_siteA_test',
            route_expression='/fifth')
        active_active_hashing_by_cookie = DirectorFactory.create(
            name='sixth_director_hashing_by_cookie',
            route_expression='/sixth',
            mode='hash',
            hashing_policy='req.http.cookie')
        active_active_hashing_by_url = DirectorFactory.create(
            name='seventh_director_hashing_by_url',
            route_expression='/seventh',
            mode='hash',
            hashing_policy='req.url')
        """ connect directors to clusters """
        non_active_active_routed_by_path.cluster.add(1, 2)
        active_active_remove_path.cluster.add(1, 2)
        active_active_routed_by_domain.cluster.add(1, 2)
        active_active_with_too_long_name.cluster.add(1, 2)
        active_active_absent_in_second_cluster.cluster.add(1)
        active_active_hashing_by_cookie.cluster.add(1, 2)
        active_active_hashing_by_url.cluster.add(1, 2)

        BackendFactory.create(address='127.0.1.1',
                              dc=dc2,
                              director=non_active_active_routed_by_path,
                              inherit_time_profile=True)
        BackendFactory.create(address='127.0.2.1',
                              dc=dc2,
                              director=active_active_remove_path)
        BackendFactory.create(address='127.4.2.1',
                              dc=dc1,
                              director=active_active_remove_path)
        BackendFactory.create(address='127.8.2.1',
                              dc=dc1,
                              director=active_active_routed_by_domain)
        BackendFactory.create(address='127.9.255.254',
                              port=65535,
                              dc=dc1,
                              director=active_active_with_too_long_name)
        BackendFactory.create(address='127.9.2.1',
                              dc=dc1,
                              director=active_active_absent_in_second_cluster)
        BackendFactory.create(address='127.10.2.1',
                              dc=dc1,
                              director=active_active_hashing_by_cookie)
        BackendFactory.create(address='127.11.2.1',
                              dc=dc1,
                              director=active_active_hashing_by_url)
        canary_backend = BackendFactory.create(
            address='127.4.2.2',
            dc=dc1,
            director=active_active_remove_path,
            weight=0)
        canary_backend.tags.add('canary')

        template_v3 = VclTemplate.objects.create(
            name='new', content='<VCL/>\n## #{vcl_variable} ##', version='3.0')
        template_v4 = VclTemplate.objects.create(name='new-v4',
                                                 content='<VCL/>',
                                                 version='4.0')

        vcl_variable = VclVariable.objects.create(key='vcl_variable',
                                                  value='vcl_variable_content',
                                                  cluster=cluster1)

        self.varnish = VarnishServer.objects.create(ip='127.0.0.1',
                                                    dc=dc2,
                                                    template=template_v3,
                                                    cluster=cluster1)
        self.varnish_dc1 = VarnishServer.objects.create(ip='127.4.0.1',
                                                        dc=dc1,
                                                        template=template_v3,
                                                        cluster=cluster1)
        self.varnish4 = VarnishServer.objects.create(ip='127.0.0.2',
                                                     dc=dc2,
                                                     template=template_v4,
                                                     cluster=cluster2)
        self.varnish3_canary = VarnishServer.objects.create(
            ip='127.0.0.3',
            dc=dc2,
            template=template_v3,
            cluster=cluster1,
            is_canary=True)
        self.varnish4_canary = VarnishServer.objects.create(
            ip='127.0.0.4',
            dc=dc2,
            template=template_v4,
            cluster=cluster2,
            is_canary=True)
Beispiel #10
0
class Post(models.Model):
    "Represents a post in a forum"

    # Post statuses.
    PENDING, OPEN, OFFTOPIC, CLOSED, DELETED = range(5)
    STATUS_CHOICES = [(PENDING, "Pending"), (OPEN, "Open"),
                      (OFFTOPIC, "Off topic"), (CLOSED, "Closed"),
                      (DELETED, "Deleted")]

    # Question types. Answers should be listed before comments.
    QUESTION, ANSWER, JOB, FORUM, PAGE, BLOG, COMMENT, DATA, TUTORIAL, BOARD, TOOL, NEWS = range(
        12)

    # Valid post types.
    TYPE_CHOICES = [(QUESTION, "Question"), (ANSWER, "Answer"),
                    (COMMENT, "Comment"), (JOB, "Job"), (FORUM, "Forum"),
                    (TUTORIAL, "Tutorial"), (DATA, "Data"), (PAGE, "Page"),
                    (TOOL, "Tool"), (NEWS, "News"), (BLOG, "Blog"),
                    (BOARD, "Bulletin Board")]
    TOP_LEVEL = {QUESTION, JOB, FORUM, BLOG, TUTORIAL, TOOL, NEWS}

    # Possile spam states.
    SPAM, NOT_SPAM, DEFAULT, SUSPECT = range(4)
    SPAM_CHOICES = [(SPAM, "Spam"), (NOT_SPAM, "Not spam"),
                    (SUSPECT, "Quarantine"), (DEFAULT, "Default")]
    # Spam labeling.
    spam = models.IntegerField(choices=SPAM_CHOICES, default=DEFAULT)

    # Spam score stores relative likely hood this post is spam.
    spam_score = models.FloatField(default=0)

    # Post status: open, closed, deleted.
    status = models.IntegerField(choices=STATUS_CHOICES,
                                 default=OPEN,
                                 db_index=True)

    # The type of the post: question, answer, comment.
    type = models.IntegerField(choices=TYPE_CHOICES, db_index=True)

    # Post title.
    title = models.CharField(max_length=200, null=False, db_index=True)

    # The user that originally created the post.
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    # The user that edited the post most recently.
    lastedit_user = models.ForeignKey(User,
                                      related_name='editor',
                                      null=True,
                                      on_delete=models.CASCADE)

    # The user that last contributed to the thread.
    last_contributor = models.ForeignKey(User,
                                         related_name='contributor',
                                         null=True,
                                         on_delete=models.CASCADE)

    # Store users contributing to the thread as "tags" to preform_search later.
    thread_users = models.ManyToManyField(User, related_name="thread_users")

    # Indicates the information value of the post.
    rank = models.FloatField(default=0, blank=True, db_index=True)

    # This post has been indexed by the search engine.
    indexed = models.BooleanField(default=False)

    # Used for efficiency
    #is_public_toplevel = models.BooleanField(default=False)

    # Show that post is top level
    is_toplevel = models.BooleanField(default=False, db_index=True)

    # Indicates whether the post has accepted answer.
    answer_count = models.IntegerField(default=0, blank=True, db_index=True)

    # The number of accepted answers.
    accept_count = models.IntegerField(default=0, blank=True)

    # The number of replies for  thread.
    reply_count = models.IntegerField(default=0, blank=True, db_index=True)

    # The number of comments that a post has.
    comment_count = models.IntegerField(default=0, blank=True)

    # Number of upvotes for the post
    vote_count = models.IntegerField(default=0, blank=True, db_index=True)

    # The total numbers of votes for a top-level post.
    thread_votecount = models.IntegerField(default=0, db_index=True)

    # The number of views for the post.
    view_count = models.IntegerField(default=0, blank=True, db_index=True)

    # Bookmark count.
    book_count = models.IntegerField(default=0)

    # How many people follow that thread.
    subs_count = models.IntegerField(default=0)

    # Post creation date.
    creation_date = models.DateTimeField(db_index=True)

    # Post last edit date.
    lastedit_date = models.DateTimeField(db_index=True)

    # Sticky posts go on top.
    sticky = models.BooleanField(default=False)

    # This will maintain the ancestor/descendant relationship bewteen posts.
    root = models.ForeignKey('self',
                             related_name="descendants",
                             null=True,
                             blank=True,
                             on_delete=models.SET_NULL)

    # This will maintain parent/child relationships between posts.
    parent = models.ForeignKey('self',
                               null=True,
                               blank=True,
                               related_name='children',
                               on_delete=models.SET_NULL)

    # This is the text that the user enters.
    content = models.TextField(default='')

    # This is the  HTML that gets displayed.
    html = models.TextField(default='')

    # The tag value is the canonical form of the post's tags
    tag_val = models.CharField(max_length=100, default="", blank=True)

    # The tag set is built from the tag string and used only for fast filtering
    tags = TaggableManager()

    # What site does the post belong to.
    site = models.ForeignKey(Site, null=True, on_delete=models.SET_NULL)

    # Unique id for the post.
    uid = models.CharField(max_length=32, unique=True, db_index=True)

    objects = PostManager()

    def parse_tags(self):
        return [tag.lower() for tag in self.tag_val.split(",") if tag]

    @property
    def get_votecount(self):

        if self.is_toplevel:
            return self.thread_votecount
        return self.vote_count

    def title_prefix(self):

        prefix = ""
        if self.is_spam:
            prefix = "Spam:"
        elif self.suspect_spam:
            prefix = "Quarantined: "
        elif not self.is_open or not self.is_question:
            prefix = f"{self.get_type_display()}:" if self.is_open else f"{self.get_status_display()}:"

        return prefix

    @property
    def suspect_spam(self):
        return self.spam == self.SUSPECT

    @property
    def is_open(self):
        return self.status == Post.OPEN and not self.is_spam and not self.suspect_spam

    def recompute_scores(self):
        # Recompute answers count
        if self.type == Post.ANSWER:
            answer_count = Post.objects.valid_posts(root=self.root,
                                                    type=Post.ANSWER).count()
            Post.objects.filter(pk=self.parent_id).update(
                answer_count=answer_count)

        reply_count = Post.objects.valid_posts(root=self.root).exclude(
            pk=self.root.pk).count()
        print(reply_count)

        Post.objects.filter(pk=self.root.id).update(reply_count=reply_count)

    def json_data(self):
        data = {
            'id':
            self.id,
            'uid':
            self.uid,
            'title':
            self.title,
            'type':
            self.get_type_display(),
            'type_id':
            self.type,
            'creation_date':
            util.datetime_to_iso(self.creation_date),
            'lastedit_date':
            util.datetime_to_iso(self.lastedit_date),
            'author_uid':
            self.author.profile.uid,
            'lastedit_user_uid':
            self.lastedit_user.profile.uid,
            'author':
            self.author.profile.name,
            'status':
            self.get_status_display(),
            'status_id':
            self.status,
            'thread_score':
            self.thread_votecount,
            'rank':
            self.rank,
            'vote_count':
            self.vote_count,
            'view_count':
            self.view_count,
            'reply_count':
            self.reply_count,
            'comment_count':
            self.comment_count,
            'book_count':
            self.book_count,
            'subs_count':
            self.subs_count,
            'answer_count':
            self.root.reply_count,
            'has_accepted':
            self.has_accepted,
            'parent_id':
            self.parent.id,
            'root_id':
            self.root_id,
            'xhtml':
            self.html,
            'content':
            self.content,
            'tag_val':
            self.tag_val,
            'url':
            f'{settings.PROTOCOL}://{settings.SITE_DOMAIN}{self.get_absolute_url()}',
        }
        return data

    @property
    def is_question(self):
        return self.type == Post.QUESTION

    @property
    def is_job(self):
        return self.type == Post.JOB

    @property
    def is_deleted(self):
        return self.status == Post.DELETED

    @property
    def not_spam(self):
        return self.spam == Post.NOT_SPAM

    @property
    def has_accepted(self):
        return bool(self.accept_count)

    def num_lines(self, offset=0):
        """
        Return number of lines in post content
        """
        return len(self.content.split("\n")) + offset

    @property
    def is_spam(self):
        return self.spam == self.SPAM

    @property
    def is_comment(self):
        return self.type == Post.COMMENT

    @property
    def is_answer(self):
        return self.type == Post.ANSWER

    def get_absolute_url(self):
        url = reverse("post_view", kwargs=dict(uid=self.root.uid))
        return url if self.is_toplevel else "%s#%s" % (url, self.uid)

    def high_spam_score(self, threshold=None):
        threshold = threshold or settings.SPAM_THRESHOLD
        return (self.spam_score >
                threshold) or self.is_spam or self.author.profile.low_rep

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

        # Needs to be imported here to avoid circular imports.
        from biostar.forum import markdown

        self.lastedit_user = self.lastedit_user or self.author

        self.creation_date = self.creation_date or util.now()
        self.lastedit_date = self.lastedit_date or util.now()
        self.last_contributor = self.lastedit_user

        # Sanitize the post body.
        self.html = markdown.parse(self.content,
                                   post=self,
                                   clean=True,
                                   escape=False)
        self.tag_val = self.tag_val.replace(' ', '')
        # Default tags
        self.tag_val = self.tag_val or "tag1,tag2"
        # Set the top level state of the post.
        self.is_toplevel = self.type in Post.TOP_LEVEL

        # This will trigger the signals
        super(Post, self).save(*args, **kwargs)

    def __str__(self):
        return "%s: %s (pk=%s)" % (self.get_type_display(), self.title,
                                   self.pk)

    def update_parent_counts(self):
        """
        Update the counts for the parent and root
        """

        descendants = Post.objects.filter(root=self.root).exclude(
            Q(pk=self.root.pk) | Q(status=Post.DELETED)
            | Q(spam=Post.SPAM))
        answer_count = descendants.filter(type=Post.ANSWER).count()
        comment_count = descendants.filter(type=Post.COMMENT).count()
        reply_count = descendants.count()
        # Update the root reply, answer, and comment counts.
        Post.objects.filter(pk=self.root.pk).update(
            reply_count=reply_count,
            answer_count=answer_count,
            comment_count=comment_count)

        children = Post.objects.filter(parent=self.parent).exclude(
            pk=self.parent.pk)
        com_count = children.filter(type=Post.COMMENT).count()

        # Update parent reply, answer, and comment counts.
        Post.objects.filter(pk=self.parent.pk, is_toplevel=False).update(
            comment_count=com_count,
            answer_count=0,
            reply_count=children.count())

    @property
    def css(self):
        # Used to simplify CSS rendering.
        status = self.get_status_display()
        if self.is_spam:
            return "spam"
        if self.suspect_spam:
            return "quarantine"

        return f"{status}".lower()

    @property
    def accepted_class(self):
        if self.status == Post.DELETED:
            return "deleted"
        if self.has_accepted and not self.is_toplevel:
            return "accepted"
        return ""

    @property
    def age_in_days(self):
        delta = util.now() - self.creation_date
        return delta.days
Beispiel #11
0
class Material(models.Model):
    ''' Materials to be used for products/bill of materials'''
    MAT_TYPE_SELECTIONS = (
        ('TIM', 'Time'),
        ('FAB', 'Fabric'),
        ('ACC', 'Accesories'),
        ('FIL', 'Filling'),
        ('SMA', 'Small Materials'),
    )

    UNIT_USAGE_SELECTIONS = (
        ('HU', 'Hours'),
        ('PI', 'Pieces'),
        ('ME', 'Meters'),
        ('KG', 'Kilograms')
    )

    UNIT_PURCHASE_SELECTIONS = (
        ('MO', 'Months'),
        ('PC', 'Pieces'),
        ('ME', 'Meters'),
        ('RO', 'Rolls'),
        ('BO', 'Box'),
        ('BA', 'Bags'),
    )    

    sku = models.CharField(max_length=50)
    sku_supplier = models.CharField(max_length=50)
    
    name = models.CharField(max_length=50)
    name_cz = models.CharField(max_length=50, blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    mat_type = models.CharField(max_length=3, choices=MAT_TYPE_SELECTIONS, 
        verbose_name="Material type")

    roll_width = models.CharField(max_length=3, verbose_name='Roll width in cm', 
        blank=True, null=True)
    fabric_width = models.CharField(max_length=3, verbose_name='Fabric width in cm', 
        blank=True, null=True)
    
    cost_per_usage_unit = models.FloatField()
    unit_usage = models.CharField(max_length=2, choices=UNIT_USAGE_SELECTIONS, 
        verbose_name="Usage unit")
    unit_purchase = models.CharField(max_length=2, choices=UNIT_PURCHASE_SELECTIONS, 
        verbose_name="Purchase unit")
    unit_usage_in_purchase = models.FloatField(verbose_name="Number of usage units in purchase unit")

    est_delivery_time = models.CharField(max_length=100, blank=True, null=True)

    supplier = models.ForeignKey(Relation, limit_choices_to={'is_supplier': True})
    sample_box_number = models.IntegerField()

    tags = TaggableManager(blank=True)


    class Meta:
        ordering = ('name',)
    
    def __unicode__(self):
        return self.name

    @property
    def usage_units_on_stock(self):
        '''Show the stock status on each location'''
        stock_status = {}
        for location in StockLocation.objects.all():
            try: 
                item_in_location = StockLocationItem.objects.get(location=location, material=self)
                stock_status[location.name] = item_in_location.quantity_in_stock
            except StockLocationItem.DoesNotExist:
                stock_status[location.name] = 0
        return stock_status
Beispiel #12
0
class ArticlePost(models.Model):
    # 文章作者。参数 on_delete 用于指定数据删除的方式
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    # 文章标题。models.CharField 为字符串字段,用于保存较短的字符串,比如标题
    title = models.CharField(max_length=100)

    # 文章正文。保存大量文本使用 TextField
    body = models.TextField()

    # 文章创建时间。参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间
    created = models.DateTimeField(default=timezone.now)

    # 文章更新时间。参数 auto_now=True 指定每次数据更新时自动写入当前时间
    updated = models.DateTimeField(auto_now=True)

    total_views = models.PositiveIntegerField(default=0)
    # 标题图
    avatar = models.ImageField(upload_to='article/%Y%m%d/', blank=True)
    # avatar = ProcessedImageField(
    #     upload_to='article/%Y%m%d',
    #     processors=[ResizeToFit(width=400)],
    #     format='JPEG',
    #     options={'quality': 100},
    # )
    # 文章栏目的 “一对多” 外键
    column = models.ForeignKey(ArticleColumn,
                               null=True,
                               blank=True,
                               on_delete=models.CASCADE,
                               related_name='article')

    tags = TaggableManager(blank=True)

    # 新增点赞数统计
    likes = models.PositiveIntegerField(default=0)

    # 内部类 class Meta 用于给 model 定义元数据
    class Meta:
        # ordering 指定模型返回的数据的排列顺序
        # '-created' 表明数据应该以倒序排列
        ordering = ('-created', )

    # 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容
    def __str__(self):
        # return self.title 将文章标题返回
        return self.title

    def get_absolute_url(self):
        return reverse('article:article_detail', args=[self.id])

    def save(self, *args, **kwargs):
        article = super(ArticlePost, self).save(*args, **kwargs)

        # 固定宽度缩放图片大小
        if self.avatar and not kwargs.get('update_fields'):
            image = Image.open(self.avatar)
            (x, y) = image.size
            new_x = 400
            new_y = int(new_x * (y / x))
            resized_image = image.resize((new_x, new_y), Image.ANTIALIAS)
            resized_image.save(self.avatar.path)

        return article
Beispiel #13
0
class ResearchProject(models.Model):
    """
    Research projects are entities for grouping collections (and by that
    resources) for future processing.

    Access to various operations on projects are described by
    :class:`ResearchProjectRole` objects.

    Research project is a kind of a middle-step between resources and classifications.
    """

    name = models.CharField(max_length=255, unique=True)
    acronym = models.CharField(max_length=10, unique=True)
    description = SafeTextField(max_length=2000, null=True, blank=True)
    abstract = SafeTextField(max_length=2000, null=True, blank=True)
    methods = SafeTextField(max_length=2000, null=True, blank=True)

    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
                              related_name='research_projects')
    collections = models.ManyToManyField('storage.Collection',
                                         through='ResearchProjectCollection',
                                         blank=True,
                                         related_name='research_projects')
    date_created = models.DateTimeField(auto_now_add=True)

    status = models.NullBooleanField(choices=ResearchProjectStatus.CHOICES)
    status_date = models.DateTimeField(blank=True, null=True, editable=False)

    keywords = TaggableManager()
    objects = ResearchProjectManager()

    class Meta:
        ordering = ['-status_date']

    def __unicode__(self):
        return unicode(self.acronym)

    def get_roles(self):
        """Return mapping between users and their roles:

        .. code-block:: python
            {
                <user>: [<role_name>, <role_name>, ...],
                <user>: [<role_name>, <role_name>, ...],
                ...
            }
        """
        role_map = {}
        roles = self.project_roles.all()
        for role in roles:
            role_map.setdefault(role.user, []).append(role)
        return role_map

    def get_user_roles(self, user=None):
        """Returns a tuple of project roles for given user.

        :param: user :class:`auth.User` instance for which the roles are
            determined
        :return: list of role names of given user withing the project
        """
        user = user or get_current_user()
        roles = self.project_roles.filter(user=user)
        return [role.get_name_display() for role in roles]

    def get_user_roles_with_profiles(self):
        return self.project_roles.all().select_related('user',
                                                       'user__userprofile')

    def can_update(self, user=None):
        """Determines whether given user can update the project.

        :param: user :class:`auth.User` instance for which test is made
        :return: True if user can update project, False otherwise
        """
        user = user or get_current_user()

        return self.status is True and user.is_authenticated() and (
            self.owner == user or self.project_roles.filter(
                user=user, name__in=ResearchProjectRoleType.EDIT).exists())

    def can_delete(self, user=None):
        """Determines whether given user can delete the project.

        :param: user :class:`auth.User` instance for which test is made
        :return: True if user can delete the project, False otherwise
        """
        user = user or get_current_user()

        return self.status is True and user.is_authenticated() and (
            self.owner == user or self.project_roles.filter(
                user=user, name__in=ResearchProjectRoleType.DELETE).exists())

    def can_view(self, user=None):
        """Determines whether given user can see the details of a project.

        :param: user :class:`auth.User` instance for which test is made
        :return: True if user can see the details of the project,
            False otherwise
        """
        user = user or get_current_user()

        return self.status is True and user.is_authenticated() and (
            self.owner == user
            or self.project_roles.filter(user=user).exists())

    def can_create_classification_project(self, user=None):
        """Determine if user can use this project to create classification
        project"""
        return self.can_view(user=user)

    def get_absolute_url(self):
        """Return url of research project details"""
        return reverse('research:project_detail', kwargs={'pk': self.pk})

    def save(self, **kwargs):
        """
        If project has been accepted, then accept date is set,
        also when project is created, two notifications are sent:

        * to user, that project has been created
        * to admins, that project has been created and waiting for
          approve or decline
        """

        if (self.status is not None and self.status_date is None):
            self.status_date = datetime_aware()
        super(ResearchProject, self).save(**kwargs)

    def get_admin_url(self):
        """
        Get full url to the research project change view in 
        admin based on project.pk
        """
        request = get_current_request()
        return request.build_absolute_uri(
            reverse('admin:research_researchproject_change', args=(self.pk, )))

    def send_create_message(self):
        """Notify all django admins about new project using
        :class:`apps.messaging.models.Message` (application messaging)
        """
        User = get_user_model()
        recipients = User.objects.filter(is_active=True, is_superuser=True)

        body_template = (
            'New research project has been created. You can approve or reject it '
            'by changing its status at:\n'
            '{url}').format(url=reverse(
                'admin:research_researchproject_change', args=(self.pk, )))

        for recipient in recipients:
            Message.objects.create(
                subject=(u"New research project: <strong>{name}</strong> "
                         u"created").format(name=self.name),
                text=body_template,
                user_from=self.owner,
                user_to=recipient,
                date_sent=datetime_aware(),
                message_type=MessageType.RESEARCH_PROJECT_CREATED)
Beispiel #14
0
class Profile(AbstractUser):

    """Fully featured Geonode user"""

    organization = models.CharField(
        _('Organization Name'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('name of the responsible organization'))
    profile = models.TextField(_('Profile'), null=True, blank=True, help_text=_('introduce yourself'))
    position = models.CharField(
        _('Position Name'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('role or position of the responsible person'))
    voice = models.CharField(_('Voice'), max_length=255, blank=True, null=True, help_text=_(
        'telephone number by which individuals can speak to the responsible organization or individual'))
    fax = models.CharField(_('Facsimile'), max_length=255, blank=True, null=True, help_text=_(
        'telephone number of a facsimile machine for the responsible organization or individual'))
    delivery = models.CharField(
        _('Delivery Point'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('physical and email address at which the organization or individual may be contacted'))
    city = models.CharField(
        _('City'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('city of the location'))
    area = models.CharField(
        _('Administrative Area'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('state, province of the location'))
    zipcode = models.CharField(
        _('Postal Code'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_('ZIP or other postal code'))
    country = models.CharField(
        choices=COUNTRIES,
        max_length=3,
        blank=True,
        null=True,
        help_text=_('country of the physical address'))
    keywords = TaggableManager(_('keywords'), blank=True, help_text=_(
        'commonly used word(s) or formalised word(s) or phrase(s) used to describe the subject \
            (space or comma-separated'))

    def get_absolute_url(self):
        return reverse('profile_detail', args=[self.username, ])

    def __unicode__(self):
        return u"%s" % (self.username)

    def class_name(value):
        return value.__class__.__name__

    USERNAME_FIELD = 'username'

    def group_list_public(self):
        return GroupProfile.objects.exclude(access="private").filter(groupmember__user=self)

    def group_list_all(self):
        return GroupProfile.objects.filter(groupmember__user=self)

    def group_list_with_default_check(self):
        group_list = GroupProfile.objects.filter(groupmember__user=self)
        default_group = GroupProfile.objects.get(slug='default')
        if len(group_list) > 1 and default_group in group_list:
            return group_list.exclude(slug='default')
        else:
            return group_list

    @property
    def is_manager_of_any_group(self):
        return GroupProfile.objects.filter(groupmember__user=self, groupmember__role="manager").exists()

    @property
    def is_member_of_any_group(self):
        return GroupProfile.objects.filter(groupmember__user=self, groupmember__role="member").exists()

    def keyword_list(self):
        """
        Returns a list of the Profile's keywords.
        """
        return [kw.name for kw in self.keywords.all()]

    @property
    def name_long(self):
        if self.first_name and self.last_name:
            return '%s %s (%s)' % (self.first_name, self.last_name, self.username)
        elif (not self.first_name) and self.last_name:
            return '%s (%s)' % (self.last_name, self.username)
        elif self.first_name and (not self.last_name):
            return '%s (%s)' % (self.first_name, self.username)
        else:
            return self.username

    @property
    def location(self):
        return format_address(self.delivery, self.zipcode, self.city, self.area, self.country)
Beispiel #15
0
 def test_internal_type_is_manytomany(self):
     self.assertEqual(TaggableManager().get_internal_type(),
                      'ManyToManyField')
Beispiel #16
0
    @property
    def post_template(self):
        return 'tumblelog/post/%s.html' % slugify(self.__class__.__name__)

    @property
    def rss_template(self):
        return [
            'tumblelog/rss/%s.html' % slugify(self.__class__.__name__),
            self.post_template,
        ]


# Add the django-taggit manager, if taggit is installed
if USE_TAGGIT:
    from taggit.managers import TaggableManager
    taggit_manager = TaggableManager()
    taggit_manager.contribute_to_class(BasePostType, 'tags')


class BaseOembedPostType(BasePostType):
    """
    Abstract post type base classes whose subclasses retrieve data from an
    oEmbed endpoint.
    """
    caption = models.TextField(_('Caption'),
        blank=True,
        null=True,
        help_text=TEXTFIELD_HELP_TEXT
    )
    version = models.CharField(_('oEmbed Version'), max_length=3, null=True, \
        blank=True, editable=True)
Beispiel #17
0
class PrimordialModel(HasEditsMixin, CreatedModifiedModel):
    """
    Common model for all object types that have these standard fields
    must use a subclass CommonModel or CommonModelNameNotUnique though
    as this lacks a name field.
    """
    class Meta:
        abstract = True

    description = models.TextField(
        blank=True,
        default='',
    )
    created_by = models.ForeignKey(
        'auth.User',
        related_name='%s(class)s_created+',
        default=None,
        null=True,
        editable=False,
        on_delete=models.SET_NULL,
    )
    modified_by = models.ForeignKey(
        'auth.User',
        related_name='%s(class)s_modified+',
        default=None,
        null=True,
        editable=False,
        on_delete=models.SET_NULL,
    )

    tags = TaggableManager(blank=True)

    def __init__(self, *args, **kwargs):
        r = super(PrimordialModel, self).__init__(*args, **kwargs)
        if self.pk:
            self._prior_values_store = self._get_fields_snapshot()
        else:
            self._prior_values_store = {}
        return r

    def save(self, *args, **kwargs):
        update_fields = kwargs.get('update_fields', [])
        user = get_current_user()
        if user and not user.id:
            user = None
        if not self.pk and not self.created_by:
            self.created_by = user
            if 'created_by' not in update_fields:
                update_fields.append('created_by')
        # Update modified_by if any editable fields have changed
        new_values = self._get_fields_snapshot()
        if (not self.pk and
                not self.modified_by) or self._values_have_edits(new_values):
            self.modified_by = user
            if 'modified_by' not in update_fields:
                update_fields.append('modified_by')
        super(PrimordialModel, self).save(*args, **kwargs)
        self._prior_values_store = new_values

    def clean_description(self):
        # Description should always be empty string, never null.
        return self.description or ''

    def validate_unique(self, exclude=None):
        super(PrimordialModel, self).validate_unique(exclude=exclude)
        model = type(self)
        if not hasattr(model, 'SOFT_UNIQUE_TOGETHER'):
            return
        errors = []
        for ut in model.SOFT_UNIQUE_TOGETHER:
            kwargs = {}
            for field_name in ut:
                kwargs[field_name] = getattr(self, field_name, None)
            try:
                obj = model.objects.get(**kwargs)
            except ObjectDoesNotExist:
                continue
            if not (self.pk and self.pk == obj.pk):
                errors.append('%s with this (%s) combination already exists.' %
                              (model.__name__,
                               ', '.join(set(ut) - {'polymorphic_ctype'})))
        if errors:
            raise ValidationError(errors)
Beispiel #18
0
class Version(models.Model):
    project = models.ForeignKey(Project,
                                verbose_name=_('Project'),
                                related_name='versions')
    type = models.CharField(
        _('Type'),
        max_length=20,
        choices=VERSION_TYPES,
        default='unknown',
    )
    # used by the vcs backend
    identifier = models.CharField(_('Identifier'), max_length=255)

    verbose_name = models.CharField(_('Verbose Name'), max_length=255)
    slug = models.CharField(_('Slug'), max_length=255)

    supported = models.BooleanField(_('Supported'), default=True)
    active = models.BooleanField(_('Active'), default=False)
    built = models.BooleanField(_('Built'), default=False)
    uploaded = models.BooleanField(_('Uploaded'), default=False)
    privacy_level = models.CharField(
        _('Privacy Level'),
        max_length=20,
        choices=constants.PRIVACY_CHOICES,
        default=DEFAULT_VERSION_PRIVACY_LEVEL,
        help_text=_("Level of privacy for this Version."))
    tags = TaggableManager(blank=True)
    machine = models.BooleanField(_('Machine Created'), default=False)
    objects = VersionManager()

    class Meta:
        unique_together = [('project', 'slug')]
        ordering = ['-verbose_name']
        permissions = (
            # Translators: Permission around whether a user can view the
            #              version
            ('view_version', _('View Version')), )

    def __unicode__(self):
        return ugettext(u"Version %(version)s of %(project)s (%(pk)s)" % {
            'version': self.verbose_name,
            'project': self.project,
            'pk': self.pk
        })

    def get_absolute_url(self):
        if not self.built and not self.uploaded:
            return ''
        return self.project.get_docs_url(version_slug=self.slug)

    def save(self, *args, **kwargs):
        """
        Add permissions to the Version for all owners on save.
        """
        obj = super(Version, self).save(*args, **kwargs)
        for owner in self.project.users.all():
            assign('view_version', owner, self)
        self.project.sync_supported_versions()
        return obj

    @property
    def remote_slug(self):
        if self.slug == 'latest':
            if self.project.default_branch:
                return self.project.default_branch
            else:
                return self.project.vcs_repo().fallback_branch
        else:
            return self.slug

    def get_subdomain_url(self):
        use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False)
        if use_subdomain:
            return "/%s/%s/" % (
                self.project.language,
                self.slug,
            )
        else:
            return reverse('docs_detail',
                           kwargs={
                               'project_slug': self.project.slug,
                               'lang_slug': self.project.language,
                               'version_slug': self.slug,
                               'filename': ''
                           })

    def get_subproject_url(self):
        return "/projects/%s/%s/%s/" % (
            self.project.slug,
            self.project.language,
            self.slug,
        )

    def get_downloads(self, pretty=False):
        project = self.project
        data = {}
        if pretty:
            if project.has_pdf(self.slug):
                data['PDF'] = project.get_production_media_url(
                    'pdf', self.slug)
            if project.has_htmlzip(self.slug):
                data['HTML'] = project.get_production_media_url(
                    'htmlzip', self.slug)
            if project.has_epub(self.slug):
                data['Epub'] = project.get_production_media_url(
                    'epub', self.slug)
        else:
            if project.has_pdf(self.slug):
                data['pdf_url'] = project.get_production_media_url(
                    'pdf', self.slug)
            if project.has_htmlzip(self.slug):
                data['htmlzip_url'] = project.get_production_media_url(
                    'htmlzip', self.slug)
            if project.has_epub(self.slug):
                data['epub_url'] = project.get_production_media_url(
                    'epub', self.slug)
        return data

    def get_conf_py_path(self):
        # Hack this for now.
        return "/docs/"
        conf_py_path = self.project.conf_file(self.slug)
        conf_py_path = conf_py_path.replace(
            self.project.checkout_path(self.slug), '')
        return conf_py_path.replace('conf.py', '')

    def get_build_path(self):
        '''Return version build path if path exists, otherwise `None`'''
        path = self.project.checkout_path(version=self.slug)
        if os.path.exists(path):
            return path
        return None

    def get_github_url(self,
                       docroot,
                       filename,
                       source_suffix='.rst',
                       action='view'):
        GITHUB_REGEXS = [
            re.compile('github.com/(.+)/(.+)(?:\.git){1}'),
            re.compile('github.com/(.+)/(.+)'),
            re.compile('github.com:(.+)/(.+).git'),
        ]
        GITHUB_URL = 'https://github.com/{user}/{repo}/{action}/{version}{docroot}{path}{source_suffix}'

        repo_url = self.project.repo
        if 'github' not in repo_url:
            return ''

        if not docroot:
            return ''
        else:
            if docroot[0] != '/':
                docroot = "/%s" % docroot
            if docroot[-1] != '/':
                docroot = "%s/" % docroot

        if action == 'view':
            action_string = 'blob'
        elif action == 'edit':
            action_string = 'edit'

        for regex in GITHUB_REGEXS:
            match = regex.search(repo_url)
            if match:
                user, repo = match.groups()
                break
        else:
            return ''
        repo = repo.rstrip('/')

        return GITHUB_URL.format(
            user=user,
            repo=repo,
            version=self.remote_slug,
            docroot=docroot,
            path=filename,
            source_suffix=source_suffix,
            action=action_string,
        )

    def get_bitbucket_url(self, docroot, filename, source_suffix='.rst'):
        BB_REGEXS = [
            re.compile('bitbucket.org/(.+)/(.+).git'),
            re.compile('bitbucket.org/(.+)/(.+)/'),
            re.compile('bitbucket.org/(.+)/(.+)'),
        ]
        BB_URL = 'https://bitbucket.org/{user}/{repo}/src/{version}{docroot}{path}{source_suffix}'

        repo_url = self.project.repo
        if 'bitbucket' not in repo_url:
            return ''
        if not docroot:
            return ''

        for regex in BB_REGEXS:
            match = regex.search(repo_url)
            if match:
                user, repo = match.groups()
                break
        else:
            return ''
        repo = repo.rstrip('/')

        return BB_URL.format(
            user=user,
            repo=repo,
            version=self.remote_slug,
            docroot=docroot,
            path=filename,
            source_suffix=source_suffix,
        )
class Publication(AbstractPublication, CreatedModifiedBy, AbstractLockable):
    slug = models.SlugField(default='', blank=True, max_length=256)
    tags = TaggableManager()
    relevance = models.IntegerField(default=0, blank=True)

    class Meta:
        verbose_name_plural = _("Publications")

    def serialize(self):
        return {
            'slug':
            self.slug,
            'image':
            self.image_url(),
            'name':
            self.name,
            'title':
            self.title,
            # 'published': self.date_start,
            'subheading':
            self.subheading,
            'categories': (i.name for i in self.categories),
            'tags': [i.name for i in self.tags.all()],
            'published_in': (f'{i.webpath.site}{i.webpath.fullpath}'
                             for i in self.publicationcontext_set.all())
        }

    def active_translations(self):
        return PublicationLocalization.objects.filter(publication=self,
                                                      is_active=True)

    def image_url(self):
        if self.preview_image:
            return self.preview_image.get_media_path()
        elif self.presentation_image:
            return self.presentation_image.get_media_path()
        else:  # pragma: no cover
            categories = self.category.all()
            for category in categories:
                if category.image:
                    return sanitize_path(
                        f'{settings.MEDIA_URL}/{category.image}')

    def image_title(self):  # pragma: no cover
        if self.preview_image: return self.preview_image.title
        if self.presentation_image: return self.presentation_image.title
        return self.title

    def image_description(self):  # pragma: no cover
        if self.preview_image: return self.preview_image.description
        if self.presentation_image: return self.presentation_image.description
        return self.subheading

    @property
    def categories(self):
        return self.category.all()

    @property
    def related_publications(self):
        related = PublicationRelated.objects.filter(publication=self,
                                                    related__is_active=True)
        # return [i for i in related if i.related.is_publicable]
        return [i for i in related]

    @property
    def related_contexts(self, unique_webpath=True, published=True):
        contexts = PublicationContext.objects.select_related('webpath')\
                                             .filter(publication=self,
                                                     is_active=True,
                                                     webpath__is_active=True)
        if published:
            now = timezone.localtime()
            contexts = contexts.filter(date_start__lte=now, date_end__gte=now)
        if not unique_webpath: return contexts
        webpaths = []
        unique_contexts = []
        for context in contexts:
            if context.webpath in webpaths: continue
            webpaths.append(context.webpath)
            unique_contexts.append(context)
        return unique_contexts

    @property
    def first_available_url(self):
        now = timezone.localtime()
        pubcontx = PublicationContext.objects.filter(publication=self,
                                                     is_active=True,
                                                     webpath__is_active=True,
                                                     date_start__lte=now,
                                                     date_end__gte=now)
        if pubcontx.exists():
            return pubcontx.first().url

    @property
    def related_links(self):
        return self.publicationlink_set.all()

    @property
    def related_embedded_links(self):
        return self.publicationlink_set.all().filter(embedded=True)

    @property
    def related_plain_links(self):
        return self.publicationlink_set.all().filter(embedded=False)

    @property
    def related_media_collections(self):
        if getattr(self, '_related_media_collections', None):
            return self._related_media_collections
        pub_collections = PublicationMediaCollection.objects.filter(
            publication=self, is_active=True, collection__is_active=True)
        self._related_media_collections = pub_collections
        return self._related_media_collections

    def translate_as(self, lang):
        """
        returns translation if available
        """
        trans = PublicationLocalization.objects.filter(publication=self,
                                                       language=lang,
                                                       is_active=True).first()
        if trans:
            self.title = trans.title
            self.subheading = trans.subheading
            self.content = trans.content

    @property
    def available_in_languages(self) -> list:
        return [(i, i.get_language_display())
                for i in PublicationLocalization.objects.filter(
                    publication=self, is_active=True)]

    def title2slug(self):
        return slugify(self.title)

    def content_save_switch(self):
        old_content_type = None
        if self.pk:
            current_entry = self.__class__.objects.filter(pk=self.pk).first()
            if current_entry:
                old_content_type = current_entry.content_type

        if all((old_content_type, self.content, self.pk,
                self.content_type != old_content_type)):

            # markdown to html
            if old_content_type == 'html':
                self.content = markdownify(self.content)
            elif old_content_type == 'markdown':
                self.content = markdown(self.content)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self.title2slug()
        self.content_save_switch()
        super(self.__class__, self).save(*args, **kwargs)

    @property
    def get_attachments(self):
        return PublicationAttachment.objects.filter(publication=self,
                                                    is_active=True).\
                                             order_by('order')

    @property
    def get_embedded_attachments(self):
        return self.get_attachments.filter(embedded=True)

    @property
    def get_plain_attachments(self):
        return self.get_attachments.filter(embedded=False)

    def get_publication_contexts(self, webpath=None):
        qdict = dict(publication=self, is_active=True)
        if webpath:
            qdict['webpath'] = webpath
        pub_context = PublicationContext.objects.filter(**qdict)
        return pub_context

    def get_publication_context(self, webpath=None):
        return self.get_publication_contexts(webpath=webpath).first()

    def url(self, webpath=None):
        pub_context = self.get_publication_context(webpath=webpath)
        if not pub_context: return ''
        return pub_context.url

    def get_url_list(self, webpath=None, category_name=None):
        pub_context = self.get_publication_context(webpath=webpath)
        if not pub_context: return ''
        return pub_context.get_url_list(category_name=category_name)

    @property
    def html_content(self):
        content = ''
        if self.content_type == 'markdown':
            content = markdown(self.content)
        elif self.content_type == 'html':
            content = self.content
        return content

    def is_localizable_by(self, user=None):
        if not user: return False

        # check if user has Django permissions to change object
        permission = check_user_permission_on_object(user, self)
        # if permission
        if permission['granted']: return True

        # if no permissions and no locks
        if not permission.get('locked', False):
            # check if user has EditorialBoard translator permissions on object
            pub_ctxs = self.get_publication_contexts()
            for pub_ctx in pub_ctxs:
                webpath = pub_ctx.webpath
                webpath_perms = webpath.is_localizable_by(user=user)
                if webpath_perms: return True
        # if no permissions
        return False

    def is_editable_by(self, user=None):
        if not user: return False

        # check if user has Django permissions to change object
        permission = check_user_permission_on_object(user, self)
        # if permission
        if permission['granted']: return True

        # if no permissions and no locks
        if not permission.get('locked', False):
            # check if user has EditorialBoard editor permissions on object
            pub_ctxs = self.get_publication_contexts()
            for pub_ctx in pub_ctxs:
                webpath = pub_ctx.webpath
                webpath_perms = webpath.is_editable_by(user=user, obj=self)
                if webpath_perms: return True
        # if no permissions
        return False

    @property
    def is_publicable(self) -> bool:
        return self.is_active

    def is_publicable_by(self, user=None):
        if not user: return False

        # check if user has Django permissions to change object
        permission = check_user_permission_on_object(user, self)
        # if permission
        if permission['granted']: return True

        # if no permissions and no locks
        if not permission.get('locked', False):
            # check if user has EditorialBoard editor permissions on object
            pub_ctxs = self.get_publication_contexts()
            for pub_ctx in pub_ctxs:
                webpath = pub_ctx.webpath
                webpath_perms = webpath.is_publicable_by(user=user, obj=self)
                if webpath_perms: return True
        # if no permissions
        return False

    def is_lockable_by(self, user):
        return True if self.is_editable_by(user) else False

    def __str__(self):
        return f'{self.name} ({self.title})'
Beispiel #20
0
class Post(ModelMeta, models.Model):
    STATUS_CHOISES = (
        ('draft', 'Draft'),
        ('published', 'Published'),
    )

    title = models.CharField(max_length=250)
    slug = models.SlugField(allow_unicode=True)

    #TODO : study about on_delete from doc

    author = models.ForeignKey(User,
                               related_name='blog_posts',
                               on_delete=models.CASCADE)
    body = models.TextField()
    thumbnailImage = FilerImageField(on_delete=models.CASCADE,
                                     related_name="blog_thumb_image")
    likes = models.ManyToManyField(User, related_name='likes', blank=True)
    tags = TaggableManager()
    categories = TreeManyToManyField(Categories, related_name="categories")
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10,
                              choices=STATUS_CHOISES,
                              default="draft")

    class Meta:
        ordering = ('-publish', )

    def __str__(self):
        return self.title

    def total_likes(self):
        return self.likes.count()

    def blog_views_count(self):
        return self.views.count()

    def cat_indexing(self):
        """«category for indexing.

        Used in Elasticsearch indexing.
        """
        data = json.dumps([cat.name for cat in self.categories.all()],
                          ensure_ascii=False)
        jsondata = json.loads(data)
        return jsondata

    title_suggest = []

    def indexing(self):
        obj = BlogIndex(meta={'index': 'blog'},
                        title=self.title,
                        title_suggest=[{
                            "input": [self.title],
                            "weight": 34
                        }, {
                            "input": self.cat_indexing(),
                        }, {
                            "input": [tag.name for tag in self.tags.all()],
                        }],
                        text=self.body,
                        cat=self.cat_indexing(),
                        publish=self.publish,
                        author=self.author.username,
                        thumbnailImage=self.thumbnailImage.url,
                        views=self.blog_views_count(),
                        url=self.get_absolute_url())
        es = Elasticsearch(['http://elasticsearch613:9200/'])
        obj.save(es, request_timeout=80)
        return obj.to_dict(include_meta=True)

    def get_absolute_url(self):
        cat = []
        for x in self.categories.all():
            cat = x.name

        return reverse('blog:post_detail', args=[cat, self.slug])

    def save(self, *args, **kwargs):
        if self.slug in (None, '', u''):
            self.slug = slugify(self.title)

        super(Post, self).save(*args, **kwargs)

    #  Keywords for django seo

    _metadata = {
        'title': 'title',
        'description': 'content',
        'keywords': 'tag_indexing'
    }
Beispiel #21
0
class Project(models.Model):
    #Auto fields
    pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True)
    modified_date = models.DateTimeField(_('Modified date'), auto_now=True)

    #Generally from conf.py
    users = models.ManyToManyField(User, verbose_name=_('User'),
                                   related_name='projects')
    name = models.CharField(_('Name'), max_length=255)
    slug = models.SlugField(_('Slug'), max_length=255, unique=True)
    description = models.TextField(_('Description'), blank=True,
                                   help_text=_('The reStructuredText '
                                               'description of the project'))
    repo = models.CharField(_('Repository URL'), max_length=100, blank=True,
                            help_text=_('Checkout URL for your code (hg, git, '
                                        'etc.). Ex. http://github.com/'
                                        'ericholscher/django-kong.git'))
    repo_type = models.CharField(_('Repository type'), max_length=10,
                                 choices=constants.REPO_CHOICES, default='git')
    project_url = models.URLField(_('Project URL'), blank=True,
                                  help_text=_('The project\'s homepage'),
                                  verify_exists=False)
    canonical_url = models.URLField(_('Canonical URL'), blank=True,
                                  help_text=_('The official URL that the docs live at. This can be at readthedocs.org, or somewhere else. Ex. http://docs.fabfile.org'),
                                  verify_exists=False)
    version = models.CharField(_('Version'), max_length=100, blank=True,
                               help_text=_('Project version these docs apply '
                                           'to, i.e. 1.0a'))
    copyright = models.CharField(_('Copyright'), max_length=255, blank=True,
                                 help_text=_('Project copyright information'))
    theme = models.CharField(
        _('Theme'), max_length=20, choices=constants.DEFAULT_THEME_CHOICES,
        default=constants.THEME_DEFAULT,
        help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-'
                   'themes" target="_blank">%s</a>') % _('Examples'))
    suffix = models.CharField(_('Suffix'), max_length=10, editable=False,
                              default='.rst')
    single_version = models.BooleanField(
        _('Single version'), default=False,
        help_text=_('A single version site has no translations and only your "latest" version, served at the root of the domain. Use this with caution, only turn it on if you will <b>never</b> have multiple versions of your docs.'))
    default_version = models.CharField(
        _('Default version'), max_length=255, default='latest',
        help_text=_('The version of your project that / redirects to'))
    # In default_branch, None max_lengtheans the backend should choose the
    # appropraite branch. Eg 'master' for git
    default_branch = models.CharField(
        _('Default branch'), max_length=255, default=None, null=True,
        blank=True, help_text=_('What branch "latest" points to. Leave empty '
                                'to use the default value for your VCS (eg. '
                                'trunk or master).'))
    requirements_file = models.CharField(
        _('Requirements file'), max_length=255, default=None, null=True,
        blank=True, help_text=_(
            'Requires Virtualenv. A <a '
            'href="http://www.pip-installer.org/en/latest/cookbook.html#requirements-files">'
            'pip requirements file</a> needed to build your documentation. '
            'Path from the root of your project.'))
    documentation_type = models.CharField(
        _('Documentation type'), max_length=20,
        choices=constants.DOCUMENTATION_CHOICES, default='sphinx',
        help_text=_('Type of documentation you are building. <a href="http://'
                    'sphinx.pocoo.org/builders.html#sphinx.builders.html.'
                    'DirectoryHTMLBuilder">More info</a>.'))
    analytics_code = models.CharField(
        _('Analytics code'), max_length=50, null=True, blank=True,
        help_text=_("Google Analytics Tracking ID (ex. UA-22345342-1). "
                    "This may slow down your page loads."))

    # Other model data.
    path = models.CharField(_('Path'), max_length=255, editable=False,
                            help_text=_("The directory where conf.py lives"))
    conf_py_file = models.CharField(
        _('Python configuration file'), max_length=255, default='', blank=True,
        help_text=_('Path from project root to conf.py file (ex. docs/conf.py)'
                    '. Leave blank if you want us to find it for you.'))

    featured = models.BooleanField(_('Featured'))
    skip = models.BooleanField(_('Skip'))
    mirror = models.BooleanField(_('Mirror'), default=False)
    use_virtualenv = models.BooleanField(
        _('Use virtualenv'),
        help_text=_("Install your project inside a virtualenv using setup.py "
                    "install"))

    # This model attribute holds the python interpreter used to create the
    # virtual environment
    python_interpreter = models.CharField(
        _('Python Interpreter'),
        max_length=20,
        choices=constants.PYTHON_CHOICES,
        default='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    use_system_packages = models.BooleanField(
        _('Use system packages'),
        help_text=_("Give the virtual environment access to the global "
                    "site-packages dir."))
    django_packages_url = models.CharField(_('Django Packages URL'),
                                           max_length=255, blank=True)
    privacy_level = models.CharField(
        _('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES,
        default='public',
        help_text=_("(Beta) Level of privacy that you want on the repository. "
                    "Protected means public but not in listings."))
    version_privacy_level = models.CharField(
        _('Version Privacy Level'), max_length=20,
        choices=constants.PRIVACY_CHOICES, default='public',
        help_text=_("(Beta) Default level of privacy you want on built "
                    "versions of documentation."))

    # Subprojects
    related_projects = models.ManyToManyField(
        'self', verbose_name=_('Related projects'), blank=True, null=True,
        symmetrical=False, through=ProjectRelationship)

    # Language bits
    language = models.CharField('Language', max_length=20, default='en',
                                help_text="The language the project "
                                "documentation is rendered in. "
                                "Note: this affects your project's URL.",
                                choices=constants.LANGUAGES)
    # A subproject pointed at it's main language, so it can be tracked
    main_language_project = models.ForeignKey('self',
                                              related_name='translations',
                                              blank=True, null=True)

    # Version State 
    num_major = models.IntegerField(
        _('Number of Major versions'), 
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X")
    )
    num_minor = models.IntegerField(
        _('Number of Minor versions'), 
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X")
    )
    num_point = models.IntegerField(
        _('Number of Point versions'), 
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0")
    )

    tags = TaggableManager(blank=True)
    objects = ProjectManager()

    class Meta:
        ordering = ('slug',)
        permissions = (
            # Translators: Permission around whether a user can view the
            # project
            ('view_project', _('View Project')),
        )

    def __unicode__(self):
        return self.name

    @property
    def subdomain(self):
        prod_domain = getattr(settings, 'PRODUCTION_DOMAIN')
        if self.canonical_domain:
            return self.canonical_domain
        else:
            subdomain_slug = self.slug.replace('_', '-')
            return "%s.%s" % (subdomain_slug, prod_domain)

    def sync_supported_versions(self):
        supported = self.supported_versions(flat=True)
        if supported:
            self.versions.filter(verbose_name__in=supported).update(supported=True)
            self.versions.exclude(verbose_name__in=supported).update(supported=False)
            self.versions.filter(verbose_name='latest').update(supported=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            # Subdomains can't have underscores in them.
            self.slug = slugify(self.name).replace('_','-')
            if self.slug == '':
                raise Exception(_("Model must have slug"))
        obj = super(Project, self).save(*args, **kwargs)
        for owner in self.users.all():
            assign('view_project', owner, self)

        # Add exceptions here for safety
        try:
            self.sync_supported_versions()
        except Exception, e:
            log.error('failed to sync supported versions', exc_info=True)
        try:
            symlink(project=self.slug)
        except Exception, e:
            log.error('failed to symlink project', exc_info=True)
Beispiel #22
0
class Resource(ClusterableModel):
    title = models.CharField(max_length=255)
    desc = RichTextField(verbose_name='Description', blank=True)

    thumbnail = models.ForeignKey('v1.CFGOVImage',
                                  null=True,
                                  blank=True,
                                  on_delete=models.SET_NULL,
                                  related_name='+')

    related_file = models.ForeignKey('wagtaildocs.Document',
                                     null=True,
                                     blank=True,
                                     on_delete=models.SET_NULL,
                                     related_name='+')

    alternate_file = models.ForeignKey('wagtaildocs.Document',
                                       null=True,
                                       blank=True,
                                       on_delete=models.SET_NULL,
                                       related_name='+')

    link = models.URLField(
        blank=True,
        help_text='Example: URL to order a few copies of a printed piece.',
        validators=[URLValidator])

    alternate_link = models.URLField(
        blank=True,
        help_text='Example: a URL to for ordering bulk copies.',
        validators=[URLValidator])

    order = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text='Resources will be listed alphabetically by title in a '
        'Resource List module, unless any in the list have a number in this '
        'field; those with an order value will appear in ascending order.')

    tags = TaggableManager(
        through=ResourceTag,
        blank=True,
        help_text='Tags can be used to filter resources in a Resource List.')

    objects = TaggableSnippetManager()

    panels = [
        FieldPanel('title'),
        FieldPanel('desc'),
        ImageChooserPanel('thumbnail'),
        DocumentChooserPanel('related_file'),
        DocumentChooserPanel('alternate_file'),
        FieldPanel('link'),
        FieldPanel('alternate_link'),
        FieldPanel('order'),
        FieldPanel('tags'),
    ]

    # Makes fields available to the Actions chooser in a Resource List module
    resource_list_field_choices = [
        ('related_file', 'Related file'),
        ('alternate_file', 'Alternate file'),
        ('link', 'Link'),
        ('alternate_link', 'Alternate link'),
    ]

    def __str__(self):
        return self.title

    class Meta:
        ordering = (
            'order',
            'title',
        )
Beispiel #23
0
class AbstractTemplate(ShareableOrgMixin, BaseConfig):
    """
    Abstract model implementing a
    netjsonconfig template
    """

    tags = TaggableManager(
        through=get_model_name('config', 'TaggedTemplate'),
        blank=True,
        help_text=_(
            'A comma-separated list of template tags, may be used '
            'to ease auto configuration with specific settings (eg: '
            '4G, mesh, WDS, VPN, ecc.)'
        ),
    )
    vpn = models.ForeignKey(
        get_model_name('config', 'Vpn'),
        verbose_name=_('VPN'),
        blank=True,
        null=True,
        on_delete=models.CASCADE,
    )
    type = models.CharField(
        _('type'),
        max_length=16,
        choices=TYPE_CHOICES,
        default='generic',
        db_index=True,
        help_text=_('template type, determines which features are available'),
    )
    default = models.BooleanField(
        _('enabled by default'),
        default=False,
        db_index=True,
        help_text=_(
            'whether new configurations will have this template enabled by default'
        ),
    )
    auto_cert = models.BooleanField(
        _('auto certificate'),
        default=default_auto_cert,
        db_index=True,
        help_text=_(
            'whether x509 client certificates should '
            'be automatically managed behind the scenes '
            'for each configuration using this template, '
            'valid only for the VPN type'
        ),
    )
    default_values = JSONField(
        _('Default Values'),
        default=dict,
        blank=True,
        help_text=_(
            'A dictionary containing the default '
            'values for the variables used by this '
            'template; these default variables will '
            'be used during schema validation.'
        ),
        load_kwargs={'object_pairs_hook': OrderedDict},
        dump_kwargs={'indent': 4},
    )
    __template__ = True

    class Meta:
        abstract = True
        verbose_name = _('template')
        verbose_name_plural = _('templates')
        unique_together = (('organization', 'name'),)

    def save(self, *args, **kwargs):
        """
        modifies status of related configs
        if key attributes have changed (queries the database)
        """
        update_related_config_status = False
        if not self._state.adding:
            current = self.__class__.objects.get(pk=self.pk)
            for attr in ['backend', 'config']:
                if getattr(self, attr) != getattr(current, attr):
                    update_related_config_status = True
                    break
        # save current changes
        super().save(*args, **kwargs)
        # update relations
        if update_related_config_status:
            transaction.on_commit(
                lambda: update_template_related_config_status.delay(self.pk)
            )

    def _update_related_config_status(self):
        changing_status = list(self.config_relations.exclude(status='modified'))
        self.config_relations.update(status='modified')
        for config in self.config_relations.select_related('device').iterator():
            # config modified signal sent regardless
            config._send_config_modified_signal()
            # config status changed signal sent only if status changed
            if config in changing_status:
                config._send_config_status_changed_signal()

    def clean(self, *args, **kwargs):
        """
        * validates org relationship of VPN if present
        * validates default_values field
        * ensures VPN is selected if type is VPN
        * clears VPN specific fields if type is not VPN
        * automatically determines configuration if necessary
        """
        self._validate_org_relation('vpn')
        if not self.default_values:
            self.default_values = {}
        if not isinstance(self.default_values, dict):
            raise ValidationError(
                {'default_values': _('the supplied value is not a JSON object')}
            )
        if self.type == 'vpn' and not self.vpn:
            raise ValidationError(
                {'vpn': _('A VPN must be selected when template type is "VPN"')}
            )
        elif self.type != 'vpn':
            self.vpn = None
            self.auto_cert = False
        if self.type == 'vpn' and not self.config:
            self.config = self.vpn.auto_client(auto_cert=self.auto_cert)
        super().clean(*args, **kwargs)
        if not self.config:
            raise ValidationError(_('The configuration field cannot be empty.'))

    def get_context(self, system=False):
        context = {}
        if self.default_values and not system:
            context = copy(self.default_values)
        context.update(super().get_context())
        return context

    def get_system_context(self):
        system_context = self.get_context(system=True)
        return OrderedDict(sorted(system_context.items()))

    def clone(self, user):
        clone = copy(self)
        clone.name = self.__get_clone_name()
        clone._state.adding = True
        clone.pk = None
        # avoid cloned templates to be flagged as default
        # to avoid potential unwanted duplications in
        # newly registrated devices
        clone.default = False
        clone.full_clean()
        clone.save()
        ct = ContentType.objects.get(model='template')
        LogEntry.objects.log_action(
            user_id=user.id,
            content_type_id=ct.pk,
            object_id=clone.pk,
            object_repr=clone.name,
            action_flag=ADDITION,
        )
        return clone

    def __get_clone_name(self):
        name = '{} (Clone)'.format(self.name)
        index = 2
        while self.__class__.objects.filter(name=name).count():
            name = '{} (Clone {})'.format(self.name, index)
            index += 1
        return name
Beispiel #24
0
class ArticlePost(models.Model):
    """
    文章的 Model
    """

    # 定义文章作者。 author 通过 models.ForeignKey 外键与内建的 User 模型关联在一起
    # 参数 on_delete 用于指定数据删除的方式,避免两个关联表的数据不一致。通常设置为 CASCADE 级联删除就可以了
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    # 文章标题图
    avatar = ProcessedImageField(
        upload_to='article/%Y%m%d',
        processors=[ResizeToFit(400, 300)],
        format='JPEG',
        options={'quality': 100},
    )

    # 文章栏目的 “一对多” 外键
    column = models.ForeignKey(ArticleColumn,
                               null=True,
                               blank=True,
                               on_delete=models.CASCADE,
                               related_name='article')

    # 文章标签
    # 采用 Django-taggit 库
    tags = TaggableManager(blank=True)

    # 文章标题。
    # models.CharField 为字符串字段,用于保存较短的字符串,比如标题
    # CharField 有一个必填参数 max_length,它规定字符的最大长度
    title = models.CharField(max_length=100)

    # 文章正文。
    # 保存大量文本使用 TextField
    body = models.TextField()

    # 浏览量
    total_views = models.PositiveIntegerField(default=0)

    # 文章点赞数
    likes = models.PositiveIntegerField(default=0)

    # 文章创建时间。
    # DateTimeField 为一个日期字段
    # 参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间
    created = models.DateTimeField(default=timezone.now)

    # 文章更新时间。
    # 参数 auto_now=True 指定每次数据更新时自动写入当前时间
    updated = models.DateTimeField(auto_now=True)

    # 内部类 class Meta 用于给 model 定义元数据
    # 元数据:不是一个字段的任何数据
    class Meta:
        # ordering 指定模型返回的数据的排列顺序
        # '-created' 表明数据应该以倒序排列
        ordering = ('-created', )

    # 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容
    # 它最常见的就是在Django管理后台中做为对象的显示值。因此应该总是为 __str__ 返回一个友好易读的字符串
    def __str__(self):
        # 将文章标题返回
        return self.title

    # 获取文章地址
    def get_absolute_url(self):
        return reverse('article:article_detail', args=[self.id])

    # 保存时处理图片
    # def save(self, *args, **kwargs):
    #     # 调用原有的 save() 的功能
    #     article = super(ArticlePost, self).save(*args, **kwargs)
    #
    #     # 固定宽度缩放图片大小
    #     if self.avatar and not kwargs.get('update_fields'):
    #         image = Image.open(self.avatar)
    #         (x, y) = image.size
    #         new_x = 400
    #         new_y = int(new_x * (y / x))
    #         resized_image = image.resize((new_x, new_y), Image.ANTIALIAS)
    #         resized_image.save(self.avatar.path)
    #
    #     return article

    def was_created_recently(self):
        # 若文章是 1 分钟内发表的,则返回 True
        diff = timezone.now() - self.created

        # if diff.days <= 0 and diff.seconds < 60:
        if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60:
            return True
        else:
            return False
Beispiel #25
0
class Manifest(models.Model):
    """
    Class to represent Manifest of dir. objects
    """
    name          = models.CharField("name", max_length=255)
    caption       = models.CharField("Caption", max_length=255, null=True, blank=True)
    subject       = models.CharField("Subject", max_length=255, default='', null=True, blank=True)

    directory     = models.ForeignKey(Directory, related_name='core_manifest_dir', on_delete=models.DO_NOTHING)
    location      = models.ForeignKey(Location, related_name='core_manifest_loc', on_delete=models.DO_NOTHING, default=1)
    section       = models.ForeignKey(Section, related_name='core_manifest_section', on_delete=models.DO_NOTHING, default=1)
    company       = models.ForeignKey(Company, related_name='core_manifest_company', on_delete=models.DO_NOTHING, default=1)
    sequence      = models.CharField("Sequence", max_length=255)

    thumb_path    = models.CharField("Thumb Path", max_length=500)
    src_path      = models.CharField("Src Path", max_length=500)

    import_status = models.CharField("Import Status", max_length=255, default='init' )

    date        = models.DateField("Date")
    exif_date   = models.DateTimeField("Date Time", null=True, blank=True)

    lat     = models.FloatField("lat", default=0)
    lng     = models.FloatField("lng", default=0)

    meta_1       = models.CharField("meta_1", max_length=255, default="", null=True, blank=True )
    meta_2       = models.CharField("meta_2", max_length=255, default="", null=True, blank=True )
    meta_3       = models.CharField("meta_3", max_length=255, default="", null=True, blank=True )

    tags         = TaggableManager()

    exif_data   = models.TextField("exif", null=True, blank=True)

    created_at  = models.DateTimeField("Created at", auto_now_add=True)
    updated_at  = models.DateTimeField("Updated at", auto_now=True)

    def __str__(self):
        return str(self.name)

    def _format(req, row, status):
        data = {'name': row.get('name'),
                'subject': req.get('subject', ''),
                'caption': req.get('caption', ''),
                'directory_id': req.get('directory_id'),
                'company_id': req.get('company_id', 2),
                'location_id': req.get('location_id'),
                'section_id': req.get('section_id'),
                'src_path':"/static/{}/{}".format(req.get('dir'), row.get('name') ),
                'thumb_path': "/static/{}/thumbs/{}".format(req.get('dir'), row.get('name')),
                'sequence': req.get('seq',0),
                'import_status': status,
        }

        exif  = Manifest.get_exif(row['exif'].items(), ['DateTime', 'DateTimeOriginal', 'latlng'])
        try:
            data['exif_data'] = json.dumps(exif)
        except Excption as e:
            pass

        date  = Manifest.get_date(exif,  get_date_from_ts(row.get('file_date')) )
        data['date'] = format_date(date)
        data['exif_date'] = get_date_from_ts(row.get('file_date'))

        lat, lng = Manifest.get_lat_lng(exif.get('latlng', []))
        data['lat'] = lat
        data['lng'] = lng

        return data

    def get_exif(exif, extract=[]):
        meta = {}
        for k, v in exif:
            if k not in extract:
                continue
            if type(v) == bytes:
                v = v.decode("utf8", errors='ignore')
            meta[k] = str(v)
        return meta

    def get_lat_lng(lat_lng):
        if lat_lng and len(lat_lng) > 0:
            try:
                return [float(lat_lng[0]), float(lat_lng[1]) ]
            except Excption as e:
                pass
        return [0, 0]

    def get_date(exif, default_date):
        if 'DateTime' not in exif:
            return default_date
        d = default_date if exif['DateTime'] == None else exif['DateTime'];
        return d


    def default_fields():
        return ( 'id', 'name', 'caption', 'subject', 'directory_id', 'directory__name',
                 'date', 'exif_date', "import_status", 'company_id', 'company__name',
                 'lat', 'lng', 'location_id', 'location__name',  'section_id',
                 'section__name', 'sequence', 'src_path', 'thumb_path', 'created_at', 'updated_at'
                  )


    class Meta:
      pass
Beispiel #26
0
class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed,
                    models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.ImageField(verbose_name=_('file'),
                             upload_to=get_upload_to,
                             width_field='width',
                             height_field='height')
    width = models.IntegerField(verbose_name=_('width'), editable=False)
    height = models.IntegerField(verbose_name=_('height'), editable=False)
    created_at = models.DateTimeField(verbose_name=_('created at'),
                                      auto_now_add=True,
                                      db_index=True)
    uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                         verbose_name=_('uploaded by user'),
                                         null=True,
                                         blank=True,
                                         editable=False,
                                         on_delete=models.SET_NULL)

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    focal_point_x = models.PositiveIntegerField(null=True, blank=True)
    focal_point_y = models.PositiveIntegerField(null=True, blank=True)
    focal_point_width = models.PositiveIntegerField(null=True, blank=True)
    focal_point_height = models.PositiveIntegerField(null=True, blank=True)

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = ImageQuerySet.as_manager()

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def get_upload_to(self, filename):
        folder_name = 'original_images'
        filename = self.file.field.storage.get_valid_name(filename)

        # do a unidecode in the filename and then
        # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
        filename = "".join(
            (i if ord(i) < 128 else '_') for i in string_to_ascii(filename))

        # Truncate filename so it fits in the 100 character limit
        # https://code.djangoproject.com/ticket/9893
        full_path = os.path.join(folder_name, filename)
        if len(full_path) >= 95:
            chars_to_trim = len(full_path) - 94
            prefix, extension = os.path.splitext(filename)
            filename = prefix[:-chars_to_trim] + extension
            full_path = os.path.join(folder_name, filename)

        return full_path

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtailimages:image_usage', args=(self.id, ))

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def __str__(self):
        return self.title

    def get_rect(self):
        return Rect(0, 0, self.width, self.height)

    def get_focal_point(self):
        if self.focal_point_x is not None and \
           self.focal_point_y is not None and \
           self.focal_point_width is not None and \
           self.focal_point_height is not None:
            return Rect.from_point(
                self.focal_point_x,
                self.focal_point_y,
                self.focal_point_width,
                self.focal_point_height,
            )

    def has_focal_point(self):
        return self.get_focal_point() is not None

    def set_focal_point(self, rect):
        if rect is not None:
            self.focal_point_x = rect.centroid_x
            self.focal_point_y = rect.centroid_y
            self.focal_point_width = rect.width
            self.focal_point_height = rect.height
        else:
            self.focal_point_x = None
            self.focal_point_y = None
            self.focal_point_width = None
            self.focal_point_height = None

    def get_suggested_focal_point(self):
        with self.get_willow_image() as willow:
            faces = willow.detect_faces()

            if faces:
                # Create a bounding box around all faces
                left = min(face[0] for face in faces)
                top = min(face[1] for face in faces)
                right = max(face[2] for face in faces)
                bottom = max(face[3] for face in faces)
                focal_point = Rect(left, top, right, bottom)
            else:
                features = willow.detect_features()
                if features:
                    # Create a bounding box around all features
                    left = min(feature[0] for feature in features)
                    top = min(feature[1] for feature in features)
                    right = max(feature[0] for feature in features)
                    bottom = max(feature[1] for feature in features)
                    focal_point = Rect(left, top, right, bottom)
                else:
                    return None

        # Add 20% to width and height and give it a minimum size
        x, y = focal_point.centroid
        width, height = focal_point.size

        width *= 1.20
        height *= 1.20

        width = max(width, 100)
        height = max(height, 100)

        return Rect.from_point(x, y, width, height)

    @classmethod
    def get_rendition_model(cls):
        """ Get the Rendition model for this Image model """
        return cls.renditions.rel.related_model

    def get_rendition(self, filter):
        if isinstance(filter, str):
            filter = Filter(spec=filter)

        cache_key = filter.get_cache_key(self)
        Rendition = self.get_rendition_model()

        try:
            rendition_caching = True
            cache = caches['renditions']
            rendition_cache_key = Rendition.construct_cache_key(
                self.id, cache_key, filter.spec)
            cached_rendition = cache.get(rendition_cache_key)
            if cached_rendition:
                return cached_rendition
        except InvalidCacheBackendError:
            rendition_caching = False

        try:
            rendition = self.renditions.get(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
            )
        except Rendition.DoesNotExist:
            # Generate the rendition image
            try:
                logger.debug(
                    "Generating '%s' rendition for image %d",
                    filter.spec,
                    self.pk,
                )

                start_time = time.time()
                generated_image = filter.run(self, BytesIO())

                logger.debug("Generated '%s' rendition for image %d in %.1fms",
                             filter.spec, self.pk,
                             (time.time() - start_time) * 1000)
            except:  # noqa:B901,E722
                logger.debug("Failed to generate '%s' rendition for image %d",
                             filter.spec, self.pk)
                raise

            # Generate filename
            input_filename = os.path.basename(self.file.name)
            input_filename_without_extension, input_extension = os.path.splitext(
                input_filename)

            # A mapping of image formats to extensions
            FORMAT_EXTENSIONS = {
                'jpeg': '.jpg',
                'png': '.png',
                'gif': '.gif',
                'webp': '.webp',
            }

            output_extension = filter.spec.replace(
                '|', '.') + FORMAT_EXTENSIONS[generated_image.format_name]
            if cache_key:
                output_extension = cache_key + '.' + output_extension

            # Truncate filename to prevent it going over 60 chars
            output_filename_without_extension = input_filename_without_extension[:(
                59 - len(output_extension))]
            output_filename = output_filename_without_extension + '.' + output_extension

            rendition, created = self.renditions.get_or_create(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
                defaults={
                    'file': File(generated_image.f, name=output_filename)
                })

        if rendition_caching:
            cache.set(rendition_cache_key, rendition)

        return rendition

    def is_portrait(self):
        return (self.width < self.height)

    def is_landscape(self):
        return (self.height < self.width)

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def default_alt_text(self):
        # by default the alt text field (used in rich text insertion) is populated
        # from the title. Subclasses might provide a separate alt field, and
        # override this
        return self.title

    def is_editable_by_user(self, user):
        from wagtail.images.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(
            user, 'change', self)

    class Meta:
        abstract = True
Beispiel #27
0
class Person(models.Model):
    '''The main class of the model. Every individual is represented by a person
    record.'''
    forename = models.CharField(max_length=20,
                                help_text='Forename / given name')
    middle_names = models.CharField(blank=True,
                                    max_length=50,
                                    help_text='Middle names(s)')
    known_as = models.CharField(blank=True,
                                max_length=20,
                                help_text='Known as')
    surname = models.CharField(max_length=30, help_text='Surname')
    maiden_name = models.CharField(
        blank=True, max_length=30,
        help_text='Maiden name')  # Maiden name is optional.
    gender = models.CharField(max_length=1,
                              choices=(('M', 'Male'), ('F', 'Female')),
                              blank=False,
                              default=None)
    birth = models.ForeignKey('Event',
                              models.SET_NULL,
                              null=True,
                              blank=True,
                              related_name='+')
    death = models.ForeignKey('Event',
                              models.SET_NULL,
                              null=True,
                              blank=True,
                              related_name='+')
    deceased = models.BooleanField(default=True)
    blood_relative = models.BooleanField(default=True)
    mother = models.ForeignKey('self',
                               blank=True,
                               null=True,
                               limit_choices_to={'gender': 'F'},
                               related_name='children_of_mother',
                               on_delete=models.SET_NULL)
    father = models.ForeignKey('self',
                               blank=True,
                               null=True,
                               limit_choices_to={'gender': 'M'},
                               related_name='children_of_father',
                               on_delete=models.SET_NULL)
    notes = HTMLField(blank=True)
    tags = TaggableManager(blank=True, help_text='Tags')
    # A person can be linked to a user account. This allows a user to see
    # information relevant to their own relationships.

    user = models.OneToOneField(User, blank=True, null=True)

    avatar = models.ImageField(upload_to=renameUploadedFile('avatar'),
                               null=True,
                               blank=True,
                               help_text='Avatar Image')

    def name(self, use_middle_names=True, use_maiden_name=False):
        '''Returns the full name of this person.'''
        name = ' '.join([
            self.forename, self.middle_names
        ]) if use_middle_names and self.middle_names else self.forename
        if self.known_as:
            name = name + ' "{0}"'.format(self.known_as)
        if self.maiden_name != '':
            return name + ' ' + (self.maiden_name
                                 if use_maiden_name else self.surname.upper() +
                                 u' (n\xe9e ' + self.maiden_name + ')')
        else:
            return name + ' ' + self.surname.upper()

    def given_names(self):
        return " ".join([self.forename, self.middle_names
                         ]) if self.middle_names else self.forename

    def birth_surname(self):
        return self.maiden_name if self.maiden_name else self.surname

    def birth_name(self):
        return '{0} {1}'.format(self.given_names(),
                                self.birth_surname().upper())

    def date_of_birth(self):
        return self.birth.date if self.birth else None

    def birth_location(self):
        return self.birth.location if self.birth else None

    def date_of_death(self):
        return self.death.date if self.death else None

    def age(self):
        '''Calculate the person's age in years.'''
        if not self.date_of_birth() or (self.deceased
                                        and not self.date_of_death()):
            return None
        end = self.date_of_death() if self.deceased else date.today()
        years = end.year - self.date_of_birth().year
        if end.month and self.date_of_birth().month:
            if end.month < self.date_of_birth().month \
               or (end.month == self.date_of_birth().month \
                   and end.day and self.date_of_birth().day and end.day < self.date_of_birth().day):
                years -= 1
        return years

    def year_range(self):
        return '{0}-{1}'.format(
            self.date_of_birth().year if self.date_of_birth() else '????',
            '' if not self.deceased else
            self.date_of_death().year if self.date_of_death() else '????')

    def spouses(self):
        '''Return a list of anybody that this person is or was married to.'''
        if self.gender == 'F':
            return [(m.husband, m.date, m.location, m.divorced)
                    for m in self.wife_of.all()]
        else:
            return [(m.wife, m.date, m.location, m.divorced)
                    for m in self.husband_of.all()]

    def siblings(self):
        '''Returns a list of this person's brothers and sisters, including
        half-siblings.'''
        return Person.objects.filter(~Q(id=self.id),
                                     Q(~Q(father=None), father=self.father) | \
                                     Q(~Q(mother=None), mother=self.mother)).order_by('birth__date')

    def children(self):
        '''Returns a list of this person's children.'''
        offspring = self.children_of_mother if self.gender == 'F' else self.children_of_father
        return offspring.select_related('birth',
                                        'death').order_by('birth__date')

    def marriages(self):
        return self.husband_of.all(
        ) if self.gender == 'M' else self.wife_of.all()

    def timeline(self):
        timeline = list(self.events.all()) + list(
            self.marriages().filter(date__isnull=False))
        timeline.sort(key=attrgetter('date'))
        timeline.sort(key=attrgetter('event_type'))
        return timeline

    def _descendant_distances(self, offset=0):
        descendants = {}
        for child in self.children():
            descendants[child] = offset + 1
            descendants.update(child._descendant_distances(offset + 1))
        return descendants

    def descendants(self):
        '''Returns a list of this person's descendants (their children and all
        of their children's descendants).'''
        for child in self.children():
            yield child
            yield from child.descendants()

    def annotated_descendants(self):
        '''Returns a list of this person's descendants annotated with the name
        of the relationship to this person (so a list of (Person, relationship)
        tuples.'''
        distances = self._descendant_distances()
        descendants = []
        for descendant in distances.keys():
            relationship = describe_relative(self, descendant, {},
                                             descendant._ancestor_distances())
            descendants.append(
                (descendant, relationship, distances[descendant]))
        descendants.sort(key=lambda x: (x[2], x[1], x[0].surname))
        return descendants

    def annotated_descendants2(self):
        '''Returns a list of this person's descendants annotated with the name
        of the relationship to this person and distance. Suitable for d3.js tree charts'''
        distances = self._descendant_distances()
        descendants = []
        for descendant in distances.keys():

            relationship = describe_relative(self, descendant, {},
                                             descendant._ancestor_distances())
            descendants.append({
                "name": descendant.name(),
                "avatar": str(descendant.avatar),
                "mother_id": descendant.mother_id,
                "father_id": descendant.father_id,
                "id": descendant.id,
                "relationship": relationship,
                "distance": distances[descendant]
            })
        #descendants.sort(key=lambda x: (x[2], x[1], x[0].surname))
        return descendants

    # Returns a dictionary of this person's ancestors.  The ancestors are the
    # keys and each value is the distance (number of generations) from this
    # person to that ancestor (e.g parent is 1, grandparent is 2, etc.)
    def _ancestor_distances(self, offset=0):
        '''Returns a dictionary of this person's ancestors (their parents and
        all of their parents's ancestors) with distance to each ancestor.'''
        ancestors = {}
        if self.mother:
            ancestors[self.mother] = offset + 1
            ancestors.update(self.mother._ancestor_distances(offset + 1))
        if self.father:
            ancestors[self.father] = offset + 1
            ancestors.update(self.father._ancestor_distances(offset + 1))
        return ancestors

    def ancestors(self):
        '''Returns a list of this person's ancestors (their parents and all of
        their parents' ancestors).'''
        if self.mother:
            yield self.mother
            yield from self.mother.ancestors()
        if self.father:
            yield self.father
            yield from self.father.ancestors()

    def annotated_ancestors(self):
        '''Returns a list of this person's ancestors annotated with the name of
        the relationship to this person and the distance between them (so a list
        of (Person, relationship, distance) tuples).'''
        distances = self._ancestor_distances()
        ancestors = []
        for ancestor in distances.keys():
            relationship = describe_relative(self, ancestor, distances, {})
            ancestors.append((ancestor, relationship, distances[ancestor]))
        ancestors.sort(key=lambda x: (x[2], x[1], x[0].surname))
        return ancestors

    def relatives(self):
        relatives = self._build_relatives_set(set())
        relatives.discard(self)  # This person can't be their own relative.
        return relatives

    def _build_relatives_set(self, relatives_set):
        '''Adds all blood relatives of this person to the specified set.'''
        relatives_set.add(self)
        for child in self.children():
            if child not in relatives_set:
                relatives_set.add(child)
                relatives_set.update(child.descendants())
        if self.father:
            self.father._build_relatives_set(relatives_set)
        if self.mother:
            self.mother._build_relatives_set(relatives_set)
        return relatives_set

    def annotated_relatives(self):
        '''Returns a list of all of this person's blood relatives. The first
        item in each tuple is the person, the second is the relationship, and
        the third is the distance between the two individuals.'''
        ancestor_distances = self._ancestor_distances()
        distances = ancestor_distances.copy()
        distances.update(self._descendant_distances())
        annotated = []
        for relative in self.relatives():
            distance = distances.get(relative, None)
            relative_distances = relative._ancestor_distances()
            if not distance:
                (_, d1, d2) = closest_common_ancestor(ancestor_distances,
                                                      relative_distances)
                distance = max(d1, d2)
            relationship = describe_relative(self, relative,
                                             ancestor_distances,
                                             relative_distances)
            annotated.append((relative, relationship, distance))
        annotated.sort(key=lambda x: (x[2], x[1], x[0].surname))
        return annotated

    def photos(self):
        '''Returns a list of all photos associated with this person.'''
        return Photograph.objects.filter(person=self)

    def has_missing_maiden_name(self):
        return self.gender == 'F' and self.wife_of.count() > 0 and (
            self.maiden_name == '' or self.maiden_name == None)

    def clean(self):
        if self.date_of_death() and not self.deceased:
            raise ValidationError(
                'Cannot specify date of death for living person.')
        if self.birth and self.birth.person.id != self.id:
            raise ValidationError(
                'Birth event must refer back to the same person.')
        if self.death and self.death.person.id != self.id:
            raise ValidationError(
                'Death event must refer back to the same person.')
        if (self.mother and self.mother == self) or (self.father
                                                     and self.father == self):
            raise ValidationError('Person cannot be their own parent.')

    def get_absolute_url(self):
        return reverse('person', args=[self.id])

    def __str__(self):
        return self.name()

    class Meta:
        ordering = ['surname', 'forename', 'middle_names', '-birth__date']
Beispiel #28
0
class Article(CachingMixin, models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    is_live = models.BooleanField('Display on site', default=True)
    show_in_lists = models.BooleanField(default=True)
    allow_comments = models.BooleanField(default=True)
    title = models.CharField(max_length=128)
    slug = models.SlugField(unique=True)
    pubdate = models.DateTimeField(default=datetime.now)
    subhead = models.CharField(max_length=128)
    authors = models.ManyToManyField(Person, blank=True, null=True, related_name='article_authors')
    image = ImageField(upload_to='img/uploads/article_images', help_text='Resized to fit 100% column width in template', blank=True, null=True)
    image_caption = models.TextField(blank=True)
    image_credit = models.CharField(max_length=128, blank=True, help_text='Optional. Will be appended to end of caption in parens. Accepts HTML.')
    body = models.TextField()
    summary = models.TextField()
    article_type = models.CharField(max_length=32, blank=True)
    category = models.ForeignKey('Category')
    people = models.ManyToManyField(Person, blank=True, null=True)
    organizations = models.ManyToManyField(Organization, blank=True, null=True)
    code = models.ManyToManyField(Code, blank=True, null=True)
    tags = TaggableManager(blank=True, help_text='Automatic combined list of Technology Tags and Concept Tags, for easy searching')
    technology_tags = TaggableManager(verbose_name='Technology Tags', help_text='A comma-separated list of tags describing relevant technologies', through=TechnologyTaggedItem, blank=True)
    concept_tags = TaggableManager(verbose_name='Concept Tags', help_text='A comma-separated list of tags describing relevant concepts', through=ConceptTaggedItem, blank=True)
    objects = models.Manager()
    live_objects = LiveArticleManager()
    disable_auto_linebreaks = models.BooleanField(default=False, help_text='Check this if body and article blocks already have HTML paragraph tags.')
    
    class Meta:
        ordering = ('-pubdate','title',)
        
    def __unicode__(self):
        return u'%s' % self.title
        
    @models.permalink
    def get_absolute_url(self):
        return ('article_detail', (), {
            'section': self.section.slug,
            'slug': self.slug
        })
        
    @property
    def section(self):
        '''follow article category through to section'''
        if self.category:
            return self.category.section
        return None
            
    @property
    def pretty_pubdate(self):
        '''pre-process for simpler template logic'''
        return dj_date(self.pubdate,"F j, Y")

    @property
    def pretty_caption(self):
        '''pre-process for simpler template logic'''
        _caption = self.image_caption or ''
        _credit = self.image_credit
        if _credit:
            _caption = '%s (%s)' % (_caption, _credit)
        return _caption
        
    @property
    def pretty_body_text(self):
        '''pre-process for simpler template logic'''
        _body = self.body
        if not self.disable_auto_linebreaks:
            # allow admin users to provide text
            # that already contains <p> tags
            _body = linebreaks(_body)
        return _body
        
    @property
    def safe_summary(self):
        '''suitable for use in places that must avoid nested anchor tags'''
        return removetags(self.summary, 'a')

    @property
    def merged_tag_list(self):
        '''return a combined list of technology_tags and concept_tags'''
        return [item for item in itertools.chain(self.technology_tags.all(), self.concept_tags.all())]

    def get_live_organization_set(self):
        return self.organizations.filter(is_live=True)

    def get_live_people_set(self):
        return self.people.filter(is_live=True)

    def get_live_author_set(self):
        author_set = self.authors.filter(is_live=True)
        return author_set

    def get_live_code_set(self):
        return self.code.filter(is_live=True)

    def get_live_author_bio_set(self):
        # only authors with acutal bio information
        author_set = self.get_live_author_set().exclude(description='')
        # filter out bio boxes for Erin, Erika, 
        authors_to_exclude = ['erin-kissane','erika-owens','kio-stark']
        author_set = author_set.exclude(slug__in=authors_to_exclude)
        return author_set
Beispiel #29
0
class ProposalBase(models.Model):

    objects = InheritanceManager()

    kind = models.ForeignKey(ProposalKind)

    title = models.CharField(max_length=100)
    description = models.TextField(
        _("Description"),
        max_length=400,  # @@@ need to enforce 400 in UI
        help_text=
        "If your talk is accepted this will be made public and printed in the "
        "program. Should be one paragraph, maximum 400 characters.")
    abstract = models.TextField(
        _("Detailed Abstract"),
        help_text=_("Detailed description. Will be made public "
                    "if your talk is accepted."))
    additional_notes = models.TextField(
        blank=True,
        help_text=_("Anything else you'd like the program committee to know "
                    "when making their selection: your past speaking "
                    "experience, open source community experience, etc."))
    submitted = models.DateTimeField(
        default=datetime.datetime.now,
        editable=False,
    )
    speaker = models.ForeignKey("speakers.Speaker", related_name="proposals")
    additional_speakers = models.ManyToManyField("speakers.Speaker",
                                                 through="AdditionalSpeaker",
                                                 blank=True)
    cancelled = models.BooleanField(default=False)
    tags = TaggableManager(blank=True)
    cached_tags = models.TextField(blank=True, default='', editable=False)

    class Meta:
        ordering = ['title']

    def __unicode__(self):
        return self.title

    def can_edit(self):
        """
        Return True if this proposal is editable - meaning no presentation exists yet.
        """
        # Putting this import at the top would result in a circular import
        from symposion.schedule.models import Presentation
        return not Presentation.objects.filter(proposal_base=self).exists()

    def cache_tags(self):
        self.cached_tags = self.get_tags_display()
        self.save()

    def get_tags_display(self):
        return u", ".join(self.tags.names())

    @property
    def section(self):
        return self.kind.section

    @property
    def speaker_email(self):
        return self.speaker.email

    @property
    def number(self):
        return str(self.pk).zfill(3)

    @property
    def status(self):
        try:
            return self.result.status
        except ObjectDoesNotExist:
            return 'undecided'

    def as_dict(self, details=False):
        """Return a dictionary representation of this proposal."""

        # Put together the base dict.
        answer = {
            'id': self.id,
            'speakers': [i.as_dict for i in self.speakers()],
            'status': self.status,
            'title': self.title,
        }

        # Include details iff they're requested.
        if details:
            answer['details'] = {
                'abstract': self.abstract,
                'description': self.description,
                'notes': self.additional_notes,
            }

            # If there is extra data that has been set, include it also.
            try:
                answer['extra'] = json.loads(self.data.data)
            except ObjectDoesNotExist:
                pass

        # Return the answer.
        return answer

    def speakers(self):
        yield self.speaker
        for speaker in self.additional_speakers.exclude(
                additionalspeaker__status=AdditionalSpeaker.
                SPEAKING_STATUS_DECLINED):
            yield speaker

    def notification_email_context(self):
        return {
            "title": self.title,
            "speaker": self.speaker.name,
            "speakers": ', '.join([x.name for x in self.speakers()]),
            "kind": self.kind.name,
        }
Beispiel #30
0
 def test_deconstruct_kwargs_kept(self):
     instance = TaggableManager(through=OfficialThroughModel)
     name, path, args, kwargs = instance.deconstruct()
     new_instance = TaggableManager(*args, **kwargs)
     self.assertEqual(instance.rel.through, new_instance.rel.through)
Beispiel #31
0
 def test_deconstruct_kwargs_kept(self):
     instance = TaggableManager(through=OfficialThroughModel, to='dummy.To')
     name, path, args, kwargs = instance.deconstruct()
     new_instance = TaggableManager(*args, **kwargs)
     self.assertEqual('tests.OfficialThroughModel', _remote_field(new_instance).through)
     self.assertEqual('dummy.To', _related_model(_remote_field(new_instance)))
Beispiel #32
0
		try: 
			user = User.objects.get(username=row__):
		except: #User not Found
			create_user()
			print "No user with those names"

	    nationality = row__
	    current_location = row__
	    work = row__
	    startup_status = row__
	    portfolio_status = row__
	    itc_program_name = row__
	    itc_program_year = row__
	    linked_in_url = row__
    
    	skills = TaggableManager()
    	skills.add(row__)

    profile_tuple = (user=user, 
	nationality=nationality, 
	current_location, 
	current_location=current_location, 
	work=work,
	startup_status=startup_status,
	portfolio_status=portfolio_status,
	itc_program_name=itc_program_name,
	itc_program_year=itc_program_year)

    UserProfile.objects.create(profile_tuple)
	return
Beispiel #33
0
class Project(models.Model):

    """Project model"""

    # Auto fields
    pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True)
    modified_date = models.DateTimeField(_('Modified date'), auto_now=True)

    # Generally from conf.py
    users = models.ManyToManyField(User, verbose_name=_('User'),
                                   related_name='projects')
    name = models.CharField(_('Name'), max_length=255)
    slug = models.SlugField(_('Slug'), max_length=255, unique=True)
    description = models.TextField(_('Description'), blank=True,
                                   help_text=_('The reStructuredText '
                                               'description of the project'))
    repo = models.CharField(_('Repository URL'), max_length=255,
                            help_text=_('Hosted documentation repository URL'))
    repo_type = models.CharField(_('Repository type'), max_length=10,
                                 choices=constants.REPO_CHOICES, default='git')
    project_url = models.URLField(_('Project homepage'), blank=True,
                                  help_text=_('The project\'s homepage'))
    canonical_url = models.URLField(_('Canonical URL'), blank=True,
                                    help_text=_('URL that documentation is expected to serve from'))
    version = models.CharField(_('Version'), max_length=100, blank=True,
                               help_text=_('Project version these docs apply '
                                           'to, i.e. 1.0a'))
    copyright = models.CharField(_('Copyright'), max_length=255, blank=True,
                                 help_text=_('Project copyright information'))
    theme = models.CharField(
        _('Theme'), max_length=20, choices=constants.DEFAULT_THEME_CHOICES,
        default=constants.THEME_DEFAULT,
        help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-'
                   'themes" target="_blank">%s</a>') % _('Examples'))
    suffix = models.CharField(_('Suffix'), max_length=10, editable=False,
                              default='.rst')
    single_version = models.BooleanField(
        _('Single version'), default=False,
        help_text=_('A single version site has no translations and only your '
                    '"latest" version, served at the root of the domain. Use '
                    'this with caution, only turn it on if you will <b>never</b> '
                    'have multiple versions of your docs.'))
    default_version = models.CharField(
        _('Default version'), max_length=255, default=LATEST,
        help_text=_('The version of your project that / redirects to'))
    # In default_branch, None max_lengtheans the backend should choose the
    # appropraite branch. Eg 'master' for git
    default_branch = models.CharField(
        _('Default branch'), max_length=255, default=None, null=True,
        blank=True, help_text=_('What branch "latest" points to. Leave empty '
                                'to use the default value for your VCS (eg. '
                                '<code>trunk</code> or <code>master</code>).'))
    requirements_file = models.CharField(
        _('Requirements file'), max_length=255, default=None, null=True,
        blank=True, help_text=_(
            'A <a '
            'href="https://pip.pypa.io/en/latest/user_guide.html#requirements-files">'
            'pip requirements file</a> needed to build your documentation. '
            'Path from the root of your project.'))
    documentation_type = models.CharField(
        _('Documentation type'), max_length=20,
        choices=constants.DOCUMENTATION_CHOICES, default='sphinx',
        help_text=_('Type of documentation you are building. <a href="http://'
                    'sphinx-doc.org/builders.html#sphinx.builders.html.'
                    'DirectoryHTMLBuilder">More info</a>.'))

    # Project features
    allow_comments = models.BooleanField(_('Allow Comments'), default=False)
    comment_moderation = models.BooleanField(_('Comment Moderation)'), default=False)
    cdn_enabled = models.BooleanField(_('CDN Enabled'), default=False)
    analytics_code = models.CharField(
        _('Analytics code'), max_length=50, null=True, blank=True,
        help_text=_("Google Analytics Tracking ID "
                    "(ex. <code>UA-22345342-1</code>). "
                    "This may slow down your page loads."))
    container_image = models.CharField(
        _('Alternative container image'), max_length=64, null=True, blank=True)
    container_mem_limit = models.CharField(
        _('Container memory limit'), max_length=10, null=True, blank=True,
        help_text=_("Memory limit in Docker format "
                    "-- example: <code>512m</code> or <code>1g</code>"))
    container_time_limit = models.CharField(
        _('Container time limit'), max_length=10, null=True, blank=True)
    build_queue = models.CharField(
        _('Alternate build queue id'), max_length=32, null=True, blank=True)
    allow_promos = models.BooleanField(
        _('Sponsor advertisements'), default=True, help_text=_(
            "Allow sponsor advertisements on my project documentation"))

    # Sphinx specific build options.
    enable_epub_build = models.BooleanField(
        _('Enable EPUB build'), default=True,
        help_text=_(
            'Create a EPUB version of your documentation with each build.'))
    enable_pdf_build = models.BooleanField(
        _('Enable PDF build'), default=True,
        help_text=_(
            'Create a PDF version of your documentation with each build.'))

    # Other model data.
    path = models.CharField(_('Path'), max_length=255, editable=False,
                            help_text=_("The directory where "
                                        "<code>conf.py</code> lives"))
    conf_py_file = models.CharField(
        _('Python configuration file'), max_length=255, default='', blank=True,
        help_text=_('Path from project root to <code>conf.py</code> file '
                    '(ex. <code>docs/conf.py</code>).'
                    'Leave blank if you want us to find it for you.'))

    featured = models.BooleanField(_('Featured'), default=False)
    skip = models.BooleanField(_('Skip'), default=False)
    mirror = models.BooleanField(_('Mirror'), default=False)
    install_project = models.BooleanField(
        _('Install Project'),
        help_text=_("Install your project inside a virtualenv using <code>setup.py "
                    "install</code>"),
        default=False
    )

    # This model attribute holds the python interpreter used to create the
    # virtual environment
    python_interpreter = models.CharField(
        _('Python Interpreter'),
        max_length=20,
        choices=constants.PYTHON_CHOICES,
        default='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    use_system_packages = models.BooleanField(
        _('Use system packages'),
        help_text=_("Give the virtual environment access to the global "
                    "site-packages dir."),
        default=False
    )
    django_packages_url = models.CharField(_('Django Packages URL'),
                                           max_length=255, blank=True)
    privacy_level = models.CharField(
        _('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES,
        default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Level of privacy that you want on the repository. "
                    "Protected means public but not in listings."))
    version_privacy_level = models.CharField(
        _('Version Privacy Level'), max_length=20,
        choices=constants.PRIVACY_CHOICES, default=getattr(
            settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Default level of privacy you want on built "
                    "versions of documentation."))

    # Subprojects
    related_projects = models.ManyToManyField(
        'self', verbose_name=_('Related projects'), blank=True,
        symmetrical=False, through=ProjectRelationship)

    # Language bits
    language = models.CharField(_('Language'), max_length=20, default='en',
                                help_text=_("The language the project "
                                            "documentation is rendered in. "
                                            "Note: this affects your project's URL."),
                                choices=constants.LANGUAGES)

    programming_language = models.CharField(
        _('Programming Language'),
        max_length=20,
        default='words',
        help_text=_("The primary programming language the project is written in."),
        choices=constants.PROGRAMMING_LANGUAGES, blank=True)
    # A subproject pointed at it's main language, so it can be tracked
    main_language_project = models.ForeignKey('self',
                                              related_name='translations',
                                              blank=True, null=True)

    # Version State
    num_major = models.IntegerField(
        _('Number of Major versions'),
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X")
    )
    num_minor = models.IntegerField(
        _('Number of Minor versions'),
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X")
    )
    num_point = models.IntegerField(
        _('Number of Point versions'),
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0")
    )

    has_valid_webhook = models.BooleanField(
        default=False, help_text=_('This project has been built with a webhook')
    )
    has_valid_clone = models.BooleanField(
        default=False, help_text=_('This project has been successfully cloned')
    )

    tags = TaggableManager(blank=True)
    objects = ProjectManager()
    all_objects = models.Manager()

    class Meta:
        ordering = ('slug',)
        permissions = (
            # Translators: Permission around whether a user can view the
            # project
            ('view_project', _('View Project')),
        )

    def __unicode__(self):
        return self.name

    @property
    def subdomain(self):
        try:
            domain = self.domains.get(canonical=True)
            return domain.domain
        except (Domain.DoesNotExist, MultipleObjectsReturned):
            subdomain_slug = self.slug.replace('_', '-')
            prod_domain = getattr(settings, 'PRODUCTION_DOMAIN')
            return "%s.%s" % (subdomain_slug, prod_domain)

    def sync_supported_versions(self):
        supported = self.supported_versions()
        if supported:
            self.versions.filter(
                verbose_name__in=supported).update(supported=True)
            self.versions.exclude(
                verbose_name__in=supported).update(supported=False)
            self.versions.filter(verbose_name=LATEST_VERBOSE_NAME).update(supported=True)

    def save(self, *args, **kwargs):
        from readthedocs.projects import tasks
        first_save = self.pk is None
        if not self.slug:
            # Subdomains can't have underscores in them.
            self.slug = slugify(self.name).replace('_', '-')
            if self.slug == '':
                raise Exception(_("Model must have slug"))
        super(Project, self).save(*args, **kwargs)
        for owner in self.users.all():
            assign('view_project', owner, self)
        try:
            if self.default_branch:
                latest = self.versions.get(slug=LATEST)
                if latest.identifier != self.default_branch:
                    latest.identifier = self.default_branch
                    latest.save()
        except Exception:
            log.error('Failed to update latest identifier', exc_info=True)

        # Add exceptions here for safety
        try:
            self.sync_supported_versions()
        except Exception:
            log.error('failed to sync supported versions', exc_info=True)
        try:
            if not first_save:
                broadcast(type='app', task=tasks.symlink_project, args=[self.pk])
        except Exception:
            log.error('failed to symlink project', exc_info=True)
        try:
            update_static_metadata(project_pk=self.pk)
        except Exception:
            log.error('failed to update static metadata', exc_info=True)
        try:
            branch = self.default_branch or self.vcs_repo().fallback_branch
            if not self.versions.filter(slug=LATEST).exists():
                self.versions.create_latest(identifier=branch)
        except Exception:
            log.error('Error creating default branches', exc_info=True)

    def get_absolute_url(self):
        return reverse('projects_detail', args=[self.slug])

    def get_docs_url(self, version_slug=None, lang_slug=None, private=None):
        """Return a url for the docs

        Always use http for now, to avoid content warnings.
        """
        return resolve(project=self, version_slug=version_slug, language=lang_slug, private=private)

    def get_builds_url(self):
        return reverse('builds_project_list', kwargs={
            'project_slug': self.slug,
        })

    def get_canonical_url(self):
        if getattr(settings, 'DONT_HIT_DB', True):
            return apiv2.project(self.pk).canonical_url().get()['url']
        else:
            return self.get_docs_url()

    def get_subproject_urls(self):
        """List subproject URLs

        This is used in search result linking
        """
        if getattr(settings, 'DONT_HIT_DB', True):
            return [(proj['slug'], proj['canonical_url'])
                    for proj in (
                        apiv2.project(self.pk)
                        .subprojects()
                        .get()['subprojects'])]
        else:
            return [(proj.child.slug, proj.child.get_docs_url())
                    for proj in self.subprojects.all()]

    def get_production_media_path(self, type_, version_slug, include_file=True):
        """
        This is used to see if these files exist so we can offer them for download.

        :param type_: Media content type, ie - 'pdf', 'zip'
        :param version_slug: Project version slug for lookup
        :param include_file: Include file name in return
        :type include_file: bool
        :returns: Full path to media file or path
        """
        if getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') == 'public' or settings.DEBUG:
            path = os.path.join(
                settings.MEDIA_ROOT, type_, self.slug, version_slug)
        else:
            path = os.path.join(
                settings.PRODUCTION_MEDIA_ARTIFACTS, type_, self.slug, version_slug)
        if include_file:
            path = os.path.join(
                path, '%s.%s' % (self.slug, type_.replace('htmlzip', 'zip')))
        return path

    def get_production_media_url(self, type_, version_slug, full_path=True):
        """Get the URL for downloading a specific media file."""
        path = reverse('project_download_media', kwargs={
            'project_slug': self.slug,
            'type_': type_,
            'version_slug': version_slug,
        })
        if full_path:
            path = '//%s%s' % (settings.PRODUCTION_DOMAIN, path)
        return path

    def get_downloads(self):
        downloads = {}
        downloads['htmlzip'] = self.get_production_media_url(
            'htmlzip', self.get_default_version())
        downloads['epub'] = self.get_production_media_url(
            'epub', self.get_default_version())
        downloads['pdf'] = self.get_production_media_url(
            'pdf', self.get_default_version())
        return downloads

    @property
    def clean_repo(self):
        if self.repo.startswith('http://github.com'):
            return self.repo.replace('http://github.com', 'https://github.com')
        return self.repo

    # Doc PATH:
    # MEDIA_ROOT/slug/checkouts/version/<repo>

    @property
    def doc_path(self):
        return os.path.join(settings.DOCROOT, self.slug.replace('_', '-'))

    def checkout_path(self, version=LATEST):
        return os.path.join(self.doc_path, 'checkouts', version)

    @property
    def pip_cache_path(self):
        """Path to pip cache"""
        return os.path.join(self.doc_path, '.cache', 'pip')

    #
    # Paths for symlinks in project doc_path.
    #
    def translations_symlink_path(self, language=None):
        """Path in the doc_path that we symlink translations"""
        if not language:
            language = self.language
        return os.path.join(self.doc_path, 'translations', language)

    #
    # End symlink paths
    #

    def full_doc_path(self, version=LATEST):
        """The path to the documentation root in the project"""
        doc_base = self.checkout_path(version)
        for possible_path in ['docs', 'doc', 'Doc']:
            if os.path.exists(os.path.join(doc_base, '%s' % possible_path)):
                return os.path.join(doc_base, '%s' % possible_path)
        # No docs directory, docs are at top-level.
        return doc_base

    def artifact_path(self, type_, version=LATEST):
        """The path to the build html docs in the project"""
        return os.path.join(self.doc_path, "artifacts", version, type_)

    def full_build_path(self, version=LATEST):
        """The path to the build html docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "html")

    def full_latex_path(self, version=LATEST):
        """The path to the build LaTeX docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "latex")

    def full_epub_path(self, version=LATEST):
        """The path to the build epub docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "epub")

    # There is currently no support for building man/dash formats, but we keep
    # the support there for existing projects. They might have already existing
    # legacy builds.

    def full_man_path(self, version=LATEST):
        """The path to the build man docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "man")

    def full_dash_path(self, version=LATEST):
        """The path to the build dash docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "dash")

    def full_json_path(self, version=LATEST):
        """The path to the build json docs in the project"""
        if 'sphinx' in self.documentation_type:
            return os.path.join(self.conf_dir(version), "_build", "json")
        elif 'mkdocs' in self.documentation_type:
            return os.path.join(self.checkout_path(version), "_build", "json")

    def full_singlehtml_path(self, version=LATEST):
        """The path to the build singlehtml docs in the project"""
        return os.path.join(self.conf_dir(version), "_build", "singlehtml")

    def rtd_build_path(self, version=LATEST):
        """The destination path where the built docs are copied"""
        return os.path.join(self.doc_path, 'rtd-builds', version)

    def static_metadata_path(self):
        """The path to the static metadata JSON settings file"""
        return os.path.join(self.doc_path, 'metadata.json')

    def conf_file(self, version=LATEST):
        """Find a ``conf.py`` file in the project checkout"""
        if self.conf_py_file:
            conf_path = os.path.join(self.checkout_path(version), self.conf_py_file)
            if os.path.exists(conf_path):
                log.info('Inserting conf.py file path from model')
                return conf_path
            else:
                log.warning("Conf file specified on model doesn't exist")
        files = self.find('conf.py', version)
        if not files:
            files = self.full_find('conf.py', version)
        if len(files) == 1:
            return files[0]
        for filename in files:
            if filename.find('doc', 70) != -1:
                return filename
        # Having this be translatable causes this odd error:
        # ProjectImportError(<django.utils.functional.__proxy__ object at
        # 0x1090cded0>,)
        raise ProjectImportError(
            u"Conf File Missing. Please make sure you have a conf.py in your project.")

    def conf_dir(self, version=LATEST):
        conf_file = self.conf_file(version)
        if conf_file:
            return os.path.dirname(conf_file)

    @property
    def is_type_sphinx(self):
        """Is project type Sphinx"""
        return 'sphinx' in self.documentation_type

    @property
    def is_type_mkdocs(self):
        """Is project type Mkdocs"""
        return 'mkdocs' in self.documentation_type

    @property
    def is_imported(self):
        return bool(self.repo)

    @property
    def has_good_build(self):
        return self.builds.filter(success=True).exists()

    @property
    def has_versions(self):
        return self.versions.exists()

    @property
    def has_aliases(self):
        return self.aliases.exists()

    def has_pdf(self, version_slug=LATEST):
        if not self.enable_pdf_build:
            return False
        return os.path.exists(self.get_production_media_path(
            type_='pdf', version_slug=version_slug))

    def has_epub(self, version_slug=LATEST):
        if not self.enable_epub_build:
            return False
        return os.path.exists(self.get_production_media_path(
            type_='epub', version_slug=version_slug))

    def has_htmlzip(self, version_slug=LATEST):
        return os.path.exists(self.get_production_media_path(
            type_='htmlzip', version_slug=version_slug))

    @property
    def sponsored(self):
        return False

    def vcs_repo(self, version=LATEST):
        backend = backend_cls.get(self.repo_type)
        if not backend:
            repo = None
        else:
            proj = VCSProject(
                self.name, self.default_branch, self.checkout_path(version), self.clean_repo)
            repo = backend(proj, version)
        return repo

    def repo_nonblockinglock(self, version, max_lock_age=5):
        return NonBlockingLock(project=self, version=version, max_lock_age=max_lock_age)

    def repo_lock(self, version, timeout=5, polling_interval=5):
        return Lock(self, version, timeout, polling_interval)

    def find(self, filename, version):
        """Find files inside the project's ``doc`` path

        :param filename: Filename to search for in project checkout
        :param version: Version instance to set version checkout path
        """
        matches = []
        for root, __, filenames in os.walk(self.full_doc_path(version)):
            for filename in fnmatch.filter(filenames, filename):
                matches.append(os.path.join(root, filename))
        return matches

    def full_find(self, filename, version):
        """Find files inside a project's checkout path

        :param filename: Filename to search for in project checkout
        :param version: Version instance to set version checkout path
        """
        matches = []
        for root, __, filenames in os.walk(self.checkout_path(version)):
            for filename in fnmatch.filter(filenames, filename):
                matches.append(os.path.join(root, filename))
        return matches

    def get_latest_build(self, finished=True):
        """
        Get latest build for project

        finished
            Return only builds that are in a finished state
        """
        kwargs = {'type': 'html'}
        if finished:
            kwargs['state'] = 'finished'
        return self.builds.filter(**kwargs).first()

    def api_versions(self):
        ret = []
        for version_data in api.version.get(project=self.pk,
                                            active=True)['objects']:
            version = make_api_version(version_data)
            ret.append(version)
        return sort_version_aware(ret)

    def active_versions(self):
        from readthedocs.builds.models import Version
        versions = Version.objects.public(project=self, only_active=True)
        return (versions.filter(built=True, active=True) |
                versions.filter(active=True, uploaded=True))

    def ordered_active_versions(self, user=None):
        from readthedocs.builds.models import Version
        kwargs = {
            'project': self,
            'only_active': True,
        }
        if user:
            kwargs['user'] = user
        versions = Version.objects.public(**kwargs)
        return sort_version_aware(versions)

    def all_active_versions(self):
        """Get queryset with all active versions

        .. note::
            This is a temporary workaround for activate_versions filtering out
            things that were active, but failed to build

        :returns: :py:cls:`Version` queryset
        """
        return self.versions.filter(active=True)

    def supported_versions(self):
        """Get the list of supported versions

        :returns: List of version strings.
        """
        if not self.num_major or not self.num_minor or not self.num_point:
            return []
        version_identifiers = self.versions.values_list('verbose_name', flat=True)
        return version_windows(
            version_identifiers,
            major=self.num_major,
            minor=self.num_minor,
            point=self.num_point,
        )

    def get_stable_version(self):
        return self.versions.filter(slug=STABLE).first()

    def update_stable_version(self):
        """Returns the version that was promoted to be the new stable version

        Return ``None`` if no update was mode or if there is no version on the
        project that can be considered stable.
        """
        versions = self.versions.all()
        new_stable = determine_stable_version(versions)
        if new_stable:
            current_stable = self.get_stable_version()
            if current_stable:
                identifier_updated = (
                    new_stable.identifier != current_stable.identifier)
                if identifier_updated and current_stable.machine:
                    log.info(
                        "Update stable version: {project}:{version}".format(
                            project=self.slug,
                            version=new_stable.identifier))
                    current_stable.identifier = new_stable.identifier
                    current_stable.save()
                    return new_stable
            else:
                log.info(
                    "Creating new stable version: {project}:{version}".format(
                        project=self.slug,
                        version=new_stable.identifier))
                current_stable = self.versions.create_stable(
                    type=new_stable.type,
                    identifier=new_stable.identifier)
                return new_stable

    def version_from_branch_name(self, branch):
        versions = self.versions_from_branch_name(branch)
        try:
            return versions[0]
        except IndexError:
            return None

    def versions_from_branch_name(self, branch):
        return (
            self.versions.filter(identifier=branch) |
            self.versions.filter(identifier='remotes/origin/%s' % branch) |
            self.versions.filter(identifier='origin/%s' % branch)
        )

    def get_default_version(self):
        """
        Get the default version (slug).

        Returns self.default_version if the version with that slug actually
        exists (is built and published). Otherwise returns 'latest'.
        """
        # latest is a special case where we don't have to check if it exists
        if self.default_version == LATEST:
            return self.default_version
        # check if the default_version exists
        version_qs = self.versions.filter(
            slug=self.default_version, active=True
        )
        if version_qs.exists():
            return self.default_version
        return LATEST

    def get_default_branch(self):
        """Get the version representing 'latest'"""
        if self.default_branch:
            return self.default_branch
        else:
            return self.vcs_repo().fallback_branch

    def add_subproject(self, child, alias=None):
        subproject, __ = ProjectRelationship.objects.get_or_create(
            parent=self, child=child, alias=alias,
        )
        return subproject

    def remove_subproject(self, child):
        ProjectRelationship.objects.filter(parent=self, child=child).delete()
        return

    def moderation_queue(self):
        # non-optimal SQL warning.
        from readthedocs.comments.models import DocumentComment
        queue = []
        comments = DocumentComment.objects.filter(node__project=self)
        for comment in comments:
            if not comment.has_been_approved_since_most_recent_node_change():
                queue.append(comment)

        return queue

    def add_node(self, content_hash, page, version, commit):
        """Add comment node

        :param content_hash: Hash of node content
        :param page: Doc page for node
        :param version: Slug for project version to apply node to
        :type version: str
        :param commit: Commit that node was updated in
        :type commit: str
        """
        from readthedocs.comments.models import NodeSnapshot, DocumentNode
        project_obj = Project.objects.get(slug=self.slug)
        version_obj = project_obj.versions.get(slug=version)
        try:
            NodeSnapshot.objects.get(hash=content_hash, node__project=project_obj,
                                     node__version=version_obj, node__page=page,
                                     commit=commit)
            return False  # ie, no new node was created.
        except NodeSnapshot.DoesNotExist:
            DocumentNode.objects.create(
                hash=content_hash,
                page=page,
                project=project_obj,
                version=version_obj,
                commit=commit
            )
        return True  # ie, it's True that a new node was created.

    def add_comment(self, version_slug, page, content_hash, commit, user, text):
        """Add comment to node

        :param version_slug: Version slug to use for node lookup
        :param page: Page to attach comment to
        :param content_hash: Hash of content to apply comment to
        :param commit: Commit that updated comment
        :param user: :py:cls:`User` instance that created comment
        :param text: Comment text
        """
        from readthedocs.comments.models import DocumentNode
        try:
            node = self.nodes.from_hash(version_slug, page, content_hash)
        except DocumentNode.DoesNotExist:
            version = self.versions.get(slug=version_slug)
            node = self.nodes.create(version=version, page=page,
                                     hash=content_hash, commit=commit)
        return node.comments.create(user=user, text=text)
Beispiel #34
0
class BlogPost(models.Model):
    class Meta:
        # ordered by pub_date descending when retriving
        ordering = ['-pub_date']

    def get_upload_md_name(self, filename):
        if self.pub_date:
            year = self.pub_date.year  # always store in pub_year folder
        else:
            year = datetime.now().year
        upload_to = upload_dir % (year, self.title + '.md')
        return upload_to

    def get_html_name(self, filename):
        if self.pub_date:
            year = self.pub_date.year
        else:
            year = datetime.now().year
        upload_to = upload_dir % (year, filename)
        return upload_to

    CATEGORY_CHOICES = (
        ('programming', 'Programming'),
        ('acg', 'Anime & Manga & Novel & Game'),
        ('nc', 'No Category'),
    )

    title = models.CharField(max_length=150)
    body = models.TextField(blank=True)
    # uploaded md file
    md_file = models.FileField(upload_to=get_upload_md_name, blank=True)
    pub_date = models.DateTimeField('date published', auto_now_add=True)
    last_edit_date = models.DateTimeField('last edited', auto_now=True)
    slug = models.SlugField(max_length=200, blank=True)
    # generated html file
    html_file = models.FileField(upload_to=get_html_name, blank=True)
    category = models.CharField(max_length=30, choices=CATEGORY_CHOICES)
    description = models.TextField(blank=True)
    tags = TaggableManager()

    def __str__(self):
        return self.title  # 根据继承搜索流程,先是实例属性,然后就是类属性,所以这样用没问题

    @property
    def filename(self):
        if self.md_file:
            return os.path.basename(self.title)
        else:
            return 'no md_file'

    def save(self, *args, **kwargs):
        self.slug = slugify(unidecode(self.title))
        if not self.body and self.md_file:
            self.body = self.md_file.read()

        html = markdown2.markdown(
            self.body,
            extras=["fenced-code-blocks", "tables", "toc", "header-ids"])
        if html.toc_html:
            content_file = ContentFile(
                html.toc_html.encode('utf-8') + html.encode('utf-8'))
        else:
            content_file = ContentFile(html.encode('utf-8'))

        self.html_file.save(self.title + '.html', content_file, save=False)
        self.html_file.close()

        super().save(*args, **kwargs)

    def display_html(self):
        with open(self.html_file.path, encoding='utf-8') as f:
            return f.read()

    def get_absolute_url(self):
        from . import views
        return reverse(views.blogpost,
                       kwargs={
                           'slug': self.slug,
                           'post_id': self.id
                       })
Beispiel #35
0
class ResourceBase(PolymorphicModel, PermissionLevelMixin):
    """
    Base Resource Object loosely based on ISO 19115:2003
    """

    VALID_DATE_TYPES = [(x.lower(), _(x))
                        for x in ['Creation', 'Publication', 'Revision']]

    date_help_text = _('reference date for the cited resource')
    date_type_help_text = _('identification of when a given event occurred')
    edition_help_text = _('version of the cited resource')
    abstract_help_text = _(
        'brief narrative summary of the content of the resource(s)')
    purpose_help_text = _(
        'summary of the intentions with which the resource(s) was developed')
    maintenance_frequency_help_text = _(
        'frequency with which modifications and deletions are made to the data after '
        'it is first produced')
    keywords_help_text = _(
        'commonly used word(s) or formalised word(s) or phrase(s) used to describe the subject '
        '(space or comma-separated')
    regions_help_text = _('keyword identifies a location')
    restriction_code_type_help_text = _(
        'limitation(s) placed upon the access or use of the data.')
    constraints_other_help_text = _(
        'other restrictions and legal prerequisites for accessing and using the resource or'
        ' metadata')
    license_help_text = _('license of the dataset')
    language_help_text = _('language used within the dataset')
    category_help_text = _(
        'high-level geographic data thematic classification to assist in the grouping and search of '
        'available geographic data sets.')
    spatial_representation_type_help_text = _(
        'method used to represent geographic information in the dataset.')
    temporal_extent_start_help_text = _(
        'time period covered by the content of the dataset (start)')
    temporal_extent_end_help_text = _(
        'time period covered by the content of the dataset (end)')
    distribution_url_help_text = _(
        'information about on-line sources from which the dataset, specification, or '
        'community profile name and extended metadata elements can be obtained'
    )
    distribution_description_help_text = _(
        'detailed text description of what the online resource is/does')
    data_quality_statement_help_text = _(
        'general explanation of the data producer\'s knowledge about the lineage of a'
        ' dataset')
    # internal fields
    uuid = models.CharField(max_length=36)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
                              blank=True,
                              null=True,
                              related_name='owned_resource',
                              verbose_name=_("Owner"))
    contacts = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                      through='ContactRole')
    title = models.CharField(
        _('title'),
        max_length=255,
        help_text=_('name by which the cited resource is known'))
    date = models.DateTimeField(_('date'),
                                default=datetime.datetime.now,
                                help_text=date_help_text)
    date_type = models.CharField(_('date type'),
                                 max_length=255,
                                 choices=VALID_DATE_TYPES,
                                 default='publication',
                                 help_text=date_type_help_text)
    edition = models.CharField(_('edition'),
                               max_length=255,
                               blank=True,
                               null=True,
                               help_text=edition_help_text)
    abstract = models.TextField(_('abstract'),
                                blank=True,
                                help_text=abstract_help_text)
    purpose = models.TextField(_('purpose'),
                               null=True,
                               blank=True,
                               help_text=purpose_help_text)
    maintenance_frequency = models.CharField(
        _('maintenance frequency'),
        max_length=255,
        choices=UPDATE_FREQUENCIES,
        blank=True,
        null=True,
        help_text=maintenance_frequency_help_text)

    keywords = TaggableManager(_('keywords'),
                               blank=True,
                               help_text=keywords_help_text)
    regions = models.ManyToManyField(Region,
                                     verbose_name=_('keywords region'),
                                     blank=True,
                                     null=True,
                                     help_text=regions_help_text)

    restriction_code_type = models.ForeignKey(
        RestrictionCodeType,
        verbose_name=_('restrictions'),
        help_text=restriction_code_type_help_text,
        null=True,
        blank=True,
        limit_choices_to=Q(is_choice=True))

    constraints_other = models.TextField(_('restrictions other'),
                                         blank=True,
                                         null=True,
                                         help_text=constraints_other_help_text)

    license = models.ForeignKey(License,
                                null=True,
                                blank=True,
                                verbose_name=_("License"),
                                help_text=license_help_text)
    language = models.CharField(_('language'),
                                max_length=3,
                                choices=ALL_LANGUAGES,
                                default='eng',
                                help_text=language_help_text)

    category = models.ForeignKey(TopicCategory,
                                 null=True,
                                 blank=True,
                                 limit_choices_to=Q(is_choice=True),
                                 help_text=category_help_text)

    spatial_representation_type = models.ForeignKey(
        SpatialRepresentationType,
        null=True,
        blank=True,
        limit_choices_to=Q(is_choice=True),
        verbose_name=_("spatial representation type"),
        help_text=spatial_representation_type_help_text)

    # Section 5
    temporal_extent_start = models.DateTimeField(
        _('temporal extent start'),
        blank=True,
        null=True,
        help_text=temporal_extent_start_help_text)
    temporal_extent_end = models.DateTimeField(
        _('temporal extent end'),
        blank=True,
        null=True,
        help_text=temporal_extent_end_help_text)

    supplemental_information = models.TextField(
        _('supplemental information'),
        default=DEFAULT_SUPPLEMENTAL_INFORMATION,
        help_text=_('any other descriptive information about the dataset'))

    # Section 6
    distribution_url = models.TextField(_('distribution URL'),
                                        blank=True,
                                        null=True,
                                        help_text=distribution_url_help_text)
    distribution_description = models.TextField(
        _('distribution description'),
        blank=True,
        null=True,
        help_text=distribution_description_help_text)

    # Section 8
    data_quality_statement = models.TextField(
        _('data quality statement'),
        blank=True,
        null=True,
        help_text=data_quality_statement_help_text)

    # Section 9
    # see metadata_author property definition below

    # Save bbox values in the database.
    # This is useful for spatial searches and for generating thumbnail images and metadata records.
    bbox_x0 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_x1 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_y0 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_y1 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    srid = models.CharField(max_length=255, default='EPSG:4326')

    # CSW specific fields
    csw_typename = models.CharField(_('CSW typename'),
                                    max_length=32,
                                    default='gmd:MD_Metadata',
                                    null=False)

    csw_schema = models.CharField(_('CSW schema'),
                                  max_length=64,
                                  default='http://www.isotc211.org/2005/gmd',
                                  null=False)

    csw_mdsource = models.CharField(_('CSW source'),
                                    max_length=256,
                                    default='local',
                                    null=False)
    csw_insert_date = models.DateTimeField(_('CSW insert date'),
                                           auto_now_add=True,
                                           null=True)
    csw_type = models.CharField(_('CSW type'),
                                max_length=32,
                                default='dataset',
                                null=False,
                                choices=HIERARCHY_LEVELS)
    csw_anytext = models.TextField(_('CSW anytext'), null=True, blank=True)
    csw_wkt_geometry = models.TextField(
        _('CSW WKT geometry'),
        null=False,
        default='POLYGON((-180 -90,-180 90,180 90,180 -90,-180 -90))')

    # metadata XML specific fields
    metadata_uploaded = models.BooleanField(default=False)
    metadata_uploaded_preserve = models.BooleanField(default=False)
    metadata_xml = models.TextField(
        null=True,
        default=
        '<gmd:MD_Metadata xmlns:gmd="http://www.isotc211.org/2005/gmd"/>',
        blank=True)

    popular_count = models.IntegerField(default=0)
    share_count = models.IntegerField(default=0)

    featured = models.BooleanField(
        _("Featured"),
        default=False,
        help_text=_('Should this resource be advertised in home page?'))
    is_published = models.BooleanField(
        _("Is Published"),
        default=True,
        help_text=_('Should this resource be published and searchable?'))

    # fields necessary for the apis
    thumbnail_url = models.TextField(null=True, blank=True)
    detail_url = models.CharField(max_length=255, null=True, blank=True)
    rating = models.IntegerField(default=0, null=True, blank=True)

    def __unicode__(self):
        return self.title

    @property
    def bbox(self):
        return [
            self.bbox_x0, self.bbox_y0, self.bbox_x1, self.bbox_y1, self.srid
        ]

    @property
    def bbox_string(self):
        return ",".join([
            str(self.bbox_x0),
            str(self.bbox_y0),
            str(self.bbox_x1),
            str(self.bbox_y1)
        ])

    @property
    def geographic_bounding_box(self):
        return bbox_to_wkt(self.bbox_x0,
                           self.bbox_x1,
                           self.bbox_y0,
                           self.bbox_y1,
                           srid=self.srid)

    @property
    def license_light(self):
        a = []
        if (not (self.license.name is None)) and (len(self.license.name) > 0):
            a.append(self.license.name)
        if (not (self.license.url is None)) and (len(self.license.url) > 0):
            a.append("(" + self.license.url + ")")
        return " ".join(a)

    @property
    def license_verbose(self):
        a = []
        if (not (self.license.name_long is None)) and (len(
                self.license.name_long) > 0):
            a.append(self.license.name_long + ":")
        if (not (self.license.description is None)) and (len(
                self.license.description) > 0):
            a.append(self.license.description)
        if (not (self.license.url is None)) and (len(self.license.url) > 0):
            a.append("(" + self.license.url + ")")
        return " ".join(a)

    def keyword_list(self):
        return [kw.name for kw in self.keywords.all()]

    def keyword_slug_list(self):
        return [kw.slug for kw in self.keywords.all()]

    def region_name_list(self):
        return [region.name for region in self.regions.all()]

    def spatial_representation_type_string(self):
        if hasattr(self.spatial_representation_type, 'identifier'):
            return self.spatial_representation_type.identifier
        else:
            if hasattr(self, 'storeType'):
                if self.storeType == 'coverageStore':
                    return 'grid'
                return 'vector'
            else:
                return None

    @property
    def keyword_csv(self):
        keywords_qs = self.get_real_instance().keywords.all()
        if keywords_qs:
            return ','.join([kw.name for kw in keywords_qs])
        else:
            return ''

    def set_latlon_bounds(self, box):
        """
        Set the four bounds in lat lon projection
        """
        self.bbox_x0 = box[0]
        self.bbox_x1 = box[1]
        self.bbox_y0 = box[2]
        self.bbox_y1 = box[3]

    def set_bounds_from_center_and_zoom(self, center_x, center_y, zoom):
        """
        Calculate zoom level and center coordinates in mercator.
        """
        self.center_x = center_x
        self.center_y = center_y
        self.zoom = zoom

        deg_len_equator = 40075160 / 360

        # covert center in lat lon
        def get_lon_lat():
            wgs84 = Proj(init='epsg:4326')
            mercator = Proj(init='epsg:3857')
            lon, lat = transform(mercator, wgs84, center_x, center_y)
            return lon, lat

        # calculate the degree length at this latitude
        def deg_len():
            lon, lat = get_lon_lat()
            return math.cos(lat) * deg_len_equator

        lon, lat = get_lon_lat()

        # taken from http://wiki.openstreetmap.org/wiki/Zoom_levels
        # it might be not precise but enough for the purpose
        distance_per_pixel = 40075160 * math.cos(lat) / 2**(zoom + 8)

        # calculate the distance from the center of the map in degrees
        # we use the calculated degree length on the x axis and the
        # normal degree length on the y axis assumin that it does not change

        # Assuming a map of 1000 px of width and 700 px of height
        distance_x_degrees = distance_per_pixel * 500 / deg_len()
        distance_y_degrees = distance_per_pixel * 350 / deg_len_equator

        self.bbox_x0 = lon - distance_x_degrees
        self.bbox_x1 = lon + distance_x_degrees
        self.bbox_y0 = lat - distance_y_degrees
        self.bbox_y1 = lat + distance_y_degrees

    def set_bounds_from_bbox(self, bbox):
        """
        Calculate zoom level and center coordinates in mercator.
        """
        self.set_latlon_bounds(bbox)

        minx, miny, maxx, maxy = [float(c) for c in bbox]
        x = (minx + maxx) / 2
        y = (miny + maxy) / 2
        (center_x, center_y) = forward_mercator((x, y))

        xdiff = maxx - minx
        ydiff = maxy - miny

        zoom = 0

        if xdiff > 0 and ydiff > 0:
            width_zoom = math.log(360 / xdiff, 2)
            height_zoom = math.log(360 / ydiff, 2)
            zoom = math.ceil(min(width_zoom, height_zoom))

        self.zoom = zoom
        self.center_x = center_x
        self.center_y = center_y

    def download_links(self):
        """assemble download links for pycsw"""
        links = []
        for url in self.link_set.all():
            if url.link_type == 'metadata':  # avoid recursion
                continue
            if url.link_type == 'html':
                links.append((self.title, 'Web address (URL)',
                              'WWW:LINK-1.0-http--link', url.url))
            elif url.link_type in ('OGC:WMS', 'OGC:WFS', 'OGC:WCS'):
                links.append((self.title, url.name, url.link_type, url.url))
            else:
                description = '%s (%s Format)' % (self.title, url.name)
                links.append((self.title, description,
                              'WWW:DOWNLOAD-1.0-http--download', url.url))
        return links

    def get_tiles_url(self):
        """Return URL for Z/Y/X mapping clients or None if it does not exist.
        """
        try:
            tiles_link = self.link_set.get(name='Tiles')
        except Link.DoesNotExist:
            return None
        else:
            return tiles_link.url

    def get_legend(self):
        """Return Link for legend or None if it does not exist.
        """
        try:
            legends_link = self.link_set.get(name='Legend')
        except Link.DoesNotExist:
            return None
        else:
            return legends_link

    def get_legend_url(self):
        """Return URL for legend or None if it does not exist.

           The legend can be either an image (for Geoserver's WMS)
           or a JSON object for ArcGIS.
        """
        legend = self.get_legend()

        if legend is None:
            return None

        return legend.url

    def get_ows_url(self):
        """Return URL for OGC WMS server None if it does not exist.
        """
        try:
            ows_link = self.link_set.get(name='OGC:WMS')
        except Link.DoesNotExist:
            return None
        else:
            return ows_link.url

    def get_thumbnail_url(self):
        """Return a thumbnail url.

           It could be a local one if it exists, a remote one (WMS GetImage) for example
           or a 'Missing Thumbnail' one.
        """
        local_thumbnails = self.link_set.filter(name='Thumbnail')
        if local_thumbnails.count() > 0:
            return local_thumbnails[0].url

        remote_thumbnails = self.link_set.filter(name='Remote Thumbnail')
        if remote_thumbnails.count() > 0:
            return remote_thumbnails[0].url

        return staticfiles.static(settings.MISSING_THUMBNAIL)

    def has_thumbnail(self):
        """Determine if the thumbnail object exists and an image exists"""
        return self.link_set.filter(name='Thumbnail').exists()

    def save_thumbnail(self, filename, image):
        thumb_folder = 'thumbs'
        upload_path = os.path.join(settings.MEDIA_ROOT, thumb_folder)
        if not os.path.exists(upload_path):
            os.makedirs(upload_path)

        with open(os.path.join(upload_path, filename), 'wb') as f:
            thumbnail = File(f)
            thumbnail.write(image)

        url_path = os.path.join(settings.MEDIA_URL, thumb_folder,
                                filename).replace('\\', '/')
        url = urljoin(settings.SITEURL, url_path)

        Link.objects.get_or_create(resource=self,
                                   url=url,
                                   defaults=dict(
                                       name='Thumbnail',
                                       extension='png',
                                       mime='image/png',
                                       link_type='image',
                                   ))

        ResourceBase.objects.filter(id=self.id).update(thumbnail_url=url)

    def set_missing_info(self):
        """Set default permissions and point of contacts.

           It is mandatory to call it from descendant classes
           but hard to enforce technically via signals or save overriding.
        """
        from guardian.models import UserObjectPermission
        logger.debug('Checking for permissions.')
        #  True if every key in the get_all_level_info dict is empty.
        no_custom_permissions = UserObjectPermission.objects.filter(
            content_type=ContentType.objects.get_for_model(
                self.get_self_resource()),
            object_pk=str(self.pk)).exists()

        if not no_custom_permissions:
            logger.debug(
                'There are no permissions for this object, setting default perms.'
            )
            self.set_default_permissions()

        if self.owner:
            user = self.owner
        else:
            user = ResourceBase.objects.admin_contact().user

        if self.poc is None:
            self.poc = user
        if self.metadata_author is None:
            self.metadata_author = user

    def maintenance_frequency_title(self):
        return [
            v for i, v in enumerate(UPDATE_FREQUENCIES)
            if v[0] == self.maintenance_frequency
        ][0][1].title()

    def language_title(self):
        return [
            v for i, v in enumerate(ALL_LANGUAGES) if v[0] == self.language
        ][0][1].title()

    def _set_poc(self, poc):
        # reset any poc assignation to this resource
        ContactRole.objects.filter(role='pointOfContact',
                                   resource=self).delete()
        # create the new assignation
        ContactRole.objects.create(role='pointOfContact',
                                   resource=self,
                                   contact=poc)

    def _get_poc(self):
        try:
            the_poc = ContactRole.objects.get(role='pointOfContact',
                                              resource=self).contact
        except ContactRole.DoesNotExist:
            the_poc = None
        return the_poc

    poc = property(_get_poc, _set_poc)

    def _set_metadata_author(self, metadata_author):
        # reset any metadata_author assignation to this resource
        ContactRole.objects.filter(role='author', resource=self).delete()
        # create the new assignation
        ContactRole.objects.create(role='author',
                                   resource=self,
                                   contact=metadata_author)

    def _get_metadata_author(self):
        try:
            the_ma = ContactRole.objects.get(role='author',
                                             resource=self).contact
        except ContactRole.DoesNotExist:
            the_ma = None
        return the_ma

    metadata_author = property(_get_metadata_author, _set_metadata_author)

    objects = ResourceBaseManager()

    class Meta:
        # custom permissions,
        # add, change and delete are standard in django-guardian
        permissions = (
            ('view_resourcebase', 'Can view resource'),
            ('change_resourcebase_permissions',
             'Can change resource permissions'),
            ('download_resourcebase', 'Can download resource'),
            ('publish_resourcebase', 'Can publish resource'),
            ('change_resourcebase_metadata', 'Can change resource metadata'),
        )
Beispiel #36
0
class AbstractDocument(CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.FileField(upload_to='documents', verbose_name=_('file'))
    created_at = models.DateTimeField(verbose_name=_('created at'),
                                      auto_now_add=True)
    Info = models.CharField(verbose_name='信息', max_length=255, blank=True)

    ClothCode = models.CharField(max_length=255,
                                 verbose_name="布匹编号",
                                 blank=True,
                                 default="000000")
    BatchNum = models.CharField(max_length=255,
                                verbose_name="产品批号",
                                blank=True,
                                default="请上传说明文档")
    Specs = models.CharField(max_length=255,
                             verbose_name="产品规格",
                             blank=True,
                             default="请上传说明文档")
    BatchNum = models.CharField(max_length=255,
                                verbose_name="产品批号",
                                blank=True,
                                default="请上传说明文档")

    uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                         verbose_name=_('uploaded by user'),
                                         null=True,
                                         blank=True,
                                         editable=False,
                                         on_delete=models.SET_NULL)

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = DocumentQuerySet.as_manager()

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    @contextmanager
    def open_file(self):
        # Open file if it is closed
        close_file = False
        f = self.file

        if f.closed:
            # Reopen the file
            if self.is_stored_locally():
                f.open('rb')
            else:
                # Some external storage backends don't allow reopening
                # the file. Get a fresh file instance. #1397
                storage = self._meta.get_field('file').storage
                f = storage.open(f.name, 'rb')

            close_file = True

        # Seek to beginning
        f.seek(0)

        try:
            yield f
        finally:
            if close_file:
                f.close()

    def get_file_size(self):
        if self.file_size is None:
            try:
                self.file_size = self.file.size
            except Exception:
                # File doesn't exist
                return

            self.save(update_fields=['file_size'])

        return self.file_size

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def __str__(self):
        return self.title

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def file_extension(self):
        return os.path.splitext(self.filename)[1][1:]

    @property
    def url(self):
        return reverse('wagtaildocs_serve', args=[self.id, self.filename])

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtaildocs:document_usage', args=(self.id, ))

    def is_editable_by_user(self, user):
        from wagtail.documents.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(
            user, 'change', self)

    class Meta:
        abstract = True
        verbose_name = _('document')
        verbose_name_plural = _('documents')
Beispiel #37
0
class FoiRequest(models.Model):
    STATUS = Status
    RESOLUTION = Resolution
    FILTER_STATUS = FilterStatus
    VISIBILITY = Visibility

    STATUS_RESOLUTION_DICT = STATUS_RESOLUTION_DICT

    # model fields
    title = models.CharField(_("Title"), max_length=255)
    slug = models.SlugField(_("Slug"), max_length=255, unique=True)
    description = models.TextField(_("Description"), blank=True)
    summary = models.TextField(_("Summary"), blank=True)

    public_body = models.ForeignKey(PublicBody, null=True, blank=True,
            on_delete=models.SET_NULL, verbose_name=_("Public Body"))

    status = models.CharField(
        _("Status"), max_length=50, choices=Status.choices
    )
    resolution = models.CharField(
        _("Resolution"), max_length=50, choices=Resolution.choices,
        blank=True
    )

    public = models.BooleanField(_("published?"), default=True)
    visibility = models.SmallIntegerField(
        _("Visibility"), default=Visibility.INVISIBLE,
        choices=Visibility.choices
    )

    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
            on_delete=models.SET_NULL,
            verbose_name=_("User"))
    team = models.ForeignKey(Team, null=True, blank=True,
            on_delete=models.SET_NULL, verbose_name=_("Team"))

    first_message = models.DateTimeField(_("Date of first message"),
            blank=True, null=True)
    last_message = models.DateTimeField(_("Date of last message"),
            blank=True, null=True)
    resolved_on = models.DateTimeField(_("Resolution date"),
            blank=True, null=True)
    due_date = models.DateTimeField(_("Due Date"),
            blank=True, null=True)

    secret_address = models.CharField(_("Secret address"), max_length=255,
            db_index=True, unique=True)
    secret = models.CharField(_("Secret"), blank=True, max_length=100)

    reference = models.CharField(_("Reference"), blank=True, max_length=255)

    same_as = models.ForeignKey('self', null=True, blank=True,
            on_delete=models.SET_NULL,
            verbose_name=_("Identical request"))
    same_as_count = models.IntegerField(_("Identical request count"), default=0)

    project = models.ForeignKey(FoiProject, null=True, blank=True,
            on_delete=models.SET_NULL, verbose_name=_('project'))
    project_order = models.IntegerField(null=True, blank=True)

    law = models.ForeignKey(FoiLaw, null=True, blank=True,
            on_delete=models.SET_NULL,
            verbose_name=_("Freedom of Information Law"))
    costs = models.FloatField(_("Cost of Information"), default=0.0)
    refusal_reason = models.CharField(_("Refusal reason"), max_length=1024,
            blank=True)
    checked = models.BooleanField(_("checked"), default=False)
    is_blocked = models.BooleanField(_("Blocked"), default=False)
    not_publishable = models.BooleanField(_('Not publishable'), default=False)
    is_foi = models.BooleanField(_("is FoI request"), default=True)
    closed = models.BooleanField(_('is closed'), default=False)

    campaign = models.ForeignKey(
        Campaign, verbose_name=_('campaign'),
        null=True, blank=True, on_delete=models.SET_NULL
    )

    jurisdiction = models.ForeignKey(
        Jurisdiction,
        verbose_name=_('Jurisdiction'),
        null=True, on_delete=models.SET_NULL
    )

    site = models.ForeignKey(Site, null=True,
            on_delete=models.SET_NULL, verbose_name=_("Site"))

    non_filtered_objects = models.Manager()
    objects = FoiRequestManager()
    published = PublishedFoiRequestManager()
    published_not_foi = PublishedNotFoiRequestManager()
    tags = TaggableManager(through=TaggedFoiRequest, blank=True)

    class Meta:
        ordering = ('-last_message',)
        get_latest_by = 'last_message'
        verbose_name = _('Freedom of Information Request')
        verbose_name_plural = _('Freedom of Information Requests')
        permissions = (
            ("see_private", _("Can see private requests")),
            ("create_batch", _("Create batch requests")),
            ("moderate", _("Can moderate requests")),
        )

    # Custom Signals
    message_sent = django.dispatch.Signal(providing_args=["message", "user"])
    message_received = django.dispatch.Signal(providing_args=["message"])
    request_created = django.dispatch.Signal(providing_args=[])
    request_to_public_body = django.dispatch.Signal(providing_args=[])
    status_changed = django.dispatch.Signal(providing_args=[
        "status", "resolution", "data", "user",
        "previous_status", "previous_resolution"
    ])
    became_overdue = django.dispatch.Signal(providing_args=[])
    became_asleep = django.dispatch.Signal(providing_args=[])
    public_body_suggested = django.dispatch.Signal(providing_args=["suggestion"])
    set_concrete_law = django.dispatch.Signal(providing_args=['name', 'user'])
    made_public = django.dispatch.Signal(providing_args=['user'])
    made_private = django.dispatch.Signal(providing_args=['user'])
    escalated = django.dispatch.Signal(providing_args=['message', 'user'])

    def __str__(self):
        return _("Request '%s'") % self.title

    @property
    def same_as_set(self):
        return FoiRequest.objects.filter(same_as=self)

    @property
    def messages(self):
        if not hasattr(self, "_messages") or self._messages is None:
            self.get_messages()
        return self._messages

    def get_messages(self, with_tags=False):
        qs = self.foimessage_set.select_related(
            "sender_user",
            "sender_public_body",
            "recipient_public_body"
        ).order_by("timestamp")
        if with_tags:
            qs = qs.prefetch_related('tags')

        self._messages = list(qs)
        return self._messages

    @property
    def get_messages_by_month(self):
        """
        Group messages by "month-year"-key, e.g. "2020-09".
        Add extra due date key.
        """
        groups = {}
        today = datetime.today()
        due_date = self.due_date
        month_highlighted = False
        for msg in self.messages:
            key = str(msg.timestamp)[:7]
            if key not in groups:
                groups[key] = {
                    'date': msg.timestamp.replace(day=1, hour=0, minute=0, second=0, microsecond=0),
                    'is_same_year': msg.timestamp.year == today.year,
                    'messages': [],
                    'has_overdue_message': False,
                    'highlight_overdue': False,
                    'first_message_id': msg.get_html_id
                }
            groups[key]['messages'].append(msg)

            if msg.timestamp > due_date:
                groups[key]['has_overdue_message'] = True
                if month_highlighted is False:
                    groups[key]['highlight_overdue'] = True
                    month_highlighted = True

        return list(groups.values())

    @property
    def status_representation(self):
        if self.due_date is not None:
            if self.is_overdue():
                return FilterStatus.OVERDUE
        return self.status if self.status != Status.RESOLVED else self.resolution

    @property
    def status_settable(self):
        return self.awaits_classification()

    @property
    def project_number(self):
        if self.project_order is not None:
            return self.project_order + 1
        return None

    @property
    def has_fee(self):
        return self.costs > 0

    def identical_count(self):
        if self.same_as:
            return self.same_as.same_as_count
        return self.same_as_count

    def get_absolute_url(self):
        return reverse('foirequest-show',
                kwargs={'slug': self.slug})

    def get_absolute_url_last_message(self):
        return self.get_absolute_url() + '#last'

    @property
    def url(self):
        return self.get_absolute_url()

    def get_absolute_short_url(self):
        return get_absolute_short_url(self.id)

    def get_absolute_domain_url(self):
        return "%s%s" % (settings.SITE_URL, self.get_absolute_url())

    def get_absolute_domain_short_url(self):
        return get_absolute_domain_short_url(self.id)

    def get_auth_link(self):
        from ..auth import get_foirequest_auth_code

        return "%s%s" % (settings.SITE_URL,
            reverse('foirequest-auth',
                kwargs={"obj_id": self.id,
                    "code": get_foirequest_auth_code(self)
                }))

    def get_upload_link(self):
        from ..auth import get_foirequest_upload_code

        return "%s%s" % (settings.SITE_URL,
            reverse('foirequest-publicbody_upload',
                kwargs={
                    "obj_id": self.id,
                    "code": get_foirequest_upload_code(self)
                }))

    def get_accessible_link(self):
        if self.visibility == self.VISIBILITY.VISIBLE_TO_REQUESTER:
            return self.get_auth_link()
        return self.get_absolute_domain_short_url()

    def get_autologin_url(self):
        return self.user.get_autologin_url(
            self.get_absolute_short_url()
        )

    def is_public(self):
        return self.visibility == self.VISIBILITY.VISIBLE_TO_PUBLIC

    def in_public_search_index(self):
        return (
            self.is_public() and self.is_foi and
            self.same_as_id is None and
            (self.project_id is None or self.project_order == 0)
        )

    def get_redaction_regexes(self):
        from ..utils import get_foi_mail_domains

        user = self.user
        domains = get_foi_mail_domains()
        email_regexes = [r'[\w\.\-]+@' + x for x in domains]
        FROIDE_CONFIG = settings.FROIDE_CONFIG
        user_regexes = []
        if user.private:
            user_regexes = [
                '%s %s' % (FROIDE_CONFIG['redact_salutation'], user.get_full_name()),
                '%s %s' % (FROIDE_CONFIG['redact_salutation'], user.last_name),
                user.get_full_name(),
                user.last_name,
                user.first_name
            ]
        all_regexes = email_regexes + user_regexes + user.address.splitlines()
        all_regexes = [re.escape(a) for a in all_regexes]
        return json.dumps([a.strip() for a in all_regexes if a.strip()])

    def get_description(self):
        return redact_plaintext(self.description, user=self.user)

    def response_messages(self):
        return list(filter(lambda m: m.is_response, self.messages))

    def sent_messages(self):
        return list(filter(lambda m: not m.is_response, self.messages))

    def reply_received(self):
        return len(self.response_messages()) > 0

    def message_needs_status(self):
        mes = list(filter(lambda m: m.status is None, self.response_messages()))
        if not mes:
            return None
        return mes[0]

    def status_is_final(self):
        return self.status == Status.RESOLVED

    def needs_public_body(self):
        return self.status == Status.PUBLICBODY_NEEDED

    def awaits_response(self):
        return self.status in (Status.AWAITING_RESPONSE, Status.ASLEEP)

    def is_actionable(self):
        return not self.needs_public_body() and (
            self.is_overdue() or self.reply_received()
        )

    @classmethod
    def get_throttle_config(cls):
        return settings.FROIDE_CONFIG.get('request_throttle', None)

    def should_apply_throttle(self):
        last_message = self.messages[-1]
        return not last_message.is_response or not self.is_actionable()

    def can_be_escalated(self):
        return self.law.mediator_id and self.is_actionable()

    def is_overdue(self):
        return self.was_overdue() and self.awaits_response()

    def is_successful(self):
        return self.resolution == Resolution.SUCCESSFUL

    def was_overdue(self):
        if self.due_date:
            return self.due_date < timezone.now()
        return False

    def has_been_refused(self):
        return self.resolution in (Resolution.REFUSED, Resolution.PARTIALLY_SUCCESSFUL)

    def awaits_classification(self):
        return self.status == Status.AWAITING_CLASSIFICATION

    def moderate_classification(self):
        return self.awaits_classification() and self.available_for_moderator_action()

    def available_for_moderator_action(self):
        ago = timezone.now() - MODERATOR_CLASSIFICATION_OFFSET
        return self.last_message < ago

    def set_awaits_classification(self):
        self.status = Status.AWAITING_CLASSIFICATION

    def follow_count(self):
        from froide.foirequestfollower.models import FoiRequestFollower
        return FoiRequestFollower.objects.filter(
            request=self, confirmed=True
        ).count()

    def public_date(self):
        if self.due_date:
            return self.due_date + timedelta(days=settings.FROIDE_CONFIG.get(
                'request_public_after_due_days', 14))
        return None

    def get_set_tags_form(self):
        from ..forms import TagFoiRequestForm
        return TagFoiRequestForm(tags=self.tags.all())

    def get_status_form(self):
        from ..forms import FoiRequestStatusForm
        if self.status not in (Status.AWAITING_RESPONSE, Status.RESOLVED):
            status = ''
        else:
            status = self.status
        return FoiRequestStatusForm(
            foirequest=self,
            initial={
                "status": status,
                'resolution': self.resolution,
                "costs": self.costs,
                "refusal_reason": self.refusal_reason
            })

    def public_body_suggestions_form(self):
        from ..forms import PublicBodySuggestionsForm
        return PublicBodySuggestionsForm(foirequest=self)

    def make_public_body_suggestion_form(self):
        from ..forms import MakePublicBodySuggestionForm
        return MakePublicBodySuggestionForm()

    def get_concrete_law_form(self):
        from ..forms import ConcreteLawForm
        return ConcreteLawForm(foirequest=self)

    def get_postal_reply_form(self):
        from ..forms import get_postal_reply_form
        return get_postal_reply_form(foirequest=self)

    def get_postal_message_form(self):
        from ..forms import get_postal_message_form
        return get_postal_message_form(foirequest=self)

    def get_send_message_form(self):
        from ..forms import get_send_message_form
        return get_send_message_form(foirequest=self)

    def get_escalation_message_form(self):
        from ..forms import get_escalation_message_form
        return get_escalation_message_form(foirequest=self)

    def quote_last_message(self):
        return list(self.messages)[-1].get_quoted()

    @property
    def readable_status(self):
        return FoiRequest.get_readable_status(self.status_representation)

    @property
    def status_description(self):
        return FoiRequest.get_status_description(self.status_representation)

    @classmethod
    def get_readable_status(cls, status, fallback=UNKNOWN_STATUS):
        return str(cls.STATUS_RESOLUTION_DICT.get(status, fallback).label)

    @classmethod
    def get_status_description(cls, status, fallback=UNKNOWN_STATUS):
        return str(cls.STATUS_RESOLUTION_DICT.get(status, fallback).description)

    def determine_visibility(self):
        if self.public:
            self.visibility = self.VISIBILITY.VISIBLE_TO_PUBLIC
        else:
            self.visibility = self.VISIBILITY.VISIBLE_TO_REQUESTER

    def set_status_after_change(self):
        if not self.user.is_active:
            self.status = Status.AWAITING_USER_CONFIRMATION
        else:
            self.determine_visibility()
            if self.public_body is None:
                self.status = Status.PUBLICBODY_NEEDED
            elif not self.public_body.confirmed:
                self.status = Status.AWAITING_PUBLICBODY_CONFIRMATION
            else:
                self.status = Status.AWAITING_RESPONSE
                return True
        return False

    def safe_send_first_message(self):
        messages = self.foimessage_set.all()
        if not len(messages) == 1:
            return None
        message = messages[0]
        if message.sent:
            return None
        message.send()
        self.message_sent.send(
            sender=self, message=message,
            user=self.user
        )

    def confirmed_public_body(self):
        send_now = self.set_status_after_change()
        self.save()
        if send_now:
            self.safe_send_first_message()
            return True
        return False

    def suggest_public_body(self, public_body, reason, user):
        from .suggestion import PublicBodySuggestion

        try:
            PublicBodySuggestion.objects.get(
                public_body=public_body,
                request=self
            )
        except PublicBodySuggestion.DoesNotExist:
            suggestion = self.publicbodysuggestion_set.create(
                    public_body=public_body,
                    reason=reason,
                    user=user)
            self.public_body_suggested.send(
                sender=self,
                suggestion=suggestion
            )
            return suggestion
        else:
            return False

    def make_public(self, user=None):
        self.public = True
        self.visibility = 2
        self.save()
        self.made_public.send(sender=self, user=user)

    def set_overdue(self):
        self.became_overdue.send(sender=self)

    def set_asleep(self):
        self.status = Status.ASLEEP
        self.save()
        self.became_asleep.send(sender=self)

    def days_to_resolution(self):
        final = None
        mes = None
        resolutions = dict(Resolution.choices)
        for mes in self.response_messages():
            if mes.status == Status.RESOLVED or mes.status in resolutions:
                final = mes.timestamp
                break
        if final is None or mes is None:
            return None
        return (mes.timestamp - self.first_message).days