Add system base class, docstrings, dynamic settings system.

Julien LepillerSat Nov 14 02:36:49+0100 2020

48af9d3

Add system base class, docstrings, dynamic settings system.

offlate/core/config.py unknown status 1

1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class ConfigSpec:
18+
    """
19+
    A configuration specification is a list of specifications.  This class
20+
    represents one such specification.
21+
    """
22+
    def __init__(self, key, name, description, optional):
23+
        self.key = key
24+
        self.name = name
25+
        self.description = description
26+
        self.optional = optional
27+
28+
    def isConfigured(self, conf):
29+
        """
30+
        :param dict conf: The configuration
31+
        :returns: Whether this key is present in the configuration and properly
32+
            configured.
33+
        :rtype: bool
34+
        """
35+
        return self.optional or self.key in conf
36+
37+
class StringConfigSpec(ConfigSpec):
38+
    """
39+
    The specification of a string.
40+
    """
41+
    def __init__(self, key, name, description, placeholder='', optional=False):
42+
        ConfigSpec.__init__(self, key, name, description, optional)
43+
        self.placeholder = placeholder
44+
45+
    def isConfigured(self, conf):
46+
        return ConfigSpec.isConfigured(self, conf) and \
47+
                (self.optional or (conf[self.key] != None and conf[self.key] != ''))
48+
49+
class ListConfigSpec(ConfigSpec):
50+
    """
51+
    The specification of a list of configurations.
52+
    """
53+
    def __init__(self, key, name, description, specifications, indexKey,
54+
            hasRequiredRow, optional=False):
55+
        ConfigSpec.__init__(self, key, name, description, optional)
56+
        self.specifications = specifications
57+
        self.indexKey = indexKey
58+
        self.hasRequiredRow = hasRequiredRow
59+
60+
    def isConfigured(self, conf):
61+
        if not ConfigSpec.isConfigured(self, conf):
62+
            return False
63+
64+
        subconf = conf[self.key]
65+
66+
        if self.hasRequiredRow:
67+
            if not self.hasRequiredRow(conf, subconf):
68+
                return False
69+
        
70+
        for spec in self.specifications:
71+
            if not spec.isConfigured(subconf):
72+
                return False
73+
74+
        return True

offlate/core/manager.py

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2018, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

1515
####
1616
1717
from pathlib import Path
18-
from .systems.tp import TPProject
19-
from .systems.transifex import TransifexProject
20-
from .systems.gitlab import GitlabProject
21-
from .systems.github import GithubProject
22-
from .formats.exception import UnsupportedFormatException
23-
from .systems.list import *
18+
from ..formats.exception import UnsupportedFormatException
19+
from ..systems.list import systems
2420
2521
import json
2622
import os

8985
            return False
9086
9187
        try:
88+
            print("a")
9289
            proj = self.loadProject(name, lang, system, data)
90+
            print("a")
9391
            proj.initialize(projectpath, callback)
92+
            print("a")
9493
            self.projects.append({"name": name, "lang": lang, "system": system,
9594
                "info": data})
9695
        except Exception as e:

103102
        self.writeProjects()
104103
        return True
105104
106-
    def loadProject(self, name, lang, system, data):
105+
    def _ensureVersion(self):
107106
        if not "Generic" in self.settings.conf:
108107
            self.settings.conf["Generic"] = {}
109108
110-
        version_file = open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
111-
            'data/VERSION'))
109+
        version_file = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/VERSION'))
112110
        self.settings.conf["Generic"]["offlate_version"] = version_file.read().strip()
113111
114-
        if system == TRANSLATION_PROJECT:
115-
            if not "TP" in self.settings.conf:
116-
                self.settings.conf["TP"] = {}
117-
            settings = self.settings.conf["TP"]
118-
            for s in self.settings.conf["Generic"].keys():
119-
                settings[s] = self.settings.conf["Generic"][s]
120-
            proj = TPProject(settings, name, lang, data)
121-
        if system == TRANSIFEX:
122-
            if not 'Transifex' in self.settings.conf:
123-
                self.settings.conf['Transifex'] = {}
124-
            settings = self.settings.conf["Transifex"]
125-
            for s in self.settings.conf["Generic"].keys():
126-
                settings[s] = self.settings.conf["Generic"][s]
127-
            proj = TransifexProject(settings, name, lang, data)
128-
        if system == GITLAB:
129-
            if not 'Gitlab' in self.settings.conf:
130-
                self.settings.conf['Gitlab'] = {}
131-
            settings = self.settings.conf["Gitlab"]
132-
            for s in self.settings.conf["Generic"].keys():
133-
                settings[s] = self.settings.conf["Generic"][s]
134-
            proj = GitlabProject(settings, name, lang, data)
135-
        if system == GITHUB:
136-
            if not 'Github' in self.settings.conf:
137-
                self.settings.conf['Github'] = {}
138-
            settings = self.settings.conf['Github']
139-
            for s in self.settings.conf["Generic"].keys():
140-
                settings[s] = self.settings.conf["Generic"][s]
141-
            proj = GithubProject(settings, name, lang, data)
112+
    def loadProject(self, name, lang, system, data):
113+
        self._ensureVersion()
114+
        system = systems[system]
115+
        if not system['key'] in self.settings.conf:
116+
            self.settings.conf[system['key']] = {}
117+
        settings = self.settings.conf[system['key']]
118+
        for s in self.settings.conf['Generic'].keys():
119+
            settings[s] = self.settings.conf['Generic'][s]
120+
        proj = system['system'](name, lang, settings, data)
142121
        self.project_list[name] = proj
143122
        return proj
144123

183162
    def isConfigured(self, system):
184163
        if not "Generic" in self.settings.conf:
185164
            self.settings.conf["Generic"] = {}
186-
        if system == TRANSLATION_PROJECT:
187-
            if not "TP" in self.settings.conf:
188-
                self.settings.conf["TP"] = {}
189-
            settings = self.settings.conf["TP"]
190-
            for s in self.settings.conf["Generic"].keys():
191-
                if not s in self.settings.conf["Generic"]:
192-
                    self.settings.conf["Generic"][s] = None
193-
                settings[s] = self.settings.conf["Generic"][s]
194-
            return TPProject.isConfigured(settings)
195-
        elif system == TRANSIFEX:
196-
            if not "Transifex" in self.settings.conf:
197-
                self.settings.conf["Transifex"] = {}
198-
            settings = self.settings.conf["Transifex"]
199-
            for s in self.settings.conf["Generic"].keys():
200-
                if not s in self.settings.conf["Generic"]:
201-
                    self.settings.conf["Generic"][s] = None
202-
                settings[s] = self.settings.conf["Generic"][s]
203-
            return TransifexProject.isConfigured(settings)
204-
        elif system == GITLAB:
205-
            if not "Gitlab" in self.settings.conf:
206-
                self.settings.conf["Gitlab"] = {}
207-
            settings = self.settings.conf["Gitlab"]
208-
            for s in self.settings.conf["Generic"].keys():
209-
                if not s in self.settings.conf["Generic"]:
210-
                    self.settings.conf["Generic"][s] = None
211-
                settings[s] = self.settings.conf["Generic"][s]
212-
            return GitlabProject.isConfigured(settings)
213-
        elif system == GITHUB:
214-
            if not "Github" in self.settings.conf:
215-
                self.settings.conf["Github"] = {}
216-
            settings = self.settings.conf["Github"]
217-
            for s in self.settings.conf["Generic"].keys():
218-
                if not s in self.settings.conf["Generic"]:
219-
                    self.settings.conf["Generic"][s] = None
220-
                settings[s] = self.settings.conf["Generic"][s]
221-
            return GithubProject.isConfigured(settings)
222-
        else:
223-
            return False
165+
166+
        system = systems[system]
167+
        if not system['key'] in self.settings.conf:
168+
            self.settings.conf[system['key']] = {}
169+
        settings = self.settings.conf[system['key']]
170+
        for s in self.settings.conf['Generic'].keys():
171+
            settings[s] = self.settings.conf['Generic'][s]
172+
        return system['system'].isConfigured(settings)

offlate/systems/callback.py unknown status 1

1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
from ..formats.callback import FormatCallback
18+
19+
class SystemCallback(FormatCallback):
20+
    """
21+
    The base class for callbacks used in the different systems.
22+
    """
23+
    def reportProgress(progress):
24+
        """
25+
        Report progress in some operation.  This method is called during download,
26+
        update and upload of translation projects.
27+
        """
28+
        raise Exception("Unimplemented method in concrete class: reportProgress")
29+
30+
    def githubBranchError(branch):
31+
        """
32+
        Report an error while accessing a branch
33+
        """
34+
        raise Exception("Unimplemented method in concrete class: githubBranchError")
35+
36+
    def gitlabBranchError(branch):
37+
        """
38+
        Report an error while accessing a branch
39+
        """
40+
        raise Exception("Unimplemented method in concrete class: gitlabBranchError")
41+
42+
    def gitlabTokenNotFoundError(server):
43+
        """
44+
        Report an error while accessing a gitlab server
45+
        """
46+
        raise Exception("Unimplemented method in concrete class: gitlabTokenNotFoundError")

offlate/systems/exception.py unknown status 1

1+
#   Copyright (c) 2019 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class ProjectNotFoundSystemException(Exception):
18+
    def __init__(self, message):
19+
        super().__init__('Project not found: '+message)
20+
        self.projectNotFound = message

offlate/systems/git.py

2020
from pathlib import Path
2121
2222
from translation_finder import discover
23+
from ..core.config import StringConfigSpec
2324
from ..formats.gettext import GettextFormat
2425
from ..formats.ts import TSFormat
2526
from ..formats.yaml import YamlFormat
2627
from ..formats.androidstrings import AndroidStringsFormat
2728
from ..formats.appstore import AppstoreFormat
2829
from ..formats.exception import UnsupportedFormatException
29-
from .systemException import ProjectNotFoundSystemException
30+
from .exception import ProjectNotFoundSystemException
31+
from .project import Project
3032
3133
def rmdir(dir):
3234
    dir = Path(dir)

4749
            self.callback.progress((100.0 * stats.received_objects) / \
4850
                    stats.total_objects)
4951
50-
class GitProject:
51-
    def __init__(self, conf, name, lang, data = {}):
52-
        self.conf = conf
53-
        self.name = name
54-
        self.lang = lang
55-
        self.data = data
52+
class GitProject(Project):
53+
    def __init__(self, name, lang, conf, data = {}):
54+
        Project.__init__(self, name, lang, conf, data)
5655
5756
    def open(self, basedir):
5857
        self.basedir = basedir
59-
        self.updateURI()
60-
        self.updateFiles()
58+
        self._updateURI()
59+
        self._updateFiles()
6160
6261
    def initialize(self, basedir, callback=None):
6362
        self.basedir = basedir
64-
        self.updateURI()
65-
        self.clone(basedir + "/current", callback)
66-
        self.updateFiles()
63+
        self._updateURI()
64+
        self._clone(basedir + "/current", callback)
65+
        self._updateFiles()
6766
    
68-
    def updateURI(self):
69-
        raise Exception("Unimplemented method in concrete class: updateURI")
67+
    def _updateURI(self):
68+
        raise Exception("Unimplemented method in concrete class: _updateURI")
7069
71-
    def updateFiles(self):
72-
        self.translationfiles = self.updateFilesFromDirectory(
70+
    def _updateFiles(self):
71+
        Project.translationfiles = self._updateFilesFromDirectory(
7372
                self.basedir + '/current')
7473
75-
    def updateFilesFromDirectory(self, directory):
74+
    def _updateFilesFromDirectory(self, directory):
7675
        translations = discover(directory)
7776
        translationfiles = []
7877
        path = directory + '/'

121120
                raise UnsupportedFormatException(resource['file_format'])
122121
        return translationfiles
123122
124-
    def clone(self, directory, callback=None):
123+
    def _clone(self, directory, callback=None):
125124
        try:
126125
            pygit2.clone_repository(self.uri, directory, callbacks=Progress(callback),
127126
                    checkout_branch=self.branch)

130129
131130
    def update(self, askmerge, callback=None):
132131
        rename(self.basedir + "/current", self.basedir + "/old")
133-
        self.clone(self.basedir + "/current", callback)
134-
        oldfiles = self.updateFilesFromDirectory(self.basedir + "/old")
135-
        self.updateFiles()
136-
        newfiles = self.translationfiles
132+
        self._clone(self.basedir + "/current", callback)
133+
        oldfiles = self._updateFilesFromDirectory(self.basedir + "/old")
134+
        self._updateFiles()
135+
        newfiles = Project.translationfiles
137136
        for mfile in newfiles:
138137
            path = mfile['filename']
139138
            newformat = mfile['format']

150149
        raise Exception("Unimplemented method in concrete class: send")
151150
152151
    def save(self):
153-
        for resource in self.translationfiles:
152+
        for resource in Project.translationfiles:
154153
            resource['format'].save()
155154
156155
    def content(self):
157156
        content = {}
158-
        for resource in self.translationfiles:
157+
        for resource in Project.translationfiles:
159158
            print(resource['format'])
160159
            content[resource['filename']] = resource['format'].content()
161160
        return content
162161
163-
    @staticmethod
164-
    def isConfigured(conf):
165-
        return 'fullname' in conf and conf['fullname'] != '' and \
166-
                conf['fullname'] != None
167-
168162
    def getExternalFiles(self):
169-
        return [x['format'].getExternalFiles() for x in self.translationfiles]
163+
        return [x['format'].getExternalFiles() for x in Project.translationfiles]
170164
    
171165
    def reload(self):
172-
        for x in self.translationfiles:
166+
        for x in Project.translationfiles:
173167
            x['format'].reload()
168+
169+
    def getSystemConfigSpec():
170+
        return [StringConfigSpec('fullname', Project.tr('Your full name'),
171+
                    Project.tr('This is saved in the file before sending it to the \
172+
translation project, for copyright assignment and used as Last-Translator.'),
173+
                    placeholder=Project.tr('John Doe <john@example.com>'))]

offlate/systems/github.py

1616
""" The gitlab system connector. """
1717
1818
from .git import GitProject
19+
from .project import Project
20+
from ..core.config import StringConfigSpec
1921
2022
from urllib.parse import urlparse
2123
from github import Github

5658
            return
5759
5860
        translationfiles = []
59-
        for mfile in self.translationfiles:
61+
        for mfile in Project.translationfiles:
6062
            translationfiles.extend(mfile['format'].translationFiles())
6163
6264
        for mfile in translationfiles:

8385
                maintainer_can_modify=True)
8486
8587
    @staticmethod
86-
    def isConfigured(conf):
87-
        res = GitProject.isConfigured(conf)
88-
        res = res and 'token' in conf and conf['token'] != ''
89-
        return res
88+
    def getProjectConfigSpec():
89+
        return [StringConfigSpec('repo', Project.tr('Repository'),
90+
            Project.tr('Full clone URL for the repository'),
91+
            placeholder=Project.tr('https://...')),
92+
                StringConfigSpec('branch', Project.tr('Branch'),
93+
                    Project.tr('Name of the branch to translate'),
94+
                    placeholder=Project.tr('master'))]
95+
96+
    @staticmethod
97+
    def getSystemConfigSpec():
98+
        specs = [StringConfigSpec('token', Project.tr('Token'),
99+
            Project.tr('You can get a token from <a href=\"#\">https://github.com/settings/tokens/new</a>. \
100+
You will need at least to grant the public_repo permission.'))]
101+
        specs.extend(GitProject.getSystemConfigSpec())
102+
        return specs

offlate/systems/gitlab.py

1616
""" The gitlab system connector. """
1717
1818
from .git import GitProject
19+
from .project import Project
20+
from ..core.config import *
1921
2022
from urllib.parse import urlparse
2123
import gitlab

3739
                break
3840
3941
        if token == "":
40-
            interface.gitlabTokenNotFound(server)
42+
            interface.gitlabTokenNotFoundError(server)
4143
            return
4244
4345
        gl = gitlab.Gitlab("https://"+server, private_token=token)

5658
        try:
5759
            branch = project.branches.create({'branch': 'translation', 'ref': self.branch})
5860
        except:
59-
            interface.gitlabTokenBranchError('translation')
61+
            interface.gitlabBranchError('translation')
6062
            return
6163
        actions = []
6264
        translationfiles = []
63-
        for mfile in self.translationfiles:
65+
        for mfile in Project.translationfiles:
6466
            translationfiles.extend(mfile['format'].translationFiles())
6567
6668
        for mfile in translationfiles:

8688
            'title': 'Update \'' + self.lang + '\' translation'})
8789
8890
    @staticmethod
89-
    def isConfigured(conf):
90-
        res = GitProject.isConfigured(conf)
91-
        res = res and 'servers' in conf and conf['servers'] != '' and \
92-
                conf['servers'] != None
93-
        return res
91+
    def getProjectConfigSpec():
92+
        return [StringConfigSpec('repo', Project.tr('Repository'),
93+
            Project.tr('Full clone URL for the repository'),
94+
            Project.tr('https://...')),
95+
                StringConfigSpec('branch', Project.tr('Branch'),
96+
                    Project.tr('Name of the branch to translate'),
97+
                    Project.tr('master'))]
98+
99+
    @staticmethod
100+
    def getSystemConfigSpec():
101+
        specs = []
102+
        specs.extend(GitProject.getSystemConfigSpec())
103+
        specs.append(
104+
                ListConfigSpec('servers', Project.tr('Configured Gitlab instances'),
105+
                    Project.tr('You need to create a token for each Gitlab instance \
106+
you have an account on. You can create a token by logging into your account, \
107+
going to your settings and in the Access Token page.'),
108+
                    [
109+
                    StringConfigSpec('server', Project.tr('Server'),
110+
                        Project.tr('Server name'),
111+
                        placeholder = Project.tr('https://gitlab.com')),
112+
                    StringConfigSpec('token', Project.tr('Token'),
113+
                        Project.tr('The token you created from your account'),
114+
                        placeholder = Project.tr('Lynid8y56urst-TdlUs6'))
115+
                    ],
116+
                    'server',
117+
                    GitlabProject.hasRequiredRow))
118+
        return specs
119+
120+
    @staticmethod
121+
    def hasRequiredRow(conf, servers):
122+
        """
123+
        Method used by the configuration system: it checks that the configuration
124+
        refers to a server whose configuration is valid.
125+
        """
126+
        server = urlparse(self.uri).hostname
127+
        
128+
        for serv in servers:
129+
            if serv["server"] == server:
130+
                return True
131+
        return False

offlate/systems/list.py

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

1515
####
1616
""" The list of system connectors. """
1717
18-
TRANSLATION_PROJECT = 0
19-
TRANSIFEX = 1
20-
GITLAB = 2
21-
GITHUB = 3
18+
from .gitlab import GitlabProject
19+
from .github import GithubProject
20+
from .tp import TPProject
21+
from .transifex import TransifexProject
22+
23+
systems = [
24+
  {'name': 'Translation Project',
25+
   'system': TPProject,
26+
   'key': 'TP'},
27+
  {'name': 'Transifex',
28+
   'system': TransifexProject,
29+
   'key': 'Transifex'},
30+
  {'name': 'Gitlab',
31+
   'system': GitlabProject,
32+
   'key': 'Gitlab'},
33+
  {'name': 'Github',
34+
   'system': GithubProject,
35+
   'key': 'Github'},
36+
]

offlate/systems/project.py unknown status 1

1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class Project:
18+
    """
19+
    The base class for translation projects.
20+
21+
    A project is a class that represents a translation project the user is
22+
    working on.  Each project corresponds to a specific system and has its
23+
    own way of dealing with local projects and sending changes upstream.
24+
    """
25+
    def __init__(self, name, lang, conf, data = {}):
26+
        self.name = name
27+
        self.lang = lang
28+
        self.conf = conf
29+
        self.data = data
30+
31+
    @staticmethod
32+
    def tr(s):
33+
        """
34+
        A no-op function that returns its only argument.  This is used so
35+
        strings are detected by Qt for translation.
36+
        """
37+
        return s
38+
39+
    def open(self, basedir):
40+
        """
41+
        Operations done when opening the project from files.
42+
43+
        :param str basedir: The base directory in which files for this project
44+
            are stored.
45+
        :rtype: None
46+
        """
47+
        raise Exception("Unimplemented method in concrete class: open")
48+
49+
    def initialize(self, basedir, callback=None):
50+
        """
51+
        Operations done when creating a new project of this type.
52+
53+
        :param str basedir: The base directory in which files for this project
54+
            are stored.
55+
        :param SystemCallback callback: An optional callback that can be used
56+
            for error handling and progress report.
57+
        :rtype: None
58+
        """
59+
        raise Exception("Unimplemented method in concrete class: initialize")
60+
61+
    def update(self, callback):
62+
        """
63+
        Update the project.
64+
65+
        :param SystemCallback callback: A callback that is used for error
66+
            handling and progress report.
67+
        :rtype: None
68+
        """
69+
        raise Exception("Unimplemented method in concrete class: update")
70+
71+
    def send(self, callback):
72+
        """
73+
        Send the current state of the project upstream.  This is used to
74+
        send the user's progress on the translation.
75+
76+
        :param SystemCallback callback: A callback that is used for error
77+
            handling and progress report.
78+
        :rtype: None
79+
        """
80+
        raise Exception("Unimplemented method in concrete class: send")
81+
82+
    def save(self):
83+
        """
84+
        Save the whole project on disk.
85+
86+
        :rtype: None
87+
        """
88+
        raise Exception("Unimplemented method in concrete class: save")
89+
90+
    def content(self):
91+
        """
92+
        Save the whole project on disk.
93+
94+
        :rtype: None
95+
        """
96+
        raise Exception("Unimplemented method in concrete class: content")
97+
98+
    def getExternalFiles(self):
99+
        """
100+
        :returns: The list of files that this project may read and write.
101+
        :rtype: str list
102+
        """
103+
        raise Exception("Unimplemented method in concrete class: getExtenalFiles")
104+
105+
    def reload(self):
106+
        """
107+
        Reload the whole project from disk.
108+
109+
        :rtype: None
110+
        """
111+
        raise Exception("Unimplemented method in concrete class: reload")
112+
113+
    @staticmethod
114+
    def isConfigured(conf):
115+
        """
116+
        :param dict conf: Offlate configuration
117+
        :returns: Whether the configuration is sufficient for this project (system)
118+
        :rtype: bool
119+
        """
120+
        for spec in getSystemConfigSpec():
121+
            if not spec.isConfigured(conf):
122+
                return False
123+
        return True
124+
125+
    @staticmethod
126+
    def getSystemConfigSpec():
127+
        """
128+
        Each system has two kinds of configuration: a global configuration used
129+
        by any instance of the same system (eg: an API key for a given system),
130+
        and a per-project configuration (eg: the name, a URL, ...)
131+
132+
        This returns the specification of the first kind of configuration:
133+
        the global configuration for the system.
134+
135+
        :returns: The specification for the configuration of this type of system.
136+
        """
137+
        raise Exception("Unimplemented method in concrete class: getSystemConfigSpec")
138+
139+
    @staticmethod
140+
    def getProjectConfigSpec():
141+
        """
142+
        Each system has two kinds of configuration: a global configuration used
143+
        by any instance of the same system (eg: an API key for a given system),
144+
        and a per-project configuration (eg: the name, a URL, ...)
145+
146+
        This returns the specification of the second kind of configuration:
147+
        the per-project configuration.
148+
149+
        :returns: The specification for the configuration of projects of this
150+
            type.
151+
        """
152+
        raise Exception("Unimplemented method in concrete class: getProjectConfigSpec")

offlate/systems/systemException.py unknown status 2

1-
#   Copyright (c) 2019 Julien Lepiller <julien@lepiller.eu>
2-
#
3-
#   This program is free software: you can redistribute it and/or modify
4-
#   it under the terms of the GNU Affero General Public License as
5-
#   published by the Free Software Foundation, either version 3 of the
6-
#   License, or (at your option) any later version.
7-
#
8-
#   This program is distributed in the hope that it will be useful,
9-
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11-
#   GNU Affero General Public License for more details.
12-
#
13-
#   You should have received a copy of the GNU Affero General Public License
14-
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15-
####
16-
17-
class ProjectNotFoundSystemException(Exception):
18-
    def __init__(self, message):
19-
        super().__init__('Project not found: '+message)
20-
        self.projectNotFound = message

offlate/systems/tp.py

3131
3232
from ..formats.entry import POEntry
3333
from ..formats.gettext import GettextFormat
34-
from .systemException import ProjectNotFoundSystemException
34+
from .exception import ProjectNotFoundSystemException
35+
from ..core.config import StringConfigSpec
36+
from .project import Project
3537
36-
class TPProject:
37-
    def __init__(self, conf, name, lang, data = {}):
38+
class TPProject(Project):
39+
    def __init__(self, name, lang, conf, data = {}):
40+
        Project.__init__(self, name, lang, conf, data)
3841
        self.uri = "https://translationproject.org"
39-
        self.conf = conf
40-
        self.name = name
41-
        self.lang = lang
4242
        self.basedir = ''
43-
        self.data = data
4443
        if "version" in data:
4544
            self.version = data['version']
4645

159158
160159
    def reload(self):
161160
        self.po.reload()
161+
162+
    @staticmethod
163+
    def getProjectConfigSpec():
164+
        return [StringConfigSpec('version', Project.tr('version'),
165+
            Project.tr('version of the project (keep empty for latest)'),
166+
            optional=True)]
167+
168+
    @staticmethod
169+
    def getSystemConfigSpec():
170+
        return [StringConfigSpec('email', Project.tr('Your email address'),
171+
            Project.tr('This is saved in the file before sending it to the \
172+
translation project, for copyright assignment and the robot policy.'),
173+
            placeholder=Project.tr('john@example.com')),
174+
            StringConfigSpec('fullname', Project.tr('Your full name'),
175+
                Project.tr('This is saved in the file before sending it to the \
176+
translation project, for copyright assignment and used as Last-Translator.'),
177+
                placeholder=Project.tr("John Doe <john@example.com>")),
178+
            StringConfigSpec('server', Project.tr('Mail server'),
179+
                Project.tr('To send your work to the translation project on \
180+
your behalf, we need to know the email server you are going to use (usually \
181+
the part on the right of the `@` in your email address).'),
182+
                placeholder=Project.tr('example.com')),
183+
            StringConfigSpec('user', Project.tr('Mail user'),
184+
                Project.tr('Username used to connect to your mail server, usually \
185+
the email address itself, or the part of the left of the `@`.'),
186+
                placeholder=Project.tr('john'))]

offlate/systems/transifex.py

1919
import os
2020
import requests
2121
from requests.auth import HTTPBasicAuth
22+
from ..core.config import StringConfigSpec
2223
from ..formats.yaml import YamlFormat
2324
from ..formats.exception import UnsupportedFormatException
24-
from .systemException import ProjectNotFoundSystemException
25+
from .exception import ProjectNotFoundSystemException
26+
from .project import Project
2527
26-
class TransifexProject:
27-
    def __init__(self, conf, name, lang, data={}):
28-
        self.conf = conf
29-
        self.name = name
30-
        self.lang = lang
28+
class TransifexProject(Project):
29+
    def __init__(self, name, lang, conf, data={}):
30+
        Project.__init__(self, name, lang, conf, data)
3131
        self.basedir = ''
32-
        self.data = data
3332
        self.contents = {}
3433
3534
    def open(self, basedir):

136135
            content[slug['slug']] = myslug.content()
137136
        return content
138137
139-
    @staticmethod
140-
    def isConfigured(conf):
141-
        return 'token' in conf and conf['token'] != '' and conf['token'] != None
142-
    
143138
    def getExternalFiles(self):
144139
        return [x.getExternalFiles() for x in self.slugs]
145140
146141
    def reload(self):
147142
        for x in self.slugs:
148143
            x.reload()
144+
145+
    @staticmethod
146+
    def getSystemConfigSpec():
147+
        return [StringConfigSpec('token', Project.tr('Token'),
148+
            Project.tr('You can get a token from <a href=\"#\">https://www.transifex.com/user/settings/api/</a>'))]
149+
150+
    @staticmethod
151+
    def getProjectConfigSpec():
152+
        return [StringConfigSpec('organization', Project.tr('Organization'),
153+
            Project.tr('The organization this project belongs in, on transifex.')),
154+
                StringConfigSpec('project', Project.tr('Project'),
155+
                    Project.tr('The name of the project on transifex'))]

offlate/ui/about.py

4646
        explain.setWordWrap(True)
4747
4848
        copyright = QLabel(self)
49-
        copyright.setText(self.tr("Copyright (C) 2018, 2019 Julien Lepiller"))
49+
        copyright.setText(self.tr("Copyright (C) 2018-2020 Julien Lepiller"))
5050
5151
        issue_button = QPushButton(self.tr("Report an issue"))
5252
        ok_button = QPushButton(self.tr("Close this window"))

offlate/ui/editor.py

2121
from .spellcheckedit import SpellCheckEdit
2222
from .tagclickedit import TagClickEdit
2323
from .parallel import RunnableCallback, RunnableSignals
24-
from ..formats.callback import FormatCallback
2524
2625
import math
2726
import platform

3635
    def __init__(self, parent = None):
3736
        super(ProjectTab, self).__init__(parent)
3837
39-
class MergeCallback(FormatCallback):
40-
    def __init__(self):
41-
        pass
42-
43-
    def mergeConflict(self, base, oldTranslation, newTranslation):
44-
        # TODO: Actually do something more intelligent
45-
        return newTranslation
46-
4738
class Interface:
4839
    def __init__(self):
4940
        self.value = None

224215
225216
    def copy(self):
226217
        if self.msgstr.__class__.__name__ == "SpellCheckEdit":
227-
            text = self.msgid.toPlainText()
218+
            text = self.msgid.document().toRawText()
228219
            self.msgstr.setText(text)
229220
        else:
230-
            text = self.msgid.currentWidget().toPlainText()
221+
            text = self.msgid.currentWidget().document().toRawText()
231222
            self.msgstr.currentWidget().setText(text)
232223
233224
    def copyTag(self, tag):

349340
        item = self.treeWidget.currentItem()
350341
        data = item.data(0, Qt.UserRole)
351342
        if self.msgstr.__class__.__name__ == "SpellCheckEdit":
352-
            msgstr = self.msgstr.toPlainText()
343+
            msgstr = self.msgstr.document().toRawText()
353344
            data.update(0, msgstr)
354345
            item.setText(1, msgstr.replace('\n', ' '))
355346
        else:

357348
            for msgstr in (data.msgstrs if isinstance(data.msgstrs, list) else \
358349
                                    list(data.msgstrs.keys())):
359350
                data.update(i if isinstance(data.msgstrs, list) else msgstr,
360-
                        self.msgstr.widget(i).toPlainText())
351+
                        self.msgstr.widget(i).document().toRawText())
361352
                i=i+1
362353
            item.setText(1, data.get(0).replace('\n', ' '))
363354
        item.setForeground(1, QBrush())

375366
        self.project.save()
376367
        self.project.send(Interface())
377368
369+
    def askmerge(self, msgid, oldstr, newstr):
370+
        # TODO: Actually do something more intelligent
371+
        return newstr
372+
378373
    def update(self, callback=None):
379374
        self.project.save()
380-
        self.project.update(MergeCallback(), callback)
375+
        self.project.update(self.askmerge, callback)
381376
        self.content = self.project.content()
382377
        self.updateContent()
383378

offlate/ui/listsettingsedit.py unknown status 1

1+
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
from PyQt5.QtWidgets import *
18+
from PyQt5.QtGui import *
19+
from PyQt5.QtCore import *
20+
21+
class ListSettingsEdit(QWidget):
22+
    textChanged = pyqtSignal()
23+
24+
    def __init__(self, conf, parent = None):
25+
        super(ListSettingsEdit, self).__init__(parent)
26+
        self.conf = conf
27+
        self.initUI()
28+
29+
    def addLine(self, data):
30+
        items = [QTreeWidgetItem(data)]
31+
        self.treeWidget.addTopLevelItems(items)
32+
33+
    def addLineSlot(self):
34+
        for i in range(0, len(self.conf.specifications)):
35+
            d.append(self.widgets[i].text())
36+
        items = [QTreeWidgetItem(d)]
37+
        self.treeWidget.addTopLevelItems(items)
38+
        self.textChanged.emit()
39+
40+
    def deleteLineSlot(self):
41+
        self.treeWidget.takeTopLevelItem(self.treeWidget.currentIndex().row())
42+
        self.textChanged.emit()
43+
44+
    def content(self):
45+
        number = self.treeWidget.topLevelItemCount()
46+
        specs = self.conf.specifications
47+
        items = []
48+
        for i in range(0, number):
49+
            item = self.treeWidget.topLevelItem(i)
50+
            data = {}
51+
            j = 0
52+
            for s in specs:
53+
                data[s.name] = item.text(j)
54+
                j += 1
55+
            items.append(data)
56+
        return items
57+
58+
    def setContent(self, data):
59+
        for d in data:
60+
            line = []
61+
            for s in self.conf.specifications:
62+
                line.append(d[s.key])
63+
            self.addLine(line)
64+
65+
    def initUI(self):
66+
        vbox = QVBoxLayout()
67+
        hbox = QHBoxLayout()
68+
        self.setLayout(vbox)
69+
        self.treeWidget = QTreeWidget()
70+
        self.treeWidget.setColumnCount(len(self.conf.specifications))
71+
        vbox.addWidget(self.treeWidget)
72+
73+
        for s in self.conf.specifications:
74+
            edit = QLineEdit()
75+
            edit.setPlaceholderText(self.tr(s.placeholder))
76+
            hbox.addWidget(edit)
77+
78+
        addbutton = QPushButton(self.tr("Add"))
79+
        addbutton.clicked.connect(self.addLineSlot)
80+
        removebutton = QPushButton(self.tr("Remove"))
81+
        removebutton.clicked.connect(self.deleteLineSlot)
82+
83+
        hbox.addWidget(addbutton)
84+
        hbox.addWidget(removebutton)
85+
        vbox.addLayout(hbox)

offlate/ui/manager.py

3131
from ..core.manager import ProjectManager
3232
3333
from ..formats.exception import UnsupportedFormatException
34-
from ..systems.systemException import ProjectNotFoundSystemException
34+
from ..systems.exception import ProjectNotFoundSystemException
3535
3636
class ProjectManagerWindow(QMainWindow):
3737
    _instance = None

100100
        about_button = QPushButton(self.tr("About Offlate"))
101101
        quit_button = QPushButton(self.tr("Exit"))
102102
103-
        filename = os.path.dirname(__file__) + '/data/icon.png'
103+
        filename = os.path.dirname(__file__) + '/../icon.png'
104104
        icon = QPixmap(filename)
105105
        iconlabel = QLabel(self)
106106
        iconlabel.setPixmap(icon)

offlate/ui/new.py

2323
from PyQt5.QtGui import *
2424
from PyQt5.QtCore import *
2525
26+
from ..core.config import *
2627
from ..systems.list import *
2728
from .multiplelineedit import MultipleLineEdit
2829

8687
        hbox.addLayout(contentbox)
8788
8889
        self.additionalFields = []
89-
        self.additionalFields.append([])
90-
        self.additionalFields.append([])
91-
        self.additionalFields.append([])
92-
        self.additionalFields.append([])
93-
        
94-
        # Transifex
95-
        self.transifexOrganisation = QLineEdit()
96-
        if self.system == TRANSIFEX:
97-
            self.transifexOrganisation.setText(self.info['organization'])
98-
        self.transifexOrganisation.textChanged.connect(self.modify)
99-
        transifexOrganisationLabel = QLabel(self.tr("Organization"))
100-
        self.additionalFields[TRANSIFEX].append({'label': transifexOrganisationLabel,
101-
            'widget': self.transifexOrganisation})
102-
103-
        # Gitlab
104-
        self.gitlabRepo = QLineEdit()
105-
        self.gitlabRepo.textChanged.connect(self.modify)
106-
        gitlabRepoLabel = QLabel(self.tr('repository'))
107-
        self.additionalFields[GITLAB].append({'label': gitlabRepoLabel,
108-
            'widget': self.gitlabRepo})
109-
        self.gitlabBranch = QLineEdit()
110-
        self.gitlabBranch.textChanged.connect(self.modify)
111-
        gitlabBranchLabel = QLabel(self.tr('branch'))
112-
        self.additionalFields[GITLAB].append({'label': gitlabBranchLabel,
113-
            'widget': self.gitlabBranch})
114-
        if self.system == GITLAB:
115-
            self.gitlabRepo.setText(self.info['repo'])
116-
            self.gitlabBranch.setText(self.info['branch'])
117-
118-
        # Github
119-
        self.githubRepo = QLineEdit()
120-
        self.githubRepo.textChanged.connect(self.modify)
121-
        githubRepoLabel = QLabel(self.tr('repository'))
122-
        self.additionalFields[GITHUB].append({'label': githubRepoLabel,
123-
            'widget': self.githubRepo})
124-
        self.githubBranch = QLineEdit()
125-
        self.githubBranch.textChanged.connect(self.modify)
126-
        githubBranchLabel = QLabel(self.tr('branch'))
127-
        self.additionalFields[GITHUB].append({'label': githubBranchLabel,
128-
            'widget': self.githubBranch})
129-
        if self.system == GITHUB:
130-
            self.githubRepo.setText(self.info['repo'])
131-
            self.githubBranch.setText(self.info['branch'])
90+
91+
        for system in systems:
92+
            fields = []
93+
            for spec in system['system'].getProjectConfigSpec():
94+
                if isinstance(spec, StringConfigSpec):
95+
                    widget = QLineEdit()
96+
                    widget.textChanged.connect(self.modify)
97+
                    if spec.placeholder is not None and spec.placeholder != '':
98+
                        widget.setPlaceholderText(spec.placeholder)
99+
                    label = QLabel(spec.name)
100+
                    fields.append({'label': label, 'widget': widget})
101+
                else:
102+
                    raise Exception("Unknown spec type: " + spec)
103+
            self.additionalFields.append(fields)
132104
133105
        self.setLayout(hbox)
134106

146118
147119
    def fill(self):
148120
        item = self.predefinedprojects.currentItem()
121+
122+
        if item is None:
123+
            return
124+
149125
        data = item.data(Qt.UserRole)
150126
        self.nameWidget.setText(data['name'])
151127
        self.combo.setCurrentIndex(int(data['system']))
152-
        if data['system'] == TRANSIFEX:
153-
            self.transifexOrganisation.setText(data['organisation'])
154-
        if data['system'] == GITLAB:
155-
            self.gitlabRepo.setText(data['repo'])
156-
            self.gitlabBranch.setText(data['branch'])
157-
        if data['system'] == GITHUB:
158-
            self.githubRepo.setText(data['repo'])
159-
            self.githubBranch.setText(data['branch'])
128+
129+
        system = systems[data['system']]
130+
        fields = self.additionalFields[data['system']]
131+
        i = 0
132+
        for spec in system['system'].getProjectConfigSpec():
133+
            widget = fields[0]['widget']
134+
            if spec.key in data:
135+
                widget.setText(data[spec.key])
136+
            i = i + 1
160137
161138
    def filter(self):
162139
        search = self.searchfield.text()

172149
        enable = False
173150
        if self.nameWidget.text() != '' and self.langWidget.text() != '':
174151
            enable = True
175-
            for widget in self.additionalFields[self.combo.currentIndex()]:
176-
                if isinstance(widget['widget'], QLineEdit) and widget['widget'].text() == '':
177-
                    enable = False
178-
                    break
152+
            system = systems[self.combo.currentIndex()]
153+
            i = 0
154+
            for spec in system['system'].getProjectConfigSpec():
155+
                if not spec.optional:
156+
                    widget = self.additionalFields[i]['widget']
157+
                    if isinstance(widget, QLineEdit) and widget.text() == '':
158+
                        enable = False
159+
                        break
160+
                i = i + 1
179161
        self.okbutton.setEnabled(enable)
180162
181163
    def wantNew(self):

191173
        return self.combo.currentIndex()
192174
193175
    def getProjectInfo(self):
194-
        if self.getProjectSystem() == TRANSLATION_PROJECT:
195-
            return {}
196-
        if self.getProjectSystem() == TRANSIFEX:
197-
            return {'organization': self.additionalFields[TRANSIFEX][0]['widget'].text()}
198-
        if self.getProjectSystem() == GITLAB:
199-
            return {'repo': self.additionalFields[GITLAB][0]['widget'].text(),
200-
                    'branch': self.additionalFields[GITLAB][1]['widget'].text()}
201-
        if self.getProjectSystem() == GITHUB:
202-
            return {'repo': self.additionalFields[GITHUB][0]['widget'].text(),
203-
                    'branch': self.additionalFields[GITHUB][1]['widget'].text()}
204-
        return {}
176+
        ans = {}
177+
        system = systems[self.getProjectSystem()]
178+
        fields = self.additionalFields[self.getProjectSystem()]
179+
        i = 0
180+
        for spec in system['system'].getProjectConfigSpec():
181+
            ans[spec.key] = fields[i]['widget'].text()
182+
            i = i + 1
183+
        return ans
205184
206185
    def othersystem(self):
207186
        for system in self.additionalFields:

offlate/ui/parallel.py

1616
1717
from PyQt5.QtCore import *
1818
from ..formats.exception import UnsupportedFormatException
19-
from ..systems.systemException import ProjectNotFoundSystemException
19+
from ..systems.exception import ProjectNotFoundSystemException
2020
2121
class RunnableCallback:
2222
    def progress(self, amount):

offlate/ui/settings.py

1818
from PyQt5.QtGui import *
1919
from PyQt5.QtCore import *
2020
21-
from .gitlabedit import GitlabEdit
21+
#from .gitlabedit import GitlabEdit
22+
from .listsettingsedit import ListSettingsEdit
23+
from ..core.config import *
24+
from ..systems.list import systems
25+
26+
class SettingsLineEdit(QLineEdit):
27+
    def content(self):
28+
        return self.text()
29+
30+
    def setContent(self, value):
31+
        self.setText(value)
2232
2333
class SettingsWindow(QDialog):
2434
    def __init__(self, preferences, system = -1, parent = None):

3242
        vbox = QVBoxLayout()
3343
3444
        tab = QTabWidget()
35-
        self.addGenericTab(tab)
36-
        self.addTPTab(tab)
37-
        self.addTransifexTab(tab)
38-
        self.addGitlabTab(tab)
39-
        self.addGithubTab(tab)
45+
46+
        self.widgets = {}
47+
        for system in systems:
48+
            name = system['name']
49+
            key = system['key']
50+
            system = system['system']
51+
            spec = system.getSystemConfigSpec()
52+
53+
            if not key in self.data:
54+
                self.data[key] = {}
55+
            if not key in self.widgets:
56+
                self.widgets[key] = {}
57+
58+
            formBox = QGroupBox(self.tr(name))
59+
            formLayout = QFormLayout()
60+
61+
            for s in spec:
62+
                label = QLabel(self.tr(s.description))
63+
                label.setWordWrap(True)
64+
                widget = None
65+
                if isinstance(s, StringConfigSpec):
66+
                    widget = SettingsLineEdit()
67+
                elif isinstance(s, ListConfigSpec):
68+
                    widget = ListSettingsEdit(s)
69+
                else:
70+
                    raise Exception('Unknown spec type ' + str(s))
71+
72+
                try:
73+
                    widget.setContent(self.data[key][s.key])
74+
                except Exception:
75+
                    pass
76+
                widget.textChanged.connect(self.update)
77+
                formLayout.addRow(QLabel(self.tr(s.name)), widget)
78+
                formLayout.addRow(label)
79+
                self.widgets[key][s.key] = widget
80+
81+
            formBox.setLayout(formLayout)
82+
            tab.addTab(formBox, name)
4083
4184
        buttonbox = QHBoxLayout()
4285
        cancel = QPushButton(self.tr("Cancel"))

5295
5396
        tab.setCurrentIndex(self.system + 1)
5497
55-
    def addTransifexTab(self, tab):
56-
        formBox = QGroupBox(self.tr("Transifex"))
57-
        formLayout = QFormLayout()
58-
        self.TransifexToken = QLineEdit()
59-
60-
        if not "Transifex" in self.data:
61-
            self.data["Transifex"] = {}
62-
        try:
63-
            self.TransifexToken.setText(self.data["Transifex"]["token"])
64-
        except Exception:
65-
            pass
66-
67-
        self.TransifexToken.textChanged.connect(self.updateTransifex)
68-
        label = QLabel(self.tr("You can get a token from <a href=\"#\">https://www.transifex.com/user/settings/api/</a>"))
69-
        label.linkActivated.connect(self.openTransifex)
70-
        label.setWordWrap(True)
71-
72-
        formLayout.addRow(QLabel(self.tr("Token:")), self.TransifexToken)
73-
        formLayout.addRow(label)
74-
75-
        formBox.setLayout(formLayout)
76-
        tab.addTab(formBox, "Transifex")
77-
78-
    def openTransifex(self):
79-
        QDesktopServices().openUrl(QUrl("https://www.transifex.com/user/settings/api/"));
80-
81-
    def updateTransifex(self):
82-
        self.data["Transifex"] = {}
83-
        self.data["Transifex"]["token"] = self.TransifexToken.text()
84-
85-
    def addGenericTab(self, tab):
86-
        formBox = QGroupBox(self.tr("Generic Settings"))
87-
        formLayout = QFormLayout()
88-
        self.genericFullname = QLineEdit()
89-
        self.genericFullname.setPlaceholderText(self.tr("John Doe <john@doe.me>"))
90-
91-
        if not "Generic" in self.data:
92-
            self.data["Generic"] = {}
93-
        if 'fullname' in self.data['Generic']:
94-
            self.genericFullname.setText(self.data["Generic"]["fullname"])
98+
    def update(self):
99+
        for system in systems:
100+
            name = system['name']
101+
            key = system['key']
102+
            system = system['system']
103+
            spec = system.getSystemConfigSpec()
95104
96-
        formLayout.addRow(QLabel(self.tr("Full Name:")), self.genericFullname)
105+
            if not key in self.data:
106+
                self.data[key] = {}
97107
98-
        self.genericFullname.textChanged.connect(self.updateGeneric)
99-
100-
        formBox.setLayout(formLayout)
101-
        tab.addTab(formBox, self.tr("Generic"))
102-
103-
    def updateGeneric(self):
104-
        self.data["Generic"] = {}
105-
        self.data["Generic"]["fullname"] = self.genericFullname.text()
106-
107-
    def addTPTab(self, tab):
108-
        formBox = QGroupBox(self.tr("Translation Project"))
109-
        formLayout = QFormLayout()
110-
111-
        self.TPemail = QLineEdit()
112-
        self.TPuser = QLineEdit()
113-
        self.TPserver = QLineEdit()
114-
115-
        if not "TP" in self.data:
116-
            self.data["TP"] = {}
117-
118-
        if 'email' in self.data['TP']:
119-
            self.TPemail.setText(self.data["TP"]["email"])
120-
        if 'user' in self.data['TP']:
121-
            self.TPuser.setText(self.data["TP"]["user"])
122-
        if 'server' in self.data['TP']:
123-
            self.TPserver.setText(self.data["TP"]["server"])
124-
125-
        self.TPemail.textChanged.connect(self.updateTP)
126-
        self.TPuser.textChanged.connect(self.updateTP)
127-
        self.TPserver.textChanged.connect(self.updateTP)
128-
129-
        formLayout.addRow(QLabel(self.tr("Email:")), self.TPemail)
130-
        formLayout.addRow(QLabel(self.tr("Server:")), self.TPserver)
131-
        formLayout.addRow(QLabel(self.tr("User Name:")), self.TPuser)
132-
133-
        formBox.setLayout(formLayout)
134-
        tab.addTab(formBox, "TP")
135-
136-
    def updateTP(self):
137-
        self.data["TP"] = {}
138-
        self.data["TP"]["email"] = self.TPemail.text()
139-
        self.data["TP"]["user"] = self.TPuser.text()
140-
        self.data["TP"]["server"] = self.TPserver.text()
108+
            for s in spec:
109+
                self.data[key][s.key] = self.widgets[key][s.key].content()
141110
142111
    def ok(self):
143112
        self.done = True
144113
        self.close()
145-
146-
    def addGithubTab(self, tab):
147-
        formBox = QGroupBox(self.tr("Github"))
148-
        formLayout = QFormLayout()
149-
        self.GithubToken = QLineEdit()
150-
151-
        if not "Github" in self.data:
152-
            self.data["Github"] = {}
153-
        try:
154-
            self.GithubToken.setText(self.data["Github"]["token"])
155-
        except Exception:
156-
            pass
157-
158-
        self.GithubToken.textChanged.connect(self.updateGithub)
159-
        label = QLabel(self.tr("You can get a token from <a href=\"#\">https://github.com/settings/tokens/new</a>. \
160-
            You will need at least to grant the public_repo permission."))
161-
        label.linkActivated.connect(self.openGithub)
162-
        label.setWordWrap(True)
163-
164-
        formLayout.addRow(QLabel(self.tr("Token:")), self.GithubToken)
165-
        formLayout.addRow(label)
166-
167-
        formBox.setLayout(formLayout)
168-
        tab.addTab(formBox, "Github")
169-
170-
    def openGithub(self):
171-
        QDesktopServices().openUrl(QUrl("https://github.com/settings/tokens/new"));
172-
173-
    def updateGithub(self):
174-
        self.data["Github"] = {}
175-
        self.data["Github"]["token"] = self.GithubToken.text()
176-
177-
    def addGitlabTab(self, tab):
178-
        formBox = QGroupBox(self.tr("Gitlab"))
179-
        formLayout = QVBoxLayout()
180-
        label = QLabel(self.tr("Add your gitlab account tokens below. You need \
181-
to create a token for every gitlab server you have an account on. You can create \
182-
a token by logging into your account, going to your settings and in the Access \
183-
Token page."))
184-
        label.setWordWrap(True)
185-
        formLayout.addWidget(label)
186-
187-
        if not "Gitlab" in self.data:
188-
            self.data["Gitlab"] = {}
189-
        if not "servers" in self.data["Gitlab"]:
190-
            self.data["Gitlab"]["servers"] = []
191-
192-
        self.gitlabEdit = GitlabEdit()
193-
        for server in self.data["Gitlab"]["servers"]:
194-
            self.gitlabEdit.addLine(server["server"], server["token"])
195-
196-
        formLayout.addSpacing(8)
197-
        formLayout.addWidget(self.gitlabEdit)
198-
        self.gitlabEdit.textChanged.connect(self.updateGitlab)
199-
200-
        formBox.setLayout(formLayout)
201-
        tab.addTab(formBox, "Gitlab")
202-
203-
    def updateGitlab(self):
204-
        if not "Gitlab" in self.data:
205-
            self.data["Gitlab"] = {}
206-
        self.data["Gitlab"]["servers"] = self.gitlabEdit.content()