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> | |
2 | 2 | # | |
3 | 3 | # This program is free software: you can redistribute it and/or modify | |
4 | 4 | # it under the terms of the GNU Affero General Public License as | |
… | |||
15 | 15 | #### | |
16 | 16 | ||
17 | 17 | 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 | |
24 | 20 | ||
25 | 21 | import json | |
26 | 22 | import os | |
… | |||
89 | 85 | return False | |
90 | 86 | ||
91 | 87 | try: | |
88 | + | print("a") | |
92 | 89 | proj = self.loadProject(name, lang, system, data) | |
90 | + | print("a") | |
93 | 91 | proj.initialize(projectpath, callback) | |
92 | + | print("a") | |
94 | 93 | self.projects.append({"name": name, "lang": lang, "system": system, | |
95 | 94 | "info": data}) | |
96 | 95 | except Exception as e: | |
… | |||
103 | 102 | self.writeProjects() | |
104 | 103 | return True | |
105 | 104 | ||
106 | - | def loadProject(self, name, lang, system, data): | |
105 | + | def _ensureVersion(self): | |
107 | 106 | if not "Generic" in self.settings.conf: | |
108 | 107 | self.settings.conf["Generic"] = {} | |
109 | 108 | ||
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')) | |
112 | 110 | self.settings.conf["Generic"]["offlate_version"] = version_file.read().strip() | |
113 | 111 | ||
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) | |
142 | 121 | self.project_list[name] = proj | |
143 | 122 | return proj | |
144 | 123 | ||
… | |||
183 | 162 | def isConfigured(self, system): | |
184 | 163 | if not "Generic" in self.settings.conf: | |
185 | 164 | 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
20 | 20 | from pathlib import Path | |
21 | 21 | ||
22 | 22 | from translation_finder import discover | |
23 | + | from ..core.config import StringConfigSpec | |
23 | 24 | from ..formats.gettext import GettextFormat | |
24 | 25 | from ..formats.ts import TSFormat | |
25 | 26 | from ..formats.yaml import YamlFormat | |
26 | 27 | from ..formats.androidstrings import AndroidStringsFormat | |
27 | 28 | from ..formats.appstore import AppstoreFormat | |
28 | 29 | from ..formats.exception import UnsupportedFormatException | |
29 | - | from .systemException import ProjectNotFoundSystemException | |
30 | + | from .exception import ProjectNotFoundSystemException | |
31 | + | from .project import Project | |
30 | 32 | ||
31 | 33 | def rmdir(dir): | |
32 | 34 | dir = Path(dir) | |
… | |||
47 | 49 | self.callback.progress((100.0 * stats.received_objects) / \ | |
48 | 50 | stats.total_objects) | |
49 | 51 | ||
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) | |
56 | 55 | ||
57 | 56 | def open(self, basedir): | |
58 | 57 | self.basedir = basedir | |
59 | - | self.updateURI() | |
60 | - | self.updateFiles() | |
58 | + | self._updateURI() | |
59 | + | self._updateFiles() | |
61 | 60 | ||
62 | 61 | def initialize(self, basedir, callback=None): | |
63 | 62 | 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() | |
67 | 66 | ||
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") | |
70 | 69 | ||
71 | - | def updateFiles(self): | |
72 | - | self.translationfiles = self.updateFilesFromDirectory( | |
70 | + | def _updateFiles(self): | |
71 | + | Project.translationfiles = self._updateFilesFromDirectory( | |
73 | 72 | self.basedir + '/current') | |
74 | 73 | ||
75 | - | def updateFilesFromDirectory(self, directory): | |
74 | + | def _updateFilesFromDirectory(self, directory): | |
76 | 75 | translations = discover(directory) | |
77 | 76 | translationfiles = [] | |
78 | 77 | path = directory + '/' | |
… | |||
121 | 120 | raise UnsupportedFormatException(resource['file_format']) | |
122 | 121 | return translationfiles | |
123 | 122 | ||
124 | - | def clone(self, directory, callback=None): | |
123 | + | def _clone(self, directory, callback=None): | |
125 | 124 | try: | |
126 | 125 | pygit2.clone_repository(self.uri, directory, callbacks=Progress(callback), | |
127 | 126 | checkout_branch=self.branch) | |
… | |||
130 | 129 | ||
131 | 130 | def update(self, askmerge, callback=None): | |
132 | 131 | 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 | |
137 | 136 | for mfile in newfiles: | |
138 | 137 | path = mfile['filename'] | |
139 | 138 | newformat = mfile['format'] | |
… | |||
150 | 149 | raise Exception("Unimplemented method in concrete class: send") | |
151 | 150 | ||
152 | 151 | def save(self): | |
153 | - | for resource in self.translationfiles: | |
152 | + | for resource in Project.translationfiles: | |
154 | 153 | resource['format'].save() | |
155 | 154 | ||
156 | 155 | def content(self): | |
157 | 156 | content = {} | |
158 | - | for resource in self.translationfiles: | |
157 | + | for resource in Project.translationfiles: | |
159 | 158 | print(resource['format']) | |
160 | 159 | content[resource['filename']] = resource['format'].content() | |
161 | 160 | return content | |
162 | 161 | ||
163 | - | @staticmethod | |
164 | - | def isConfigured(conf): | |
165 | - | return 'fullname' in conf and conf['fullname'] != '' and \ | |
166 | - | conf['fullname'] != None | |
167 | - | ||
168 | 162 | def getExternalFiles(self): | |
169 | - | return [x['format'].getExternalFiles() for x in self.translationfiles] | |
163 | + | return [x['format'].getExternalFiles() for x in Project.translationfiles] | |
170 | 164 | ||
171 | 165 | def reload(self): | |
172 | - | for x in self.translationfiles: | |
166 | + | for x in Project.translationfiles: | |
173 | 167 | 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
16 | 16 | """ The gitlab system connector. """ | |
17 | 17 | ||
18 | 18 | from .git import GitProject | |
19 | + | from .project import Project | |
20 | + | from ..core.config import StringConfigSpec | |
19 | 21 | ||
20 | 22 | from urllib.parse import urlparse | |
21 | 23 | from github import Github | |
… | |||
56 | 58 | return | |
57 | 59 | ||
58 | 60 | translationfiles = [] | |
59 | - | for mfile in self.translationfiles: | |
61 | + | for mfile in Project.translationfiles: | |
60 | 62 | translationfiles.extend(mfile['format'].translationFiles()) | |
61 | 63 | ||
62 | 64 | for mfile in translationfiles: | |
… | |||
83 | 85 | maintainer_can_modify=True) | |
84 | 86 | ||
85 | 87 | @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
16 | 16 | """ The gitlab system connector. """ | |
17 | 17 | ||
18 | 18 | from .git import GitProject | |
19 | + | from .project import Project | |
20 | + | from ..core.config import * | |
19 | 21 | ||
20 | 22 | from urllib.parse import urlparse | |
21 | 23 | import gitlab | |
… | |||
37 | 39 | break | |
38 | 40 | ||
39 | 41 | if token == "": | |
40 | - | interface.gitlabTokenNotFound(server) | |
42 | + | interface.gitlabTokenNotFoundError(server) | |
41 | 43 | return | |
42 | 44 | ||
43 | 45 | gl = gitlab.Gitlab("https://"+server, private_token=token) | |
… | |||
56 | 58 | try: | |
57 | 59 | branch = project.branches.create({'branch': 'translation', 'ref': self.branch}) | |
58 | 60 | except: | |
59 | - | interface.gitlabTokenBranchError('translation') | |
61 | + | interface.gitlabBranchError('translation') | |
60 | 62 | return | |
61 | 63 | actions = [] | |
62 | 64 | translationfiles = [] | |
63 | - | for mfile in self.translationfiles: | |
65 | + | for mfile in Project.translationfiles: | |
64 | 66 | translationfiles.extend(mfile['format'].translationFiles()) | |
65 | 67 | ||
66 | 68 | for mfile in translationfiles: | |
… | |||
86 | 88 | 'title': 'Update \'' + self.lang + '\' translation'}) | |
87 | 89 | ||
88 | 90 | @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> | |
2 | 2 | # | |
3 | 3 | # This program is free software: you can redistribute it and/or modify | |
4 | 4 | # it under the terms of the GNU Affero General Public License as | |
… | |||
15 | 15 | #### | |
16 | 16 | """ The list of system connectors. """ | |
17 | 17 | ||
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
31 | 31 | ||
32 | 32 | from ..formats.entry import POEntry | |
33 | 33 | 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 | |
35 | 37 | ||
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) | |
38 | 41 | self.uri = "https://translationproject.org" | |
39 | - | self.conf = conf | |
40 | - | self.name = name | |
41 | - | self.lang = lang | |
42 | 42 | self.basedir = '' | |
43 | - | self.data = data | |
44 | 43 | if "version" in data: | |
45 | 44 | self.version = data['version'] | |
46 | 45 | ||
… | |||
159 | 158 | ||
160 | 159 | def reload(self): | |
161 | 160 | 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
19 | 19 | import os | |
20 | 20 | import requests | |
21 | 21 | from requests.auth import HTTPBasicAuth | |
22 | + | from ..core.config import StringConfigSpec | |
22 | 23 | from ..formats.yaml import YamlFormat | |
23 | 24 | from ..formats.exception import UnsupportedFormatException | |
24 | - | from .systemException import ProjectNotFoundSystemException | |
25 | + | from .exception import ProjectNotFoundSystemException | |
26 | + | from .project import Project | |
25 | 27 | ||
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) | |
31 | 31 | self.basedir = '' | |
32 | - | self.data = data | |
33 | 32 | self.contents = {} | |
34 | 33 | ||
35 | 34 | def open(self, basedir): | |
… | |||
136 | 135 | content[slug['slug']] = myslug.content() | |
137 | 136 | return content | |
138 | 137 | ||
139 | - | @staticmethod | |
140 | - | def isConfigured(conf): | |
141 | - | return 'token' in conf and conf['token'] != '' and conf['token'] != None | |
142 | - | ||
143 | 138 | def getExternalFiles(self): | |
144 | 139 | return [x.getExternalFiles() for x in self.slugs] | |
145 | 140 | ||
146 | 141 | def reload(self): | |
147 | 142 | for x in self.slugs: | |
148 | 143 | 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
46 | 46 | explain.setWordWrap(True) | |
47 | 47 | ||
48 | 48 | 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")) | |
50 | 50 | ||
51 | 51 | issue_button = QPushButton(self.tr("Report an issue")) | |
52 | 52 | ok_button = QPushButton(self.tr("Close this window")) |
offlate/ui/editor.py
21 | 21 | from .spellcheckedit import SpellCheckEdit | |
22 | 22 | from .tagclickedit import TagClickEdit | |
23 | 23 | from .parallel import RunnableCallback, RunnableSignals | |
24 | - | from ..formats.callback import FormatCallback | |
25 | 24 | ||
26 | 25 | import math | |
27 | 26 | import platform | |
… | |||
36 | 35 | def __init__(self, parent = None): | |
37 | 36 | super(ProjectTab, self).__init__(parent) | |
38 | 37 | ||
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 | - | ||
47 | 38 | class Interface: | |
48 | 39 | def __init__(self): | |
49 | 40 | self.value = None | |
… | |||
224 | 215 | ||
225 | 216 | def copy(self): | |
226 | 217 | if self.msgstr.__class__.__name__ == "SpellCheckEdit": | |
227 | - | text = self.msgid.toPlainText() | |
218 | + | text = self.msgid.document().toRawText() | |
228 | 219 | self.msgstr.setText(text) | |
229 | 220 | else: | |
230 | - | text = self.msgid.currentWidget().toPlainText() | |
221 | + | text = self.msgid.currentWidget().document().toRawText() | |
231 | 222 | self.msgstr.currentWidget().setText(text) | |
232 | 223 | ||
233 | 224 | def copyTag(self, tag): | |
… | |||
349 | 340 | item = self.treeWidget.currentItem() | |
350 | 341 | data = item.data(0, Qt.UserRole) | |
351 | 342 | if self.msgstr.__class__.__name__ == "SpellCheckEdit": | |
352 | - | msgstr = self.msgstr.toPlainText() | |
343 | + | msgstr = self.msgstr.document().toRawText() | |
353 | 344 | data.update(0, msgstr) | |
354 | 345 | item.setText(1, msgstr.replace('\n', ' ')) | |
355 | 346 | else: | |
… | |||
357 | 348 | for msgstr in (data.msgstrs if isinstance(data.msgstrs, list) else \ | |
358 | 349 | list(data.msgstrs.keys())): | |
359 | 350 | data.update(i if isinstance(data.msgstrs, list) else msgstr, | |
360 | - | self.msgstr.widget(i).toPlainText()) | |
351 | + | self.msgstr.widget(i).document().toRawText()) | |
361 | 352 | i=i+1 | |
362 | 353 | item.setText(1, data.get(0).replace('\n', ' ')) | |
363 | 354 | item.setForeground(1, QBrush()) | |
… | |||
375 | 366 | self.project.save() | |
376 | 367 | self.project.send(Interface()) | |
377 | 368 | ||
369 | + | def askmerge(self, msgid, oldstr, newstr): | |
370 | + | # TODO: Actually do something more intelligent | |
371 | + | return newstr | |
372 | + | ||
378 | 373 | def update(self, callback=None): | |
379 | 374 | self.project.save() | |
380 | - | self.project.update(MergeCallback(), callback) | |
375 | + | self.project.update(self.askmerge, callback) | |
381 | 376 | self.content = self.project.content() | |
382 | 377 | self.updateContent() | |
383 | 378 |
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
31 | 31 | from ..core.manager import ProjectManager | |
32 | 32 | ||
33 | 33 | from ..formats.exception import UnsupportedFormatException | |
34 | - | from ..systems.systemException import ProjectNotFoundSystemException | |
34 | + | from ..systems.exception import ProjectNotFoundSystemException | |
35 | 35 | ||
36 | 36 | class ProjectManagerWindow(QMainWindow): | |
37 | 37 | _instance = None | |
… | |||
100 | 100 | about_button = QPushButton(self.tr("About Offlate")) | |
101 | 101 | quit_button = QPushButton(self.tr("Exit")) | |
102 | 102 | ||
103 | - | filename = os.path.dirname(__file__) + '/data/icon.png' | |
103 | + | filename = os.path.dirname(__file__) + '/../icon.png' | |
104 | 104 | icon = QPixmap(filename) | |
105 | 105 | iconlabel = QLabel(self) | |
106 | 106 | iconlabel.setPixmap(icon) |
offlate/ui/new.py
23 | 23 | from PyQt5.QtGui import * | |
24 | 24 | from PyQt5.QtCore import * | |
25 | 25 | ||
26 | + | from ..core.config import * | |
26 | 27 | from ..systems.list import * | |
27 | 28 | from .multiplelineedit import MultipleLineEdit | |
28 | 29 | ||
… | |||
86 | 87 | hbox.addLayout(contentbox) | |
87 | 88 | ||
88 | 89 | 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) | |
132 | 104 | ||
133 | 105 | self.setLayout(hbox) | |
134 | 106 | ||
… | |||
146 | 118 | ||
147 | 119 | def fill(self): | |
148 | 120 | item = self.predefinedprojects.currentItem() | |
121 | + | ||
122 | + | if item is None: | |
123 | + | return | |
124 | + | ||
149 | 125 | data = item.data(Qt.UserRole) | |
150 | 126 | self.nameWidget.setText(data['name']) | |
151 | 127 | 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 | |
160 | 137 | ||
161 | 138 | def filter(self): | |
162 | 139 | search = self.searchfield.text() | |
… | |||
172 | 149 | enable = False | |
173 | 150 | if self.nameWidget.text() != '' and self.langWidget.text() != '': | |
174 | 151 | 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 | |
179 | 161 | self.okbutton.setEnabled(enable) | |
180 | 162 | ||
181 | 163 | def wantNew(self): | |
… | |||
191 | 173 | return self.combo.currentIndex() | |
192 | 174 | ||
193 | 175 | 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 | |
205 | 184 | ||
206 | 185 | def othersystem(self): | |
207 | 186 | for system in self.additionalFields: |
offlate/ui/parallel.py
16 | 16 | ||
17 | 17 | from PyQt5.QtCore import * | |
18 | 18 | from ..formats.exception import UnsupportedFormatException | |
19 | - | from ..systems.systemException import ProjectNotFoundSystemException | |
19 | + | from ..systems.exception import ProjectNotFoundSystemException | |
20 | 20 | ||
21 | 21 | class RunnableCallback: | |
22 | 22 | def progress(self, amount): |
offlate/ui/settings.py
18 | 18 | from PyQt5.QtGui import * | |
19 | 19 | from PyQt5.QtCore import * | |
20 | 20 | ||
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) | |
22 | 32 | ||
23 | 33 | class SettingsWindow(QDialog): | |
24 | 34 | def __init__(self, preferences, system = -1, parent = None): | |
… | |||
32 | 42 | vbox = QVBoxLayout() | |
33 | 43 | ||
34 | 44 | 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) | |
40 | 83 | ||
41 | 84 | buttonbox = QHBoxLayout() | |
42 | 85 | cancel = QPushButton(self.tr("Cancel")) | |
… | |||
52 | 95 | ||
53 | 96 | tab.setCurrentIndex(self.system + 1) | |
54 | 97 | ||
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() | |
95 | 104 | ||
96 | - | formLayout.addRow(QLabel(self.tr("Full Name:")), self.genericFullname) | |
105 | + | if not key in self.data: | |
106 | + | self.data[key] = {} | |
97 | 107 | ||
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() | |
141 | 110 | ||
142 | 111 | def ok(self): | |
143 | 112 | self.done = True | |
144 | 113 | 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() |