示例#1
0
class AutoComponentList(models.Model):
    project_match = RegexField(
        verbose_name=_('Project regular expression'),
        max_length=200,
        default='^$',
        help_text=_('Regular expression which is used to match project slug.'),
    )
    component_match = RegexField(
        verbose_name=_('Component regular expression'),
        max_length=200,
        default='^$',
        help_text=_(
            'Regular expression which is used to match component slug.'),
    )
    componentlist = models.ForeignKey(
        ComponentList,
        verbose_name=_('Component list to assign'),
    )

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

    def check_match(self, component):
        if not re.match(self.project_match, component.project.slug):
            return
        if not re.match(self.component_match, component.slug):
            return
        self.componentlist.components.add(component)

    class Meta(object):
        verbose_name = _('Automatic component list assignment')
        verbose_name_plural = _('Automatic component list assignments')
示例#2
0
class AutoComponentList(models.Model):
    project_match = RegexField(
        verbose_name=_("Project regular expression"),
        max_length=200,
        default="^$",
        help_text=_("Regular expression which is used to match project slug."),
    )
    component_match = RegexField(
        verbose_name=_("Component regular expression"),
        max_length=200,
        default="^$",
        help_text=_(
            "Regular expression which is used to match component slug."),
    )
    componentlist = models.ForeignKey(
        ComponentList,
        verbose_name=_("Component list to assign"),
        on_delete=models.deletion.CASCADE,
    )

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

    def check_match(self, component):
        if not re.match(self.project_match, component.project.slug):
            return
        if not re.match(self.component_match, component.slug):
            return
        self.componentlist.components.add(component)

    class Meta:
        verbose_name = _("Automatic component list assignment")
        verbose_name_plural = _("Automatic component list assignments")
示例#3
0
class Shaping(models.Model):
    component = models.ForeignKey("Component", on_delete=models.deletion.CASCADE)
    shaping_regex = RegexField(max_length=190)
    key = models.CharField(max_length=190, db_index=True)

    class Meta(object):
        unique_together = (("key", "component", "shaping_regex"),)

    def __str__(self):
        return '{}: {}'.format(self.component, self.key)
示例#4
0
class Variant(models.Model):
    component = models.ForeignKey("Component", on_delete=models.deletion.CASCADE)
    variant_regex = RegexField(max_length=190)
    key = models.CharField(max_length=190, db_index=True)

    class Meta:
        unique_together = (("key", "component", "variant_regex"),)
        verbose_name = "variant definition"
        verbose_name_plural = "variant definitions"

    def __str__(self):
        return f"{self.component}: {self.key}"
示例#5
0
class Shaping(models.Model):
    component = models.ForeignKey("Component",
                                  on_delete=models.deletion.CASCADE)
    shaping_regex = RegexField(max_length=190)
    key = models.CharField(max_length=190, db_index=True)

    class Meta:
        unique_together = (("key", "component", "shaping_regex"), )
        verbose_name = "shaping definition"
        verbose_name_plural = "shaping definitions"

    def __str__(self):
        return "{}: {}".format(self.component, self.key)
示例#6
0
class Variant(models.Model):
    component = models.ForeignKey("Component", on_delete=models.deletion.CASCADE)
    variant_regex = RegexField(max_length=190, blank=True)
    # This really should be a TextField, but it does not work with unique
    # index and MySQL
    key = models.CharField(max_length=576)
    defining_units = models.ManyToManyField("Unit", related_name="defined_variants")

    class Meta:
        unique_together = (("key", "component", "variant_regex"),)
        verbose_name = "variant definition"
        verbose_name_plural = "variant definitions"

    def __str__(self):
        return f"{self.component}: {self.key}"
示例#7
0
文件: models.py 项目: sou-y/weblate
class AutoGroup(models.Model):
    match = RegexField(
        verbose_name=_("E-mail regular expression"),
        max_length=200,
        default="^.*$",
        help_text=_("Regular expression used to match user e-mail."),
    )
    group = models.ForeignKey(Group,
                              verbose_name=_("Group to assign"),
                              on_delete=models.deletion.CASCADE)

    class Meta:
        verbose_name = _("Automatic group assignment")
        verbose_name_plural = _("Automatic group assignments")

    def __str__(self):
        return "Automatic rule for {0}".format(self.group)
示例#8
0
文件: models.py 项目: mul1sh/weblate
class AutoGroup(models.Model):
    match = RegexField(
        verbose_name=_('E-mail regular expression'),
        max_length=200,
        default='^.*$',
        help_text=_('Regular expression used to match user e-mail.'),
    )
    group = models.ForeignKey(Group,
                              verbose_name=_('Group to assign'),
                              on_delete=models.deletion.CASCADE)

    class Meta:
        verbose_name = _('Automatic group assignment')
        verbose_name_plural = _('Automatic group assignments')

    def __str__(self):
        return 'Automatic rule for {0}'.format(self.group)
示例#9
0
class AutoGroup(models.Model):
    match = RegexField(
        verbose_name=_("Regular expression for e-mail address"),
        max_length=200,
        default="^.*$",
        help_text=_(
            "Users with matching e-mail address will be added to this group."),
    )
    group = models.ForeignKey(Group,
                              verbose_name=_("Group to assign"),
                              on_delete=models.deletion.CASCADE)

    class Meta:
        verbose_name = _("Automatic group assignment")
        verbose_name_plural = _("Automatic group assignments")

    def __str__(self):
        return f"Automatic rule for {self.group}"
示例#10
0
class AutoGroup(models.Model):
    match = RegexField(
        verbose_name=_('Email regular expression'),
        max_length=200,
        default='^.*$',
        help_text=_('Regular expression which is used to match user email.'),
    )
    group = models.ForeignKey(
        Group,
        verbose_name=_('Group to assign'),
    )

    class Meta(object):
        verbose_name = _('Automatic group assignment')
        verbose_name_plural = _('Automatic group assignments')
        ordering = ('group__name', )

    def __str__(self):
        return 'Automatic rule for {0}'.format(self.group)
示例#11
0
class SubProject(models.Model, PercentMixin, URLMixin, PathMixin):
    name = models.CharField(
        verbose_name=ugettext_lazy('Component name'),
        max_length=100,
        help_text=ugettext_lazy('Name to display')
    )
    slug = models.SlugField(
        verbose_name=ugettext_lazy('URL slug'),
        db_index=True,
        max_length=100,
        help_text=ugettext_lazy('Name used in URLs and file names.')
    )
    project = models.ForeignKey(
        'Project',
        verbose_name=ugettext_lazy('Project'),
    )
    vcs = models.CharField(
        verbose_name=ugettext_lazy('Version control system'),
        max_length=20,
        help_text=ugettext_lazy(
            'Version control system to use to access your '
            'repository with translations.'
        ),
        choices=VCS_CHOICES,
        default='git',
    )
    repo = models.CharField(
        verbose_name=ugettext_lazy('Source code repository'),
        max_length=200,
        help_text=ugettext_lazy(
            'URL of a repository, use weblate://project/component '
            'for sharing with other component.'
        ),
    )
    push = models.CharField(
        verbose_name=ugettext_lazy('Repository push URL'),
        max_length=200,
        help_text=ugettext_lazy(
            'URL of a push repository, pushing is disabled if empty.'
        ),
        blank=True
    )
    repoweb = models.URLField(
        verbose_name=ugettext_lazy('Repository browser'),
        help_text=ugettext_lazy(
            'Link to repository browser, use %(branch)s for branch, '
            '%(file)s and %(line)s as filename and line placeholders.'
        ),
        validators=[validate_repoweb],
        blank=True,
    )
    git_export = models.CharField(
        verbose_name=ugettext_lazy('Exported repository URL'),
        max_length=200,
        help_text=ugettext_lazy(
            'URL of a repository where users can fetch changes from Weblate'
        ),
        blank=True
    )
    report_source_bugs = models.EmailField(
        verbose_name=ugettext_lazy('Source string bug report address'),
        help_text=ugettext_lazy(
            'Email address where errors in source string will be reported, '
            'keep empty for no emails.'
        ),
        max_length=254,
        blank=True,
    )
    branch = models.CharField(
        verbose_name=ugettext_lazy('Repository branch'),
        max_length=50,
        help_text=ugettext_lazy('Repository branch to translate'),
        default='',
        blank=True
    )
    filemask = models.CharField(
        verbose_name=ugettext_lazy('File mask'),
        max_length=200,
        validators=[validate_filemask],
        help_text=ugettext_lazy(
            'Path of files to translate, use * instead of language code, '
            'for example: po/*.po or locale/*/LC_MESSAGES/django.po.'
        )
    )
    template = models.CharField(
        verbose_name=ugettext_lazy('Monolingual base language file'),
        max_length=200,
        blank=True,
        help_text=ugettext_lazy(
            'Filename of translations base file, which contains all strings '
            'and their source; this is recommended to use '
            'for monolingual translation formats.'
        )
    )
    edit_template = models.BooleanField(
        verbose_name=ugettext_lazy('Edit base file'),
        default=True,
        help_text=ugettext_lazy(
            'Whether users will be able to edit base file '
            'for monolingual translations.'
        )
    )
    new_base = models.CharField(
        verbose_name=ugettext_lazy('Base file for new translations'),
        max_length=200,
        blank=True,
        help_text=ugettext_lazy(
            'Filename of file which is used for creating new translations. '
            'For Gettext choose .pot file.'
        )
    )
    file_format = models.CharField(
        verbose_name=ugettext_lazy('File format'),
        max_length=50,
        default='auto',
        choices=FILE_FORMAT_CHOICES,
        help_text=ugettext_lazy(
            'Automatic detection might fail for some formats '
            'and is slightly slower.'
        ),
    )
    extra_commit_file = models.CharField(
        verbose_name=ugettext_lazy('Additional commit file'),
        max_length=200,
        default='',
        blank=True,
        validators=[validate_extra_file],
        help_text=ugettext_lazy(
            'Additional file to include in commits; please check '
            'documentation for more details.',
        )
    )
    post_update_script = models.CharField(
        verbose_name=ugettext_lazy('Post-update script'),
        max_length=200,
        default='',
        blank=True,
        choices=POST_UPDATE_SCRIPT_CHOICES,
        help_text=ugettext_lazy(
            'Script to be executed after receiving a repository update, '
            'please check documentation for more details.'
        ),
    )
    pre_commit_script = models.CharField(
        verbose_name=ugettext_lazy('Pre-commit script'),
        max_length=200,
        default='',
        blank=True,
        choices=PRE_COMMIT_SCRIPT_CHOICES,
        help_text=ugettext_lazy(
            'Script to be executed before committing translation, '
            'please check documentation for more details.'
        ),
    )
    post_commit_script = models.CharField(
        verbose_name=ugettext_lazy('Post-commit script'),
        max_length=200,
        default='',
        blank=True,
        choices=POST_COMMIT_SCRIPT_CHOICES,
        help_text=ugettext_lazy(
            'Script to be executed after committing translation, '
            'please check documentation for more details.'
        ),
    )
    post_push_script = models.CharField(
        verbose_name=ugettext_lazy('Post-push script'),
        max_length=200,
        default='',
        blank=True,
        choices=POST_PUSH_SCRIPT_CHOICES,
        help_text=ugettext_lazy(
            'Script to be executed after pushing translation to remote, '
            'please check documentation for more details.'
        ),
    )

    locked = models.BooleanField(
        verbose_name=ugettext_lazy('Locked'),
        default=False,
        help_text=ugettext_lazy(
            'Whether component is locked for translation updates.'
        )
    )
    allow_translation_propagation = models.BooleanField(
        verbose_name=ugettext_lazy('Allow translation propagation'),
        default=True,
        db_index=True,
        help_text=ugettext_lazy(
            'Whether translation updates in other components '
            'will cause automatic translation in this one'
        )
    )
    save_history = models.BooleanField(
        verbose_name=ugettext_lazy('Save translation history'),
        default=True,
        help_text=ugettext_lazy(
            'Whether Weblate should keep history of translations'
        )
    )
    enable_suggestions = models.BooleanField(
        verbose_name=ugettext_lazy('Enable suggestions'),
        default=True,
        help_text=ugettext_lazy(
            'Whether to allow translation suggestions at all.'
        )
    )
    suggestion_voting = models.BooleanField(
        verbose_name=ugettext_lazy('Suggestion voting'),
        default=False,
        help_text=ugettext_lazy(
            'Whether users can vote for suggestions.'
        )
    )
    suggestion_autoaccept = models.PositiveSmallIntegerField(
        verbose_name=ugettext_lazy('Autoaccept suggestions'),
        default=0,
        help_text=ugettext_lazy(
            'Automatically accept suggestions with this number of votes,'
            ' use 0 to disable.'
        ),
        validators=[validate_autoaccept],
    )
    check_flags = models.TextField(
        verbose_name=ugettext_lazy('Quality checks flags'),
        default='',
        help_text=ugettext_lazy(
            'Additional comma-separated flags to influence quality checks, '
            'check documentation for possible values.'
        ),
        validators=[validate_check_flags],
        blank=True,
    )

    # Licensing
    license = models.CharField(
        verbose_name=ugettext_lazy('Translation license'),
        max_length=150,
        blank=True,
        default='',
        help_text=ugettext_lazy(
            'Optional short summary of license used for translations.'
        ),
    )
    license_url = models.URLField(
        verbose_name=ugettext_lazy('License URL'),
        blank=True,
        default='',
        help_text=ugettext_lazy('Optional URL with license details.'),
    )
    agreement = models.TextField(
        verbose_name=ugettext_lazy('Contributor agreement'),
        blank=True,
        default='',
        help_text=ugettext_lazy(
            'Agreement which needs to be approved before user can '
            'translate this component.'
        )
    )

    # Adding new language
    new_lang = models.CharField(
        verbose_name=ugettext_lazy('New language'),
        max_length=10,
        choices=NEW_LANG_CHOICES,
        default='contact',
        help_text=ugettext_lazy(
            'How to handle requests for creating new languages. Please note '
            'that availability of choices depends on the file format.'
        ),
    )

    # VCS config
    merge_style = models.CharField(
        verbose_name=ugettext_lazy('Merge style'),
        max_length=10,
        choices=MERGE_CHOICES,
        default='merge',
        help_text=ugettext_lazy(
            'Define whether Weblate should merge upstream repository '
            'or rebase changes onto it.'
        ),
    )
    commit_message = models.TextField(
        verbose_name=ugettext_lazy('Commit message'),
        help_text=ugettext_lazy(
            'You can use format strings for various information, '
            'please check documentation for more details.'
        ),
        validators=[validate_commit_message],
        default=DEFAULT_COMMIT_MESSAGE,
    )
    committer_name = models.CharField(
        verbose_name=ugettext_lazy('Committer name'),
        max_length=200,
        default='Weblate'
    )
    committer_email = models.EmailField(
        verbose_name=ugettext_lazy('Committer email'),
        max_length=254,
        default='*****@*****.**'
    )

    language_regex = RegexField(
        verbose_name=ugettext_lazy('Language filter'),
        max_length=200,
        default='^[^.]+$',
        help_text=ugettext_lazy(
            'Regullar expression which is used to filter '
            'translation when scanning for file mask.'
        ),
    )

    objects = SubProjectManager()

    is_lockable = True

    class Meta(object):
        ordering = ['project__name', 'name']
        unique_together = (
            ('project', 'name'),
            ('project', 'slug'),
        )
        permissions = (
            ('lock_subproject', "Can lock translation for translating"),
            ('can_see_git_repository', "Can see VCS repository URL"),
        )
        app_label = 'trans'
        verbose_name = ugettext_lazy('Component')
        verbose_name_plural = ugettext_lazy('Components')

    def __init__(self, *args, **kwargs):
        '''
        Constructor to initialize some cache properties.
        '''
        super(SubProject, self).__init__(*args, **kwargs)
        self._repository_lock = None
        self._file_format = None
        self._template_store = None
        self._all_flags = None
        self._linked_subproject = None
        self._repository = None

    @property
    def filemask_re(self):
        return re.compile(
            fnmatch.translate(self.filemask).replace('.*', '(.*)')
        )

    @property
    def log_prefix(self):
        return '{0}/{1}: '.format(self.project.slug, self.slug)

    def has_acl(self, user):
        '''
        Checks whether current user is allowed to access this
        subproject.
        '''
        return self.project.has_acl(user)

    def check_acl(self, request):
        '''
        Raises an error if user is not allowed to access this project.
        '''
        self.project.check_acl(request)

    def _reverse_url_name(self):
        '''
        Returns base name for URL reversing.
        '''
        return 'subproject'

    def _reverse_url_kwargs(self):
        '''
        Returns kwargs for URL reversing.
        '''
        return {
            'project': self.project.slug,
            'subproject': self.slug
        }

    def get_widgets_url(self):
        '''
        Returns absolute URL for widgets.
        '''
        return get_site_url(
            reverse('widgets', kwargs={'project': self.project.slug})
        )

    def get_share_url(self):
        '''
        Returns absolute URL usable for sharing.
        '''
        return get_site_url(
            reverse('engage', kwargs={'project': self.project.slug})
        )

    def __unicode__(self):
        return '%s/%s' % (self.project.__unicode__(), self.name)

    def get_full_slug(self):
        return '%s__%s' % (self.project.slug, self.slug)

    def _get_path(self):
        '''
        Returns full path to subproject VCS repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.get_path()
        else:
            return os.path.join(self.project.get_path(), self.slug)

    @property
    def repository_lock(self):
        '''
        Returns lock object for current translation instance.
        '''
        if self.is_repo_link:
            return self.linked_subproject.repository_lock

        if self._repository_lock is None:
            lock_path = os.path.join(
                self.project.get_path(),
                self.slug + '.lock'
            )
            self._repository_lock = FileLock(
                lock_path,
                timeout=30
            )
        return self._repository_lock

    def can_push(self):
        '''
        Returns true if push is possible for this subproject.
        '''
        if self.is_repo_link:
            return self.linked_subproject.can_push()
        return self.push != '' and self.push is not None

    @property
    def is_repo_link(self):
        '''
        Checks whether repository is just a link for other one.
        '''
        return is_repo_link(self.repo)

    def can_add_language(self):
        '''
        Returns true if new languages can be added.
        '''
        return self.new_lang != 'none'

    @property
    def linked_subproject(self):
        '''
        Returns subproject for linked repo.
        '''
        if self._linked_subproject is None:
            self._linked_subproject = SubProject.objects.get_linked(self.repo)
        return self._linked_subproject

    @property
    def repository(self):
        """
        VCS repository object.
        """
        if self.is_repo_link:
            return self.linked_subproject.repository

        if self._repository is None:
            self._repository = VCS_REGISTRY[self.vcs](self.get_path())
            cache_key = '{0}-config-check'.format(self.get_full_slug())
            if cache.get(cache_key) is None:
                self._repository.check_config()
                cache.set(cache_key, True)

        return self._repository

    def get_last_remote_commit(self):
        '''
        Returns latest remote commit we know.
        '''
        cache_key = '{0}-last-commit'.format(self.get_full_slug())

        result = cache.get(cache_key)

        if result is None:
            result = self.repository.get_revision_info(
                self.repository.last_remote_revision
            )
            cache.set(cache_key, result)
        return result

    def get_repo_url(self):
        '''
        Returns link to repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.get_repo_url()
        if not HIDE_REPO_CREDENTIALS:
            return self.repo
        return cleanup_repo_url(self.repo)

    def get_repo_branch(self):
        '''
        Returns branch in repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.branch
        return self.branch

    def get_export_url(self):
        '''
        Returns URL of exported VCS repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.git_export
        return self.git_export

    def get_repoweb_link(self, filename, line):
        '''
        Generates link to source code browser for given file and line.

        For linked repositories, it is possible to override linked
        repository path here.
        '''
        if len(self.repoweb) == 0:
            if self.is_repo_link:
                return self.linked_subproject.get_repoweb_link(filename, line)
            return None

        return self.repoweb % {
            'file': filename,
            'line': line,
            'branch': self.branch
        }

    def update_remote_branch(self, validate=False):
        '''
        Pulls from remote repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.update_remote_branch(validate)

        # Update
        self.log_info('updating repository')
        try:
            with self.repository_lock:
                start = time.time()
                self.repository.update_remote()
                timediff = time.time() - start
                self.log_info('update took %.2f seconds:', timediff)
                for line in self.repository.last_output.splitlines():
                    self.log_debug('update: %s', line)
            return True
        except RepositoryException as error:
            error_text = str(error)
            self.log_error('failed to update repository: %s', error_text)
            if validate:
                if 'Host key verification failed' in error_text:
                    raise ValidationError(_(
                        'Failed to verify SSH host key, please add '
                        'them in SSH page in the admin interface.'
                    ))
                raise ValidationError(
                    _('Failed to fetch repository: %s') % error_text
                )
            return False

    def configure_repo(self, validate=False):
        '''
        Ensures repository is correctly configured and points to current
        remote.
        '''
        if self.is_repo_link:
            return

        self.repository.configure_remote(self.repo, self.push, self.branch)
        self.repository.set_committer(
            self.committer_name,
            self.committer_email
        )

        self.update_remote_branch(validate)

    def configure_branch(self):
        '''
        Ensures local tracking branch exists and is checkouted.
        '''
        if self.is_repo_link:
            return

        self.repository.configure_branch(self.branch)

    def do_update(self, request=None, method=None):
        '''
        Wrapper for doing repository update and pushing them to translations.
        '''
        if self.is_repo_link:
            return self.linked_subproject.do_update(request)

        # pull remote
        if not self.update_remote_branch():
            return False

        # do we have something to merge?
        if not self.repo_needs_merge() and method != 'rebase':
            return True

        # commit possible pending changes
        self.commit_pending(request)

        # update local branch
        ret = self.update_branch(request, method=method)

        # create translation objects for all files
        try:
            self.create_translations(request=request)
        except ParseError:
            ret = False

        # Push after possible merge
        if (self.repo_needs_push() and
                ret and
                self.project.push_on_commit and
                self.can_push()):
            self.do_push(request, force_commit=False, do_update=False)

        return ret

    def do_push(self, request, force_commit=True, do_update=True):
        '''
        Wrapper for pushing changes to remote repo.
        '''
        if self.is_repo_link:
            return self.linked_subproject.do_push(request)

        # Do we have push configured
        if not self.can_push():
            if request is not None:
                messages.error(
                    request,
                    _('Push is disabled for %s.') % self.__unicode__()
                )
            return False

        # Commit any pending changes
        if force_commit:
            self.commit_pending(request, skip_push=True)

        # Do we have anything to push?
        if not self.repo_needs_push():
            return False

        if do_update:
            # Update the repo
            self.do_update(request)

            # Were all changes merged?
            if self.repo_needs_merge():
                return False

        # Do actual push
        try:
            self.log_info('pushing to remote repo')
            with self.repository_lock:
                self.repository.push(self.branch)

            Change.objects.create(
                action=Change.ACTION_PUSH,
                user=request.user if request else None,
                subproject=self,
            )

            vcs_post_push.send(sender=self.__class__, component=self)

            return True
        except RepositoryException as error:
            self.log_error('failed to push on repo: %s', error)
            msg = 'Error:\n%s' % str(error)
            mail_admins(
                'failed push on repo %s' % self.__unicode__(),
                msg
            )
            if request is not None:
                messages.error(
                    request,
                    _('Failed to push to remote branch on %s.') %
                    self.__unicode__()
                )
            return False

    def do_reset(self, request=None):
        '''
        Wrapper for reseting repo to same sources as remote.
        '''
        if self.is_repo_link:
            return self.linked_subproject.do_reset(request)

        # First check we're up to date
        self.update_remote_branch()

        # Do actual reset
        try:
            self.log_info('reseting to remote repo')
            with self.repository_lock:
                self.repository.reset(self.branch)

            Change.objects.create(
                action=Change.ACTION_RESET,
                user=request.user if request else None,
                subproject=self,
            )
        except RepositoryException as error:
            self.log_error('failed to reset on repo')
            msg = 'Error:\n%s' % str(error)
            mail_admins(
                'failed reset on repo %s' % self.__unicode__(),
                msg
            )
            if request is not None:
                messages.error(
                    request,
                    _('Failed to reset to remote branch on %s.') %
                    self.__unicode__()
                )
            return False

        # create translation objects for all files
        self.create_translations(request=request)

        return True

    def get_repo_link_url(self):
        return 'weblate://%s/%s' % (self.project.slug, self.slug)

    def get_linked_childs(self):
        '''
        Returns list of subprojects which link repository to us.
        '''
        return SubProject.objects.filter(
            repo=self.get_repo_link_url()
        )

    def commit_pending(self, request, from_link=False, skip_push=False):
        '''
        Checks whether there is any translation which needs commit.
        '''
        if not from_link and self.is_repo_link:
            return self.linked_subproject.commit_pending(
                request, True, skip_push=skip_push
            )

        for translation in self.translation_set.all():
            translation.commit_pending(request, skip_push=skip_push)

        # Process linked projects
        for subproject in self.get_linked_childs():
            subproject.commit_pending(request, True, skip_push=skip_push)

    def notify_merge_failure(self, error, status):
        '''
        Sends out notifications on merge failure.
        '''
        # Notify subscribed users about failure
        notify_merge_failure(self, error, status)

    def handle_parse_error(self, error):
        """Handler for parse error."""
        report_error(error, sys.exc_info())
        self.notify_merge_failure(
            str(error),
            u''.join(traceback.format_stack()),
        )
        raise ParseError(str(error))

    def update_branch(self, request=None, method=None):
        '''
        Updates current branch to match remote (if possible).
        '''
        if self.is_repo_link:
            return self.linked_subproject.update_branch(request)

        if method is None:
            method = self.merge_style

        # Merge/rebase
        if method == 'rebase':
            method = self.repository.rebase
            error_msg = _('Failed to rebase our branch onto remote branch %s.')
            action = Change.ACTION_REBASE
            action_failed = Change.ACTION_FAILED_REBASE
        else:
            method = self.repository.merge
            error_msg = _('Failed to merge remote branch into %s.')
            action = Change.ACTION_MERGE
            action_failed = Change.ACTION_FAILED_MERGE

        with self.repository_lock:
            try:
                # Try to merge it
                method(self.branch)
                self.log_info(
                    '%s remote into repo',
                    self.merge_style,
                )
                if self.id:
                    Change.objects.create(
                        subproject=self,
                        user=request.user if request else None,
                        action=action,
                    )

                    # run post update hook
                    vcs_post_update.send(sender=self.__class__, component=self)
                return True
            except RepositoryException as error:
                # In case merge has failer recover
                error = str(error)
                status = self.repository.status()

                # Log error
                self.log_error(
                    'failed %s on repo: %s',
                    self.merge_style,
                    error
                )

                # Reset repo back
                method(abort=True)

        if self.id:
            Change.objects.create(
                subproject=self,
                user=request.user if request else None,
                action=action_failed,
                target=str(error),
            )

        # Notify subscribers and admins
        self.notify_merge_failure(error, status)

        # Tell user (if there is any)
        if request is not None:
            messages.error(
                request,
                error_msg % self.__unicode__()
            )

        return False

    def get_mask_matches(self):
        '''
        Returns files matching current mask.
        '''
        prefix = os.path.join(self.get_path(), '')
        matches = glob(os.path.join(self.get_path(), self.filemask))
        matches = set([f.replace(prefix, '') for f in matches])
        # We want to list template among translations as well
        if self.has_template():
            if self.edit_template:
                matches.add(self.template)
            else:
                matches.discard(self.template)
        if self.new_base and self.new_base != self.template:
            matches.discard(self.new_base)
        return sorted(matches)

    def create_translations(self, force=False, langs=None, request=None):
        '''
        Loads translations from VCS.
        '''
        translations = set()
        languages = set()
        matches = self.get_mask_matches()
        language_re = re.compile(self.language_regex)
        for pos, path in enumerate(matches):
            code = self.get_lang_code(path)
            if langs is not None and code not in langs:
                self.log_info('skipping %s', path)
                continue
            if not language_re.match(code):
                self.log_info('skipping language %s', code)
                continue

            self.log_info(
                'checking %s (%s) [%d/%d]',
                path,
                code,
                pos + 1,
                len(matches)
            )
            lang = Language.objects.auto_get_or_create(code=code)
            if lang.code in languages:
                self.log_error('duplicate language found: %s', lang.code)
                continue
            translation = Translation.objects.check_sync(
                self, lang, code, path, force, request=request
            )
            translations.add(translation.id)
            languages.add(lang.code)

        # Delete possibly no longer existing translations
        if langs is None:
            todelete = self.translation_set.exclude(id__in=translations)
            if todelete.exists():
                self.log_info(
                    'removing stale translations: %s',
                    ','.join([trans.language.code for trans in todelete])
                )
                todelete.delete()

        # Process linked repos
        for subproject in self.get_linked_childs():
            self.log_info(
                'updating linked project %s',
                subproject
            )
            subproject.create_translations(force, langs, request=request)

        self.log_info('updating completed')

    def get_lang_code(self, path):
        '''
        Parses language code from path.
        '''
        # Parse filename
        matches = self.filemask_re.match(path)

        if not matches or not matches.lastindex:
            # Assume English language for template
            if path == self.template:
                return 'en'
            return ''

        code = matches.group(1)

        # Remove possible encoding part
        if '.' in code and ('.utf' in code.lower() or '.iso' in code.lower()):
            return code.split('.')[0]
        return code

    def sync_git_repo(self, validate=False):
        '''
        Brings VCS repo in sync with current model.
        '''
        if self.is_repo_link:
            return
        self.configure_repo(validate)
        self.commit_pending(None)
        self.configure_branch()
        self.update_branch()

    def set_default_branch(self):
        '''
        Set default VCS branch if empty
        '''
        if self.branch == '':
            self.branch = VCS_REGISTRY[self.vcs].default_branch

    def clean_repo_link(self):
        '''
        Validates repository link.
        '''
        try:
            repo = SubProject.objects.get_linked(self.repo)
            if repo is not None and repo.is_repo_link:
                raise ValidationError(
                    _(
                        'Invalid link to a Weblate project, '
                        'can not link to linked repository!'
                    )
                )
            if repo.pk == self.pk:
                raise ValidationError(
                    _(
                        'Invalid link to a Weblate project, '
                        'can not link to self!'
                    )
                )
        except (SubProject.DoesNotExist, ValueError):
            raise ValidationError(
                _(
                    'Invalid link to a Weblate project, '
                    'use weblate://project/component.'
                )
            )
        if self.push != '':
            raise ValidationError(
                _('Push URL is not used when repository is linked!')
            )
        if self.git_export != '':
            raise ValidationError(
                _('Export URL is not used when repository is linked!')
            )

    def clean_lang_codes(self, matches):
        '''
        Validates that there are no double language codes found in the files.
        '''
        if len(matches) == 0:
            raise ValidationError(_('The mask did not match any files!'))
        langs = set()
        translated_langs = set()
        for match in matches:
            code = self.get_lang_code(match)
            if not code:
                raise ValidationError(_(
                    'Got empty language code for %s, please check filemask!'
                ) % match)
            lang = Language.objects.auto_get_or_create(code=code)
            if code in langs:
                raise ValidationError(_(
                    'There are more files for single language, please '
                    'adjust the mask and use components for translating '
                    'different resources.'
                ))
            if lang.code in translated_langs:
                raise ValidationError(_(
                    'Multiple translations were mapped to a single language '
                    'code (%s). You should disable SIMPLIFY_LANGUAGES '
                    'to prevent Weblate mapping similar languages to one.'
                ) % lang.code)
            langs.add(code)
            translated_langs.add(lang.code)

    def clean_files(self, matches):
        '''
        Validates whether we can parse translation files.
        '''
        notrecognized = []
        errors = []
        dir_path = self.get_path()
        for match in matches:
            try:
                parsed = self.file_format_cls.load(
                    os.path.join(dir_path, match),
                )
                if not self.file_format_cls.is_valid(parsed):
                    errors.append('%s: %s' % (
                        match, _('File does not seem to be valid!')
                    ))
            except ValueError:
                notrecognized.append(match)
            except Exception as error:
                errors.append('%s: %s' % (match, str(error)))
        if len(notrecognized) > 0:
            msg = (
                _('Format of %d matched files could not be recognized.') %
                len(notrecognized)
            )
            raise ValidationError('%s\n%s' % (
                msg,
                '\n'.join(notrecognized)
            ))
        if len(errors) > 0:
            raise ValidationError('%s\n%s' % (
                (_('Failed to parse %d matched files!') % len(errors)),
                '\n'.join(errors)
            ))

    def clean_new_lang(self):
        '''
        Validates new language choices.
        '''
        if self.new_lang == 'add':
            if not self.file_format_cls.supports_new_language():
                raise ValidationError(_(
                    'Chosen file format does not support adding '
                    'new translations as chosen in project settings.'
                ))
            filename = self.get_new_base_filename()
            if not self.file_format_cls.is_valid_base_for_new(filename):
                raise ValidationError(_(
                    'Format of base file for new translations '
                    'was not recognized!'
                ))
        elif self.new_lang != 'add' and self.new_base:
            raise ValidationError(_(
                'Base file for new translations is not used because of '
                'project settings.'
            ))

    def clean_template(self):
        """
        Validates template value.
        """
        # Test for unexpected template usage
        if self.template != '' and self.file_format_cls.monolingual is False:
            raise ValidationError(
                _('You can not use base file with bilingual translation!')
            )

        # Special case for Gettext
        if self.template.endswith('.pot') and self.filemask.endswith('.po'):
            raise ValidationError(
                _('Using .pot file as base file is not supported!')
            )

        # Validate template loading
        if self.has_template():
            full_path = os.path.join(self.get_path(), self.template)
            if not os.path.exists(full_path):
                raise ValidationError(_('Template file not found!'))

            try:
                self.load_template_store()
            except ValueError:
                raise ValidationError(
                    _(
                        'Format of translation base file '
                        'could not be recognized.'
                    )
                )
            except Exception as exc:
                raise ValidationError(
                    _('Failed to parse translation base file: %s') % str(exc)
                )

        elif self.file_format_cls.monolingual:
            raise ValidationError(
                _('You can not use monolingual translation without base file!')
            )

    def clean(self):
        '''
        Validator fetches repository and tries to find translation files.
        Then it checks them for validity.
        '''
        if self.new_lang == 'url' and self.project.instructions == '':
            raise ValidationError(_(
                'Please either fill in instructions URL '
                'or use different option for adding new language.'
            ))

        if self.license == '' and self.license_url != '':
            raise ValidationError(_(
                'License URL can not be used without license summary.'
            ))

        # Skip validation if we don't have valid project
        if self.project_id is None:
            return

        self.set_default_branch()

        # Check if we should rename
        if self.id:
            old = SubProject.objects.get(pk=self.id)
            self.check_rename(old)

            if old.vcs != self.vcs:
                # This could work, but the problem is that before changed
                # object is saved the linked repos still see old vcs leading
                # to horrible mess. Changing vcs from the manage.py shell
                # works fine though.
                raise ValidationError(
                    _('Changing version control system is not supported!')
                )

        # Check file format
        if self.file_format not in FILE_FORMATS:
            raise ValidationError(
                _('Unsupported file format: {0}').format(self.file_format)
            )

        # Validate VCS repo
        try:
            self.sync_git_repo(True)
        except RepositoryException as exc:
            raise ValidationError(_('Failed to update repository: %s') % exc)

        # Push repo is not used with link
        if self.is_repo_link:
            self.clean_repo_link()

        # Template validation
        self.clean_template()

        matches = self.get_mask_matches()

        # Verify language codes
        self.clean_lang_codes(matches)

        # Try parsing files
        self.clean_files(matches)

        # New language options
        self.clean_new_lang()

        # Suggestions
        if self.suggestion_autoaccept and not self.suggestion_voting:
            raise ValidationError(_(
                'Automatically accepting suggestions can work only with '
                'voting enabled!'
            ))

    def get_template_filename(self):
        '''
        Creates absolute filename for template.
        '''
        return os.path.join(self.get_path(), self.template)

    def get_new_base_filename(self):
        '''
        Creates absolute filename for base file for new translations.
        '''
        if not self.new_base:
            return None
        return os.path.join(self.get_path(), self.new_base)

    def save(self, *args, **kwargs):
        '''
        Save wrapper which updates backend repository and regenerates
        translation data.
        '''
        self.set_default_branch()

        # Detect if VCS config has changed (so that we have to pull the repo)
        changed_git = True
        changed_setup = False
        if self.id:
            old = SubProject.objects.get(pk=self.id)
            changed_git = (
                (old.repo != self.repo) or
                (old.branch != self.branch) or
                (old.filemask != self.filemask)
            )
            changed_setup = (
                (old.file_format != self.file_format) or
                (old.edit_template != self.edit_template) or
                (old.template != self.template)
            )
            # Detect slug changes and rename git repo
            self.check_rename(old)

        # Remove leading ./ from paths
        self.filemask = cleanup_path(self.filemask)
        self.template = cleanup_path(self.template)
        self.extra_commit_file = cleanup_path(self.extra_commit_file)

        # Save/Create object
        super(SubProject, self).save(*args, **kwargs)

        # Configure git repo if there were changes
        if changed_git:
            self.sync_git_repo()

        # Rescan for possibly new translations if there were changes, needs to
        # be done after actual creating the object above
        if changed_setup:
            self.create_translations(force=True)
        elif changed_git:
            self.create_translations()

    def _get_percents(self):
        '''
        Returns percentages of translation status.
        '''
        return self.translation_set.get_percents()

    def repo_needs_commit(self):
        '''
        Checks whether there are some not committed changes.
        '''
        if self.is_repo_link:
            return self.linked_subproject.repo_needs_commit()
        return self.repository.needs_commit()

    def repo_needs_merge(self):
        '''
        Checks whether there is something to merge from remote repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.repo_needs_merge()
        return self.repository.needs_merge(self.branch)

    def repo_needs_push(self):
        '''
        Checks whether there is something to push to remote repository.
        '''
        if self.is_repo_link:
            return self.linked_subproject.repo_needs_push()
        return self.repository.needs_push(self.branch)

    @property
    def file_format_cls(self):
        '''
        Returns file format object.
        '''
        if (self._file_format is None or
                self._file_format.name != self.file_format):
            self._file_format = FILE_FORMATS[self.file_format]
        return self._file_format

    def has_template(self):
        '''
        Returns true if subproject is using template for translation
        '''
        monolingual = self.file_format_cls.monolingual
        return (
            (monolingual or monolingual is None) and
            len(self.template) > 0 and
            not self.template.endswith('.pot')
        )

    def load_template_store(self):
        '''
        Loads translate-toolkit store for template.
        '''
        return self.file_format_cls.load(
            self.get_template_filename(),
        )

    @property
    def template_store(self):
        '''
        Gets translate-toolkit store for template.
        '''
        # Do we need template?
        if not self.has_template():
            return None

        if self._template_store is None:
            try:
                self._template_store = self.load_template_store()
            except Exception as exc:
                self.handle_parse_error(exc)

        return self._template_store

    @property
    def last_change(self):
        '''
        Returns date of last change done in Weblate.
        '''
        try:
            change = Change.objects.content().filter(
                translation__subproject=self
            )
            return change[0].timestamp
        except IndexError:
            return None

    @property
    def all_flags(self):
        '''
        Returns parsed list of flags.
        '''
        if self._all_flags is None:
            self._all_flags = (
                self.check_flags.split(',') +
                list(self.file_format_cls.check_flags)
            )
        return self._all_flags

    def add_new_language(self, language, request):
        '''
        Creates new language file.
        '''
        if self.new_lang != 'add':
            raise ValueError('Not supported operation!')

        if not self.file_format_cls.supports_new_language():
            raise ValueError('Not supported operation!')

        base_filename = self.get_new_base_filename()
        if not self.file_format_cls.is_valid_base_for_new(base_filename):
            raise ValueError('Not supported operation!')

        filename = self.file_format_cls.get_language_filename(
            self.filemask,
            language.code
        )
        fullname = os.path.join(self.get_path(), filename)

        self.file_format_cls.add_language(
            fullname,
            language.code,
            base_filename
        )

        translation = Translation.objects.create(
            subproject=self,
            language=language,
            filename=filename,
            language_code=language.code,
            commit_message='Created new translation.'
        )
        translation.git_commit(
            request,
            translation.get_author_name(request.user),
            timezone.now(),
            force_commit=True,
            force_new=True,
        )
        translation.check_sync(
            force=True,
            request=request
        )

    def do_lock(self, user):
        """Locks component."""
        self.locked = True
        self.save()
        if self.translation_set.exists():
            Change.objects.create(
                subproject=self,
                user=user,
                action=Change.ACTION_LOCK,
            )

    def do_unlock(self, user):
        """Locks component."""
        self.locked = False
        self.save()
        if self.translation_set.exists():
            Change.objects.create(
                subproject=self,
                user=user,
                action=Change.ACTION_UNLOCK,
            )

    def get_editable_template(self):
        if not self.edit_template or not self.has_template():
            return None
        return self.translation_set.get(filename=self.template)