예제 #1
0
class Addon(models.Model):
    component = models.ForeignKey(Component, on_delete=models.deletion.CASCADE)
    name = models.CharField(max_length=100)
    configuration = JSONField()
    state = JSONField()
    project_scope = models.BooleanField(default=False, db_index=True)
    repo_scope = models.BooleanField(default=False, db_index=True)

    objects = AddonQuerySet.as_manager()

    def __str__(self):
        return '{}: {}'.format(self.addon.verbose, self.component)

    def configure_events(self, events):
        for event in events:
            Event.objects.get_or_create(addon=self, event=event)
        self.event_set.exclude(event__in=events).delete()

    @cached_property
    def addon(self):
        return ADDONS[self.name](self)

    def get_absolute_url(self):
        return reverse('addon-detail',
                       kwargs={
                           'project': self.component.project.slug,
                           'component': self.component.slug,
                           'pk': self.pk,
                       })
예제 #2
0
class Addon(models.Model):
    component = models.ForeignKey(SubProject,
                                  on_delete=models.deletion.CASCADE)
    name = models.CharField(max_length=100)
    configuration = JSONField()
    state = JSONField()

    objects = AddonQuerySet.as_manager()

    class Meta(object):
        unique_together = ('component', 'name')

    def __str__(self):
        return '{}: {}'.format(self.addon.verbose, self.component)

    def configure_events(self, events):
        for event in events:
            Event.objects.get_or_create(addon=self, event=event)
        self.event_set.exclude(event__in=events).delete()

    @cached_property
    def addon(self):
        return ADDONS[self.name](self)

    def get_absolute_url(self):
        return reverse('addon-detail',
                       kwargs={
                           'project': self.component.project.slug,
                           'subproject': self.component.slug,
                           'pk': self.pk,
                       })
예제 #3
0
파일: alert.py 프로젝트: Habi-Thapa/weblate
class Alert(models.Model):
    component = models.ForeignKey("Component", on_delete=models.deletion.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    name = models.CharField(max_length=150)
    dismissed = models.BooleanField(default=False, db_index=True)
    details = JSONField(default={})

    class Meta:
        unique_together = ("component", "name")
        verbose_name = "component alert"
        verbose_name_plural = "component alerts"

    def __str__(self):
        return str(self.obj.verbose)

    def save(self, *args, **kwargs):
        is_new = not self.id
        super().save(*args, **kwargs)
        if is_new:
            from weblate.trans.models import Change

            Change.objects.create(
                action=Change.ACTION_ALERT,
                component=self.component,
                alert=self,
                details={"alert": self.name},
            )

    @cached_property
    def obj(self):
        return ALERTS[self.name](self, **self.details)

    def render(self, user):
        return self.obj.render(user)
예제 #4
0
파일: comment.py 프로젝트: rheehot/weblate
class Comment(models.Model, UserDisplayMixin):
    unit = models.ForeignKey("trans.Unit", on_delete=models.deletion.CASCADE)
    comment = models.TextField()
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.deletion.CASCADE,
    )
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
    resolved = models.BooleanField(default=False, db_index=True)
    userdetails = JSONField()

    objects = CommentManager.from_queryset(CommentQuerySet)()
    weblate_unsafe_delete = True

    class Meta:
        app_label = "trans"
        verbose_name = "string comment"
        verbose_name_plural = "string comments"

    def __str__(self):
        return "comment for {0} by {1}".format(
            self.unit, self.user.username if self.user else "unknown")

    def report_spam(self):
        report_spam(self.userdetails["address"], self.userdetails["agent"],
                    self.comment)
예제 #5
0
class Alert(models.Model):
    component = models.ForeignKey('Component', on_delete=models.deletion.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=150)
    details = JSONField(default={})

    class Meta(object):
        unique_together = ('component', 'name')

    @cached_property
    def obj(self):
        return ALERTS[self.name](self, **self.details)

    def __str__(self):
        return force_text(self.obj.verbose)

    def render(self):
        return self.obj.render()

    def save(self, *args, **kwargs):
        is_new = not self.id
        super(Alert, self).save(*args, **kwargs)
        if is_new:
            from weblate.trans.models import Change

            Change.objects.create(
                action=Change.ACTION_ALERT,
                component=self.component,
                alert=self,
                details={'alert': self.name},
            )
예제 #6
0
class AuditLog(models.Model):
    """User audit log storage."""

    user = models.ForeignKey(
        User,
        on_delete=models.deletion.CASCADE,
    )
    activity = models.CharField(
        max_length=20,
        choices=[(a, a) for a in sorted(ACCOUNT_ACTIVITY.keys())],
        db_index=True,
    )
    params = JSONField()
    address = models.GenericIPAddressField()
    user_agent = models.CharField(max_length=200, default='')
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)

    objects = AuditLogManager.from_queryset(AuditLogQuerySet)()

    class Meta(object):
        ordering = ['-timestamp']

    def get_message(self):
        return ACCOUNT_ACTIVITY[self.activity].format(**self.params)

    get_message.short_description = _('Account activity')

    def should_notify(self):
        return self.activity in NOTIFY_ACTIVITY

    def __str__(self):
        return '{0} for {1} from {2}'.format(self.activity, self.user.username,
                                             self.address)
예제 #7
0
파일: models.py 프로젝트: PowerKiKi/weblate
class Invoice(models.Model):
    CURRENCY_EUR = 0
    CURRENCY_BTC = 1
    CURRENCY_USD = 2
    CURRENCY_CZK = 3

    billing = models.ForeignKey(Billing, on_delete=models.deletion.CASCADE)
    start = models.DateField()
    end = models.DateField()
    payment = models.FloatField()
    currency = models.IntegerField(
        choices=(
            (CURRENCY_EUR, 'EUR'),
            (CURRENCY_BTC, 'mBTC'),
            (CURRENCY_USD, 'USD'),
            (CURRENCY_CZK, 'CZK'),
        ),
        default=CURRENCY_EUR,
    )
    ref = models.CharField(blank=True, max_length=50)
    note = models.TextField(blank=True)
    # Payment detailed information, used for integration
    # with payment processor
    payment = JSONField(editable=False, default={})

    class Meta(object):
        ordering = ['billing', '-start']

    def __str__(self):
        return '{0} - {1}: {2}'.format(
            self.start, self.end, self.billing if self.billing_id else None)

    @property
    def filename(self):
        if self.ref:
            return '{0}.pdf'.format(self.ref)
        return None

    def clean(self):
        if self.end is None or self.start is None:
            return

        if self.end <= self.start:
            raise ValidationError('Start has be to before end!')

        if not self.billing_id:
            return

        overlapping = Invoice.objects.filter(
            (Q(start__lte=self.end) & Q(end__gte=self.end))
            | (Q(start__lte=self.start) & Q(end__gte=self.start))).filter(
                billing=self.billing)

        if self.pk:
            overlapping = overlapping.exclude(pk=self.pk)

        if overlapping.exists():
            raise ValidationError('Overlapping invoices exist: {0}'.format(
                ', '.join([str(x) for x in overlapping])))
예제 #8
0
class AuditLog(models.Model):
    """User audit log storage."""

    user = models.ForeignKey(
        User,
        on_delete=models.deletion.CASCADE,
    )
    activity = models.CharField(
        max_length=20,
        choices=[(a, a) for a in sorted(ACCOUNT_ACTIVITY.keys())],
        db_index=True,
    )
    params = JSONField()
    address = models.GenericIPAddressField(null=True)
    user_agent = models.CharField(max_length=200, default='')
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)

    objects = AuditLogManager.from_queryset(AuditLogQuerySet)()

    ordering = ['-timestamp']

    def get_params(self):
        result = {}
        result.update(self.params)
        if 'method' in result:
            result['method'] = ugettext(result['method'])
        return result

    def get_message(self):
        method = self.params.get('method')
        activity = self.activity
        if activity in ACCOUNT_ACTIVITY_METHOD.get(method, {}):
            message = ACCOUNT_ACTIVITY_METHOD[method][activity]
        else:
            message = ACCOUNT_ACTIVITY[activity]
        return message.format(**self.get_params())
    get_message.short_description = _('Account activity')

    def get_extra_message(self):
        if self.activity in EXTRA_MESSAGES:
            return EXTRA_MESSAGES[self.activity].format(
                **self.params
            )
        return None

    def should_notify(self):
        return self.activity in NOTIFY_ACTIVITY and not self.user.is_demo

    def __str__(self):
        return '{0} for {1} from {2}'.format(
            self.activity,
            self.user.username,
            self.address
        )
예제 #9
0
파일: models.py 프로젝트: zzazang/weblate
class Addon(models.Model):
    component = models.ForeignKey(Component, on_delete=models.deletion.CASCADE)
    name = models.CharField(max_length=100)
    configuration = JSONField()
    state = JSONField()
    project_scope = models.BooleanField(default=False, db_index=True)
    repo_scope = models.BooleanField(default=False, db_index=True)

    objects = AddonQuerySet.as_manager()

    class Meta:
        verbose_name = "add-on"
        verbose_name_plural = "add-ons"

    def __str__(self):
        return "{}: {}".format(self.addon.verbose, self.component)

    def get_absolute_url(self):
        return reverse(
            "addon-detail",
            kwargs={
                "project": self.component.project.slug,
                "component": self.component.slug,
                "pk": self.pk,
            },
        )

    def configure_events(self, events):
        for event in events:
            Event.objects.get_or_create(addon=self, event=event)
        self.event_set.exclude(event__in=events).delete()

    @cached_property
    def addon(self):
        return ADDONS[self.name](self)

    def delete(self, *args, **kwargs):
        # Delete any addon alerts
        if self.addon.alert:
            self.component.alert_set.filter(name=self.addon.alert).delete()
        super().delete(*args, **kwargs)
예제 #10
0
파일: alert.py 프로젝트: atallahade/weblate
class Alert(models.Model):
    component = models.ForeignKey('Component',
                                  on_delete=models.deletion.CASCADE)
    timestamp = models.DateTimeField(auto_now=True)
    name = models.CharField(max_length=150)
    details = JSONField(default={})

    class Meta(object):
        ordering = ['name']
        unique_together = ('component', 'name')

    @cached_property
    def obj(self):
        return ALERTS[self.name](self, **self.details)

    def __str__(self):
        return force_text(self.obj.verbose)

    def render(self):
        return self.obj.render()
예제 #11
0
class AuditLog(models.Model):
    """User audit log storage."""

    user = models.ForeignKey(User, on_delete=models.deletion.CASCADE)
    activity = models.CharField(
        max_length=20,
        choices=[(a, a) for a in sorted(ACCOUNT_ACTIVITY.keys())],
        db_index=True,
    )
    params = JSONField()
    address = models.GenericIPAddressField(null=True)
    user_agent = models.CharField(max_length=200, default="")
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)

    objects = AuditLogManager.from_queryset(AuditLogQuerySet)()

    def __str__(self):
        return f"{self.activity} for {self.user.username} from {self.address}"

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.should_notify():
            email = self.user.email
            transaction.on_commit(
                lambda: notify_auditlog.delay(self.pk, email))

    def get_params(self):
        from weblate.accounts.templatetags.authnames import get_auth_name

        result = {}
        result.update(self.params)
        if "method" in result:
            # The gettext is here for legacy entries which contained method name
            result["method"] = gettext(get_auth_name(result["method"]))
        return result

    def get_message(self):
        method = self.params.get("method")
        activity = self.activity
        if activity in ACCOUNT_ACTIVITY_METHOD.get(method, {}):
            message = ACCOUNT_ACTIVITY_METHOD[method][activity]
        else:
            message = ACCOUNT_ACTIVITY[activity]
        return message.format(**self.get_params())

    get_message.short_description = _("Account activity")

    def get_extra_message(self):
        if self.activity in EXTRA_MESSAGES:
            return EXTRA_MESSAGES[self.activity].format(**self.params)
        return None

    def should_notify(self):
        return self.user.is_active and self.activity in NOTIFY_ACTIVITY

    def check_rate_limit(self, request):
        """Check whether the activity should be rate limited."""
        if self.activity == "failed-auth" and self.user.has_usable_password():
            failures = AuditLog.objects.get_after(self.user, "login",
                                                  "failed-auth")
            if failures.count() >= settings.AUTH_LOCK_ATTEMPTS:
                self.user.set_unusable_password()
                self.user.save(update_fields=["password"])
                AuditLog.objects.create(self.user, request, "locked")
                return True

        elif self.activity == "reset-request":
            failures = AuditLog.objects.filter(
                user=self.user,
                timestamp__gte=timezone.now() - datetime.timedelta(days=1),
                activity="reset-request",
            )
            if failures.count() >= settings.AUTH_LOCK_ATTEMPTS:
                return True

        return False
예제 #12
0
파일: change.py 프로젝트: zypA13510/weblate
class Change(models.Model, UserDisplayMixin):
    ACTION_UPDATE = 0
    ACTION_COMPLETE = 1
    ACTION_CHANGE = 2
    ACTION_COMMENT = 3
    ACTION_SUGGESTION = 4
    ACTION_NEW = 5
    ACTION_AUTO = 6
    ACTION_ACCEPT = 7
    ACTION_REVERT = 8
    ACTION_UPLOAD = 9
    ACTION_DICTIONARY_NEW = 10
    ACTION_DICTIONARY_EDIT = 11
    ACTION_DICTIONARY_UPLOAD = 12
    ACTION_NEW_SOURCE = 13
    ACTION_LOCK = 14
    ACTION_UNLOCK = 15
    ACTION_DUPLICATE_STRING = 16
    ACTION_COMMIT = 17
    ACTION_PUSH = 18
    ACTION_RESET = 19
    ACTION_MERGE = 20
    ACTION_REBASE = 21
    ACTION_FAILED_MERGE = 22
    ACTION_FAILED_REBASE = 23
    ACTION_PARSE_ERROR = 24
    ACTION_REMOVE_TRANSLATION = 25
    ACTION_SUGGESTION_DELETE = 26
    ACTION_REPLACE = 27
    ACTION_FAILED_PUSH = 28
    ACTION_SUGGESTION_CLEANUP = 29
    ACTION_SOURCE_CHANGE = 30
    ACTION_NEW_UNIT = 31
    ACTION_MASS_STATE = 32
    ACTION_ACCESS_EDIT = 33
    ACTION_ADD_USER = 34
    ACTION_REMOVE_USER = 35
    ACTION_APPROVE = 36
    ACTION_MARKED_EDIT = 37
    ACTION_REMOVE_COMPONENT = 38
    ACTION_REMOVE_PROJECT = 39
    ACTION_DUPLICATE_LANGUAGE = 40
    ACTION_RENAME_PROJECT = 41
    ACTION_RENAME_COMPONENT = 42
    ACTION_MOVE_COMPONENT = 43
    ACTION_NEW_STRING = 44
    ACTION_NEW_CONTRIBUTOR = 45
    ACTION_MESSAGE = 46
    ACTION_ALERT = 47
    ACTION_ADDED_LANGUAGE = 48
    ACTION_REQUESTED_LANGUAGE = 49
    ACTION_CREATE_PROJECT = 50
    ACTION_CREATE_COMPONENT = 51

    ACTION_CHOICES = (
        (ACTION_UPDATE, ugettext_lazy('Resource update')),
        (ACTION_COMPLETE, ugettext_lazy('Translation completed')),
        (ACTION_CHANGE, ugettext_lazy('Translation changed')),
        (ACTION_NEW, ugettext_lazy('New translation')),
        (ACTION_COMMENT, ugettext_lazy('Comment added')),
        (ACTION_SUGGESTION, ugettext_lazy('Suggestion added')),
        (ACTION_AUTO, ugettext_lazy('Automatic translation')),
        (ACTION_ACCEPT, ugettext_lazy('Suggestion accepted')),
        (ACTION_REVERT, ugettext_lazy('Translation reverted')),
        (ACTION_UPLOAD, ugettext_lazy('Translation uploaded')),
        (ACTION_DICTIONARY_NEW, ugettext_lazy('Glossary added')),
        (ACTION_DICTIONARY_EDIT, ugettext_lazy('Glossary updated')),
        (ACTION_DICTIONARY_UPLOAD, ugettext_lazy('Glossary uploaded')),
        (ACTION_NEW_SOURCE, ugettext_lazy('New source string')),
        (ACTION_LOCK, ugettext_lazy('Component locked')),
        (ACTION_UNLOCK, ugettext_lazy('Component unlocked')),
        (ACTION_DUPLICATE_STRING, ugettext_lazy('Found duplicated string')),
        (ACTION_COMMIT, ugettext_lazy('Committed changes')),
        (ACTION_PUSH, ugettext_lazy('Pushed changes')),
        (ACTION_RESET, ugettext_lazy('Reset repository')),
        (ACTION_MERGE, ugettext_lazy('Merged repository')),
        (ACTION_REBASE, ugettext_lazy('Rebased repository')),
        (ACTION_FAILED_MERGE, ugettext_lazy('Failed merge on repository')),
        (ACTION_FAILED_REBASE, ugettext_lazy('Failed rebase on repository')),
        (ACTION_FAILED_PUSH, ugettext_lazy('Failed push on repository')),
        (ACTION_PARSE_ERROR, ugettext_lazy('Parse error')),
        (ACTION_REMOVE_TRANSLATION, ugettext_lazy('Removed translation')),
        (ACTION_SUGGESTION_DELETE, ugettext_lazy('Suggestion removed')),
        (ACTION_REPLACE, ugettext_lazy('Search and replace')),
        (ACTION_SUGGESTION_CLEANUP,
         ugettext_lazy('Suggestion removed during cleanup')),
        (ACTION_SOURCE_CHANGE, ugettext_lazy('Source string changed')),
        (ACTION_NEW_UNIT, ugettext_lazy('New string added')),
        (ACTION_MASS_STATE, ugettext_lazy('Bulk status change')),
        (ACTION_ACCESS_EDIT, ugettext_lazy('Changed visibility')),
        (ACTION_ADD_USER, ugettext_lazy('Added user')),
        (ACTION_REMOVE_USER, ugettext_lazy('Removed user')),
        (ACTION_APPROVE, ugettext_lazy('Translation approved')),
        (ACTION_MARKED_EDIT, ugettext_lazy('Marked for edit')),
        (ACTION_REMOVE_COMPONENT, ugettext_lazy('Removed component')),
        (ACTION_REMOVE_PROJECT, ugettext_lazy('Removed project')),
        (ACTION_DUPLICATE_LANGUAGE,
         ugettext_lazy('Found duplicated language')),
        (ACTION_RENAME_PROJECT, ugettext_lazy('Renamed project')),
        (ACTION_RENAME_COMPONENT, ugettext_lazy('Renamed component')),
        (ACTION_MOVE_COMPONENT, ugettext_lazy('Moved component')),
        (ACTION_NEW_STRING, ugettext_lazy('New string to translate')),
        (ACTION_NEW_CONTRIBUTOR, ugettext_lazy('New contributor')),
        (ACTION_MESSAGE, ugettext_lazy('New whiteboard message')),
        (ACTION_ALERT, ugettext_lazy('New alert')),
        (ACTION_ADDED_LANGUAGE, ugettext_lazy('Added new language')),
        (ACTION_REQUESTED_LANGUAGE, ugettext_lazy('Requested new language')),
        (ACTION_CREATE_PROJECT, ugettext_lazy('Created project')),
        (ACTION_CREATE_COMPONENT, ugettext_lazy('Created component')),
    )

    ACTIONS_REVERTABLE = frozenset((
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_CHANGE,
        ACTION_UPLOAD,
        ACTION_NEW,
        ACTION_REPLACE,
    ))

    ACTIONS_CONTENT = frozenset((
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_MASS_STATE,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
    ))

    ACTIONS_REPOSITORY = frozenset((
        ACTION_PUSH,
        ACTION_RESET,
        ACTION_MERGE,
        ACTION_REBASE,
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
        ACTION_LOCK,
        ACTION_UNLOCK,
        ACTION_DUPLICATE_LANGUAGE,
    ))

    ACTIONS_MERGE_FAILURE = frozenset((
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
    ))

    unit = models.ForeignKey('Unit',
                             null=True,
                             on_delete=models.deletion.CASCADE)
    project = models.ForeignKey('Project',
                                null=True,
                                on_delete=models.deletion.CASCADE)
    component = models.ForeignKey('Component',
                                  null=True,
                                  on_delete=models.deletion.CASCADE)
    translation = models.ForeignKey('Translation',
                                    null=True,
                                    on_delete=models.deletion.CASCADE)
    dictionary = models.ForeignKey('Dictionary',
                                   null=True,
                                   on_delete=models.deletion.CASCADE)
    comment = models.ForeignKey('Comment',
                                null=True,
                                on_delete=models.deletion.SET_NULL)
    suggestion = models.ForeignKey('Suggestion',
                                   null=True,
                                   on_delete=models.deletion.SET_NULL)
    whiteboard = models.ForeignKey('WhiteboardMessage',
                                   null=True,
                                   on_delete=models.deletion.SET_NULL)
    alert = models.ForeignKey('Alert',
                              null=True,
                              on_delete=models.deletion.SET_NULL)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             null=True,
                             on_delete=models.deletion.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL,
                               null=True,
                               related_name='author_set',
                               on_delete=models.deletion.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
    action = models.IntegerField(choices=ACTION_CHOICES, default=ACTION_CHANGE)
    target = models.TextField(default='', blank=True)
    old = models.TextField(default='', blank=True)
    details = JSONField()

    objects = ChangeManager.from_queryset(ChangeQuerySet)()

    class Meta(object):
        app_label = 'trans'

    def __init__(self, *args, **kwargs):
        self.notify_state = {}
        super(Change, self).__init__(*args, **kwargs)

    def __str__(self):
        return _('%(action)s at %(time)s on %(translation)s by %(user)s') % {
            'action': self.get_action_display(),
            'time': self.timestamp,
            'translation': self.translation,
            'user': self.get_user_display(False),
        }

    def is_merge_failure(self):
        return self.action in self.ACTIONS_MERGE_FAILURE

    def get_absolute_url(self):
        """Return link either to unit or translation."""
        if self.unit is not None:
            return self.unit.get_absolute_url()
        return self.get_translation_url()

    def get_translation_url(self):
        """Return URL for translation."""
        if self.translation is not None:
            return self.translation.get_absolute_url()
        if self.component is not None:
            return self.component.get_absolute_url()
        if self.dictionary is not None:
            return self.dictionary.get_parent_url()
        if self.project is not None:
            return self.project.get_absolute_url()
        return None

    def get_translation_display(self):
        """Return display name for translation."""
        if self.translation is not None:
            return force_text(self.translation)
        elif self.component is not None:
            return force_text(self.component)
        elif self.dictionary is not None:
            return '{0}/{1}'.format(self.dictionary.project,
                                    self.dictionary.language)
        elif self.project is not None:
            return force_text(self.project)
        return None

    def can_revert(self):
        return (self.unit is not None and self.target
                and self.action in self.ACTIONS_REVERTABLE)

    def show_source(self):
        """Whether to show content as source change."""
        return self.action == self.ACTION_SOURCE_CHANGE

    def show_content(self):
        """Whether to show content as translation."""
        return self.action in (
            self.ACTION_SUGGESTION,
            self.ACTION_SUGGESTION_DELETE,
            self.ACTION_SUGGESTION_CLEANUP,
            self.ACTION_NEW_UNIT,
        )

    def get_details_display(self):
        if not self.details:
            return ''
        if self.action == self.ACTION_ACCESS_EDIT:
            for number, name in Project.ACCESS_CHOICES:
                if number == self.details['access_control']:
                    return name
            return 'Unknonwn {}'.format(self.details['access_control'])
        elif self.action in (self.ACTION_ADD_USER, self.ACTION_REMOVE_USER):
            if 'group' in self.details:
                return '{username} ({group})'.format(**self.details)
            return self.details['username']
        elif self.action in (self.ACTION_ADDED_LANGUAGE,
                             self.ACTION_REQUESTED_LANGUAGE):  # noqa: E501
            try:
                return Language.objects.get(code=self.details['language'])
            except Language.DoesNotExist:
                return self.details['language']
        elif self.action == self.ACTION_ALERT:
            try:
                return ALERTS[self.details['alert']].verbose
            except KeyError:
                return self.details['alert']

        return ''

    def save(self, *args, **kwargs):
        from weblate.accounts.tasks import notify_change
        if self.unit:
            self.translation = self.unit.translation
        if self.translation:
            self.component = self.translation.component
        if self.component:
            self.project = self.component.project
        if self.dictionary:
            self.project = self.dictionary.project
        super(Change, self).save(*args, **kwargs)
        notify_change.delay(self.pk)
예제 #13
0
파일: change.py 프로젝트: renatofb/weblate
class Change(models.Model, UserDisplayMixin):
    ACTION_UPDATE = 0
    ACTION_COMPLETE = 1
    ACTION_CHANGE = 2
    ACTION_COMMENT = 3
    ACTION_SUGGESTION = 4
    ACTION_NEW = 5
    ACTION_AUTO = 6
    ACTION_ACCEPT = 7
    ACTION_REVERT = 8
    ACTION_UPLOAD = 9
    ACTION_NEW_SOURCE = 13
    ACTION_LOCK = 14
    ACTION_UNLOCK = 15
    ACTION_DUPLICATE_STRING = 16
    ACTION_COMMIT = 17
    ACTION_PUSH = 18
    ACTION_RESET = 19
    ACTION_MERGE = 20
    ACTION_REBASE = 21
    ACTION_FAILED_MERGE = 22
    ACTION_FAILED_REBASE = 23
    ACTION_PARSE_ERROR = 24
    ACTION_REMOVE_TRANSLATION = 25
    ACTION_SUGGESTION_DELETE = 26
    ACTION_REPLACE = 27
    ACTION_FAILED_PUSH = 28
    ACTION_SUGGESTION_CLEANUP = 29
    ACTION_SOURCE_CHANGE = 30
    ACTION_NEW_UNIT = 31
    ACTION_BULK_EDIT = 32
    ACTION_ACCESS_EDIT = 33
    ACTION_ADD_USER = 34
    ACTION_REMOVE_USER = 35
    ACTION_APPROVE = 36
    ACTION_MARKED_EDIT = 37
    ACTION_REMOVE_COMPONENT = 38
    ACTION_REMOVE_PROJECT = 39
    ACTION_DUPLICATE_LANGUAGE = 40
    ACTION_RENAME_PROJECT = 41
    ACTION_RENAME_COMPONENT = 42
    ACTION_MOVE_COMPONENT = 43
    ACTION_NEW_STRING = 44
    ACTION_NEW_CONTRIBUTOR = 45
    ACTION_ANNOUNCEMENT = 46
    ACTION_ALERT = 47
    ACTION_ADDED_LANGUAGE = 48
    ACTION_REQUESTED_LANGUAGE = 49
    ACTION_CREATE_PROJECT = 50
    ACTION_CREATE_COMPONENT = 51
    ACTION_INVITE_USER = 52
    ACTION_HOOK = 53
    ACTION_REPLACE_UPLOAD = 54
    ACTION_LICENSE_CHANGE = 55
    ACTION_AGREEMENT_CHANGE = 56
    ACTION_SCREENSHOT_ADDED = 57
    ACTION_SCREENSHOT_UPLOADED = 58
    ACTION_STRING_REPO_UPDATE = 59

    ACTION_CHOICES = (
        # Translators: Name of event in the history
        (ACTION_UPDATE, gettext_lazy("Resource update")),
        # Translators: Name of event in the history
        (ACTION_COMPLETE, gettext_lazy("Translation completed")),
        # Translators: Name of event in the history
        (ACTION_CHANGE, gettext_lazy("Translation changed")),
        # Translators: Name of event in the history
        (ACTION_NEW, gettext_lazy("New translation")),
        # Translators: Name of event in the history
        (ACTION_COMMENT, gettext_lazy("Comment added")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION, gettext_lazy("Suggestion added")),
        # Translators: Name of event in the history
        (ACTION_AUTO, gettext_lazy("Automatic translation")),
        # Translators: Name of event in the history
        (ACTION_ACCEPT, gettext_lazy("Suggestion accepted")),
        # Translators: Name of event in the history
        (ACTION_REVERT, gettext_lazy("Translation reverted")),
        # Translators: Name of event in the history
        (ACTION_UPLOAD, gettext_lazy("Translation uploaded")),
        # Translators: Name of event in the history
        (ACTION_NEW_SOURCE, gettext_lazy("New source string")),
        # Translators: Name of event in the history
        (ACTION_LOCK, gettext_lazy("Component locked")),
        # Translators: Name of event in the history
        (ACTION_UNLOCK, gettext_lazy("Component unlocked")),
        # Translators: Name of event in the history
        (ACTION_DUPLICATE_STRING, gettext_lazy("Found duplicated string")),
        # Translators: Name of event in the history
        (ACTION_COMMIT, gettext_lazy("Committed changes")),
        # Translators: Name of event in the history
        (ACTION_PUSH, gettext_lazy("Pushed changes")),
        # Translators: Name of event in the history
        (ACTION_RESET, gettext_lazy("Reset repository")),
        # Translators: Name of event in the history
        (ACTION_MERGE, gettext_lazy("Merged repository")),
        # Translators: Name of event in the history
        (ACTION_REBASE, gettext_lazy("Rebased repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_MERGE, gettext_lazy("Failed merge on repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_REBASE, gettext_lazy("Failed rebase on repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_PUSH, gettext_lazy("Failed push on repository")),
        # Translators: Name of event in the history
        (ACTION_PARSE_ERROR, gettext_lazy("Parse error")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_TRANSLATION, gettext_lazy("Removed translation")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION_DELETE, gettext_lazy("Suggestion removed")),
        # Translators: Name of event in the history
        (ACTION_REPLACE, gettext_lazy("Search and replace")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION_CLEANUP,
         gettext_lazy("Suggestion removed during cleanup")),
        # Translators: Name of event in the history
        (ACTION_SOURCE_CHANGE, gettext_lazy("Source string changed")),
        # Translators: Name of event in the history
        (ACTION_NEW_UNIT, gettext_lazy("New string added")),
        # Translators: Name of event in the history
        (ACTION_BULK_EDIT, gettext_lazy("Bulk status change")),
        # Translators: Name of event in the history
        (ACTION_ACCESS_EDIT, gettext_lazy("Changed visibility")),
        # Translators: Name of event in the history
        (ACTION_ADD_USER, gettext_lazy("Added user")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_USER, gettext_lazy("Removed user")),
        # Translators: Name of event in the history
        (ACTION_APPROVE, gettext_lazy("Translation approved")),
        # Translators: Name of event in the history
        (ACTION_MARKED_EDIT, gettext_lazy("Marked for edit")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_COMPONENT, gettext_lazy("Removed component")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_PROJECT, gettext_lazy("Removed project")),
        # Translators: Name of event in the history
        (ACTION_DUPLICATE_LANGUAGE, gettext_lazy("Found duplicated language")),
        # Translators: Name of event in the history
        (ACTION_RENAME_PROJECT, gettext_lazy("Renamed project")),
        # Translators: Name of event in the history
        (ACTION_RENAME_COMPONENT, gettext_lazy("Renamed component")),
        # Translators: Name of event in the history
        (ACTION_MOVE_COMPONENT, gettext_lazy("Moved component")),
        # Using pgettext to differentiate from the plural
        # Translators: Name of event in the history
        (
            ACTION_NEW_STRING,
            pgettext_lazy("Name of event in the history",
                          "New string to translate"),
        ),
        # Translators: Name of event in the history
        (ACTION_NEW_CONTRIBUTOR, gettext_lazy("New contributor")),
        # Translators: Name of event in the history
        (ACTION_ANNOUNCEMENT, gettext_lazy("New announcement")),
        # Translators: Name of event in the history
        (ACTION_ALERT, gettext_lazy("New alert")),
        # Translators: Name of event in the history
        (ACTION_ADDED_LANGUAGE, gettext_lazy("Added new language")),
        # Translators: Name of event in the history
        (ACTION_REQUESTED_LANGUAGE, gettext_lazy("Requested new language")),
        # Translators: Name of event in the history
        (ACTION_CREATE_PROJECT, gettext_lazy("Created project")),
        # Translators: Name of event in the history
        (ACTION_CREATE_COMPONENT, gettext_lazy("Created component")),
        # Translators: Name of event in the history
        (ACTION_INVITE_USER, gettext_lazy("Invited user")),
        # Translators: Name of event in the history
        (ACTION_HOOK, gettext_lazy("Received repository notification")),
        # Translators: Name of event in the history
        (ACTION_REPLACE_UPLOAD, gettext_lazy("Replaced file by upload")),
        # Translators: Name of event in the history
        (ACTION_LICENSE_CHANGE, gettext_lazy("License changed")),
        # Translators: Name of event in the history
        (ACTION_AGREEMENT_CHANGE, gettext_lazy("Contributor agreement changed")
         ),
        # Translators: Name of event in the history
        (ACTION_SCREENSHOT_ADDED, gettext_lazy("Screnshot added")),
        # Translators: Name of event in the history
        (ACTION_SCREENSHOT_UPLOADED, gettext_lazy("Screnshot uploaded")),
        # Translators: Name of event in the history
        (ACTION_STRING_REPO_UPDATE,
         gettext_lazy("String updated in the repository")),
    )
    ACTIONS_DICT = dict(ACTION_CHOICES)
    ACTION_STRINGS = {
        name.lower().replace(" ", "-"): value
        for value, name in ACTION_CHOICES
    }
    ACTION_NAMES = {str(name): value for value, name in ACTION_CHOICES}

    # Actions which can be reverted
    ACTIONS_REVERTABLE = {
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_CHANGE,
        ACTION_UPLOAD,
        ACTION_NEW,
        ACTION_REPLACE,
        ACTION_AUTO,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
        ACTION_STRING_REPO_UPDATE,
    }

    # Content changes considered when looking for last author
    ACTIONS_CONTENT = {
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_BULK_EDIT,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
    }

    # Actions shown on the repository management page
    ACTIONS_REPOSITORY = {
        ACTION_COMMIT,
        ACTION_PUSH,
        ACTION_RESET,
        ACTION_MERGE,
        ACTION_REBASE,
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
        ACTION_LOCK,
        ACTION_UNLOCK,
        ACTION_DUPLICATE_LANGUAGE,
    }

    # Actions where target is rendered as translation string
    ACTIONS_SHOW_CONTENT = {
        ACTION_SUGGESTION,
        ACTION_SUGGESTION_DELETE,
        ACTION_SUGGESTION_CLEANUP,
        ACTION_BULK_EDIT,
        ACTION_NEW_UNIT,
        ACTION_STRING_REPO_UPDATE,
    }

    # Actions indicating a repository merge failure
    ACTIONS_MERGE_FAILURE = {
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
    }

    PLURAL_ACTIONS = {
        ACTION_NEW_STRING:
        ngettext_lazy("New string to translate", "New strings to translate"),
    }
    AUTO_ACTIONS = {
        # Translators: Name of event in the history
        ACTION_LOCK:
        gettext_lazy(
            "The component was automatically locked because of an alert."),
        # Translators: Name of event in the history
        ACTION_UNLOCK:
        gettext_lazy("Fixing an alert automatically unlocked the component."),
    }

    unit = models.ForeignKey("Unit",
                             null=True,
                             on_delete=models.deletion.CASCADE)
    language = models.ForeignKey("lang.Language",
                                 null=True,
                                 on_delete=models.deletion.CASCADE)
    project = models.ForeignKey("Project",
                                null=True,
                                on_delete=models.deletion.CASCADE)
    component = models.ForeignKey("Component",
                                  null=True,
                                  on_delete=models.deletion.CASCADE)
    translation = models.ForeignKey("Translation",
                                    null=True,
                                    on_delete=models.deletion.CASCADE)
    comment = models.ForeignKey("Comment",
                                null=True,
                                on_delete=models.deletion.SET_NULL)
    suggestion = models.ForeignKey("Suggestion",
                                   null=True,
                                   on_delete=models.deletion.SET_NULL)
    announcement = models.ForeignKey("Announcement",
                                     null=True,
                                     on_delete=models.deletion.SET_NULL)
    screenshot = models.ForeignKey("screenshots.Screenshot",
                                   null=True,
                                   on_delete=models.deletion.SET_NULL)
    alert = models.ForeignKey("Alert",
                              null=True,
                              on_delete=models.deletion.SET_NULL)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             null=True,
                             on_delete=models.deletion.CASCADE)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        related_name="author_set",
        on_delete=models.deletion.CASCADE,
    )
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
    action = models.IntegerField(choices=ACTION_CHOICES,
                                 default=ACTION_CHANGE,
                                 db_index=True)
    target = models.TextField(default="", blank=True)
    old = models.TextField(default="", blank=True)
    details = JSONField()

    objects = ChangeManager.from_queryset(ChangeQuerySet)()

    class Meta:
        app_label = "trans"
        index_together = [
            ("translation", "action", "timestamp"),
        ]
        verbose_name = "history event"
        verbose_name_plural = "history events"

    def __str__(self):
        return _("%(action)s at %(time)s on %(translation)s by %(user)s") % {
            "action": self.get_action_display(),
            "time": self.timestamp,
            "translation": self.translation,
            "user": self.get_user_display(False),
        }

    def save(self, *args, **kwargs):
        from weblate.accounts.tasks import notify_change

        self.fixup_refereces()

        super().save(*args, **kwargs)
        transaction.on_commit(lambda: notify_change.delay(self.pk))

    def get_absolute_url(self):
        """Return link either to unit or translation."""
        if self.unit is not None:
            return self.unit.get_absolute_url()
        if self.screenshot is not None:
            return self.screenshot.get_absolute_url()
        if self.translation is not None:
            if self.action == self.ACTION_NEW_STRING:
                return self.translation.get_translate_url(
                ) + "?q=is:untranslated"
            return self.translation.get_absolute_url()
        if self.component is not None:
            return self.component.get_absolute_url()
        if self.project is not None:
            return self.project.get_absolute_url()
        return None

    def __init__(self, *args, **kwargs):
        self.notify_state = {}
        for attr in ("user", "author"):
            user = kwargs.get(attr)
            if user is not None and hasattr(user, "get_token_user"):
                # ProjectToken / ProjectUser integration
                kwargs[attr] = user.get_token_user()
        super().__init__(*args, **kwargs)
        if not self.pk:
            self.fixup_refereces()

    def fixup_refereces(self):
        """Updates refereces based to least specific one."""
        if self.unit:
            self.translation = self.unit.translation
        if self.screenshot:
            self.translation = self.screenshot.translation
        if self.translation:
            self.component = self.translation.component
            self.language = self.translation.language
        if self.component:
            self.project = self.component.project

    @property
    def plural_count(self):
        return self.details.get("count", 1)

    @property
    def auto_status(self):
        return self.details.get("auto", False)

    def get_action_display(self):
        if self.action in self.PLURAL_ACTIONS:
            return self.PLURAL_ACTIONS[self.action] % self.plural_count
        return str(self.ACTIONS_DICT.get(self.action, self.action))

    def get_state_display(self):
        state = self.details.get("state")
        if not state:
            return ""
        return STATE_LOOKUP[state]

    def is_merge_failure(self):
        return self.action in self.ACTIONS_MERGE_FAILURE

    def can_revert(self):
        return (self.unit is not None and self.old
                and self.action in self.ACTIONS_REVERTABLE)

    def show_source(self):
        """Whether to show content as source change."""
        return self.action == self.ACTION_SOURCE_CHANGE

    def show_content(self):
        """Whether to show content as translation."""
        return (self.action in self.ACTIONS_SHOW_CONTENT
                or self.action in self.ACTIONS_REVERTABLE)

    def get_details_display(self):  # noqa: C901
        from weblate.utils.markdown import render_markdown

        details = self.details

        if self.action == self.ACTION_NEW_STRING:
            result = ngettext(
                "%d new string to translate appeared in the translation.",
                "%d new strings to translate appeared to the translation.",
                self.plural_count,
            )
            try:
                return result % self.plural_count
            except TypeError:
                # The string does not contain %d
                return result

        if self.action in (self.ACTION_ANNOUNCEMENT,
                           self.ACTION_AGREEMENT_CHANGE):
            return render_markdown(self.target)

        if self.action in self.AUTO_ACTIONS and self.auto_status:
            return str(self.AUTO_ACTIONS[self.action])

        if self.action == self.ACTION_UPDATE:
            reason = details.get("reason", "content changed")
            filename = "<code>{}</code>".format(
                escape(
                    details.get(
                        "filename",
                        self.translation.filename if self.translation else "",
                    )))
            if reason == "content changed":
                return mark_safe(_('The "%s" file was changed.') % filename)
            if reason == "check forced":
                return mark_safe(
                    _('Parsing of the "%s" file was enforced.') % filename)
            if reason == "new file":
                return mark_safe(_("File %s was added.") % filename)
            raise ValueError(f"Unknown reason: {reason}")

        if self.action == self.ACTION_LICENSE_CHANGE:
            not_available = pgettext("License information not available",
                                     "N/A")
            return _(
                'The license of the "%(component)s" component was changed '
                "from %(old)s to %(target)s.") % {
                    "component": self.component,
                    "old": self.old or not_available,
                    "target": self.target or not_available,
                }

        # Following rendering relies on details present
        if not details:
            return ""
        user_actions = {
            self.ACTION_ADD_USER,
            self.ACTION_INVITE_USER,
            self.ACTION_REMOVE_USER,
        }
        if self.action == self.ACTION_ACCESS_EDIT:
            for number, name in Project.ACCESS_CHOICES:
                if number == details["access_control"]:
                    return name
            return "Unknonwn {}".format(details["access_control"])
        if self.action in user_actions:
            if "group" in details:
                return "{username} ({group})".format(**details)
            return details["username"]
        if self.action in (
                self.ACTION_ADDED_LANGUAGE,
                self.ACTION_REQUESTED_LANGUAGE,
        ):  # noqa: E501
            try:
                return Language.objects.get(code=details["language"])
            except Language.DoesNotExist:
                return details["language"]
        if self.action == self.ACTION_ALERT:
            try:
                return ALERTS[details["alert"]].verbose
            except KeyError:
                return details["alert"]
        if self.action == self.ACTION_PARSE_ERROR:
            return "{filename}: {error_message}".format(**details)
        if self.action == self.ACTION_HOOK:
            return "{service_long_name}: {repo_url}, {branch}".format(
                **details)
        if self.action == self.ACTION_COMMENT and "comment" in details:
            return render_markdown(details["comment"])

        return ""

    def get_distance(self):
        try:
            return damerau_levenshtein_distance(self.old, self.target)
        except MemoryError:
            # Too long strings
            return abs(len(self.old) - len(self.target))

    def get_source(self):
        return self.details.get("source", self.unit.source)
예제 #14
0
파일: models.py 프로젝트: PowerKiKi/weblate
class Billing(models.Model):
    STATE_ACTIVE = 0
    STATE_TRIAL = 1
    STATE_EXPIRED = 2

    EXPIRING_STATES = (STATE_TRIAL, )

    plan = models.ForeignKey(
        Plan,
        on_delete=models.deletion.CASCADE,
        verbose_name=_('Billing plan'),
    )
    projects = models.ManyToManyField(
        Project,
        blank=True,
        verbose_name=_('Billed projects'),
    )
    owners = models.ManyToManyField(
        User,
        blank=True,
        verbose_name=_('Billing owners'),
    )
    state = models.IntegerField(
        choices=(
            (STATE_ACTIVE, _('Active')),
            (STATE_TRIAL, _('Trial')),
            (STATE_EXPIRED, _('Expired')),
        ),
        default=STATE_ACTIVE,
        verbose_name=_('Billing state'),
    )
    expiry = models.DateTimeField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_('Trial expiry date'),
    )
    paid = models.BooleanField(
        default=False,
        verbose_name=_('Paid'),
        editable=False,
    )
    # Translators: Whether the package is inside actual (hard) limits
    in_limits = models.BooleanField(
        default=True,
        verbose_name=_('In limits'),
        editable=False,
    )
    # Payment detailed information, used for integration
    # with payment processor
    payment = JSONField(editable=False, default={})

    objects = BillingManager.from_queryset(BillingQuerySet)()

    def __str__(self):
        projects = self.projects.all()
        owners = self.owners.all()
        if projects:
            base = ', '.join([str(x) for x in projects])
        elif owners:
            base = ', '.join([x.get_author_name(False) for x in owners])
        else:
            base = 'Unassigned'
        return '{0} ({1})'.format(base, self.plan)

    def count_changes(self, interval):
        return Change.objects.filter(
            component__project__in=self.projects.all(),
            timestamp__gt=timezone.now() - interval,
        ).count()

    def count_changes_1m(self):
        return self.count_changes(timedelta(days=31))

    count_changes_1m.short_description = _('Changes in last month')

    def count_changes_1q(self):
        return self.count_changes(timedelta(days=93))

    count_changes_1q.short_description = _('Changes in last quarter')

    def count_changes_1y(self):
        return self.count_changes(timedelta(days=365))

    count_changes_1y.short_description = _('Changes in last year')

    def count_repositories(self):
        return Component.objects.filter(
            project__in=self.projects.all(), ).exclude(
                repo__startswith='weblate:/').count()

    def display_repositories(self):
        return '{0} / {1}'.format(self.count_repositories(),
                                  self.plan.display_limit_repositories)

    display_repositories.short_description = _('VCS repositories')

    def count_projects(self):
        return self.projects.count()

    def display_projects(self):
        return '{0} / {1}'.format(self.count_projects(),
                                  self.plan.display_limit_projects)

    display_projects.short_description = _('Projects')

    def count_strings(self):
        return sum((p.stats.source_strings for p in self.projects.all()))

    def display_strings(self):
        return '{0} / {1}'.format(self.count_strings(),
                                  self.plan.display_limit_strings)

    display_strings.short_description = _('Source strings')

    def count_words(self):
        return sum((p.stats.source_words for p in self.projects.all()))

    def display_words(self):
        return '{0}'.format(self.count_words(), )

    display_words.short_description = _('Source words')

    def count_languages(self):
        return Language.objects.filter(
            translation__component__project__in=self.projects.all()).distinct(
            ).count()

    def display_languages(self):
        return '{0} / {1}'.format(self.count_languages(),
                                  self.plan.display_limit_languages)

    display_languages.short_description = _('Languages')

    def check_in_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return ((plan.limit_repositories == 0
                 or self.count_repositories() <= plan.limit_repositories)
                and (plan.limit_projects == 0
                     or self.count_projects() <= plan.limit_projects)
                and (plan.limit_strings == 0
                     or self.count_strings() <= plan.limit_strings)
                and (plan.limit_languages == 0
                     or self.count_languages() <= plan.limit_languages))

    def check_expiry(self):
        return (self.state in Billing.EXPIRING_STATES and self.expiry
                and self.expiry < timezone.now())

    def unit_count(self):
        return Unit.objects.filter(
            translation__component__project__in=self.projects.all()).count()

    unit_count.short_description = _('Number of strings')

    def last_invoice(self):
        try:
            invoice = self.invoice_set.order_by('-start')[0]
            return '{0} - {1}'.format(invoice.start, invoice.end)
        except IndexError:
            return _('N/A')

    last_invoice.short_description = _('Last invoice')

    def in_display_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return ((plan.display_limit_repositories == 0 or
                 self.count_repositories() <= plan.display_limit_repositories)
                and (plan.display_limit_projects == 0
                     or self.count_projects() <= plan.display_limit_projects)
                and (plan.display_limit_strings == 0
                     or self.count_strings() <= plan.display_limit_strings) and
                (plan.display_limit_languages == 0
                 or self.count_languages() <= plan.display_limit_languages))

    in_display_limits.boolean = True
    # Translators: Whether the package is inside displayed (soft) limits
    in_display_limits.short_description = _('In display limits')

    def check_payment_status(self):
        """Check current payment status.

        Compared to paid attribute, this does not include grace period.
        """
        return (self.plan.price == 0
                or self.invoice_set.filter(end__gte=timezone.now()).exists()
                or self.state == Billing.STATE_TRIAL)

    def check_limits(self, grace=30, save=True):
        due_date = timezone.now() - timedelta(days=grace)
        in_limits = self.check_in_limits()
        paid = (self.plan.price == 0
                or self.invoice_set.filter(end__gt=due_date).exists()
                or self.state == Billing.STATE_TRIAL)
        modified = False

        if self.check_expiry():
            self.state = Billing.STATE_EXPIRED
            self.expiry = None
            modified = True

        if self.state not in Billing.EXPIRING_STATES and self.expiry:
            self.expiry = None
            modified = True

        if self.in_limits != in_limits or self.paid != paid:
            self.in_limits = in_limits
            self.paid = paid
            modified = True

        if save and modified:
            self.save(skip_limits=True)

    def save(self, *args, **kwargs):
        if not kwargs.pop('skip_limits', False) and self.pk:
            self.check_limits(save=False)
        super(Billing, self).save(*args, **kwargs)

    def is_active(self):
        return self.state in (Billing.STATE_ACTIVE, Billing.STATE_TRIAL)
예제 #15
0
class Suggestion(models.Model, UserDisplayMixin):
    unit = models.ForeignKey("trans.Unit", on_delete=models.deletion.CASCADE)
    target = models.TextField()
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.deletion.CASCADE,
    )
    userdetails = JSONField()
    timestamp = models.DateTimeField(auto_now_add=True)

    votes = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   through='Vote',
                                   related_name='user_votes')

    objects = SuggestionManager.from_queryset(SuggestionQuerySet)()

    class Meta:
        app_label = 'trans'

    def __str__(self):
        return 'suggestion for {0} by {1}'.format(
            self.unit, self.user.username if self.user else 'unknown')

    @transaction.atomic
    def accept(self, translation, request, permission='suggestion.accept'):
        if not request.user.has_perm(permission, self.unit):
            messages.error(request, _('Failed to accept suggestion!'))
            return

        # Skip if there is no change
        if self.unit.target != self.target or self.unit.state < STATE_TRANSLATED:
            self.unit.target = self.target
            self.unit.state = STATE_TRANSLATED
            self.unit.save_backend(request.user,
                                   author=self.user,
                                   change_action=Change.ACTION_ACCEPT)

        # Delete the suggestion
        self.delete()

    def delete_log(self,
                   user,
                   change=Change.ACTION_SUGGESTION_DELETE,
                   is_spam=False):
        """Delete with logging change."""
        if is_spam and self.userdetails:
            report_spam(self.userdetails['address'], self.userdetails['agent'],
                        self.target)
        Change.objects.create(unit=self.unit,
                              action=change,
                              user=user,
                              target=self.target,
                              author=user)
        self.delete()

    def get_num_votes(self):
        """Return number of votes."""
        return self.vote_set.aggregate(Sum('value'))['value__sum'] or 0

    def add_vote(self, translation, request, value):
        """Add (or updates) vote for a suggestion."""
        if not request.user.is_authenticated:
            return

        vote, created = Vote.objects.get_or_create(suggestion=self,
                                                   user=request.user,
                                                   defaults={'value': value})
        if not created or vote.value != value:
            vote.value = value
            vote.save()

        # Automatic accepting
        required_votes = translation.component.suggestion_autoaccept
        if required_votes and self.get_num_votes() >= required_votes:
            self.accept(translation, request, 'suggestion.vote')

    def get_checks(self):
        # Build fake unit to run checks
        fake_unit = copy(self.unit)
        fake_unit.target = self.target
        fake_unit.state = STATE_TRANSLATED
        source = fake_unit.get_source_plurals()
        target = fake_unit.get_target_plurals()

        result = []
        for check, check_obj in CHECKS.target.items():
            if check_obj.check_target(source, target, fake_unit):
                result.append(Check(unit=fake_unit, ignore=False, check=check))
        return result
예제 #16
0
class Suggestion(models.Model, UserDisplayMixin):
    unit = models.ForeignKey("trans.Unit", on_delete=models.deletion.CASCADE)
    target = models.TextField()
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.deletion.CASCADE,
    )
    userdetails = JSONField()
    timestamp = models.DateTimeField(auto_now_add=True)

    votes = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   through="Vote",
                                   related_name="user_votes")

    objects = SuggestionManager.from_queryset(SuggestionQuerySet)()
    weblate_unsafe_delete = True

    class Meta:
        app_label = "trans"
        verbose_name = "string suggestion"
        verbose_name_plural = "string suggestions"

    def __str__(self):
        return "suggestion for {} by {}".format(
            self.unit, self.user.username if self.user else "unknown")

    @transaction.atomic
    def accept(self, request, permission="suggestion.accept"):
        if not request.user.has_perm(permission, self.unit):
            messages.error(request, _("Failed to accept suggestion!"))
            return

        # Skip if there is no change
        if self.unit.target != self.target or self.unit.state < STATE_TRANSLATED:
            if self.user and not self.user.is_anonymous:
                author = self.user
            else:
                author = request.user
            self.unit.translate(
                request.user,
                split_plural(self.target),
                STATE_TRANSLATED,
                author=author,
                change_action=Change.ACTION_ACCEPT,
            )

        # Delete the suggestion
        self.delete()

    def delete_log(self,
                   user,
                   change=Change.ACTION_SUGGESTION_DELETE,
                   is_spam=False):
        """Delete with logging change."""
        if is_spam and self.userdetails:
            report_spam(self.userdetails["address"], self.userdetails["agent"],
                        self.target)
        Change.objects.create(unit=self.unit,
                              action=change,
                              user=user,
                              target=self.target,
                              author=user)
        self.delete()

    def get_num_votes(self):
        """Return number of votes."""
        return self.vote_set.aggregate(Sum("value"))["value__sum"] or 0

    def add_vote(self, request, value):
        """Add (or updates) vote for a suggestion."""
        if request is None or not request.user.is_authenticated:
            return

        vote, created = Vote.objects.get_or_create(suggestion=self,
                                                   user=request.user,
                                                   defaults={"value": value})
        if not created or vote.value != value:
            vote.value = value
            vote.save()

        # Automatic accepting
        required_votes = self.unit.translation.component.suggestion_autoaccept
        if required_votes and self.get_num_votes() >= required_votes:
            self.accept(request, "suggestion.vote")

    def get_checks(self):
        # Build fake unit to run checks
        fake_unit = copy(self.unit)
        fake_unit.target = self.target
        fake_unit.state = STATE_TRANSLATED
        source = fake_unit.get_source_plurals()
        target = fake_unit.get_target_plurals()

        result = []
        for check, check_obj in CHECKS.target.items():
            if check_obj.check_target(source, target, fake_unit):
                result.append(
                    Check(unit=fake_unit, dismissed=False, check=check))
        return result
예제 #17
0
class Invoice(models.Model):
    CURRENCY_EUR = 0
    CURRENCY_BTC = 1
    CURRENCY_USD = 2
    CURRENCY_CZK = 3

    billing = models.ForeignKey(Billing, on_delete=models.deletion.CASCADE)
    start = models.DateField()
    end = models.DateField()
    amount = models.FloatField()
    currency = models.IntegerField(
        choices=(
            (CURRENCY_EUR, "EUR"),
            (CURRENCY_BTC, "mBTC"),
            (CURRENCY_USD, "USD"),
            (CURRENCY_CZK, "CZK"),
        ),
        default=CURRENCY_EUR,
    )
    ref = models.CharField(blank=True, max_length=50)
    note = models.TextField(blank=True)
    # Payment detailed information, used for integration
    # with payment processor
    payment = JSONField(editable=False, default={})

    objects = InvoiceQuerySet.as_manager()

    def __str__(self):
        return "{0} - {1}: {2}".format(
            self.start, self.end, self.billing if self.billing_id else None)

    @cached_property
    def filename(self):
        if self.ref:
            return "{0}.pdf".format(self.ref)
        return None

    @cached_property
    def full_filename(self):
        return os.path.join(settings.INVOICE_PATH, self.filename)

    @cached_property
    def filename_valid(self):
        return os.path.exists(self.full_filename)

    def clean(self):
        if self.end is None or self.start is None:
            return

        if self.end <= self.start:
            raise ValidationError("Start has be to before end!")

        if not self.billing_id:
            return

        overlapping = Invoice.objects.filter(
            (Q(start__lte=self.end) & Q(end__gte=self.end))
            | (Q(start__lte=self.start) & Q(end__gte=self.start))).filter(
                billing=self.billing)

        if self.pk:
            overlapping = overlapping.exclude(pk=self.pk)

        if overlapping.exists():
            raise ValidationError("Overlapping invoices exist: {0}".format(
                ", ".join(str(x) for x in overlapping)))
예제 #18
0
class Billing(models.Model):
    STATE_ACTIVE = 0
    STATE_TRIAL = 1
    STATE_EXPIRED = 2
    STATE_TERMINATED = 3

    EXPIRING_STATES = (STATE_TRIAL, )

    plan = models.ForeignKey(Plan,
                             on_delete=models.deletion.CASCADE,
                             verbose_name=_("Billing plan"))
    projects = models.ManyToManyField(Project,
                                      blank=True,
                                      verbose_name=_("Billed projects"))
    owners = models.ManyToManyField(User,
                                    blank=True,
                                    verbose_name=_("Billing owners"))
    state = models.IntegerField(
        choices=(
            (STATE_ACTIVE, _("Active")),
            (STATE_TRIAL, _("Trial")),
            (STATE_EXPIRED, _("Expired")),
            (STATE_TERMINATED, _("Terminated")),
        ),
        default=STATE_ACTIVE,
        verbose_name=_("Billing state"),
    )
    expiry = models.DateTimeField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_("Trial expiry date"),
        help_text=
        "After expiry removal with 15 days grace period is scheduled.",
    )
    removal = models.DateTimeField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_("Scheduled removal"),
        help_text="This is automatically set after trial expiry.",
    )
    paid = models.BooleanField(default=True,
                               verbose_name=_("Paid"),
                               editable=False)
    # Translators: Whether the package is inside actual (hard) limits
    in_limits = models.BooleanField(default=True,
                                    verbose_name=_("In limits"),
                                    editable=False)
    grace_period = models.IntegerField(
        default=0, verbose_name=_("Grace period for payments"))
    # Payment detailed information, used for integration
    # with payment processor
    payment = JSONField(editable=False, default={})

    objects = BillingManager.from_queryset(BillingQuerySet)()

    def __str__(self):
        projects = self.projects_display
        owners = self.owners.order()
        if projects:
            base = projects
        elif owners:
            base = ", ".join(x.get_author_name(False) for x in owners)
        else:
            base = "Unassigned"
        return "{0} ({1})".format(base, self.plan)

    def save(
        self,
        force_insert=False,
        force_update=False,
        using=None,
        update_fields=None,
        skip_limits=False,
    ):
        if not skip_limits and self.pk:
            self.check_limits(save=False)
        super().save(
            force_insert=force_insert,
            force_update=force_update,
            using=using,
            update_fields=update_fields,
        )

    def get_absolute_url(self):
        return "{}#billing-{}".format(reverse("billing"), self.pk)

    @cached_property
    def all_projects(self):
        return prefetch_stats(self.projects.order())

    @cached_property
    def projects_display(self):
        return ", ".join(str(x) for x in self.all_projects)

    @property
    def is_trial(self):
        return self.state == Billing.STATE_TRIAL

    @cached_property
    def can_be_paid(self):
        if self.state in (Billing.STATE_ACTIVE, Billing.STATE_TRIAL):
            return True
        return self.count_projects > 0

    @cached_property
    def monthly_changes(self):
        return sum(project.stats.monthly_changes
                   for project in self.all_projects)

    monthly_changes.short_description = _("Changes in last month")

    @cached_property
    def total_changes(self):
        return sum(project.stats.total_changes
                   for project in self.all_projects)

    total_changes.short_description = _("Number of changes")

    @cached_property
    def count_projects(self):
        return len(self.all_projects)

    def display_projects(self):
        return "{0} / {1}".format(self.count_projects,
                                  self.plan.display_limit_projects)

    display_projects.short_description = _("Projects")

    @cached_property
    def count_strings(self):
        return sum(p.stats.source_strings for p in self.all_projects)

    def display_strings(self):
        return "{0} / {1}".format(self.count_strings,
                                  self.plan.display_limit_strings)

    display_strings.short_description = _("Source strings")

    @cached_property
    def count_words(self):
        return sum(p.stats.source_words for p in self.all_projects)

    def display_words(self):
        return "{0}".format(self.count_words)

    display_words.short_description = _("Source words")

    @cached_property
    def count_languages(self):
        if not self.all_projects:
            return 0
        return max(p.stats.languages for p in self.all_projects)

    def display_languages(self):
        return "{0} / {1}".format(self.count_languages,
                                  self.plan.display_limit_languages)

    display_languages.short_description = _("Languages")

    def flush_cache(self):
        keys = list(self.__dict__.keys())
        for key in keys:
            if key.startswith("count_"):
                del self.__dict__[key]

    def check_in_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return ((plan.limit_projects == 0
                 or self.count_projects <= plan.limit_projects)
                and (plan.limit_strings == 0
                     or self.count_strings <= plan.limit_strings)
                and (plan.limit_languages == 0
                     or self.count_languages <= plan.limit_languages))

    def check_expiry(self):
        return (self.state in Billing.EXPIRING_STATES and self.expiry
                and self.expiry < timezone.now())

    def unit_count(self):
        return sum(p.stats.all for p in self.all_projects)

    unit_count.short_description = _("Number of strings")

    def last_invoice(self):
        try:
            invoice = self.invoice_set.order_by("-start")[0]
            return "{0} - {1}".format(invoice.start, invoice.end)
        except IndexError:
            return _("N/A")

    last_invoice.short_description = _("Last invoice")

    def in_display_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return ((plan.display_limit_projects == 0
                 or self.count_projects <= plan.display_limit_projects)
                and (plan.display_limit_strings == 0
                     or self.count_strings <= plan.display_limit_strings)
                and (plan.display_limit_languages == 0
                     or self.count_languages <= plan.display_limit_languages))

    in_display_limits.boolean = True
    # Translators: Whether the package is inside displayed (soft) limits
    in_display_limits.short_description = _("In display limits")

    def check_payment_status(self, grace=None):
        """Check current payment status.

        Compared to paid attribute, this does not include grace period.
        """
        end = timezone.now() - timedelta(days=grace or self.grace_period)
        return (self.plan.is_free
                or self.invoice_set.filter(end__gte=end).exists()
                or self.state == Billing.STATE_TRIAL)

    def check_limits(self, grace=30, save=True):
        self.flush_cache()
        in_limits = self.check_in_limits()
        paid = self.check_payment_status(grace)
        modified = False

        if self.check_expiry():
            self.state = Billing.STATE_EXPIRED
            self.expiry = None
            self.removal = timezone.now() + timedelta(days=30)
            modified = True

        if self.state not in Billing.EXPIRING_STATES and self.expiry:
            self.expiry = None
            modified = True

        if self.in_limits != in_limits or self.paid != paid:
            self.in_limits = in_limits
            self.paid = paid
            modified = True

        if save and modified:
            self.save(skip_limits=True)

    def is_active(self):
        return self.state in (Billing.STATE_ACTIVE, Billing.STATE_TRIAL)

    def get_notify_users(self):
        users = self.owners.distinct()
        for project in self.projects.iterator():
            users |= User.objects.having_perm("billing.view", project)
        return users

    def _get_libre_checklist(self):
        yield LibreCheck(
            self.count_projects == 1,
            ngettext("Contains %d project", "Contains %d projects",
                     self.count_projects) % self.count_projects,
        )
        for project in self.all_projects:
            yield LibreCheck(
                bool(project.web),
                mark_safe(
                    '<a href="{0}">{1}</a>, <a href="{2}">{2}</a>'.format(
                        escape(project.get_absolute_url()),
                        escape(project),
                        escape(project.web),
                    )),
            )
        components = Component.objects.filter(project__in=self.all_projects)
        yield LibreCheck(
            len(components) > 0,
            ngettext("Contains %d component", "Contains %d components",
                     len(components)) % len(components),
        )
        for component in components:
            yield LibreCheck(
                component.libre_license,
                mark_safe("""
                    <a href="{0}">{1}</a>,
                    <a href="{2}">{3}</a>,
                    <a href="{4}">{4}</a>,
                    {5}""".format(
                    escape(component.get_absolute_url()),
                    escape(component.name),
                    escape(component.license_url or "#"),
                    escape(component.get_license_display()
                           or _("Missing license")),
                    escape(component.repo),
                    escape(component.get_file_format_display()),
                )),
            )

    @cached_property
    def libre_checklist(self):
        return list(self._get_libre_checklist())

    @property
    def valid_libre(self):
        return all(self.libre_checklist)
예제 #19
0
파일: models.py 프로젝트: yante/weblate
class Billing(models.Model):
    STATE_ACTIVE = 0
    STATE_TRIAL = 1
    STATE_EXPIRED = 2
    STATE_TERMINATED = 3

    EXPIRING_STATES = (STATE_TRIAL,)

    plan = models.ForeignKey(
        Plan, on_delete=models.deletion.CASCADE, verbose_name=_("Billing plan")
    )
    projects = models.ManyToManyField(
        Project, blank=True, verbose_name=_("Billed projects")
    )
    owners = models.ManyToManyField(User, blank=True, verbose_name=_("Billing owners"))
    state = models.IntegerField(
        choices=(
            (STATE_ACTIVE, _("Active")),
            (STATE_TRIAL, _("Trial")),
            (STATE_EXPIRED, _("Expired")),
            (STATE_TERMINATED, _("Terminated")),
        ),
        default=STATE_ACTIVE,
        verbose_name=_("Billing state"),
    )
    expiry = models.DateTimeField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_("Trial expiry date"),
        help_text="After expiry removal with 15 days grace period is scheduled.",
    )
    removal = models.DateTimeField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_("Scheduled removal"),
        help_text="This is automatically set after trial expiry.",
    )
    paid = models.BooleanField(default=True, verbose_name=_("Paid"), editable=False)
    # Translators: Whether the package is inside actual (hard) limits
    in_limits = models.BooleanField(
        default=True, verbose_name=_("In limits"), editable=False
    )
    grace_period = models.IntegerField(
        default=0, verbose_name=_("Grace period for payments")
    )
    # Payment detailed information, used for integration
    # with payment processor
    payment = JSONField(editable=False, default={})

    objects = BillingManager.from_queryset(BillingQuerySet)()

    def __str__(self):
        projects = self.projects.order()
        owners = self.owners.order()
        if projects:
            base = ", ".join(str(x) for x in projects)
        elif owners:
            base = ", ".join(x.get_author_name(False) for x in owners)
        else:
            base = "Unassigned"
        return "{0} ({1})".format(base, self.plan)

    def get_absolute_url(self):
        return "{}#billing-{}".format(reverse("billing"), self.pk)

    @cached_property
    def can_be_paid(self):
        if self.state in (Billing.STATE_ACTIVE, Billing.STATE_TRIAL):
            return True
        return self.count_projects > 0

    def count_changes(self, interval):
        return Change.objects.filter(
            component__project__in=self.projects.all(),
            timestamp__gt=timezone.now() - interval,
        ).count()

    @cached_property
    def count_changes_1m(self):
        return self.count_changes(timedelta(days=31))

    count_changes_1m.short_description = _("Changes in last month")

    @cached_property
    def count_changes_1q(self):
        return self.count_changes(timedelta(days=93))

    count_changes_1q.short_description = _("Changes in last quarter")

    @cached_property
    def count_changes_1y(self):
        return self.count_changes(timedelta(days=365))

    count_changes_1y.short_description = _("Changes in last year")

    @cached_property
    def count_projects(self):
        return self.projects.count()

    def display_projects(self):
        return "{0} / {1}".format(self.count_projects, self.plan.display_limit_projects)

    display_projects.short_description = _("Projects")

    @cached_property
    def count_strings(self):
        return sum(p.stats.source_strings for p in self.projects.iterator())

    def display_strings(self):
        return "{0} / {1}".format(self.count_strings, self.plan.display_limit_strings)

    display_strings.short_description = _("Source strings")

    @cached_property
    def count_words(self):
        return sum(p.stats.source_words for p in self.projects.iterator())

    def display_words(self):
        return "{0}".format(self.count_words)

    display_words.short_description = _("Source words")

    @cached_property
    def count_languages(self):
        return (
            Language.objects.filter(
                translation__component__project__in=self.projects.all()
            )
            .distinct()
            .count()
        )

    def display_languages(self):
        return "{0} / {1}".format(
            self.count_languages, self.plan.display_limit_languages
        )

    display_languages.short_description = _("Languages")

    def flush_cache(self):
        keys = list(self.__dict__.keys())
        for key in keys:
            if key.startswith("count_"):
                del self.__dict__[key]

    def check_in_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return (
            (plan.limit_projects == 0 or self.count_projects <= plan.limit_projects)
            and (plan.limit_strings == 0 or self.count_strings <= plan.limit_strings)
            and (
                plan.limit_languages == 0
                or self.count_languages <= plan.limit_languages
            )
        )

    def check_expiry(self):
        return (
            self.state in Billing.EXPIRING_STATES
            and self.expiry
            and self.expiry < timezone.now()
        )

    def unit_count(self):
        return Unit.objects.filter(
            translation__component__project__in=self.projects.all()
        ).count()

    unit_count.short_description = _("Number of strings")

    def last_invoice(self):
        try:
            invoice = self.invoice_set.order_by("-start")[0]
            return "{0} - {1}".format(invoice.start, invoice.end)
        except IndexError:
            return _("N/A")

    last_invoice.short_description = _("Last invoice")

    def in_display_limits(self, plan=None):
        if plan is None:
            plan = self.plan
        return (
            (
                plan.display_limit_projects == 0
                or self.count_projects <= plan.display_limit_projects
            )
            and (
                plan.display_limit_strings == 0
                or self.count_strings <= plan.display_limit_strings
            )
            and (
                plan.display_limit_languages == 0
                or self.count_languages <= plan.display_limit_languages
            )
        )

    in_display_limits.boolean = True
    # Translators: Whether the package is inside displayed (soft) limits
    in_display_limits.short_description = _("In display limits")

    def check_payment_status(self, grace=None):
        """Check current payment status.

        Compared to paid attribute, this does not include grace period.
        """
        end = timezone.now() - timedelta(days=grace or self.grace_period)
        return (
            self.plan.is_free
            or self.invoice_set.filter(end__gte=end).exists()
            or self.state == Billing.STATE_TRIAL
        )

    def check_limits(self, grace=30, save=True):
        self.flush_cache()
        in_limits = self.check_in_limits()
        paid = self.check_payment_status(grace)
        modified = False

        if self.check_expiry():
            self.state = Billing.STATE_EXPIRED
            self.expiry = None
            self.removal = timezone.now() + timedelta(days=30)
            modified = True

        if self.state not in Billing.EXPIRING_STATES and self.expiry:
            self.expiry = None
            modified = True

        if self.in_limits != in_limits or self.paid != paid:
            self.in_limits = in_limits
            self.paid = paid
            modified = True

        if save and modified:
            self.save(skip_limits=True)

    def save(self, *args, **kwargs):
        if not kwargs.pop("skip_limits", False) and self.pk:
            self.check_limits(save=False)
        super().save(*args, **kwargs)

    def is_active(self):
        return self.state in (Billing.STATE_ACTIVE, Billing.STATE_TRIAL)

    def get_notify_users(self):
        users = self.owners.distinct()
        for project in self.projects.iterator():
            users |= User.objects.having_perm("billing.view", project)
        return users
예제 #20
0
class Payment(models.Model):
    NEW = 1
    PENDING = 2
    REJECTED = 3
    ACCEPTED = 4
    PROCESSED = 5

    uuid = models.UUIDField(primary_key=True,
                            default=uuid.uuid4,
                            editable=False)
    amount = models.IntegerField()
    description = models.TextField()
    recurring = models.CharField(
        choices=RECURRENCE_CHOICES,
        default='',
        blank=True,
        max_length=10,
    )
    created = models.DateTimeField(auto_now_add=True)
    state = models.IntegerField(choices=[
        (NEW, 'New'),
        (PENDING, 'Pending'),
        (REJECTED, 'Rejected'),
        (ACCEPTED, 'Accepted'),
        (PROCESSED, 'Processed'),
    ],
                                db_index=True,
                                default=NEW)
    backend = models.CharField(max_length=100, default='', blank=True)
    # Payment details from the gateway
    details = JSONField(default={}, blank=True)
    # Payment extra information from the origin
    extra = JSONField(default={}, blank=True)
    customer = models.ForeignKey(Customer,
                                 on_delete=models.deletion.CASCADE,
                                 blank=True)
    repeat = models.ForeignKey('Payment',
                               on_delete=models.deletion.CASCADE,
                               null=True,
                               blank=True)
    invoice = models.CharField(max_length=20, blank=True, default='')
    amount_fixed = models.BooleanField(blank=True, default=False)

    class Meta:
        ordering = ['-created']

    @cached_property
    def invoice_filename(self):
        return '{0}.pdf'.format(self.invoice)

    @cached_property
    def invoice_full_filename(self):
        return os.path.join(settings.PAYMENT_FAKTURACE, 'pdf',
                            self.invoice_filename)

    @cached_property
    def invoice_filename_valid(self):
        return os.path.exists(self.invoice_full_filename)

    @property
    def vat_amount(self):
        if self.customer.needs_vat and not self.amount_fixed:
            rate = 100 + self.customer.vat_rate
            return round(1.0 * rate * self.amount / 100, 2)
        return self.amount

    @property
    def amount_without_vat(self):
        if self.customer.needs_vat and self.amount_fixed:
            return 100.0 * self.amount / (100 + self.customer.vat_rate)
        return self.amount

    def get_payment_url(self):
        language = get_language()
        if language not in SUPPORTED_LANGUAGES:
            language = 'en'
        return settings.PAYMENT_REDIRECT_URL.format(language=language,
                                                    uuid=self.uuid)

    def repeat_payment(self, **kwargs):
        # Check if backend is still valid
        from wlhosted.payments.backends import get_backend
        try:
            get_backend(self.backend)
        except KeyError:
            return False

        with transaction.atomic(using='payments_db'):
            # Check for failed payments
            previous = Payment.objects.filter(repeat=self)
            if previous.exists():
                failures = previous.filter(state=Payment.REJECTED)
                try:
                    last_good = previous.filter(
                        state=Payment.PROCESSED).order_by('-created')[0]
                    failures = failures.filter(created__gt=last_good.created)
                except IndexError:
                    pass
                if failures.count() >= 3:
                    return False

            # Create new payment object
            extra = {}
            extra.update(self.extra)
            extra.update(kwargs)
            payment = Payment.objects.create(amount=self.amount,
                                             backend=self.backend,
                                             description=self.description,
                                             recurring='',
                                             customer=self.customer,
                                             amount_fixed=self.amount_fixed,
                                             repeat=self,
                                             extra=extra)
        return payment

    def trigger_remotely(self):
        # Trigger payment processing remotely
        requests.post(self.get_payment_url(),
                      allow_redirects=False,
                      data={
                          'method': self.backend,
                          'secret': settings.PAYMENT_SECRET,
                      })
예제 #21
0
class Change(models.Model, UserDisplayMixin):
    ACTION_UPDATE = 0
    ACTION_COMPLETE = 1
    ACTION_CHANGE = 2
    ACTION_COMMENT = 3
    ACTION_SUGGESTION = 4
    ACTION_NEW = 5
    ACTION_AUTO = 6
    ACTION_ACCEPT = 7
    ACTION_REVERT = 8
    ACTION_UPLOAD = 9
    ACTION_DICTIONARY_NEW = 10
    ACTION_DICTIONARY_EDIT = 11
    ACTION_DICTIONARY_UPLOAD = 12
    ACTION_NEW_SOURCE = 13
    ACTION_LOCK = 14
    ACTION_UNLOCK = 15
    ACTION_DUPLICATE_STRING = 16
    ACTION_COMMIT = 17
    ACTION_PUSH = 18
    ACTION_RESET = 19
    ACTION_MERGE = 20
    ACTION_REBASE = 21
    ACTION_FAILED_MERGE = 22
    ACTION_FAILED_REBASE = 23
    ACTION_PARSE_ERROR = 24
    ACTION_REMOVE_TRANSLATION = 25
    ACTION_SUGGESTION_DELETE = 26
    ACTION_REPLACE = 27
    ACTION_FAILED_PUSH = 28
    ACTION_SUGGESTION_CLEANUP = 29
    ACTION_SOURCE_CHANGE = 30
    ACTION_NEW_UNIT = 31
    ACTION_BULK_EDIT = 32
    ACTION_ACCESS_EDIT = 33
    ACTION_ADD_USER = 34
    ACTION_REMOVE_USER = 35
    ACTION_APPROVE = 36
    ACTION_MARKED_EDIT = 37
    ACTION_REMOVE_COMPONENT = 38
    ACTION_REMOVE_PROJECT = 39
    ACTION_DUPLICATE_LANGUAGE = 40
    ACTION_RENAME_PROJECT = 41
    ACTION_RENAME_COMPONENT = 42
    ACTION_MOVE_COMPONENT = 43
    ACTION_NEW_STRING = 44
    ACTION_NEW_CONTRIBUTOR = 45
    ACTION_MESSAGE = 46
    ACTION_ALERT = 47
    ACTION_ADDED_LANGUAGE = 48
    ACTION_REQUESTED_LANGUAGE = 49
    ACTION_CREATE_PROJECT = 50
    ACTION_CREATE_COMPONENT = 51
    ACTION_INVITE_USER = 52
    ACTION_HOOK = 53
    ACTION_REPLACE_UPLOAD = 54

    ACTION_CHOICES = (
        # Translators: Name of event in the history
        (ACTION_UPDATE, gettext_lazy("Resource update")),
        # Translators: Name of event in the history
        (ACTION_COMPLETE, gettext_lazy("Translation completed")),
        # Translators: Name of event in the history
        (ACTION_CHANGE, gettext_lazy("Translation changed")),
        # Translators: Name of event in the history
        (ACTION_NEW, gettext_lazy("New translation")),
        # Translators: Name of event in the history
        (ACTION_COMMENT, gettext_lazy("Comment added")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION, gettext_lazy("Suggestion added")),
        # Translators: Name of event in the history
        (ACTION_AUTO, gettext_lazy("Automatic translation")),
        # Translators: Name of event in the history
        (ACTION_ACCEPT, gettext_lazy("Suggestion accepted")),
        # Translators: Name of event in the history
        (ACTION_REVERT, gettext_lazy("Translation reverted")),
        # Translators: Name of event in the history
        (ACTION_UPLOAD, gettext_lazy("Translation uploaded")),
        # Translators: Name of event in the history
        (ACTION_DICTIONARY_NEW, gettext_lazy("Added to glossary")),
        # Translators: Name of event in the history
        (ACTION_DICTIONARY_EDIT, gettext_lazy("Glossary updated")),
        # Translators: Name of event in the history
        (ACTION_DICTIONARY_UPLOAD, gettext_lazy("Glossary uploaded")),
        # Translators: Name of event in the history
        (ACTION_NEW_SOURCE, gettext_lazy("New source string")),
        # Translators: Name of event in the history
        (ACTION_LOCK, gettext_lazy("Component locked")),
        # Translators: Name of event in the history
        (ACTION_UNLOCK, gettext_lazy("Component unlocked")),
        # Translators: Name of event in the history
        (ACTION_DUPLICATE_STRING, gettext_lazy("Found duplicated string")),
        # Translators: Name of event in the history
        (ACTION_COMMIT, gettext_lazy("Committed changes")),
        # Translators: Name of event in the history
        (ACTION_PUSH, gettext_lazy("Pushed changes")),
        # Translators: Name of event in the history
        (ACTION_RESET, gettext_lazy("Reset repository")),
        # Translators: Name of event in the history
        (ACTION_MERGE, gettext_lazy("Merged repository")),
        # Translators: Name of event in the history
        (ACTION_REBASE, gettext_lazy("Rebased repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_MERGE, gettext_lazy("Failed merge on repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_REBASE, gettext_lazy("Failed rebase on repository")),
        # Translators: Name of event in the history
        (ACTION_FAILED_PUSH, gettext_lazy("Failed push on repository")),
        # Translators: Name of event in the history
        (ACTION_PARSE_ERROR, gettext_lazy("Parse error")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_TRANSLATION, gettext_lazy("Removed translation")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION_DELETE, gettext_lazy("Suggestion removed")),
        # Translators: Name of event in the history
        (ACTION_REPLACE, gettext_lazy("Search and replace")),
        # Translators: Name of event in the history
        (ACTION_SUGGESTION_CLEANUP,
         gettext_lazy("Suggestion removed during cleanup")),
        # Translators: Name of event in the history
        (ACTION_SOURCE_CHANGE, gettext_lazy("Source string changed")),
        # Translators: Name of event in the history
        (ACTION_NEW_UNIT, gettext_lazy("New string added")),
        # Translators: Name of event in the history
        (ACTION_BULK_EDIT, gettext_lazy("Bulk status change")),
        # Translators: Name of event in the history
        (ACTION_ACCESS_EDIT, gettext_lazy("Changed visibility")),
        # Translators: Name of event in the history
        (ACTION_ADD_USER, gettext_lazy("Added user")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_USER, gettext_lazy("Removed user")),
        # Translators: Name of event in the history
        (ACTION_APPROVE, gettext_lazy("Translation approved")),
        # Translators: Name of event in the history
        (ACTION_MARKED_EDIT, gettext_lazy("Marked for edit")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_COMPONENT, gettext_lazy("Removed component")),
        # Translators: Name of event in the history
        (ACTION_REMOVE_PROJECT, gettext_lazy("Removed project")),
        # Translators: Name of event in the history
        (ACTION_DUPLICATE_LANGUAGE, gettext_lazy("Found duplicated language")),
        # Translators: Name of event in the history
        (ACTION_RENAME_PROJECT, gettext_lazy("Renamed project")),
        # Translators: Name of event in the history
        (ACTION_RENAME_COMPONENT, gettext_lazy("Renamed component")),
        # Translators: Name of event in the history
        (ACTION_MOVE_COMPONENT, gettext_lazy("Moved component")),
        # Translators: Name of event in the history
        (ACTION_NEW_STRING, gettext_lazy("New string to translate")),
        # Translators: Name of event in the history
        (ACTION_NEW_CONTRIBUTOR, gettext_lazy("New contributor")),
        # Translators: Name of event in the history
        (ACTION_MESSAGE, gettext_lazy("New announcement")),
        # Translators: Name of event in the history
        (ACTION_ALERT, gettext_lazy("New alert")),
        # Translators: Name of event in the history
        (ACTION_ADDED_LANGUAGE, gettext_lazy("Added new language")),
        # Translators: Name of event in the history
        (ACTION_REQUESTED_LANGUAGE, gettext_lazy("Requested new language")),
        # Translators: Name of event in the history
        (ACTION_CREATE_PROJECT, gettext_lazy("Created project")),
        # Translators: Name of event in the history
        (ACTION_CREATE_COMPONENT, gettext_lazy("Created component")),
        # Translators: Name of event in the history
        (ACTION_INVITE_USER, gettext_lazy("Invited user")),
        # Translators: Name of event in the history
        (ACTION_HOOK, gettext_lazy("Received repository notification")),
        # Translators: Name of event in the history
        (ACTION_REPLACE_UPLOAD, gettext_lazy("Replaced file by upload")),
    )

    # Actions which can be reverted
    ACTIONS_REVERTABLE = {
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_CHANGE,
        ACTION_UPLOAD,
        ACTION_NEW,
        ACTION_REPLACE,
        ACTION_AUTO,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
    }

    # Content changes considered when looking for last author
    ACTIONS_CONTENT = {
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_BULK_EDIT,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
    }

    # Actions considered as being translated in consistency check
    ACTIONS_TRANSLATED = {
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_APPROVE,
    }

    # Actions shown on the repository management page
    ACTIONS_REPOSITORY = {
        ACTION_COMMIT,
        ACTION_PUSH,
        ACTION_RESET,
        ACTION_MERGE,
        ACTION_REBASE,
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
        ACTION_LOCK,
        ACTION_UNLOCK,
        ACTION_DUPLICATE_LANGUAGE,
    }

    # Actions where target is rendered as translation string
    ACTIONS_SHOW_CONTENT = {
        ACTION_SUGGESTION,
        ACTION_SUGGESTION_DELETE,
        ACTION_SUGGESTION_CLEANUP,
        ACTION_NEW_UNIT,
        ACTION_DICTIONARY_NEW,
        ACTION_DICTIONARY_EDIT,
    }

    # Actions indicating a repository merge failure
    ACTIONS_MERGE_FAILURE = {
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
    }

    unit = models.ForeignKey("Unit",
                             null=True,
                             on_delete=models.deletion.CASCADE)
    language = models.ForeignKey("lang.Language",
                                 null=True,
                                 on_delete=models.deletion.CASCADE)
    project = models.ForeignKey("Project",
                                null=True,
                                on_delete=models.deletion.CASCADE)
    component = models.ForeignKey("Component",
                                  null=True,
                                  on_delete=models.deletion.CASCADE)
    translation = models.ForeignKey("Translation",
                                    null=True,
                                    on_delete=models.deletion.CASCADE)
    dictionary = models.ForeignKey("Dictionary",
                                   null=True,
                                   on_delete=models.deletion.CASCADE)
    comment = models.ForeignKey("Comment",
                                null=True,
                                on_delete=models.deletion.SET_NULL)
    suggestion = models.ForeignKey("Suggestion",
                                   null=True,
                                   on_delete=models.deletion.SET_NULL)
    announcement = models.ForeignKey("Announcement",
                                     null=True,
                                     on_delete=models.deletion.SET_NULL)
    alert = models.ForeignKey("Alert",
                              null=True,
                              on_delete=models.deletion.SET_NULL)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             null=True,
                             on_delete=models.deletion.CASCADE)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        related_name="author_set",
        on_delete=models.deletion.CASCADE,
    )
    timestamp = models.DateTimeField(auto_now_add=True)
    action = models.IntegerField(choices=ACTION_CHOICES,
                                 default=ACTION_CHANGE,
                                 db_index=True)
    target = models.TextField(default="", blank=True)
    old = models.TextField(default="", blank=True)
    details = JSONField()

    objects = ChangeManager.from_queryset(ChangeQuerySet)()

    class Meta:
        app_label = "trans"
        index_together = [
            ("timestamp", "translation"),
        ]

    def __init__(self, *args, **kwargs):
        self.notify_state = {}
        super().__init__(*args, **kwargs)

    def __str__(self):
        return _("%(action)s at %(time)s on %(translation)s by %(user)s") % {
            "action": self.get_action_display(),
            "time": self.timestamp,
            "translation": self.translation,
            "user": self.get_user_display(False),
        }

    def is_merge_failure(self):
        return self.action in self.ACTIONS_MERGE_FAILURE

    def get_absolute_url(self):
        """Return link either to unit or translation."""
        if self.unit is not None:
            return self.unit.get_absolute_url()
        if self.translation is not None:
            return self.translation.get_absolute_url()
        if self.component is not None:
            return self.component.get_absolute_url()
        if self.dictionary is not None:
            return self.dictionary.get_parent_url()
        if self.project is not None:
            return self.project.get_absolute_url()
        return None

    def can_revert(self):
        return (self.unit is not None and self.old
                and self.action in self.ACTIONS_REVERTABLE)

    def show_source(self):
        """Whether to show content as source change."""
        return self.action == self.ACTION_SOURCE_CHANGE

    def show_content(self):
        """Whether to show content as translation."""
        return (self.action in self.ACTIONS_SHOW_CONTENT
                or self.action in self.ACTIONS_REVERTABLE)

    def get_details_display(self):
        from weblate.utils.markdown import render_markdown

        if not self.details:
            return ""
        user_actions = {
            self.ACTION_ADD_USER,
            self.ACTION_INVITE_USER,
            self.ACTION_REMOVE_USER,
        }

        if self.action == self.ACTION_ACCESS_EDIT:
            for number, name in Project.ACCESS_CHOICES:
                if number == self.details["access_control"]:
                    return name
            return "Unknonwn {}".format(self.details["access_control"])
        if self.action in user_actions:
            if "group" in self.details:
                return "{username} ({group})".format(**self.details)
            return self.details["username"]
        if self.action in (
                self.ACTION_ADDED_LANGUAGE,
                self.ACTION_REQUESTED_LANGUAGE,
        ):  # noqa: E501
            try:
                return Language.objects.get(code=self.details["language"])
            except Language.DoesNotExist:
                return self.details["language"]
        if self.action == self.ACTION_ALERT:
            try:
                return ALERTS[self.details["alert"]].verbose
            except KeyError:
                return self.details["alert"]
        if self.action == self.ACTION_PARSE_ERROR:
            return "{filename}: {error_message}".format(**self.details)
        if self.action == self.ACTION_HOOK:
            return "{service_long_name}: {repo_url}, {branch}".format(
                **self.details)
        if self.action == self.ACTION_COMMENT and "comment" in self.details:
            return render_markdown(self.details["comment"])

        return ""

    def save(self, *args, **kwargs):
        from weblate.accounts.tasks import notify_change

        if self.unit:
            self.translation = self.unit.translation
        if self.translation:
            self.component = self.translation.component
            self.language = self.translation.language
        if self.component:
            self.project = self.component.project
        if self.dictionary:
            self.project = self.dictionary.project
            self.language = self.dictionary.language
        super().save(*args, **kwargs)
        transaction.on_commit(lambda: notify_change.delay(self.pk))

    def get_distance(self):
        return damerau_levenshtein_distance(self.old, self.target)
예제 #22
0
class AuditLog(models.Model):
    """User audit log storage."""

    user = models.ForeignKey(User, on_delete=models.deletion.CASCADE)
    activity = models.CharField(
        max_length=20,
        choices=[(a, a) for a in sorted(ACCOUNT_ACTIVITY.keys())],
        db_index=True,
    )
    params = JSONField()
    address = models.GenericIPAddressField(null=True)
    user_agent = models.CharField(max_length=200, default='')
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)

    objects = AuditLogManager.from_queryset(AuditLogQuerySet)()

    def get_params(self):
        result = {}
        result.update(self.params)
        if 'method' in result:
            result['method'] = ugettext(result['method'])
        return result

    def get_message(self):
        method = self.params.get('method')
        activity = self.activity
        if activity in ACCOUNT_ACTIVITY_METHOD.get(method, {}):
            message = ACCOUNT_ACTIVITY_METHOD[method][activity]
        else:
            message = ACCOUNT_ACTIVITY[activity]
        return message.format(**self.get_params())

    get_message.short_description = _('Account activity')

    def get_extra_message(self):
        if self.activity in EXTRA_MESSAGES:
            return EXTRA_MESSAGES[self.activity].format(**self.params)
        return None

    def should_notify(self):
        return self.activity in NOTIFY_ACTIVITY

    def __str__(self):
        return '{0} for {1} from {2}'.format(self.activity, self.user.username,
                                             self.address)

    def check_rate_limit(self, request):
        """Check whether the activity should be rate limited."""
        if self.activity == 'failed-auth' and self.user.has_usable_password():
            failures = AuditLog.objects.get_after(self.user, 'login',
                                                  'failed-auth')
            if failures.count() >= settings.AUTH_LOCK_ATTEMPTS:
                self.user.set_unusable_password()
                self.user.save(update_fields=['password'])
                AuditLog.objects.create(self.user, request, 'locked')
                return True

        elif self.activity == 'reset-request':
            failures = AuditLog.objects.filter(
                timestamp__gte=timezone.now() - datetime.timedelta(days=1),
                activity='reset-request',
            )
            if failures.count() >= settings.AUTH_LOCK_ATTEMPTS:
                return True

        return False

    def save(self, *args, **kwargs):
        super(AuditLog, self).save(*args, **kwargs)
        if self.should_notify():
            notify_auditlog.delay(self.pk, self.user.email)
예제 #23
0
class Suggestion(UnitData, UserDisplayMixin):
    target = models.TextField()
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.deletion.CASCADE,
    )
    userdetails = JSONField()
    language = models.ForeignKey(Language, on_delete=models.deletion.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)

    votes = models.ManyToManyField(
        settings.AUTH_USER_MODEL, through='Vote', related_name='user_votes'
    )

    objects = SuggestionManager.from_queryset(SuggestionQuerySet)()

    class Meta(object):
        app_label = 'trans'
        index_together = [('project', 'language', 'content_hash')]

    def __str__(self):
        return 'suggestion for {0} by {1}'.format(
            self.content_hash, self.user.username if self.user else 'unknown'
        )

    @transaction.atomic
    def accept(self, translation, request, permission='suggestion.accept'):
        allunits = translation.unit_set.select_for_update().filter(
            content_hash=self.content_hash
        )
        failure = False
        for unit in allunits:
            if not request.user.has_perm(permission, unit):
                failure = True
                messages.error(request, _('Failed to accept suggestion!'))
                continue

            # Skip if there is no change
            if unit.target == self.target and unit.state >= STATE_TRANSLATED:
                continue

            unit.target = self.target
            unit.state = STATE_TRANSLATED
            unit.save_backend(request.user, change_action=Change.ACTION_ACCEPT)

        if not failure:
            self.delete()

    def delete_log(self, user, change=Change.ACTION_SUGGESTION_DELETE, is_spam=False):
        """Delete with logging change"""
        if is_spam and self.userdetails:
            report_spam(
                self.userdetails['address'], self.userdetails['agent'], self.target
            )
        for unit in self.related_units:
            Change.objects.create(
                unit=unit, action=change, user=user, target=self.target, author=user
            )
        self.delete()

    def get_num_votes(self):
        """Return number of votes."""
        return self.vote_set.aggregate(Sum('value'))['value__sum'] or 0

    def add_vote(self, translation, request, value):
        """Add (or updates) vote for a suggestion."""
        if not request.user.is_authenticated:
            return

        vote, created = Vote.objects.get_or_create(
            suggestion=self, user=request.user, defaults={'value': value}
        )
        if not created or vote.value != value:
            vote.value = value
            vote.save()

        # Automatic accepting
        required_votes = translation.component.suggestion_autoaccept
        if required_votes and self.get_num_votes() >= required_votes:
            self.accept(translation, request, 'suggestion.vote')
예제 #24
0
class Addon(models.Model):
    component = models.ForeignKey(Component, on_delete=models.deletion.CASCADE)
    name = models.CharField(max_length=100)
    configuration = JSONField()
    state = JSONField()
    project_scope = models.BooleanField(default=False, db_index=True)
    repo_scope = models.BooleanField(default=False, db_index=True)

    objects = AddonQuerySet.as_manager()

    class Meta:
        verbose_name = "add-on"
        verbose_name_plural = "add-ons"

    def __str__(self):
        return f"{self.addon.verbose}: {self.component}"

    def save(
        self, force_insert=False, force_update=False, using=None, update_fields=None
    ):
        cls = self.addon_class
        self.project_scope = cls.project_scope
        self.repo_scope = cls.repo_scope
        if self.component:
            # Reallocate to repository
            if self.repo_scope and self.component.linked_component:
                self.component = self.component.linked_component
            # Clear add-on cache
            self.component.drop_addons_cache()
        return super().save(
            force_insert=force_insert,
            force_update=force_update,
            using=using,
            update_fields=update_fields,
        )

    def get_absolute_url(self):
        return reverse(
            "addon-detail",
            kwargs={
                "project": self.component.project.slug,
                "component": self.component.slug,
                "pk": self.pk,
            },
        )

    def configure_events(self, events):
        for event in events:
            Event.objects.get_or_create(addon=self, event=event)
        self.event_set.exclude(event__in=events).delete()

    @cached_property
    def addon_class(self):
        return ADDONS[self.name]

    @cached_property
    def addon(self):
        return self.addon_class(self)

    def delete(self, *args, **kwargs):
        # Delete any addon alerts
        if self.addon.alert:
            self.component.delete_alert(self.addon.alert)
        super().delete(*args, **kwargs)

    def disable(self):
        self.component.log_warning(
            "disabling no longer compatible add-on: %s", self.name
        )
        self.delete()