Ejemplo n.º 1
0
class ScheduledBanner(models.Model):
    class Meta:
        # Custom permissions for use in the OSF Admin App
        permissions = (('view_scheduledbanner',
                        'Can view scheduled banner details'), )

    name = models.CharField(unique=True, max_length=256)
    start_date = NonNaiveDateTimeField()
    end_date = NonNaiveDateTimeField()
    color = models.CharField(max_length=7)
    license = models.CharField(blank=True, null=True, max_length=256)
    link = models.URLField(
        blank=True, default='https://www.crowdrise.com/centerforopenscience')

    default_photo = models.FileField(storage=BannerImageStorage())
    default_alt_text = models.TextField()

    mobile_photo = models.FileField(storage=BannerImageStorage())
    mobile_alt_text = models.TextField(blank=True, null=True)

    def save(self, *args, **kwargs):
        self.start_date = datetime.combine(self.start_date,
                                           datetime.min.time())
        self.end_date = datetime.combine(self.end_date, datetime.max.time())
        validate_banner_dates(self.id, self.start_date, self.end_date)
        super(ScheduledBanner, self).save(*args, **kwargs)
Ejemplo n.º 2
0
class Conference(ObjectIDMixin, BaseModel):
    #: Determines the email address for submission and the OSF url
    # Example: If endpoint is spsp2014, then submission email will be
    # [email protected] or [email protected] and the OSF url will
    # be osf.io/view/spsp2014
    endpoint = models.CharField(max_length=255, unique=True, db_index=True)
    #: Full name, e.g. "SPSP 2014"
    name = models.CharField(max_length=255)
    info_url = models.URLField(blank=True)
    logo_url = models.URLField(blank=True)
    location = models.CharField(max_length=2048, null=True, blank=True)
    start_date = NonNaiveDateTimeField(blank=True, null=True)
    end_date = NonNaiveDateTimeField(blank=True, null=True)
    is_meeting = models.BooleanField(default=True)
    active = models.BooleanField()
    admins = models.ManyToManyField('OSFUser')
    #: Whether to make submitted projects public
    public_projects = models.BooleanField(default=True)
    poster = models.BooleanField(default=True)
    talk = models.BooleanField(default=True)
    # field_names are used to customize the text on the conference page, the categories
    # of submissions, and the email adress to send material to.
    field_names = DateTimeAwareJSONField(default=get_default_field_names)

    # Cached number of submissions
    num_submissions = models.IntegerField(default=0)

    objects = ConferenceManager()

    def __repr__(self):
        return (
            '<Conference(endpoint={self.endpoint!r}, active={self.active})>'.
            format(self=self))

    @classmethod
    def get_by_endpoint(cls, endpoint, active):
        return cls.objects.get_by_endpoint(endpoint, active)

    @property
    def absolute_url(self):
        return urlparse.urljoin(settings.DOMAIN,
                                '/view/{}'.format(self.endpoint))

    @property
    def submissions(self):
        """
        Returns valid conference submissions with at least one file attached
        """
        tags = Tag.objects.filter(
            system=False, name__iexact=self.endpoint).values_list('pk',
                                                                  flat=True)
        return AbstractNode.objects.filter(tags__in=tags,
                                           is_public=True,
                                           is_deleted=False)

    class Meta:
        # custom permissions for use in the OSF Admin App
        permissions = (('view_conference',
                        'Can view conference details in the admin app.'), )
Ejemplo n.º 3
0
class QueuedMail(ObjectIDMixin, BaseModel):
    user = models.ForeignKey('OSFUser', db_index=True, null=True)
    to_addr = models.CharField(max_length=255)
    send_at = NonNaiveDateTimeField(db_index=True, null=False)

    # string denoting the template, presend to be used. Has to be an index of queue_mail types
    email_type = models.CharField(max_length=255, db_index=True, null=False)

    # dictionary with variables used to populate mako template and store information used in presends
    # Example:
    # self.data = {
    #    'nid' : 'ShIpTo',
    #    'fullname': 'Florence Welch',
    #}
    data = DateTimeAwareJSONField(default=dict, blank=True)
    sent_at = NonNaiveDateTimeField(db_index=True, null=True, blank=True)

    def __repr__(self):
        if self.sent_at is not None:
            return '<QueuedMail {} of type {} sent to {} at {}>'.format(
                self._id, self.email_type, self.to_addr, self.sent_at)
        return '<QueuedMail {} of type {} to be sent to {} on {}>'.format(
            self._id, self.email_type, self.to_addr, self.send_at)

    def send_mail(self):
        """
        Grabs the data from this email, checks for user subscription to help mails,

        constructs the mail object and checks presend. Then attempts to send the email
        through send_mail()
        :return: boolean based on whether email was sent.
        """
        mail_struct = queue_mail_types[self.email_type]
        presend = mail_struct['presend'](self)
        mail = Mail(mail_struct['template'],
                    subject=mail_struct['subject'],
                    categories=mail_struct.get('categories', None))
        self.data['osf_url'] = osf_settings.DOMAIN
        if presend and self.user.is_active and self.user.osf_mailing_lists.get(
                osf_settings.OSF_HELP_LIST):
            send_mail(self.to_addr or self.user.username,
                      mail,
                      mimetype='html',
                      **(self.data or {}))
            self.sent_at = timezone.now()
            self.save()
            return True
        else:
            self.__class__.remove_one(self)
            return False

    def find_sent_of_same_type_and_user(self):
        """
        Queries up for all emails of the same type as self, sent to the same user as self.
        Does not look for queue-up emails.
        :return: a list of those emails
        """
        return self.__class__.objects.filter(
            email_type=self.email_type, user=self.user).exclude(sent_at=None)
Ejemplo n.º 4
0
class ExternalAccount(base.ObjectIDMixin, base.BaseModel):
    """An account on an external service.

    Note that this object is not and should not be aware of what other objects
    are associated with it. This is by design, and this object should be kept as
    thin as possible, containing only those fields that must be stored in the
    database.

    The ``provider`` field is a de facto foreign key to an ``ExternalProvider``
    object, as providers are not stored in the database.
    """

    # The OAuth credentials. One or both of these fields should be populated.
    # For OAuth1, this is usually the "oauth_token"
    # For OAuth2, this is usually the "access_token"
    oauth_key = EncryptedTextField(blank=True, null=True)

    # For OAuth1, this is usually the "oauth_token_secret"
    # For OAuth2, this is not used
    oauth_secret = EncryptedTextField(blank=True, null=True)

    # Used for OAuth2 only
    refresh_token = EncryptedTextField(blank=True, null=True)
    date_last_refreshed = NonNaiveDateTimeField(blank=True, null=True)
    expires_at = NonNaiveDateTimeField(blank=True, null=True)
    scopes = ArrayField(models.CharField(max_length=128),
                        default=list,
                        blank=True)

    # The `name` of the service
    # This lets us query for only accounts on a particular provider
    # TODO We should make provider an actual FK someday.
    provider = models.CharField(max_length=50, blank=False, null=False)
    # The proper 'name' of the service
    # Needed for account serialization
    provider_name = models.CharField(max_length=255, blank=False, null=False)

    # The unique, persistent ID on the remote service.
    provider_id = models.CharField(max_length=255, blank=False, null=False)

    # The user's name on the external service
    display_name = EncryptedTextField(blank=True, null=True)
    # A link to the user's profile on the external service
    profile_url = EncryptedTextField(blank=True, null=True)

    def __repr__(self):
        return '<ExternalAccount: {}/{}>'.format(self.provider,
                                                 self.provider_id)

    def _natural_key(self):
        if self.pk:
            return self.pk
        return hash(str(self.provider_id) + str(self.provider))

    class Meta:
        unique_together = [(
            'provider',
            'provider_id',
        )]
class Conference(ObjectIDMixin, BaseModel):
    #: Determines the email address for submission and the OSF url
    # Example: If endpoint is spsp2014, then submission email will be
    # [email protected] or [email protected] and the OSF url will
    # be osf.io/view/spsp2014
    endpoint = models.CharField(max_length=255, unique=True, db_index=True)
    #: Full name, e.g. "SPSP 2014"
    name = models.CharField(max_length=255)
    info_url = models.URLField(blank=True)
    logo_url = models.URLField(blank=True)
    location = models.CharField(max_length=2048, null=True, blank=True)
    start_date = NonNaiveDateTimeField(blank=True, null=True)
    end_date = NonNaiveDateTimeField(blank=True, null=True)
    is_meeting = models.BooleanField(default=True)
    active = models.BooleanField()
    admins = models.ManyToManyField('OSFUser')
    # Temporary field on conference model to link Conferences and AbstractNodes
    submissions = models.ManyToManyField('AbstractNode',
                                         related_name='conferences')
    # Whether to make submitted projects public
    public_projects = models.BooleanField(default=True)
    poster = models.BooleanField(default=True)
    talk = models.BooleanField(default=True)
    # field_names are used to customize the text on the conference page, the categories
    # of submissions, and the email adress to send material to.
    field_names = DateTimeAwareJSONField(default=get_default_field_names)

    auto_check_spam = models.BooleanField(default=True)

    objects = ConferenceManager()

    def __repr__(self):
        return (
            '<Conference(endpoint={self.endpoint!r}, active={self.active})>'.
            format(self=self))

    @classmethod
    def get_by_endpoint(cls, endpoint, active):
        return cls.objects.get_by_endpoint(endpoint, active)

    @property
    def absolute_url(self):
        return urljoin(settings.DOMAIN, '/view/{}'.format(self.endpoint))

    @property
    def valid_submissions(self):
        """
        Returns valid conference submissions - nodes can't be public or deleted
        """
        return self.submissions.filter(is_public=True, is_deleted=False)

    class Meta:
        # custom permissions for use in the OSF Admin App
        permissions = (('view_conference',
                        'Can view conference details in the admin app.'), )
Ejemplo n.º 6
0
class Session(ObjectIDMixin, BaseModel):
    date_created = NonNaiveDateTimeField(auto_now_add=True)
    date_modified = NonNaiveDateTimeField(auto_now=True)
    data = DateTimeAwareJSONField(default=dict, blank=True)

    @property
    def is_authenticated(self):
        return 'auth_user_id' in self.data

    @property
    def is_external_first_login(self):
        return 'auth_user_external_first_login' in self.data
Ejemplo n.º 7
0
class CitationStyle(BaseModel):
    """Persistent representation of a CSL style.

    These are parsed from .csl files, so that metadata fields can be indexed.
    """

    primary_identifier_name = '_id'

    # The name of the citation file, sans extension
    _id = models.CharField(max_length=255, db_index=True)

    # The full title of the style
    title = models.CharField(max_length=255)

    # Datetime the file was last parsed
    date_parsed = NonNaiveDateTimeField(default=timezone.now)

    short_title = models.CharField(max_length=2048, null=True, blank=True)
    summary = models.CharField(max_length=4200, null=True,
                               blank=True)  # longest value was 3,812 8/23/2016

    class Meta:
        ordering = ['_id']

    def to_json(self):
        return {
            'id': self._id,
            'title': self.title,
            'short_title': self.short_title,
            'summary': self.summary,
        }
Ejemplo n.º 8
0
class RdmAnnouncement(BaseModel):
    user = models.ForeignKey('OSFUser', null=True)
    title = models.CharField(max_length=256, blank=True, null=False)
    body = models.TextField(max_length=63206, null=False)
    announcement_type = models.CharField(max_length=256, null=False)
    date_sent = NonNaiveDateTimeField(auto_now_add=True)
    is_success = models.BooleanField(default=False)
Ejemplo n.º 9
0
class Action(ObjectIDMixin, BaseModel):

    objects = IncludeManager()

    target = models.ForeignKey('PreprintService', related_name='actions')
    creator = models.ForeignKey('OSFUser', related_name='+')

    trigger = models.CharField(max_length=31, choices=Triggers.choices())
    from_state = models.CharField(max_length=31, choices=States.choices())
    to_state = models.CharField(max_length=31, choices=States.choices())

    comment = models.TextField(blank=True)

    is_deleted = models.BooleanField(default=False)
    date_created = NonNaiveDateTimeField(auto_now_add=True)
    date_modified = NonNaiveDateTimeField(auto_now=True)
Ejemplo n.º 10
0
class Guid(BaseModel):
    """Stores either a short guid or long object_id for any model that inherits from BaseIDMixin.
    Each ID field (e.g. 'guid', 'object_id') MUST have an accompanying method, named with
    'initialize_<ID type>' (e.g. 'initialize_guid') that generates and sets the field.
    """
    primary_identifier_name = '_id'

    id = models.AutoField(primary_key=True)
    _id = LowercaseCharField(max_length=255, null=False, blank=False, default=generate_guid, db_index=True,
                           unique=True)
    referent = GenericForeignKey()
    content_type = models.ForeignKey(ContentType, null=True, blank=True)
    object_id = models.PositiveIntegerField(null=True, blank=True)
    created = NonNaiveDateTimeField(db_index=True, auto_now_add=True)

    def __repr__(self):
        return '<id:{0}, referent:({1})>'.format(self._id, self.referent.__repr__())

    # Override load in order to load by GUID
    @classmethod
    def load(cls, data):
        try:
            return cls.objects.get(_id=data)
        except cls.DoesNotExist:
            return None

    class Meta:
        ordering = ['-created']
        get_latest_by = 'created'
        index_together = (
            ('content_type', 'object_id', 'created'),
        )
Ejemplo n.º 11
0
class RecentlyAddedContributor(models.Model):
    user = models.ForeignKey('OSFUser', on_delete=models.CASCADE)  # the user who added the contributor
    contributor = models.ForeignKey('OSFUser', related_name='recently_added_by', on_delete=models.CASCADE)  # the added contributor
    date_added = NonNaiveDateTimeField(auto_now=True)

    class Meta:
        unique_together = ('user', 'contributor')
Ejemplo n.º 12
0
class DraftRegistrationLog(ObjectIDMixin, BaseModel):
    """ Simple log to show status changes for DraftRegistrations

    field - _id - primary key
    field - date - date of the action took place
    field - action - simple action to track what happened
    field - user - user who did the action
    """
    date = NonNaiveDateTimeField(default=timezone.now)
    action = models.CharField(max_length=255)
    draft = models.ForeignKey('DraftRegistration',
                              related_name='logs',
                              null=True,
                              blank=True,
                              on_delete=models.CASCADE)
    user = models.ForeignKey('OSFUser', null=True, on_delete=models.CASCADE)

    SUBMITTED = 'submitted'
    REGISTERED = 'registered'
    APPROVED = 'approved'
    REJECTED = 'rejected'

    def __repr__(self):
        return ('<DraftRegistrationLog({self.action!r}, date={self.date!r}), '
                'user={self.user!r} '
                'with id {self._id!r}>').format(self=self)
Ejemplo n.º 13
0
class DraftRegistrationLog(ObjectIDMixin, BaseModel):
    """ Simple log to show status changes for DraftRegistrations
    Also, editable fields on registrations are logged.
    field - _id - primary key
    field - date - date of the action took place
    field - action - simple action to track what happened
    field - user - user who did the action
    """
    date = NonNaiveDateTimeField(default=timezone.now)
    action = models.CharField(max_length=255)
    draft = models.ForeignKey('DraftRegistration',
                              related_name='logs',
                              null=True,
                              blank=True,
                              on_delete=models.CASCADE)
    user = models.ForeignKey('OSFUser',
                             db_index=True,
                             null=True,
                             blank=True,
                             on_delete=models.CASCADE)
    params = DateTimeAwareJSONField(default=dict)

    SUBMITTED = 'submitted'
    REGISTERED = 'registered'
    APPROVED = 'approved'
    REJECTED = 'rejected'

    EDITED_TITLE = 'edit_title'
    EDITED_DESCRIPTION = 'edit_description'
    CATEGORY_UPDATED = 'category_updated'

    CONTRIB_ADDED = 'contributor_added'
    CONTRIB_REMOVED = 'contributor_removed'
    CONTRIB_REORDERED = 'contributors_reordered'
    PERMISSIONS_UPDATED = 'permissions_updated'

    MADE_CONTRIBUTOR_VISIBLE = 'made_contributor_visible'
    MADE_CONTRIBUTOR_INVISIBLE = 'made_contributor_invisible'

    AFFILIATED_INSTITUTION_ADDED = 'affiliated_institution_added'
    AFFILIATED_INSTITUTION_REMOVED = 'affiliated_institution_removed'

    CHANGED_LICENSE = 'license_changed'

    TAG_ADDED = 'tag_added'
    TAG_REMOVED = 'tag_removed'

    def __repr__(self):
        return ('<DraftRegistrationLog({self.action!r}, date={self.date!r}), '
                'user={self.user!r} '
                'with id {self._id!r}>').format(self=self)

    class Meta:
        ordering = ['-created']
        get_latest_by = 'created'
Ejemplo n.º 14
0
class Conference(ObjectIDMixin, BaseModel):
    #: Determines the email address for submission and the OSF url
    # Example: If endpoint is spsp2014, then submission email will be
    # [email protected] or [email protected] and the OSF url will
    # be osf.io/view/spsp2014
    endpoint = models.CharField(max_length=255, unique=True, db_index=True)
    #: Full name, e.g. "SPSP 2014"
    name = models.CharField(max_length=255)
    info_url = models.URLField(blank=True)
    logo_url = models.URLField(blank=True)
    location = models.CharField(max_length=2048, null=True, blank=True)
    start_date = NonNaiveDateTimeField(blank=True, null=True)
    end_date = NonNaiveDateTimeField(blank=True, null=True)
    is_meeting = models.BooleanField(default=True)
    active = models.BooleanField()
    admins = models.ManyToManyField('OSFUser')
    #: Whether to make submitted projects public
    public_projects = models.BooleanField(default=True)
    poster = models.BooleanField(default=True)
    talk = models.BooleanField(default=True)
    # field_names are used to customize the text on the conference page, the categories
    # of submissions, and the email adress to send material to.
    field_names = DateTimeAwareJSONField(default=get_default_field_names)

    # Cached number of submissions
    num_submissions = models.IntegerField(default=0)

    objects = ConferenceManager()

    def __repr__(self):
        return (
            '<Conference(endpoint={self.endpoint!r}, active={self.active})>'.
            format(self=self))

    @classmethod
    def get_by_endpoint(cls, endpoint, active):
        return cls.objects.get_by_endpoint(endpoint, active)

    class Meta:
        # custom permissions for use in the OSF Admin App
        permissions = (('view_conference',
                        'Can view conference details in the admin app.'), )
Ejemplo n.º 15
0
class NotificationDigest(ObjectIDMixin, BaseModel):
    user = models.ForeignKey('OSFUser', null=True, blank=True)
    timestamp = NonNaiveDateTimeField()
    send_type = models.CharField(max_length=50,
                                 db_index=True,
                                 validators=[
                                     validate_subscription_type,
                                 ])
    event = models.CharField(max_length=50)
    message = models.TextField()
    # TODO: Could this be a m2m with or without an order field?
    node_lineage = ArrayField(models.CharField(max_length=5))
Ejemplo n.º 16
0
class RegistrationBulkUploadJob(BaseModel):
    """Defines a database table that stores registration bulk upload jobs.
    """

    # The hash of the CSV template payload
    payload_hash = models.CharField(blank=False,
                                    null=False,
                                    unique=True,
                                    max_length=255)

    # The status/state of the bulk upload
    state = models.IntegerField(choices=[(state, state.name)
                                         for state in JobState],
                                default=JobState.PENDING)

    # The user / admin who started this bulk upload
    initiator = models.ForeignKey('OSFUser',
                                  blank=False,
                                  null=True,
                                  on_delete=models.CASCADE)

    # The registration provider this bulk upload targets
    provider = models.ForeignKey('RegistrationProvider',
                                 blank=False,
                                 null=True,
                                 on_delete=models.CASCADE)

    # The registration template this bulk upload uses
    schema = models.ForeignKey('RegistrationSchema',
                               blank=False,
                               null=True,
                               on_delete=models.CASCADE)

    # The date when success / failure emails are sent after the creation of registrations in this upload has been done
    email_sent = NonNaiveDateTimeField(blank=True, null=True)

    @classmethod
    def create(cls,
               payload_hash,
               initiator,
               provider,
               schema,
               state=JobState.PENDING,
               email_sent=None):
        upload = cls(
            payload_hash=payload_hash,
            state=state,
            initiator=initiator,
            provider=provider,
            schema=schema,
            email_sent=email_sent,
        )
        return upload
Ejemplo n.º 17
0
class Loggable(models.Model):

    last_logged = NonNaiveDateTimeField(db_index=True,
                                        null=True,
                                        blank=True,
                                        default=timezone.now)

    def add_log(self,
                action,
                params,
                auth,
                foreign_user=None,
                log_date=None,
                save=True,
                request=None):
        AbstractNode = apps.get_model('osf.AbstractNode')
        user = None
        if auth:
            user = auth.user
        elif request:
            user = request.user

        params['node'] = params.get('node') or params.get(
            'project') or self._id
        original_node = self if self._id == params[
            'node'] else AbstractNode.load(params.get('node'))

        log = NodeLog(action=action,
                      user=user,
                      foreign_user=foreign_user,
                      params=params,
                      node=self,
                      original_node=original_node)

        if log_date:
            log.date = log_date
        log.save()

        if self.logs.count() == 1:
            self.last_logged = log.date.replace(tzinfo=pytz.utc)
        else:
            self.last_logged = self.logs.first().date

        if save:
            self.save()
        if user and not self.is_collection:
            increment_user_activity_counters(user._primary_key, action,
                                             log.date.isoformat())

        return log

    class Meta:
        abstract = True
Ejemplo n.º 18
0
class PrivateLink(ObjectIDMixin, BaseModel):
    date_created = NonNaiveDateTimeField(default=timezone.now)
    key = models.CharField(max_length=512,
                           null=False,
                           unique=True,
                           blank=False)
    name = models.CharField(max_length=255, blank=True, null=True)
    is_deleted = models.BooleanField(default=False)
    anonymous = models.BooleanField(default=False)

    nodes = models.ManyToManyField('AbstractNode',
                                   related_name='private_links')
    creator = models.ForeignKey('OSFUser',
                                null=True,
                                blank=True,
                                on_delete=models.SET_NULL)

    @property
    def node_ids(self):
        return self.nodes.filter(is_deleted=False).values_list('guids___id',
                                                               flat=True)

    def node_scale(self, node):
        # node may be None if previous node's parent is deleted
        if node is None or node.parent_id not in self.node_ids:
            return -40
        else:
            offset = 20 if node.parent_node is not None else 0
            return offset + self.node_scale(node.parent_node)

    def to_json(self):
        return {
            'id':
            self._id,
            'date_created':
            iso8601format(self.date_created),
            'key':
            self.key,
            'name':
            sanitize.unescape_entities(self.name),
            'creator': {
                'fullname': self.creator.fullname,
                'url': self.creator.profile_url
            },
            'nodes': [{
                'title': x.title,
                'url': x.url,
                'scale': str(self.node_scale(x)) + 'px',
                'category': x.category
            } for x in self.nodes.filter(is_deleted=False)],
            'anonymous':
            self.anonymous
        }
Ejemplo n.º 19
0
class FileLog(ObjectIDMixin, BaseModel):

    objects = IncludeManager()

    DATE_FORMAT = '%m/%d/%Y %H:%M UTC'

    # Log action constants -- NOTE: templates stored in log_templates.mako
    CREATED_FROM = 'created_from'

    CHECKED_IN = 'checked_in'
    CHECKED_OUT = 'checked_out'

    FILE_TAG_ADDED = 'file_tag_added'
    FILE_TAG_REMOVED = 'file_tag_removed'

    FILE_MOVED = 'addon_file_moved'
    FILE_COPIED = 'addon_file_copied'
    FILE_RENAMED = 'addon_file_renamed'

    FOLDER_CREATED = 'folder_created'

    FILE_ADDED = 'file_added'
    FILE_UPDATED = 'file_updated'
    FILE_REMOVED = 'file_removed'
    FILE_RESTORED = 'file_restored'

    PREPRINT_FILE_UPDATED = 'preprint_file_updated'

    actions = ([CHECKED_IN, CHECKED_OUT, FILE_TAG_REMOVED, FILE_TAG_ADDED,
               FILE_MOVED, FILE_COPIED, FOLDER_CREATED, FILE_ADDED, FILE_UPDATED, FILE_REMOVED,
                FILE_RESTORED, PREPRINT_FILE_UPDATED, ] + list(sum([
                    config.actions for config in apps.get_app_configs() if config.name.startswith('addons.')
                ], tuple())))
    action_choices = [(action, action.upper()) for action in actions]
    project_id = models.CharField(max_length=255, null=True, blank=True, db_index=True)
    date = NonNaiveDateTimeField(db_index=True, null=True, blank=True, default=timezone.now)
    # TODO build action choices on the fly with the addon stuff
    action = models.CharField(max_length=255, db_index=True)  # , choices=action_choices)
    user = models.ForeignKey('OSFUser', related_name='filelogs', db_index=True, null=True, blank=True)
    path = models.CharField(max_length=255, db_index=True, null=True)

    def __unicode__(self):
        return ('({self.action!r}, user={self.user!r},, file={self.file!r}, params={self.params!r}) '
                'with id {self.id!r}').format(self=self)

    class Meta:
        ordering = ['-date']
        get_latest_by = 'date'
Ejemplo n.º 20
0
class BaseAddonSettings(ObjectIDMixin, BaseModel):
    is_deleted = models.BooleanField(default=False)
    deleted = NonNaiveDateTimeField(null=True, blank=True)

    class Meta:
        abstract = True

    @property
    def config(self):
        return self._meta.app_config

    @property
    def short_name(self):
        return self.config.short_name

    def delete(self, save=True):
        self.is_deleted = True
        self.deleted = timezone.now()
        self.on_delete()
        if save:
            self.save()

    def undelete(self, save=True):
        self.is_deleted = False
        self.deleted = None
        self.on_add()
        if save:
            self.save()

    def to_json(self, user):
        return {
            'addon_short_name': self.config.short_name,
            'addon_full_name': self.config.full_name,
        }

    #############
    # Callbacks #
    #############

    def on_add(self):
        """Called when the addon is added (or re-added) to the owner (User or Node)."""
        pass

    def on_delete(self):
        """Called when the addon is deleted from the owner (User or Node)."""
        pass
Ejemplo n.º 21
0
class Identifier(ObjectIDMixin, BaseModel):
    """A persistent identifier model for DOIs, ARKs, and the like."""

    # object to which the identifier points
    object_id = models.PositiveIntegerField(null=True, blank=True)
    content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE)
    referent = GenericForeignKey()
    # category: e.g. 'ark', 'doi'
    category = models.CharField(max_length=20)  # longest was 3, 8/19/2016
    # value: e.g. 'FK424601'
    value = models.CharField(max_length=50)  # longest was 21, 8/19/2016
    deleted = NonNaiveDateTimeField(blank=True, null=True)

    class Meta:
        unique_together = ('object_id', 'content_type', 'category')

    def remove(self, save=True):
        """Mark an identifier as deleted, which excludes it from being returned in get_identifier"""
        self.deleted = timezone.now()
        if save:
            self.save()
Ejemplo n.º 22
0
class Retraction(EmailApprovableSanction):
    """
    Retraction object for public registrations.
    Externally (specifically in user-facing language) retractions should be referred to as "Withdrawals", i.e.
    "Retract Registration" -> "Withdraw Registration", "Retracted" -> "Withdrawn", etc.
    """
    DISPLAY_NAME = 'Retraction'
    SHORT_NAME = 'retraction'

    AUTHORIZER_NOTIFY_EMAIL_TEMPLATE = mails.PENDING_RETRACTION_ADMIN
    NON_AUTHORIZER_NOTIFY_EMAIL_TEMPLATE = mails.PENDING_RETRACTION_NON_ADMIN

    VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
    APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
    REJECT_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'

    initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE)
    justification = models.CharField(max_length=2048, null=True, blank=True)
    date_retracted = NonNaiveDateTimeField(null=True, blank=True)

    def _get_registration(self):
        return self.registrations.first()

    def _view_url_context(self, user_id, node):
        registration = self.registrations.first()
        return {
            'node_id': registration._id
        }

    def _approval_url_context(self, user_id):
        user_approval_state = self.approval_state.get(user_id, {})
        approval_token = user_approval_state.get('approval_token')
        if approval_token:
            root_registration = self.registrations.first()
            node_id = user_approval_state.get('node_id', root_registration._id)
            return {
                'node_id': node_id,
                'token': approval_token,
            }

    def _rejection_url_context(self, user_id):
        user_approval_state = self.approval_state.get(user_id, {})
        rejection_token = user_approval_state.get('rejection_token')
        if rejection_token:
            Registration = apps.get_model('osf.Registration')
            node_id = user_approval_state.get('node_id', None)
            registration = Registration.objects.select_related(
                'registered_from'
            ).get(
                guids___id=node_id, guids___id__isnull=False
            ) if node_id else self.registrations.first()

            return {
                'node_id': registration.registered_from._id,
                'token': rejection_token,
            }

    def _email_template_context(self, user, node, is_authorizer=False, urls=None):
        urls = urls or self.stashed_urls.get(user._id, {})
        registration_link = urls.get('view', self._view_url(user._id, node))
        if is_authorizer:
            approval_link = urls.get('approve', '')
            disapproval_link = urls.get('reject', '')
            approval_time_span = osf_settings.RETRACTION_PENDING_TIME.days * 24

            return {
                'is_initiator': self.initiated_by == user,
                'initiated_by': self.initiated_by.fullname,
                'project_name': self.registrations.filter().values_list('title', flat=True).get(),
                'registration_link': registration_link,
                'approval_link': approval_link,
                'disapproval_link': disapproval_link,
                'approval_time_span': approval_time_span,
            }
        else:
            return {
                'initiated_by': self.initiated_by.fullname,
                'registration_link': registration_link,
            }

    def _on_reject(self, user):
        Registration = apps.get_model('osf.Registration')
        NodeLog = apps.get_model('osf.NodeLog')

        parent_registration = Registration.objects.get(retraction=self)
        parent_registration.registered_from.add_log(
            action=NodeLog.RETRACTION_CANCELLED,
            params={
                'node': parent_registration.registered_from._id,
                'registration': parent_registration._id,
                'retraction_id': self._id,
            },
            auth=Auth(user),
            save=True,
        )

    def _on_complete(self, user):
        Registration = apps.get_model('osf.Registration')
        NodeLog = apps.get_model('osf.NodeLog')

        self.date_retracted = timezone.now()
        self.save()

        parent_registration = Registration.objects.get(retraction=self)
        parent_registration.registered_from.add_log(
            action=NodeLog.RETRACTION_APPROVED,
            params={
                'node': parent_registration.registered_from._id,
                'retraction_id': self._id,
                'registration': parent_registration._id
            },
            auth=Auth(self.initiated_by),
        )
        # Remove any embargoes associated with the registration
        if parent_registration.embargo_end_date or parent_registration.is_pending_embargo:
            parent_registration.embargo.state = self.REJECTED
            parent_registration.registered_from.add_log(
                action=NodeLog.EMBARGO_CANCELLED,
                params={
                    'node': parent_registration.registered_from._id,
                    'registration': parent_registration._id,
                    'embargo_id': parent_registration.embargo._id,
                },
                auth=Auth(self.initiated_by),
            )
            parent_registration.embargo.save()
        # Ensure retracted registration is public
        # Pass auth=None because the registration initiator may not be
        # an admin on components (component admins had the opportunity
        # to disapprove the retraction by this point)
        for node in parent_registration.node_and_primary_descendants():
            node.set_privacy('public', auth=None, save=True, log=False)
            node.update_search()
        # force a save before sending data to share or retraction will not be updated
        self.save()
        project_tasks.update_node_share(parent_registration)

    def approve_retraction(self, user, token):
        self.approve(user, token)

    def disapprove_retraction(self, user, token):
        self.reject(user, token)
Ejemplo n.º 23
0
class Retraction(EmailApprovableSanction):
    """
    Retraction object for public registrations.
    Externally (specifically in user-facing language) retractions should be referred to as "Withdrawals", i.e.
    "Retract Registration" -> "Withdraw Registration", "Retracted" -> "Withdrawn", etc.
    """
    SANCTION_TYPE = SanctionTypes.RETRACTION
    DISPLAY_NAME = 'Retraction'
    SHORT_NAME = 'retraction'

    AUTHORIZER_NOTIFY_EMAIL_TEMPLATE = mails.PENDING_RETRACTION_ADMIN
    NON_AUTHORIZER_NOTIFY_EMAIL_TEMPLATE = mails.PENDING_RETRACTION_NON_ADMIN

    VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
    APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
    REJECT_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'

    initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                     null=True,
                                     blank=True,
                                     on_delete=models.CASCADE)
    justification = models.CharField(max_length=2048, null=True, blank=True)
    date_retracted = NonNaiveDateTimeField(null=True, blank=True)

    def _get_registration(self):
        Registration = apps.get_model('osf.Registration')
        parent_registration = Registration.objects.get(retraction=self)

        return parent_registration

    def _view_url_context(self, user_id, node):
        registration = self.registrations.first() or node
        return {'node_id': registration._id}

    def _approval_url_context(self, user_id):
        user_approval_state = self.approval_state.get(user_id, {})
        approval_token = user_approval_state.get('approval_token')
        if approval_token:
            root_registration = self.registrations.first()
            node_id = user_approval_state.get('node_id', root_registration._id)
            return {
                'node_id': node_id,
                'token': approval_token,
            }

    def _rejection_url_context(self, user_id):
        user_approval_state = self.approval_state.get(user_id, {})
        rejection_token = user_approval_state.get('rejection_token')
        if rejection_token:
            Registration = apps.get_model('osf.Registration')
            node_id = user_approval_state.get('node_id', None)
            registration = Registration.objects.select_related(
                'registered_from').get(
                    guids___id=node_id, guids___id__isnull=False
                ) if node_id else self.registrations.first()

            return {
                'node_id': registration.registered_from._id,
                'token': rejection_token,
            }

    def _email_template_context(self,
                                user,
                                node,
                                is_authorizer=False,
                                urls=None):
        urls = urls or self.stashed_urls.get(user._id, {})
        registration_link = urls.get('view', self._view_url(user._id, node))
        approval_time_span = osf_settings.RETRACTION_PENDING_TIME.days * 24
        if is_authorizer:
            approval_link = urls.get('approve', '')
            disapproval_link = urls.get('reject', '')

            return {
                'is_initiator':
                self.initiated_by == user,
                'is_moderated':
                self.is_moderated,
                'reviewable':
                self._get_registration(),
                'initiated_by':
                self.initiated_by.fullname,
                'project_name':
                self.registrations.filter().values_list('title',
                                                        flat=True).get(),
                'registration_link':
                registration_link,
                'approval_link':
                approval_link,
                'disapproval_link':
                disapproval_link,
                'approval_time_span':
                approval_time_span,
            }
        else:
            return {
                'initiated_by': self.initiated_by.fullname,
                'registration_link': registration_link,
                'is_moderated': self.is_moderated,
                'reviewable': self._get_registration(),
                'approval_time_span': approval_time_span,
            }

    def _on_reject(self, event_data):
        user = event_data.kwargs.get('user')
        if user is None and event_data.args:
            user = event_data.args[0]

        NodeLog = apps.get_model('osf.NodeLog')
        parent_registration = self.target_registration
        parent_registration.registered_from.add_log(
            action=NodeLog.RETRACTION_CANCELLED,
            params={
                'node': parent_registration.registered_from._id,
                'registration': parent_registration._id,
                'retraction_id': self._id,
            },
            auth=Auth(user),
            save=True,
        )

    def _on_complete(self, event_data):
        super()._on_complete(event_data)
        self.date_retracted = timezone.now()
        registration = self.target_registration
        registration.withdraw()
        self.save()

    def approve_retraction(self, user, token):
        '''Test function'''
        self.approve(user=user, token=token)

    def disapprove_retraction(self, user, token):
        '''Test function'''
        self.reject(user=user, token=token)
Ejemplo n.º 24
0
class Sanction(ObjectIDMixin, BaseModel):
    """Sanction class is a generic way to track approval states"""
    # Neither approved not cancelled
    UNAPPROVED = ApprovalStates.UNAPPROVED.db_name
    # Has approval
    APPROVED = ApprovalStates.APPROVED.db_name
    # Rejected by at least one contributor
    REJECTED = ApprovalStates.REJECTED.db_name
    # Embargo has been completed
    COMPLETED = ApprovalStates.COMPLETED.db_name
    # Approved by admins but pending moderator approval/rejection
    PENDING_MODERATION = ApprovalStates.PENDING_MODERATION.db_name
    # Rejected by a moderator
    MODERATOR_REJECTED = ApprovalStates.MODERATOR_REJECTED.db_name

    SANCTION_TYPE = SanctionTypes.UNDEFINED
    DISPLAY_NAME = 'Sanction'
    # SHORT_NAME must correspond with the associated foreign field to query against,
    # e.g. Node.find_one(Q(sanction.SHORT_NAME, 'eq', sanction))
    SHORT_NAME = 'sanction'

    ACTION_NOT_AUTHORIZED_MESSAGE = 'This user is not authorized to {ACTION} this {DISPLAY_NAME}'
    APPROVAL_INVALID_TOKEN_MESSAGE = 'Invalid approval token provided for this {DISPLAY_NAME}.'
    REJECTION_INVALID_TOKEN_MESSAGE = 'Invalid rejection token provided for this {DISPLAY_NAME}.'

    # Controls whether or not the Sanction needs unanimous approval or just a single approval
    ANY = 'any'
    UNANIMOUS = 'unanimous'
    mode = UNANIMOUS

    # Sanction subclasses must have an initiated_by field
    # initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)

    # Expanded: Dictionary field mapping admin IDs their approval status and relevant tokens:
    # {
    #   'b3k97': {
    #     'has_approved': False,
    #     'approval_token': 'Pew7wj1Puf7DENUPFPnXSwa1rf3xPN',
    #     'rejection_token': 'TwozClTFOic2PYxHDStby94bCQMwJy'}
    # }
    approval_state = DateTimeAwareJSONField(default=dict, blank=True)

    # Expiration date-- Sanctions in the UNAPPROVED state that are older than their end_date
    # are automatically made ACTIVE by a daily cron job
    # Use end_date=None for a non-expiring Sanction
    end_date = NonNaiveDateTimeField(null=True, blank=True, default=None)
    initiation_date = NonNaiveDateTimeField(default=timezone.now,
                                            null=True,
                                            blank=True)

    state = models.CharField(choices=ApprovalStates.char_field_choices(),
                             default=UNAPPROVED,
                             max_length=255)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.approvals_machine = ApprovalsMachine(
            model=self,
            state_property_name='approval_stage',
            active_state=self.approval_stage)

    def __str__(self):
        return f'<{self.__class__.__name__}(end_date={self.end_date!r}) with _id {self._id!r}>'

    @property
    def is_pending_approval(self):
        '''The sanction is awaiting admin approval.'''
        return self.approval_stage is ApprovalStates.UNAPPROVED

    @property
    def is_approved(self):
        '''The sanction has received all required admin and moderator approvals.'''
        return self.approval_stage is ApprovalStates.APPROVED

    @property
    def is_rejected(self):
        '''The sanction has been rejected by either an admin or a moderator.'''
        rejected_states = [
            ApprovalStates.REJECTED, ApprovalStates.MODERATOR_REJECTED
        ]
        return self.approval_stage in rejected_states

    @property
    def is_moderated(self):
        return self.target_registration.is_moderated

    @property
    def approval_stage(self):
        return ApprovalStates.from_db_name(self.state)

    @approval_stage.setter
    def approval_stage(self, state):
        self.state = state.db_name

    @property
    def target_registration(self):
        return self._get_registration()

    @property
    def revisable(self):
        '''Controls state machine flow on a 'reject' trigger.

        True -> IN_PROGRESS
        False -> [MODERATOR_]REJECTED

        Sanctions do not represent a revisable entity, so return False
        '''
        return False

    # The Sanction object will also inherit the following functions from the SanctionStateMachine:
    #
    # approve(self, user, token)
    # accept(self, user, token)
    # reject(self, user, token)
    #
    # Overriding these functions will divorce the offending Sanction class from that trigger's
    # functionality on the state machine.

    def _get_registration(self):
        """Get the Registration that is waiting on this sanction."""
        raise NotImplementedError(
            'Sanction subclasses must implement a #_get_registration method')

    def _on_approve(self, event_data):
        """Callback for individual admin approval of a sanction.

        Invoked by state machine as the last step of an 'approve' trigger

        :param EventData event_data: An EventData object from transitions.core
            contains information about the active state transition and arbitrary args and kwargs
        """
        raise NotImplementedError(
            'Sanction subclasses must implement an #_on_approve method')

    def _on_reject(self, event_data):
        """Callback for rejection of a Sanction.

        Invoked by state machine as the last step of a 'reject' trigger

        :param EventData event_data: An EventData object from transitions.core
            contains information about the active state transition and arbitrary args and kwargs
        """
        raise NotImplementedError(
            'Sanction subclasses must implement an #_on_reject method')

    def _on_complete(self, user):
        """Callback for when a Sanction is fully approved.

        Invoked by state machine as the last step of an 'accept' trigger

        :param EventData event_data: An EventData object from transitions.core
            contains information about the active state transition and arbitrary args and kwargs
        """
        raise NotImplementedError(
            'Sanction subclasses must implement an #_on_complete method')

    def forcibly_reject(self):
        self.state = Sanction.REJECTED

    def _save_transition(self, event_data):
        """Record the effects of a state transition in the database."""
        self.save()
        new_state = event_data.transition.dest
        # No need to update registration state with no sanction state change
        if new_state is None:
            return

        user = event_data.kwargs.get('user')
        if user is None and event_data.args:
            user = event_data.args[0]
        comment = event_data.kwargs.get('comment', '')
        self.target_registration.update_moderation_state(initiated_by=user,
                                                         comment=comment)

    class Meta:
        abstract = True
Ejemplo n.º 25
0
class PreprintService(DirtyFieldsMixin, GuidMixin, IdentifierMixin,
                      ReviewableMixin, BaseModel):
    date_created = NonNaiveDateTimeField(auto_now_add=True)
    date_modified = NonNaiveDateTimeField(auto_now=True)
    provider = models.ForeignKey('osf.PreprintProvider',
                                 on_delete=models.SET_NULL,
                                 related_name='preprint_services',
                                 null=True,
                                 blank=True,
                                 db_index=True)
    node = models.ForeignKey('osf.AbstractNode',
                             on_delete=models.SET_NULL,
                             related_name='preprints',
                             null=True,
                             blank=True,
                             db_index=True)
    is_published = models.BooleanField(default=False, db_index=True)
    date_published = NonNaiveDateTimeField(null=True, blank=True)
    original_publication_date = NonNaiveDateTimeField(null=True, blank=True)
    license = models.ForeignKey('osf.NodeLicenseRecord',
                                on_delete=models.SET_NULL,
                                null=True,
                                blank=True)

    subjects = models.ManyToManyField(blank=True,
                                      to='osf.Subject',
                                      related_name='preprint_services')

    identifiers = GenericRelation(Identifier,
                                  related_query_name='preprintservices')

    class Meta:
        unique_together = ('node', 'provider')
        permissions = (
            ('view_preprintservice',
             'Can view preprint service details in the admin app.'), )

    def __unicode__(self):
        return '{} preprint (guid={}) of {}'.format(
            'published' if self.is_published else 'unpublished', self._id,
            self.node.__unicode__())

    @property
    def verified_publishable(self):
        return self.is_published and self.node.is_preprint and not self.node.is_deleted

    @property
    def primary_file(self):
        if not self.node:
            return
        return self.node.preprint_file

    @property
    def article_doi(self):
        if not self.node:
            return
        return self.node.preprint_article_doi

    @property
    def preprint_doi(self):
        return self.get_identifier_value('doi')

    @property
    def is_preprint_orphan(self):
        if not self.node:
            return
        return self.node.is_preprint_orphan

    @cached_property
    def subject_hierarchy(self):
        return [
            s.object_hierarchy
            for s in self.subjects.exclude(children__in=self.subjects.all())
        ]

    @property
    def deep_url(self):
        # Required for GUID routing
        return '/preprints/{}/'.format(self._primary_key)

    @property
    def url(self):
        if (self.provider.domain_redirect_enabled
                and self.provider.domain) or self.provider._id == 'osf':
            return '/{}/'.format(self._id)

        return '/preprints/{}/{}/'.format(self.provider._id, self._id)

    @property
    def absolute_url(self):
        return urlparse.urljoin(
            self.provider.domain if self.provider.domain_redirect_enabled else
            settings.DOMAIN, self.url)

    @property
    def absolute_api_v2_url(self):
        path = '/preprints/{}/'.format(self._id)
        return api_v2_url(path)

    def has_permission(self, *args, **kwargs):
        return self.node.has_permission(*args, **kwargs)

    def get_subjects(self):
        ret = []
        for subj_list in self.subject_hierarchy:
            subj_hierarchy = []
            for subj in subj_list:
                if subj:
                    subj_hierarchy += ({'id': subj._id, 'text': subj.text}, )
            if subj_hierarchy:
                ret.append(subj_hierarchy)
        return ret

    def set_subjects(self, preprint_subjects, auth):
        if not self.node.has_permission(auth.user, ADMIN):
            raise PermissionsError(
                'Only admins can change a preprint\'s subjects.')

        old_subjects = list(self.subjects.values_list('id', flat=True))
        self.subjects.clear()
        for subj_list in preprint_subjects:
            subj_hierarchy = []
            for s in subj_list:
                subj_hierarchy.append(s)
            if subj_hierarchy:
                validate_subject_hierarchy(subj_hierarchy)
                for s_id in subj_hierarchy:
                    self.subjects.add(Subject.load(s_id))

        self.save(old_subjects=old_subjects)

    def set_primary_file(self, preprint_file, auth, save=False):
        if not self.node.has_permission(auth.user, ADMIN):
            raise PermissionsError(
                'Only admins can change a preprint\'s primary file.')

        if preprint_file.node != self.node or preprint_file.provider != 'osfstorage':
            raise ValueError(
                'This file is not a valid primary file for this preprint.')

        existing_file = self.node.preprint_file
        self.node.preprint_file = preprint_file

        # only log if updating the preprint file, not adding for the first time
        if existing_file:
            self.node.add_log(action=NodeLog.PREPRINT_FILE_UPDATED,
                              params={'preprint': self._id},
                              auth=auth,
                              save=False)

        if save:
            self.save()
            self.node.save()

    def set_published(self, published, auth, save=False):
        if not self.node.has_permission(auth.user, ADMIN):
            raise PermissionsError('Only admins can publish a preprint.')

        if self.is_published and not published:
            raise ValueError('Cannot unpublish preprint.')

        self.is_published = published

        if published:
            if not (self.node.preprint_file
                    and self.node.preprint_file.node == self.node):
                raise ValueError(
                    'Preprint node is not a valid preprint; cannot publish.')
            if not self.provider:
                raise ValueError(
                    'Preprint provider not specified; cannot publish.')
            if not self.subjects.exists():
                raise ValueError(
                    'Preprint must have at least one subject to be published.')
            self.date_published = timezone.now()
            self.node._has_abandoned_preprint = False

            # In case this provider is ever set up to use a reviews workflow, put this preprint in a sensible state
            self.reviews_state = States.ACCEPTED.value
            self.date_last_transitioned = self.date_published

            self.node.add_log(
                action=NodeLog.PREPRINT_INITIATED,
                params={'preprint': self._id},
                auth=auth,
                save=False,
            )

            if not self.node.is_public:
                self.node.set_privacy(self.node.PUBLIC, auth=None, log=True)

            # This should be called after all fields for EZID metadta have been set
            enqueue_postcommit_task(get_and_set_preprint_identifiers, (),
                                    {'preprint': self},
                                    celery=True)

            self._send_preprint_confirmation(auth)

        if save:
            self.node.save()
            self.save()

    def set_preprint_license(self, license_detail, auth, save=False):
        license_record, license_changed = set_license(self,
                                                      license_detail,
                                                      auth,
                                                      node_type='preprint')

        if license_changed:
            self.node.add_log(action=NodeLog.PREPRINT_LICENSE_UPDATED,
                              params={
                                  'preprint': self._id,
                                  'new_license':
                                  license_record.node_license.name
                              },
                              auth=auth,
                              save=False)

        if save:
            self.save()

    def set_identifier_values(self, doi, ark, save=False):
        self.set_identifier_value('doi', doi)
        self.set_identifier_value('ark', ark)

        if save:
            self.save()

    def save(self, *args, **kwargs):
        first_save = not bool(self.pk)
        saved_fields = self.get_dirty_fields() or []
        old_subjects = kwargs.pop('old_subjects', [])
        ret = super(PreprintService, self).save(*args, **kwargs)

        if (not first_save
                and 'is_published' in saved_fields) or self.is_published:
            enqueue_postcommit_task(on_preprint_updated, (self._id, ),
                                    {'old_subjects': old_subjects},
                                    celery=True)
        return ret

    def _send_preprint_confirmation(self, auth):
        # Send creator confirmation email
        if self.provider._id == 'osf':
            email_template = getattr(mails, 'PREPRINT_CONFIRMATION_DEFAULT')
        else:
            email_template = getattr(mails, 'PREPRINT_CONFIRMATION_BRANDED')(
                self.provider)

        mails.send_mail(auth.user.username,
                        email_template,
                        user=auth.user,
                        node=self.node,
                        preprint=self)
Ejemplo n.º 26
0
class DraftRegistration(ObjectIDMixin, BaseModel):
    URL_TEMPLATE = settings.DOMAIN + 'project/{node_id}/drafts/{draft_id}'

    datetime_initiated = NonNaiveDateTimeField(auto_now_add=True)
    datetime_updated = NonNaiveDateTimeField(auto_now=True)
    deleted = NonNaiveDateTimeField(null=True, blank=True)

    # Original Node a draft registration is associated with
    branched_from = models.ForeignKey('Node',
                                      related_name='registered_draft',
                                      null=True,
                                      on_delete=models.CASCADE)

    initiator = models.ForeignKey('OSFUser',
                                  null=True,
                                  on_delete=models.CASCADE)
    provider = models.ForeignKey('RegistrationProvider',
                                 related_name='draft_registrations',
                                 null=True)

    # Dictionary field mapping question id to a question's comments and answer
    # {
    #   <qid>: {
    #     'comments': [{
    #       'user': {
    #         'id': <uid>,
    #         'name': <name>
    #       },
    #       value: <value>,
    #       lastModified: <datetime>
    #     }],
    #     'value': <value>
    #   }
    # }
    registration_metadata = DateTimeAwareJSONField(default=dict, blank=True)
    registration_schema = models.ForeignKey('RegistrationSchema',
                                            null=True,
                                            on_delete=models.CASCADE)
    registered_node = models.ForeignKey('Registration',
                                        null=True,
                                        blank=True,
                                        related_name='draft_registration',
                                        on_delete=models.CASCADE)

    approval = models.ForeignKey('DraftRegistrationApproval',
                                 null=True,
                                 blank=True,
                                 on_delete=models.CASCADE)

    # Dictionary field mapping extra fields defined in the RegistrationSchema.schema to their
    # values. Defaults should be provided in the schema (e.g. 'paymentSent': false),
    # and these values are added to the DraftRegistration
    # TODO: Use "FIELD_ALIASES"?
    _metaschema_flags = DateTimeAwareJSONField(default=dict, blank=True)
    notes = models.TextField(blank=True)

    def __repr__(self):
        return ('<DraftRegistration(branched_from={self.branched_from!r}) '
                'with id {self._id!r}>').format(self=self)

    # lazily set flags
    @property
    def flags(self):
        if not self._metaschema_flags:
            self._metaschema_flags = {}
        meta_schema = self.registration_schema
        if meta_schema:
            schema = meta_schema.schema
            flags = schema.get('flags', {})
            dirty = False
            for flag, value in flags.items():
                if flag not in self._metaschema_flags:
                    self._metaschema_flags[flag] = value
                    dirty = True
            if dirty:
                self.save()
        return self._metaschema_flags

    @flags.setter
    def flags(self, flags):
        self._metaschema_flags.update(flags)

    @property
    def url(self):
        return self.URL_TEMPLATE.format(node_id=self.branched_from._id,
                                        draft_id=self._id)

    @property
    def absolute_url(self):
        return urlparse.urljoin(settings.DOMAIN, self.url)

    @property
    def absolute_api_v2_url(self):
        node = self.branched_from
        path = '/nodes/{}/draft_registrations/{}/'.format(node._id, self._id)
        return api_v2_url(path)

    # used by django and DRF
    def get_absolute_url(self):
        return self.absolute_api_v2_url

    @property
    def requires_approval(self):
        return self.registration_schema.requires_approval

    @property
    def is_pending_review(self):
        return self.approval.is_pending_approval if (
            self.requires_approval and self.approval) else False

    @property
    def is_approved(self):
        if self.requires_approval:
            if not self.approval:
                return bool(self.registered_node)
            else:
                return self.approval.is_approved
        else:
            return False

    @property
    def is_rejected(self):
        if self.requires_approval:
            if not self.approval:
                return False
            else:
                return self.approval.is_rejected
        else:
            return False

    @property
    def status_logs(self):
        """ List of logs associated with this node"""
        return self.logs.all().order_by('date')

    @classmethod
    def create_from_node(cls, node, user, schema, data=None, provider=None):
        if not provider:
            provider = RegistrationProvider.load('osf')
        draft = cls(
            initiator=user,
            branched_from=node,
            registration_schema=schema,
            registration_metadata=data or {},
            provider=provider,
        )
        draft.save()
        return draft

    def update_metadata(self, metadata):
        changes = []
        # Prevent comments on approved drafts
        if not self.is_approved:
            for question_id, value in metadata.items():
                old_value = self.registration_metadata.get(question_id)
                if old_value:
                    old_comments = {
                        comment['created']: comment
                        for comment in old_value.get('comments', [])
                    }
                    new_comments = {
                        comment['created']: comment
                        for comment in value.get('comments', [])
                    }
                    old_comments.update(new_comments)
                    metadata[question_id]['comments'] = sorted(
                        old_comments.values(), key=lambda c: c['created'])
                    if old_value.get('value') != value.get('value'):
                        changes.append(question_id)
                else:
                    changes.append(question_id)
        self.registration_metadata.update(metadata)
        return changes

    def submit_for_review(self, initiated_by, meta, save=False):
        approval = DraftRegistrationApproval(meta=meta)
        approval.save()
        self.approval = approval
        self.add_status_log(initiated_by, DraftRegistrationLog.SUBMITTED)
        if save:
            self.save()

    def register(self, auth, save=False, child_ids=None):
        node = self.branched_from

        # Create the registration
        register = node.register_node(schema=self.registration_schema,
                                      auth=auth,
                                      data=self.registration_metadata,
                                      child_ids=child_ids,
                                      provider=self.provider)
        self.registered_node = register
        self.add_status_log(auth.user, DraftRegistrationLog.REGISTERED)
        if save:
            self.save()
        return register

    def approve(self, user):
        self.approval.approve(user)
        self.refresh_from_db()
        self.add_status_log(user, DraftRegistrationLog.APPROVED)
        self.approval.save()

    def reject(self, user):
        self.approval.reject(user)
        self.add_status_log(user, DraftRegistrationLog.REJECTED)
        self.approval.save()

    def add_status_log(self, user, action):
        log = DraftRegistrationLog(action=action, user=user, draft=self)
        log.save()

    def validate_metadata(self, *args, **kwargs):
        """
        Validates draft's metadata
        """
        return self.registration_schema.validate_metadata(*args, **kwargs)
Ejemplo n.º 27
0
class Sanction(ObjectIDMixin, BaseModel):
    """Sanction class is a generic way to track approval states"""
    # Neither approved not cancelled
    UNAPPROVED = 'unapproved'
    # Has approval
    APPROVED = 'approved'
    # Rejected by at least one person
    REJECTED = 'rejected'
    # Embargo has been completed
    COMPLETED = 'completed'

    STATE_CHOICES = ((UNAPPROVED, UNAPPROVED.title()),
                     (APPROVED, APPROVED.title()),
                     (REJECTED, REJECTED.title()),
                     (COMPLETED, COMPLETED.title()),)

    DISPLAY_NAME = 'Sanction'
    # SHORT_NAME must correspond with the associated foreign field to query against,
    # e.g. Node.find_one(Q(sanction.SHORT_NAME, 'eq', sanction))
    SHORT_NAME = 'sanction'

    APPROVAL_NOT_AUTHORIZED_MESSAGE = 'This user is not authorized to approve this {DISPLAY_NAME}'
    APPROVAL_INVALID_TOKEN_MESSAGE = 'Invalid approval token provided for this {DISPLAY_NAME}.'
    REJECTION_NOT_AUTHORIZED_MESSAEGE = 'This user is not authorized to reject this {DISPLAY_NAME}'
    REJECTION_INVALID_TOKEN_MESSAGE = 'Invalid rejection token provided for this {DISPLAY_NAME}.'

    # Controls whether or not the Sanction needs unanimous approval or just a single approval
    ANY = 'any'
    UNANIMOUS = 'unanimous'
    mode = UNANIMOUS

    # Sanction subclasses must have an initiated_by field
    # initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)

    # Expanded: Dictionary field mapping admin IDs their approval status and relevant tokens:
    # {
    #   'b3k97': {
    #     'has_approved': False,
    #     'approval_token': 'Pew7wj1Puf7DENUPFPnXSwa1rf3xPN',
    #     'rejection_token': 'TwozClTFOic2PYxHDStby94bCQMwJy'}
    # }
    approval_state = DateTimeAwareJSONField(default=dict, blank=True)

    # Expiration date-- Sanctions in the UNAPPROVED state that are older than their end_date
    # are automatically made ACTIVE by a daily cron job
    # Use end_date=None for a non-expiring Sanction
    end_date = NonNaiveDateTimeField(null=True, blank=True, default=None)
    initiation_date = NonNaiveDateTimeField(default=timezone.now, null=True, blank=True)

    state = models.CharField(choices=STATE_CHOICES,
                             default=UNAPPROVED,
                             max_length=255)

    def __repr__(self):
        return '<{self.__class__.__name__}(end_date={self.end_date!r}) with _id {self._id!r}>'.format(
            self=self)

    @property
    def is_pending_approval(self):
        return self.state == Sanction.UNAPPROVED

    @property
    def is_approved(self):
        return self.state == Sanction.APPROVED

    @property
    def is_rejected(self):
        return self.state == Sanction.REJECTED

    def approve(self, user):
        raise NotImplementedError('Sanction subclasses must implement an approve method.')

    def reject(self, user):
        raise NotImplementedError('Sanction subclasses must implement an approve method.')

    def _on_reject(self, user):
        """Callback for rejection of a Sanction

        :param User user:
        """
        raise NotImplementedError(
            'Sanction subclasses must implement an #_on_reject method')

    def _on_complete(self, user):
        """Callback for when a Sanction has approval and enters the ACTIVE state

        :param User user:
        """
        raise NotImplementedError(
            'Sanction subclasses must implement an #_on_complete method')

    def forcibly_reject(self):
        self.state = Sanction.REJECTED

    class Meta:
        abstract = True
Ejemplo n.º 28
0
class Registration(AbstractNode):

    WRITABLE_WHITELIST = [
        'article_doi',
        'description',
        'is_public',
        'node_license',
    ]

    article_doi = models.CharField(max_length=128,
                                   validators=[validate_doi],
                                   null=True,
                                   blank=True)
    provider = models.ForeignKey('RegistrationProvider',
                                 related_name='registrations',
                                 null=True)
    registered_date = NonNaiveDateTimeField(db_index=True,
                                            null=True,
                                            blank=True)
    registered_user = models.ForeignKey(OSFUser,
                                        related_name='related_to',
                                        on_delete=models.SET_NULL,
                                        null=True,
                                        blank=True)

    registered_schema = models.ManyToManyField(RegistrationSchema)

    registered_meta = DateTimeAwareJSONField(default=dict, blank=True)
    registered_from = models.ForeignKey('self',
                                        related_name='registrations',
                                        on_delete=models.SET_NULL,
                                        null=True,
                                        blank=True)
    # Sanctions
    registration_approval = models.ForeignKey('RegistrationApproval',
                                              related_name='registrations',
                                              null=True,
                                              blank=True,
                                              on_delete=models.SET_NULL)
    retraction = models.ForeignKey('Retraction',
                                   related_name='registrations',
                                   null=True,
                                   blank=True,
                                   on_delete=models.SET_NULL)
    embargo = models.ForeignKey('Embargo',
                                related_name='registrations',
                                null=True,
                                blank=True,
                                on_delete=models.SET_NULL)
    embargo_termination_approval = models.ForeignKey(
        'EmbargoTerminationApproval',
        related_name='registrations',
        null=True,
        blank=True,
        on_delete=models.SET_NULL)

    @staticmethod
    def find_failed_registrations():
        expired_if_before = timezone.now() - settings.ARCHIVE_TIMEOUT_TIMEDELTA
        node_id_list = ArchiveJob.objects.filter(
            sent=False,
            datetime_initiated__lt=expired_if_before,
            status=ARCHIVER_INITIATED).values_list('dst_node', flat=True)
        root_nodes_id = AbstractNode.objects.filter(
            id__in=node_id_list).values_list('root', flat=True).distinct()
        stuck_regs = AbstractNode.objects.filter(id__in=root_nodes_id,
                                                 is_deleted=False)
        return stuck_regs

    @property
    def registered_schema_id(self):
        if self.registered_schema.exists():
            return self.registered_schema.first()._id
        return None

    @property
    def is_registration(self):
        """For v1 compat."""
        return True

    @property
    def is_stuck_registration(self):
        return self in self.find_failed_registrations()

    @property
    def is_collection(self):
        """For v1 compat."""
        return False

    @property
    def archive_job(self):
        return self.archive_jobs.first() if self.archive_jobs.count() else None

    @property
    def sanction(self):
        root = self._dirty_root
        sanction = (root.embargo_termination_approval or root.retraction
                    or root.embargo or root.registration_approval)
        if sanction:
            return sanction
        else:
            return None

    @property
    def is_registration_approved(self):
        root = self._dirty_root
        if root.registration_approval is None:
            return False
        return root.registration_approval.is_approved

    @property
    def is_pending_embargo(self):
        root = self._dirty_root
        if root.embargo is None:
            return False
        return root.embargo.is_pending_approval

    @property
    def is_pending_embargo_for_existing_registration(self):
        """ Returns True if Node has an Embargo pending approval for an
        existing registrations. This is used specifically to ensure
        registrations pre-dating the Embargo feature do not get deleted if
        their respective Embargo request is rejected.
        """
        root = self._dirty_root
        if root.embargo is None:
            return False
        return root.embargo.pending_registration

    @property
    def is_retracted(self):
        root = self._dirty_root
        if root.retraction is None:
            return False
        return root.retraction.is_approved

    @property
    def is_pending_registration(self):
        root = self._dirty_root
        if root.registration_approval is None:
            return False
        return root.registration_approval.is_pending_approval

    @property
    def is_pending_retraction(self):
        root = self._dirty_root
        if root.retraction is None:
            return False
        return root.retraction.is_pending_approval

    @property
    def is_pending_embargo_termination(self):
        root = self._dirty_root
        if root.embargo_termination_approval is None:
            return False
        return root.embargo_termination_approval.is_pending_approval

    @property
    def is_embargoed(self):
        """A Node is embargoed if:
        - it has an associated Embargo record
        - that record has been approved
        - the node is not public (embargo not yet lifted)
        """
        root = self._dirty_root
        if root.is_public or root.embargo is None:
            return False
        return root.embargo.is_approved

    @property
    def embargo_end_date(self):
        root = self._dirty_root
        if root.embargo is None:
            return False
        return root.embargo.embargo_end_date

    @property
    def archiving(self):
        job = self.archive_job
        return job and not job.done and not job.archive_tree_finished()

    @property
    def _dirty_root(self):
        """Equivalent to `self.root`, but don't let Django fetch a clean copy
        when `self == self.root`. Use when it's important to reflect unsaved
        state rather than database state.
        """
        if self.id == self.root_id:
            return self
        return self.root

    def date_withdrawn(self):
        return getattr(self.root.retraction, 'date_retracted', None)

    @property
    def withdrawal_justification(self):
        return getattr(self.root.retraction, 'justification', None)

    def _initiate_embargo(self,
                          user,
                          end_date,
                          for_existing_registration=False,
                          notify_initiator_on_complete=False):
        """Initiates the retraction process for a registration
        :param user: User who initiated the retraction
        :param end_date: Date when the registration should be made public
        """
        end_date_midnight = datetime.datetime.combine(
            end_date,
            datetime.datetime.min.time()).replace(tzinfo=end_date.tzinfo)
        self.embargo = Embargo.objects.create(
            initiated_by=user,
            end_date=end_date_midnight,
            for_existing_registration=for_existing_registration,
            notify_initiator_on_complete=notify_initiator_on_complete)
        self.save()  # Set foreign field reference Node.embargo
        admins = self.get_admin_contributors_recursive(unique_users=True)
        for (admin, node) in admins:
            self.embargo.add_authorizer(admin, node)
        self.embargo.save()  # Save embargo's approval_state
        return self.embargo

    def embargo_registration(self,
                             user,
                             end_date,
                             for_existing_registration=False,
                             notify_initiator_on_complete=False):
        """Enter registration into an embargo period at end of which, it will
        be made public
        :param user: User initiating the embargo
        :param end_date: Date when the registration should be made public
        :raises: NodeStateError if Node is not a registration
        :raises: PermissionsError if user is not an admin for the Node
        :raises: ValidationError if end_date is not within time constraints
        """
        if not self.has_permission(user, 'admin'):
            raise PermissionsError('Only admins may embargo a registration')
        if not self._is_embargo_date_valid(end_date):
            if (end_date - timezone.now()) >= settings.EMBARGO_END_DATE_MIN:
                raise ValidationError(
                    'Registrations can only be embargoed for up to four years.'
                )
            raise ValidationError(
                'Embargo end date must be at least three days in the future.')

        embargo = self._initiate_embargo(
            user,
            end_date,
            for_existing_registration=for_existing_registration,
            notify_initiator_on_complete=notify_initiator_on_complete)

        self.registered_from.add_log(
            action=NodeLog.EMBARGO_INITIATED,
            params={
                'node': self.registered_from._id,
                'registration': self._id,
                'embargo_id': embargo._id,
            },
            auth=Auth(user),
            save=True,
        )
        if self.is_public:
            self.set_privacy('private', Auth(user))

    def request_embargo_termination(self, auth):
        """Initiates an EmbargoTerminationApproval to lift this Embargoed Registration's
        embargo early."""
        if not self.is_embargoed:
            raise NodeStateError('This node is not under active embargo')
        if not self.root == self:
            raise NodeStateError(
                'Only the root of an embargoed registration can request termination'
            )

        approval = EmbargoTerminationApproval(
            initiated_by=auth.user,
            embargoed_registration=self,
        )
        admins = [
            admin for admin in self.root.get_admin_contributors_recursive(
                unique_users=True)
        ]
        for (admin, node) in admins:
            approval.add_authorizer(admin, node=node)
        approval.save()
        approval.ask(admins)
        self.embargo_termination_approval = approval
        self.save()
        return approval

    def terminate_embargo(self, auth):
        """Handles the actual early termination of an Embargoed registration.
        Adds a log to the registered_from Node.
        """
        if not self.is_embargoed:
            raise NodeStateError('This node is not under active embargo')

        self.registered_from.add_log(action=NodeLog.EMBARGO_TERMINATED,
                                     params={
                                         'project': self._id,
                                         'node': self.registered_from._id,
                                         'registration': self._id,
                                     },
                                     auth=None,
                                     save=True)
        self.embargo.mark_as_completed()
        for node in self.node_and_primary_descendants():
            node.set_privacy(self.PUBLIC, auth=None, log=False, save=True)
        return True

    def _initiate_retraction(self, user, justification=None):
        """Initiates the retraction process for a registration
        :param user: User who initiated the retraction
        :param justification: Justification, if given, for retraction
        """
        self.retraction = Retraction.objects.create(
            initiated_by=user,
            justification=justification or None,  # make empty strings None
            state=Retraction.UNAPPROVED)
        self.save()
        admins = self.get_admin_contributors_recursive(unique_users=True)
        for (admin, node) in admins:
            self.retraction.add_authorizer(admin, node)
        self.retraction.save()  # Save retraction approval state
        return self.retraction

    def retract_registration(self, user, justification=None, save=True):
        """Retract public registration. Instantiate new Retraction object
        and associate it with the respective registration.
        """

        if not self.is_public and not (self.embargo_end_date
                                       or self.is_pending_embargo):
            raise NodeStateError(
                'Only public or embargoed registrations may be withdrawn.')

        if self.root_id != self.id:
            raise NodeStateError(
                'Withdrawal of non-parent registrations is not permitted.')

        retraction = self._initiate_retraction(user, justification)
        self.registered_from.add_log(
            action=NodeLog.RETRACTION_INITIATED,
            params={
                'node': self.registered_from._id,
                'registration': self._id,
                'retraction_id': retraction._id,
            },
            auth=Auth(user),
        )
        self.retraction = retraction
        if save:
            self.save()
        return retraction

    def copy_unclaimed_records(self):
        """Copies unclaimed_records to unregistered contributors from the registered_from node"""
        registered_from_id = self.registered_from._id
        for contributor in self.contributors.filter(is_registered=False):
            record = contributor.unclaimed_records.get(registered_from_id)
            if record:
                contributor.unclaimed_records[self._id] = record
                contributor.save()

    def delete_registration_tree(self, save=False):
        logger.debug('Marking registration {} as deleted'.format(self._id))
        self.is_deleted = True
        for draft_registration in DraftRegistration.objects.filter(
                registered_node=self):
            # Allow draft registration to be submitted
            if draft_registration.approval:
                draft_registration.approval = None
                draft_registration.save()
        if not getattr(self.embargo, 'for_existing_registration', False):
            self.registered_from = None
        if save:
            self.save()
        self.update_search()
        for child in self.nodes_primary:
            child.delete_registration_tree(save=save)

    def add_tag(self, tag, auth=None, save=True, log=True, system=False):
        if self.retraction is None:
            super(Registration, self).add_tag(tag, auth, save, log, system)
        else:
            raise NodeStateError('Cannot add tags to withdrawn registrations.')

    def add_tags(self, tags, auth=None, save=True, log=True, system=False):
        if self.retraction is None:
            super(Registration, self).add_tags(tags, auth, save, log, system)
        else:
            raise NodeStateError('Cannot add tags to withdrawn registrations.')

    def remove_tag(self, tag, auth, save=True):
        if self.retraction is None:
            super(Registration, self).remove_tag(tag, auth, save)
        else:
            raise NodeStateError(
                'Cannot remove tags of withdrawn registrations.')

    def remove_tags(self, tags, auth, save=True):
        if self.retraction is None:
            super(Registration, self).remove_tags(tags, auth, save)
        else:
            raise NodeStateError(
                'Cannot remove tags of withdrawn registrations.')

    class Meta:
        # custom permissions for use in the OSF Admin App
        permissions = (('view_registration',
                        'Can view registration details'), )
Ejemplo n.º 29
0
class NodeLog(ObjectIDMixin, BaseModel):
    FIELD_ALIASES = {
        # TODO: Find a better way
        'node': 'node__guids___id',
        'user': '******',
        'original_node': 'original_node__guids___id'
    }

    objects = IncludeManager()

    DATE_FORMAT = '%m/%d/%Y %H:%M UTC'

    # Log action constants -- NOTE: templates stored in log_templates.mako
    CREATED_FROM = 'created_from'

    PROJECT_CREATED = 'project_created'
    # Nodes created as part of the registration process
    PROJECT_CREATED_FROM_DRAFT_REG = 'project_created_from_draft_reg'
    PROJECT_REGISTERED = 'project_registered'
    PROJECT_DELETED = 'project_deleted'

    NODE_CREATED = 'node_created'
    NODE_FORKED = 'node_forked'
    NODE_REMOVED = 'node_removed'
    NODE_ACCESS_REQUESTS_ENABLED = 'node_access_requests_enabled'
    NODE_ACCESS_REQUESTS_DISABLED = 'node_access_requests_disabled'

    POINTER_CREATED = NODE_LINK_CREATED = 'pointer_created'
    POINTER_FORKED = NODE_LINK_FORKED = 'pointer_forked'
    POINTER_REMOVED = NODE_LINK_REMOVED = 'pointer_removed'

    WIKI_UPDATED = 'wiki_updated'
    WIKI_DELETED = 'wiki_deleted'
    WIKI_RENAMED = 'wiki_renamed'

    MADE_WIKI_PUBLIC = 'made_wiki_public'
    MADE_WIKI_PRIVATE = 'made_wiki_private'

    CONTRIB_ADDED = 'contributor_added'
    CONTRIB_REMOVED = 'contributor_removed'
    CONTRIB_REORDERED = 'contributors_reordered'

    CHECKED_IN = 'checked_in'
    CHECKED_OUT = 'checked_out'

    PERMISSIONS_UPDATED = 'permissions_updated'

    MADE_PRIVATE = 'made_private'
    MADE_PUBLIC = 'made_public'

    TAG_ADDED = 'tag_added'
    TAG_REMOVED = 'tag_removed'

    FILE_TAG_ADDED = 'file_tag_added'
    FILE_TAG_REMOVED = 'file_tag_removed'

    FILE_METADATA_UPDATED = 'file_metadata_updated'

    EDITED_TITLE = 'edit_title'
    EDITED_DESCRIPTION = 'edit_description'
    CHANGED_LICENSE = 'license_changed'

    UPDATED_FIELDS = 'updated_fields'

    FILE_MOVED = 'addon_file_moved'
    FILE_COPIED = 'addon_file_copied'
    FILE_RENAMED = 'addon_file_renamed'

    FOLDER_CREATED = 'folder_created'

    FILE_ADDED = 'file_added'
    FILE_UPDATED = 'file_updated'
    FILE_REMOVED = 'file_removed'
    FILE_RESTORED = 'file_restored'

    CATEGORY_UPDATED = 'category_updated'
    ARTICLE_DOI_UPDATED = 'article_doi_updated'

    ADDON_ADDED = 'addon_added'
    ADDON_REMOVED = 'addon_removed'
    COMMENT_ADDED = 'comment_added'
    COMMENT_REMOVED = 'comment_removed'
    COMMENT_UPDATED = 'comment_updated'
    COMMENT_RESTORED = 'comment_restored'

    CUSTOM_CITATION_ADDED = 'custom_citation_added'
    CUSTOM_CITATION_EDITED = 'custom_citation_edited'
    CUSTOM_CITATION_REMOVED = 'custom_citation_removed'

    MADE_CONTRIBUTOR_VISIBLE = 'made_contributor_visible'
    MADE_CONTRIBUTOR_INVISIBLE = 'made_contributor_invisible'

    EXTERNAL_IDS_ADDED = 'external_ids_added'

    EMBARGO_APPROVED = 'embargo_approved'
    EMBARGO_CANCELLED = 'embargo_cancelled'
    EMBARGO_COMPLETED = 'embargo_completed'
    EMBARGO_INITIATED = 'embargo_initiated'
    EMBARGO_TERMINATED = 'embargo_terminated'

    GROUP_ADDED = 'group_added'
    GROUP_UPDATED = 'group_updated'
    GROUP_REMOVED = 'group_removed'

    RETRACTION_APPROVED = 'retraction_approved'
    RETRACTION_CANCELLED = 'retraction_cancelled'
    RETRACTION_INITIATED = 'retraction_initiated'

    EXTERNAL_REGISTRATION_CREATED = 'external_registration_created'
    EXTERNAL_REGISTRATION_IMPORTED = 'external_registration_imported'

    REGISTRATION_APPROVAL_CANCELLED = 'registration_cancelled'
    REGISTRATION_APPROVAL_INITIATED = 'registration_initiated'
    REGISTRATION_APPROVAL_APPROVED = 'registration_approved'
    PREREG_REGISTRATION_INITIATED = 'prereg_registration_initiated'

    AFFILIATED_INSTITUTION_ADDED = 'affiliated_institution_added'
    AFFILIATED_INSTITUTION_REMOVED = 'affiliated_institution_removed'

    PREPRINT_INITIATED = 'preprint_initiated'
    PREPRINT_FILE_UPDATED = 'preprint_file_updated'
    PREPRINT_LICENSE_UPDATED = 'preprint_license_updated'

    SUBJECTS_UPDATED = 'subjects_updated'

    VIEW_ONLY_LINK_ADDED = 'view_only_link_added'
    VIEW_ONLY_LINK_REMOVED = 'view_only_link_removed'

    CONFIRM_HAM = 'confirm_ham'
    FLAG_SPAM = 'flag_spam'
    CONFIRM_SPAM = 'confirm_spam'

    actions = ([
        CHECKED_IN, CHECKED_OUT, FILE_TAG_REMOVED, FILE_TAG_ADDED,
        CREATED_FROM, PROJECT_CREATED, PROJECT_REGISTERED, PROJECT_DELETED,
        NODE_CREATED, NODE_FORKED, NODE_REMOVED, NODE_ACCESS_REQUESTS_ENABLED,
        NODE_ACCESS_REQUESTS_DISABLED, NODE_LINK_CREATED, NODE_LINK_FORKED,
        NODE_LINK_REMOVED, WIKI_UPDATED, WIKI_DELETED, WIKI_RENAMED,
        MADE_WIKI_PUBLIC, MADE_WIKI_PRIVATE, CONTRIB_ADDED, CONTRIB_REMOVED,
        CONTRIB_REORDERED, PERMISSIONS_UPDATED, MADE_PRIVATE, MADE_PUBLIC,
        TAG_ADDED, TAG_REMOVED, EDITED_TITLE, EDITED_DESCRIPTION,
        UPDATED_FIELDS, FILE_MOVED, FILE_COPIED, FILE_METADATA_UPDATED,
        FOLDER_CREATED, FILE_ADDED, FILE_UPDATED, FILE_REMOVED, FILE_RESTORED,
        ADDON_ADDED, ADDON_REMOVED, COMMENT_ADDED, COMMENT_REMOVED,
        COMMENT_UPDATED, COMMENT_RESTORED, MADE_CONTRIBUTOR_VISIBLE,
        CONFIRM_HAM, FLAG_SPAM, CONFIRM_SPAM, MADE_CONTRIBUTOR_INVISIBLE,
        EXTERNAL_IDS_ADDED, EMBARGO_APPROVED, EMBARGO_TERMINATED,
        EMBARGO_CANCELLED, EMBARGO_COMPLETED, EMBARGO_INITIATED,
        RETRACTION_APPROVED, RETRACTION_CANCELLED, RETRACTION_INITIATED,
        EXTERNAL_REGISTRATION_CREATED, EXTERNAL_REGISTRATION_IMPORTED,
        REGISTRATION_APPROVAL_CANCELLED, REGISTRATION_APPROVAL_INITIATED,
        REGISTRATION_APPROVAL_APPROVED, PREREG_REGISTRATION_INITIATED,
        PROJECT_CREATED_FROM_DRAFT_REG, GROUP_ADDED, GROUP_UPDATED,
        GROUP_REMOVED, AFFILIATED_INSTITUTION_ADDED,
        AFFILIATED_INSTITUTION_REMOVED, PREPRINT_INITIATED,
        PREPRINT_FILE_UPDATED, PREPRINT_LICENSE_UPDATED, VIEW_ONLY_LINK_ADDED,
        VIEW_ONLY_LINK_REMOVED
    ] + list(
        sum([
            config.actions for config in apps.get_app_configs()
            if config.name.startswith('addons.')
        ], tuple())))
    action_choices = [(action, action.upper()) for action in actions]
    date = NonNaiveDateTimeField(db_index=True,
                                 null=True,
                                 blank=True,
                                 default=timezone.now)
    # TODO build action choices on the fly with the addon stuff
    action = models.CharField(max_length=255,
                              db_index=True)  # , choices=action_choices)
    params = DateTimeAwareJSONField(default=dict)
    should_hide = models.BooleanField(default=False)
    user = models.ForeignKey('OSFUser',
                             related_name='logs',
                             db_index=True,
                             null=True,
                             blank=True,
                             on_delete=models.CASCADE)
    foreign_user = models.CharField(max_length=255, null=True, blank=True)
    node = models.ForeignKey('AbstractNode',
                             related_name='logs',
                             db_index=True,
                             null=True,
                             blank=True,
                             on_delete=models.CASCADE)
    original_node = models.ForeignKey('AbstractNode',
                                      db_index=True,
                                      null=True,
                                      blank=True,
                                      on_delete=models.CASCADE)

    def __repr__(self):
        return (
            '({self.action!r}, user={self.user!r},, node={self.node!r}, params={self.params!r}) '
            'with id {self.id!r}').format(self=self)

    class Meta:
        ordering = ['-date']
        get_latest_by = 'date'

    @property
    def absolute_api_v2_url(self):
        path = '/logs/{}/'.format(self._id)
        return api_v2_url(path)

    def get_absolute_url(self):
        return self.absolute_api_v2_url

    @property
    def absolute_url(self):
        return self.absolute_api_v2_url

    def _natural_key(self):
        return self._id
Ejemplo n.º 30
0
class Comment(GuidMixin, SpamMixin, CommentableMixin, BaseModel):
    __guid_min_length__ = 12
    OVERVIEW = 'node'
    FILES = 'files'
    WIKI = 'wiki'

    user = models.ForeignKey('OSFUser', null=True)
    # the node that the comment belongs to
    node = models.ForeignKey('AbstractNode', null=True)

    # The file or project overview page that the comment is for
    root_target = models.ForeignKey(Guid,
                                    on_delete=models.SET_NULL,
                                    related_name='comments',
                                    null=True,
                                    blank=True)

    # the direct 'parent' of the comment (e.g. the target of a comment reply is another comment)
    target = models.ForeignKey(Guid,
                               on_delete=models.SET_NULL,
                               related_name='child_comments',
                               null=True,
                               blank=True)

    date_created = NonNaiveDateTimeField(auto_now_add=True)
    date_modified = NonNaiveDateTimeField(auto_now=True)
    modified = models.BooleanField(default=False)
    is_deleted = models.BooleanField(default=False)
    # The type of root_target: node/files
    page = models.CharField(max_length=255, blank=True)
    content = models.TextField(validators=[
        validators.CommentMaxLength(settings.COMMENT_MAXLENGTH),
        validators.string_required
    ])

    # The mentioned users
    # TODO This should be made into an M2M STAT
    ever_mentioned = ArrayField(models.CharField(max_length=10, blank=True),
                                default=list,
                                blank=True)

    @property
    def url(self):
        return '/{}/'.format(self._id)

    @property
    def absolute_api_v2_url(self):
        path = '/comments/{}/'.format(self._id)
        return api_v2_url(path)

    @property
    def target_type(self):
        """The object "type" used in the OSF v2 API."""
        return 'comments'

    @property
    def root_target_page(self):
        """The page type associated with the object/Comment.root_target."""
        return None

    def belongs_to_node(self, node_id):
        """Check whether the comment is attached to the specified node."""
        return self.node._id == node_id

    # used by django and DRF
    def get_absolute_url(self):
        return self.absolute_api_v2_url

    def get_comment_page_url(self):
        if isinstance(self.root_target.referent, Node):
            return self.node.absolute_url
        return settings.DOMAIN + str(self.root_target._id) + '/'

    def get_content(self, auth):
        """ Returns the comment content if the user is allowed to see it. Deleted comments
        can only be viewed by the user who created the comment."""
        if not auth and not self.node.is_public:
            raise PermissionsError

        if self.is_deleted and ((not auth or auth.user.is_anonymous()) or
                                (auth and not auth.user.is_anonymous()
                                 and self.user._id != auth.user._id)):
            return None

        return self.content

    def get_comment_page_title(self):
        if self.page == Comment.FILES:
            return self.root_target.referent.name
        elif self.page == Comment.WIKI:
            return self.root_target.referent.page_name
        return ''

    def get_comment_page_type(self):
        if self.page == Comment.FILES:
            return 'file'
        elif self.page == Comment.WIKI:
            return 'wiki'
        return self.node.project_or_component

    @classmethod
    def find_n_unread(cls, user, node, page, root_id=None):
        if node.is_contributor(user):
            if page == Comment.OVERVIEW:
                view_timestamp = user.get_node_comment_timestamps(
                    target_id=node._id)
                root_target = Guid.load(node._id)
            elif page == Comment.FILES or page == Comment.WIKI:
                view_timestamp = user.get_node_comment_timestamps(
                    target_id=root_id)
                root_target = Guid.load(root_id)
            else:
                raise ValueError('Invalid page')

            if not view_timestamp.tzinfo:
                view_timestamp = view_timestamp.replace(tzinfo=pytz.utc)

            return cls.objects.filter(
                Q(node=node) & ~Q(user=user) & Q(is_deleted=False)
                & (Q(date_created__gt=view_timestamp)
                   | Q(date_modified__gt=view_timestamp))
                & Q(root_target=root_target)).count()

        return 0

    @classmethod
    def create(cls, auth, **kwargs):
        comment = cls(**kwargs)
        if not comment.node.can_comment(auth):
            raise PermissionsError(
                '{0!r} does not have permission to comment on this node'.
                format(auth.user))
        log_dict = {
            'project': comment.node.parent_id,
            'node': comment.node._id,
            'user': comment.user._id,
            'comment': comment._id,
        }
        if isinstance(comment.target.referent, Comment):
            comment.root_target = comment.target.referent.root_target
        else:
            comment.root_target = comment.target

        page = getattr(comment.root_target.referent, 'root_target_page', None)
        if not page:
            raise ValueError('Invalid root target.')
        comment.page = page

        log_dict.update(
            comment.root_target.referent.get_extra_log_params(comment))

        if comment.content:
            new_mentions = get_valid_mentioned_users_guids(
                comment, comment.node.contributors)
            if new_mentions:
                project_signals.mention_added.send(comment,
                                                   new_mentions=new_mentions,
                                                   auth=auth)
                comment.ever_mentioned.extend(new_mentions)

        comment.save()

        comment.node.add_log(
            NodeLog.COMMENT_ADDED,
            log_dict,
            auth=auth,
            save=False,
        )

        comment.node.save()
        project_signals.comment_added.send(comment, auth=auth)

        return comment

    def edit(self, content, auth, save=False):
        if not self.node.can_comment(auth) or self.user._id != auth.user._id:
            raise PermissionsError(
                '{0!r} does not have permission to edit this comment'.format(
                    auth.user))
        log_dict = {
            'project': self.node.parent_id,
            'node': self.node._id,
            'user': self.user._id,
            'comment': self._id,
        }
        log_dict.update(self.root_target.referent.get_extra_log_params(self))
        self.content = content
        self.modified = True
        self.date_modified = timezone.now()
        new_mentions = get_valid_mentioned_users_guids(self,
                                                       self.node.contributors)

        if save:
            if new_mentions:
                project_signals.mention_added.send(self,
                                                   new_mentions=new_mentions,
                                                   auth=auth)
                self.ever_mentioned.extend(new_mentions)
            self.save()
            self.node.add_log(
                NodeLog.COMMENT_UPDATED,
                log_dict,
                auth=auth,
                save=False,
            )
            self.node.save()

    def delete(self, auth, save=False):
        if not self.node.can_comment(auth) or self.user._id != auth.user._id:
            raise PermissionsError(
                '{0!r} does not have permission to comment on this node'.
                format(auth.user))
        log_dict = {
            'project': self.node.parent_id,
            'node': self.node._id,
            'user': self.user._id,
            'comment': self._id,
        }
        self.is_deleted = True
        log_dict.update(self.root_target.referent.get_extra_log_params(self))
        self.date_modified = timezone.now()
        if save:
            self.save()
            self.node.add_log(
                NodeLog.COMMENT_REMOVED,
                log_dict,
                auth=auth,
                save=False,
            )
            self.node.save()

    def undelete(self, auth, save=False):
        if not self.node.can_comment(auth) or self.user._id != auth.user._id:
            raise PermissionsError(
                '{0!r} does not have permission to comment on this node'.
                format(auth.user))
        self.is_deleted = False
        log_dict = {
            'project': self.node.parent_id,
            'node': self.node._id,
            'user': self.user._id,
            'comment': self._id,
        }
        log_dict.update(self.root_target.referent.get_extra_log_params(self))
        self.date_modified = timezone.now()
        if save:
            self.save()
            self.node.add_log(
                NodeLog.COMMENT_RESTORED,
                log_dict,
                auth=auth,
                save=False,
            )
            self.node.save()