Merge lp:~jtv/launchpad/recife-check-conflicts into lp:~launchpad/launchpad/recife
- recife-check-conflicts
- Merge into recife
Status: | Merged |
---|---|
Approved by: | Graham Binns |
Approved revision: | no longer in the source branch. |
Merged at revision: | 9163 |
Proposed branch: | lp:~jtv/launchpad/recife-check-conflicts |
Merge into: | lp:~launchpad/launchpad/recife |
Prerequisite: | lp:~jtv/launchpad/recife-retire-traits-helpers |
Diff against target: |
469 lines (+219/-27) 7 files modified
lib/lp/translations/interfaces/potmsgset.py (+8/-2) lib/lp/translations/interfaces/translationmessage.py (+2/-1) lib/lp/translations/model/potmsgset.py (+98/-15) lib/lp/translations/model/translationmessage.py (+4/-2) lib/lp/translations/tests/test_clearcurrenttranslation.py (+14/-1) lib/lp/translations/tests/test_potmsgset.py (+77/-5) lib/lp/translations/tests/test_translationmessage.py (+16/-1) |
To merge this branch: | bzr merge lp:~jtv/launchpad/recife-check-conflicts |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | code | Approve | |
Review via email: mp+33494@code.launchpad.net |
Commit message
Consistent support for conflict checks.
Description of the change
= Check for Translation Conflicts =
For the Recife feature branch. This implements more consistent support for translation conflict checks.
It's something we're already doing in the data model we have in production: when a translation is changed (whether through the web UI or through an import), a timestamp can be given that basically says, "this is when I looked at the state of this translation and decided to change it." The methods that change the translation can then check whether someone else might have set a new translation in the meantime; if they have, the later change is effectively obsolete.
Here I change the higher-level methods that set translations to accept a lock_timestamp, and check.
You'll notice that _checkForConflict builds on the existing _maybeRaiseTran
To test:
{{{
./bin/test -vvc -m lp.translations
}}}
Jeroen
Graham Binns (gmb) : | # |
Preview Diff
1 | === modified file 'lib/lp/translations/interfaces/potmsgset.py' | |||
2 | --- lib/lp/translations/interfaces/potmsgset.py 2010-08-10 17:28:08 +0000 | |||
3 | +++ lib/lp/translations/interfaces/potmsgset.py 2010-08-24 07:55:55 +0000 | |||
4 | @@ -267,7 +267,8 @@ | |||
5 | 267 | """ | 267 | """ |
6 | 268 | 268 | ||
7 | 269 | def setCurrentTranslation(pofile, submitter, translations, origin, | 269 | def setCurrentTranslation(pofile, submitter, translations, origin, |
9 | 270 | share_with_other_side=False): | 270 | share_with_other_side=False, |
10 | 271 | lock_timestamp=None): | ||
11 | 271 | """Set the message's translation in Ubuntu, or upstream, or both. | 272 | """Set the message's translation in Ubuntu, or upstream, or both. |
12 | 272 | 273 | ||
13 | 273 | :param pofile: `POFile` you're setting translations in. Other | 274 | :param pofile: `POFile` you're setting translations in. Other |
14 | @@ -279,6 +280,8 @@ | |||
15 | 279 | :param origin: A `RosettaTranslationOrigin`. | 280 | :param origin: A `RosettaTranslationOrigin`. |
16 | 280 | :param share_with_other_side: When sharing this translation, | 281 | :param share_with_other_side: When sharing this translation, |
17 | 281 | share it with the other `TranslationSide` as well. | 282 | share it with the other `TranslationSide` as well. |
18 | 283 | :param lock_timestamp: Timestamp of the original translation state | ||
19 | 284 | that this change is based on. | ||
20 | 282 | """ | 285 | """ |
21 | 283 | 286 | ||
22 | 284 | def resetCurrentTranslation(pofile, lock_timestamp): | 287 | def resetCurrentTranslation(pofile, lock_timestamp): |
23 | @@ -294,7 +297,8 @@ | |||
24 | 294 | """ | 297 | """ |
25 | 295 | 298 | ||
26 | 296 | def clearCurrentTranslation(pofile, submitter, origin, | 299 | def clearCurrentTranslation(pofile, submitter, origin, |
28 | 297 | share_with_other_side=False): | 300 | share_with_other_side=False, |
29 | 301 | lock_timestamp=None): | ||
30 | 298 | """Set the current message in `pofile` to be untranslated. | 302 | """Set the current message in `pofile` to be untranslated. |
31 | 299 | 303 | ||
32 | 300 | If the current message is shared, this will also clear it in | 304 | If the current message is shared, this will also clear it in |
33 | @@ -310,6 +314,8 @@ | |||
34 | 310 | current on the other side (i.e. the Ubuntu side if working | 314 | current on the other side (i.e. the Ubuntu side if working |
35 | 311 | upstream, or vice versa) then should it be cleared there as | 315 | upstream, or vice versa) then should it be cleared there as |
36 | 312 | well? | 316 | well? |
37 | 317 | :param lock_timestamp: Timestamp of the original translation state | ||
38 | 318 | that this change is based on. | ||
39 | 313 | """ | 319 | """ |
40 | 314 | 320 | ||
41 | 315 | def applySanityFixes(unicode_text): | 321 | def applySanityFixes(unicode_text): |
42 | 316 | 322 | ||
43 | === modified file 'lib/lp/translations/interfaces/translationmessage.py' | |||
44 | --- lib/lp/translations/interfaces/translationmessage.py 2010-08-20 05:56:43 +0000 | |||
45 | +++ lib/lp/translations/interfaces/translationmessage.py 2010-08-24 07:55:55 +0000 | |||
46 | @@ -240,7 +240,8 @@ | |||
47 | 240 | It must not be referenced by any other object. | 240 | It must not be referenced by any other object. |
48 | 241 | """ | 241 | """ |
49 | 242 | 242 | ||
51 | 243 | def approve(pofile, reviewer, share_with_other_side=False): | 243 | def approve(pofile, reviewer, share_with_other_side=False, |
52 | 244 | lock_timestamp=None): | ||
53 | 244 | """Approve this suggestion, making it a current translation.""" | 245 | """Approve this suggestion, making it a current translation.""" |
54 | 245 | 246 | ||
55 | 246 | # XXX CarlosPerelloMarin 20071022: We should move this into browser code. | 247 | # XXX CarlosPerelloMarin 20071022: We should move this into browser code. |
56 | 247 | 248 | ||
57 | === modified file 'lib/lp/translations/model/potmsgset.py' | |||
58 | --- lib/lp/translations/model/potmsgset.py 2010-08-24 07:55:54 +0000 | |||
59 | +++ lib/lp/translations/model/potmsgset.py 2010-08-24 07:55:55 +0000 | |||
60 | @@ -37,8 +37,7 @@ | |||
61 | 37 | IPOTMsgSet, | 37 | IPOTMsgSet, |
62 | 38 | POTMsgSetInIncompatibleTemplatesError, | 38 | POTMsgSetInIncompatibleTemplatesError, |
63 | 39 | TranslationCreditsType) | 39 | TranslationCreditsType) |
66 | 40 | from lp.translations.interfaces.side import ( | 40 | from lp.translations.interfaces.side import ITranslationSideTraitsSet |
65 | 41 | ITranslationSideTraitsSet, TranslationSide) | ||
67 | 42 | from lp.translations.interfaces.translationfileformat import ( | 41 | from lp.translations.interfaces.translationfileformat import ( |
68 | 43 | TranslationFileFormat) | 42 | TranslationFileFormat) |
69 | 44 | from lp.translations.interfaces.translationimporter import ( | 43 | from lp.translations.interfaces.translationimporter import ( |
70 | @@ -89,6 +88,22 @@ | |||
71 | 89 | incumbent_unknown = object() | 88 | incumbent_unknown = object() |
72 | 90 | 89 | ||
73 | 91 | 90 | ||
74 | 91 | def dictify_msgstrs(potranslations): | ||
75 | 92 | """Represent `potranslations` as a normalized dict. | ||
76 | 93 | |||
77 | 94 | :param potranslations: a dict or sequence of `POTranslation`s per | ||
78 | 95 | plural form. | ||
79 | 96 | :return: a dict mapping each translated plural form to a | ||
80 | 97 | `POTranslation`. Untranslated forms are omitted. | ||
81 | 98 | """ | ||
82 | 99 | if not isinstance(potranslations, dict): | ||
83 | 100 | potranslations = dict(enumerate(potranslations)) | ||
84 | 101 | return dict( | ||
85 | 102 | (form, msgstr) | ||
86 | 103 | for form, msgstr in potranslations.iteritems() | ||
87 | 104 | if msgstr is not None) | ||
88 | 105 | |||
89 | 106 | |||
90 | 92 | class POTMsgSet(SQLBase): | 107 | class POTMsgSet(SQLBase): |
91 | 93 | implements(IPOTMsgSet) | 108 | implements(IPOTMsgSet) |
92 | 94 | 109 | ||
93 | @@ -932,6 +947,60 @@ | |||
94 | 932 | origin=RosettaTranslationOrigin.ROSETTAWEB, submitter=submitter, | 947 | origin=RosettaTranslationOrigin.ROSETTAWEB, submitter=submitter, |
95 | 933 | **forms) | 948 | **forms) |
96 | 934 | 949 | ||
97 | 950 | def _checkForConflict(self, current_message, lock_timestamp, | ||
98 | 951 | potranslations=None): | ||
99 | 952 | """Check `message` for conflicting changes since `lock_timestamp`. | ||
100 | 953 | |||
101 | 954 | Call this before changing this message's translations, to ensure | ||
102 | 955 | that a read-modify-write operation on a message does not | ||
103 | 956 | accidentally overwrite newer changes based on older information. | ||
104 | 957 | |||
105 | 958 | One example of a read-modify-write operation is: user downloads | ||
106 | 959 | translation file, translates a message, then re-uploads. | ||
107 | 960 | Another is: user looks at a message in the web UI, decides that | ||
108 | 961 | neither the current translation nor any of the suggestions are | ||
109 | 962 | right, and clears the message. | ||
110 | 963 | |||
111 | 964 | In these scenarios, it's possible for someone else to come along | ||
112 | 965 | and change the message's translation between the time we provide | ||
113 | 966 | the user with a view of the current state and the time we | ||
114 | 967 | receive a change from the user. We call this a conflict. | ||
115 | 968 | |||
116 | 969 | Raises `TranslationConflict` if a conflict exists. | ||
117 | 970 | |||
118 | 971 | :param currentmessage: The `TranslationMessage` that is current | ||
119 | 972 | now. This is where we'll see any conflicting changes | ||
120 | 973 | reflected in the date_reviewed timestamp. | ||
121 | 974 | :param lock_timestamp: The timestamp of the translation state | ||
122 | 975 | that the change is based on. | ||
123 | 976 | :param potranslations: `POTranslation`s dict for the new | ||
124 | 977 | translation. If these are given, and identical to those of | ||
125 | 978 | `current_message`, there is no conflict. | ||
126 | 979 | """ | ||
127 | 980 | if lock_timestamp is None: | ||
128 | 981 | # We're not really being asked to check for conflicts. | ||
129 | 982 | return | ||
130 | 983 | if current_message is None: | ||
131 | 984 | # There is no current message to conflict with. | ||
132 | 985 | return | ||
133 | 986 | try: | ||
134 | 987 | self._maybeRaiseTranslationConflict( | ||
135 | 988 | current_message, lock_timestamp) | ||
136 | 989 | except TranslationConflict: | ||
137 | 990 | if potranslations is None: | ||
138 | 991 | # We don't know what translations are going to be set; | ||
139 | 992 | # based on the timestamps this is a conflict. | ||
140 | 993 | raise | ||
141 | 994 | old_msgstrs = dictify_msgstrs(current_message.all_msgstrs) | ||
142 | 995 | new_msgstrs = dictify_msgstrs(potranslations) | ||
143 | 996 | if new_msgstrs != old_msgstrs: | ||
144 | 997 | # Yup, there really is a difference. This is a proper | ||
145 | 998 | # conflict. | ||
146 | 999 | raise | ||
147 | 1000 | else: | ||
148 | 1001 | # Two identical translations crossed. Not a conflict. | ||
149 | 1002 | pass | ||
150 | 1003 | |||
151 | 935 | def _maybeRaiseTranslationConflict(self, message, lock_timestamp): | 1004 | def _maybeRaiseTranslationConflict(self, message, lock_timestamp): |
152 | 936 | """Checks if there is a translation conflict for the message. | 1005 | """Checks if there is a translation conflict for the message. |
153 | 937 | 1006 | ||
154 | @@ -1008,7 +1077,7 @@ | |||
155 | 1008 | **translation_args) | 1077 | **translation_args) |
156 | 1009 | 1078 | ||
157 | 1010 | def approveSuggestion(self, pofile, suggestion, reviewer, | 1079 | def approveSuggestion(self, pofile, suggestion, reviewer, |
159 | 1011 | share_with_other_side=False): | 1080 | share_with_other_side=False, lock_timestamp=None): |
160 | 1012 | """Approve a suggestion. | 1081 | """Approve a suggestion. |
161 | 1013 | 1082 | ||
162 | 1014 | :param pofile: The `POFile` that the suggestion is being approved for. | 1083 | :param pofile: The `POFile` that the suggestion is being approved for. |
163 | @@ -1017,6 +1086,8 @@ | |||
164 | 1017 | suggestion. | 1086 | suggestion. |
165 | 1018 | :param share_with_other_side: Policy selector: share this change with | 1087 | :param share_with_other_side: Policy selector: share this change with |
166 | 1019 | the other translation side if possible? | 1088 | the other translation side if possible? |
167 | 1089 | :param lock_timestamp: Timestamp of the original translation state | ||
168 | 1090 | that this change is based on. | ||
169 | 1020 | """ | 1091 | """ |
170 | 1021 | template = pofile.potemplate | 1092 | template = pofile.potemplate |
171 | 1022 | traits = getUtility(ITranslationSideTraitsSet).getTraits( | 1093 | traits = getUtility(ITranslationSideTraitsSet).getTraits( |
172 | @@ -1030,7 +1101,7 @@ | |||
173 | 1030 | activated_message = self._setTranslation( | 1101 | activated_message = self._setTranslation( |
174 | 1031 | pofile, translator, suggestion.origin, potranslations, | 1102 | pofile, translator, suggestion.origin, potranslations, |
175 | 1032 | share_with_other_side=share_with_other_side, | 1103 | share_with_other_side=share_with_other_side, |
177 | 1033 | identical_message=suggestion) | 1104 | identical_message=suggestion, lock_timestamp=lock_timestamp) |
178 | 1034 | 1105 | ||
179 | 1035 | activated_message.markReviewed(reviewer) | 1106 | activated_message.markReviewed(reviewer) |
180 | 1036 | if reviewer != translator: | 1107 | if reviewer != translator: |
181 | @@ -1038,7 +1109,8 @@ | |||
182 | 1038 | template.awardKarma(reviewer, 'translationreview') | 1109 | template.awardKarma(reviewer, 'translationreview') |
183 | 1039 | 1110 | ||
184 | 1040 | def setCurrentTranslation(self, pofile, submitter, translations, origin, | 1111 | def setCurrentTranslation(self, pofile, submitter, translations, origin, |
186 | 1041 | share_with_other_side=False): | 1112 | share_with_other_side=False, |
187 | 1113 | lock_timestamp=None): | ||
188 | 1042 | """See `IPOTMsgSet`.""" | 1114 | """See `IPOTMsgSet`.""" |
189 | 1043 | potranslations = self._findPOTranslations(translations) | 1115 | potranslations = self._findPOTranslations(translations) |
190 | 1044 | identical_message = self._findTranslationMessage( | 1116 | identical_message = self._findTranslationMessage( |
191 | @@ -1046,10 +1118,12 @@ | |||
192 | 1046 | return self._setTranslation( | 1118 | return self._setTranslation( |
193 | 1047 | pofile, submitter, origin, potranslations, | 1119 | pofile, submitter, origin, potranslations, |
194 | 1048 | share_with_other_side=share_with_other_side, | 1120 | share_with_other_side=share_with_other_side, |
196 | 1049 | identical_message=identical_message) | 1121 | identical_message=identical_message, |
197 | 1122 | lock_timestamp=lock_timestamp) | ||
198 | 1050 | 1123 | ||
199 | 1051 | def _setTranslation(self, pofile, submitter, origin, potranslations, | 1124 | def _setTranslation(self, pofile, submitter, origin, potranslations, |
201 | 1052 | identical_message=None, share_with_other_side=False): | 1125 | identical_message=None, share_with_other_side=False, |
202 | 1126 | lock_timestamp=None): | ||
203 | 1053 | """Set the current translation. | 1127 | """Set the current translation. |
204 | 1054 | 1128 | ||
205 | 1055 | :param pofile: The `POFile` to set the translation in. | 1129 | :param pofile: The `POFile` to set the translation in. |
206 | @@ -1060,11 +1134,13 @@ | |||
207 | 1060 | :param identical_message: The already existing message, if any, | 1134 | :param identical_message: The already existing message, if any, |
208 | 1061 | that's either shared or diverged for `pofile.potemplate`, | 1135 | that's either shared or diverged for `pofile.potemplate`, |
209 | 1062 | whose translations are identical to the ones we're setting. | 1136 | whose translations are identical to the ones we're setting. |
212 | 1063 | :param share_with_other_side: | 1137 | :param share_with_other_side: Propagate this change to the other |
213 | 1064 | :return: | 1138 | translation side if appropriate. |
214 | 1139 | :param lock_timestamp: The timestamp of the translation state | ||
215 | 1140 | that the change is based on. | ||
216 | 1141 | :return: The `TranslationMessage` that is current after | ||
217 | 1142 | completion. | ||
218 | 1065 | """ | 1143 | """ |
219 | 1066 | # XXX JeroenVermeulen 2010-08-16: Update pofile.date_changed. | ||
220 | 1067 | |||
221 | 1068 | twin = identical_message | 1144 | twin = identical_message |
222 | 1069 | 1145 | ||
223 | 1070 | traits = getUtility(ITranslationSideTraitsSet).getTraits( | 1146 | traits = getUtility(ITranslationSideTraitsSet).getTraits( |
224 | @@ -1074,6 +1150,9 @@ | |||
225 | 1074 | incumbent_message = traits.getCurrentMessage( | 1150 | incumbent_message = traits.getCurrentMessage( |
226 | 1075 | self, pofile.potemplate, pofile.language) | 1151 | self, pofile.potemplate, pofile.language) |
227 | 1076 | 1152 | ||
228 | 1153 | self._checkForConflict( | ||
229 | 1154 | incumbent_message, lock_timestamp, potranslations=potranslations) | ||
230 | 1155 | |||
231 | 1077 | # Summary of the matrix: | 1156 | # Summary of the matrix: |
232 | 1078 | # * If the incumbent message is diverged and we're setting a | 1157 | # * If the incumbent message is diverged and we're setting a |
233 | 1079 | # translation that's already shared: converge. | 1158 | # translation that's already shared: converge. |
234 | @@ -1178,7 +1257,7 @@ | |||
235 | 1178 | traits.other_side_traits.getCurrentMessage( | 1257 | traits.other_side_traits.getCurrentMessage( |
236 | 1179 | self, pofile.potemplate, pofile.language)) | 1258 | self, pofile.potemplate, pofile.language)) |
237 | 1180 | if other_incumbent is None: | 1259 | if other_incumbent is None: |
239 | 1181 | traits.other_side_side.setFlag(message, True) | 1260 | traits.other_side_traits.setFlag(message, True) |
240 | 1182 | elif character == '+': | 1261 | elif character == '+': |
241 | 1183 | if share_with_other_side: | 1262 | if share_with_other_side: |
242 | 1184 | traits.other_side_traits.setFlag(message, True) | 1263 | traits.other_side_traits.setFlag(message, True) |
243 | @@ -1189,7 +1268,9 @@ | |||
244 | 1189 | if decisions == '': | 1268 | if decisions == '': |
245 | 1190 | message = twin | 1269 | message = twin |
246 | 1191 | 1270 | ||
248 | 1192 | traits.setFlag(message, True) | 1271 | if not traits.getFlag(message): |
249 | 1272 | traits.setFlag(message, True) | ||
250 | 1273 | pofile.markChanged(translator=submitter) | ||
251 | 1193 | 1274 | ||
252 | 1194 | return message | 1275 | return message |
253 | 1195 | 1276 | ||
254 | @@ -1214,11 +1295,13 @@ | |||
255 | 1214 | pofile.date_changed = UTC_NOW | 1295 | pofile.date_changed = UTC_NOW |
256 | 1215 | 1296 | ||
257 | 1216 | def clearCurrentTranslation(self, pofile, submitter, origin, | 1297 | def clearCurrentTranslation(self, pofile, submitter, origin, |
259 | 1217 | share_with_other_side=False): | 1298 | share_with_other_side=False, |
260 | 1299 | lock_timestamp=None): | ||
261 | 1218 | """See `IPOTMsgSet`.""" | 1300 | """See `IPOTMsgSet`.""" |
262 | 1219 | message = self.setCurrentTranslation( | 1301 | message = self.setCurrentTranslation( |
263 | 1220 | pofile, submitter, [], origin, | 1302 | pofile, submitter, [], origin, |
265 | 1221 | share_with_other_side=share_with_other_side) | 1303 | share_with_other_side=share_with_other_side, |
266 | 1304 | lock_timestamp=lock_timestamp) | ||
267 | 1222 | message.markReviewed(submitter) | 1305 | message.markReviewed(submitter) |
268 | 1223 | 1306 | ||
269 | 1224 | def applySanityFixes(self, text): | 1307 | def applySanityFixes(self, text): |
270 | 1225 | 1308 | ||
271 | === modified file 'lib/lp/translations/model/translationmessage.py' | |||
272 | --- lib/lp/translations/model/translationmessage.py 2010-08-20 05:56:43 +0000 | |||
273 | +++ lib/lp/translations/model/translationmessage.py 2010-08-24 07:55:55 +0000 | |||
274 | @@ -326,11 +326,13 @@ | |||
275 | 326 | date_reviewed = current.date_created | 326 | date_reviewed = current.date_created |
276 | 327 | return date_reviewed > self.date_created | 327 | return date_reviewed > self.date_created |
277 | 328 | 328 | ||
279 | 329 | def approve(self, pofile, reviewer, share_with_other_side=False): | 329 | def approve(self, pofile, reviewer, share_with_other_side=False, |
280 | 330 | lock_timestamp=None): | ||
281 | 330 | """See `ITranslationMessage`.""" | 331 | """See `ITranslationMessage`.""" |
282 | 331 | self.potmsgset.approveSuggestion( | 332 | self.potmsgset.approveSuggestion( |
283 | 332 | pofile, self, reviewer, | 333 | pofile, self, reviewer, |
285 | 333 | share_with_other_side=share_with_other_side) | 334 | share_with_other_side=share_with_other_side, |
286 | 335 | lock_timestamp=lock_timestamp) | ||
287 | 334 | 336 | ||
288 | 335 | def getOnePOFile(self): | 337 | def getOnePOFile(self): |
289 | 336 | """See `ITranslationMessage`.""" | 338 | """See `ITranslationMessage`.""" |
290 | 337 | 339 | ||
291 | === modified file 'lib/lp/translations/tests/test_clearcurrenttranslation.py' | |||
292 | --- lib/lp/translations/tests/test_clearcurrenttranslation.py 2010-08-20 05:43:19 +0000 | |||
293 | +++ lib/lp/translations/tests/test_clearcurrenttranslation.py 2010-08-24 07:55:55 +0000 | |||
294 | @@ -21,7 +21,9 @@ | |||
295 | 21 | from lp.testing import TestCaseWithFactory | 21 | from lp.testing import TestCaseWithFactory |
296 | 22 | from lp.translations.interfaces.side import ITranslationSideTraitsSet | 22 | from lp.translations.interfaces.side import ITranslationSideTraitsSet |
297 | 23 | from lp.translations.interfaces.translationmessage import ( | 23 | from lp.translations.interfaces.translationmessage import ( |
299 | 24 | RosettaTranslationOrigin) | 24 | RosettaTranslationOrigin, |
300 | 25 | TranslationConflict, | ||
301 | 26 | ) | ||
302 | 25 | 27 | ||
303 | 26 | ORIGIN = RosettaTranslationOrigin.SCM | 28 | ORIGIN = RosettaTranslationOrigin.SCM |
304 | 27 | 29 | ||
305 | @@ -249,6 +251,17 @@ | |||
306 | 249 | self.assertEqual(new_reviewer, current.reviewer) | 251 | self.assertEqual(new_reviewer, current.reviewer) |
307 | 250 | self.assertSqlAttributeEqualsDate(current, 'date_reviewed', UTC_NOW) | 252 | self.assertSqlAttributeEqualsDate(current, 'date_reviewed', UTC_NOW) |
308 | 251 | 253 | ||
309 | 254 | def test_detects_conflict(self): | ||
310 | 255 | pofile = self._makePOFile() | ||
311 | 256 | current_message = self.factory.makeCurrentTranslationMessage( | ||
312 | 257 | pofile=pofile) | ||
313 | 258 | old = datetime.now(UTC) - timedelta(days=7) | ||
314 | 259 | |||
315 | 260 | self.assertRaises( | ||
316 | 261 | TranslationConflict, | ||
317 | 262 | current_message.potmsgset.clearCurrentTranslation, | ||
318 | 263 | pofile, self.factory.makePerson(), ORIGIN, lock_timestamp=old) | ||
319 | 264 | |||
320 | 252 | 265 | ||
321 | 253 | class TestClearCurrentTranslationUpstream(TestCaseWithFactory, | 266 | class TestClearCurrentTranslationUpstream(TestCaseWithFactory, |
322 | 254 | ScenarioMixin): | 267 | ScenarioMixin): |
323 | 255 | 268 | ||
324 | === modified file 'lib/lp/translations/tests/test_potmsgset.py' | |||
325 | --- lib/lp/translations/tests/test_potmsgset.py 2010-08-24 07:55:54 +0000 | |||
326 | +++ lib/lp/translations/tests/test_potmsgset.py 2010-08-24 07:55:55 +0000 | |||
327 | @@ -14,6 +14,8 @@ | |||
328 | 14 | from zope.security.proxy import isinstance as zope_isinstance | 14 | from zope.security.proxy import isinstance as zope_isinstance |
329 | 15 | from zope.security.proxy import removeSecurityProxy | 15 | from zope.security.proxy import removeSecurityProxy |
330 | 16 | 16 | ||
331 | 17 | from storm.locals import Store | ||
332 | 18 | |||
333 | 17 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 19 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
334 | 18 | from lp.registry.interfaces.person import IPersonSet | 20 | from lp.registry.interfaces.person import IPersonSet |
335 | 19 | from lp.registry.interfaces.product import IProductSet | 21 | from lp.registry.interfaces.product import IProductSet |
336 | @@ -24,7 +26,6 @@ | |||
337 | 24 | TranslationFileFormat) | 26 | TranslationFileFormat) |
338 | 25 | from lp.translations.interfaces.translationmessage import ( | 27 | from lp.translations.interfaces.translationmessage import ( |
339 | 26 | RosettaTranslationOrigin, TranslationConflict) | 28 | RosettaTranslationOrigin, TranslationConflict) |
340 | 27 | from lp.translations.interfaces.side import TranslationSide | ||
341 | 28 | from lp.translations.model.translationmessage import ( | 29 | from lp.translations.model.translationmessage import ( |
342 | 29 | DummyTranslationMessage) | 30 | DummyTranslationMessage) |
343 | 30 | 31 | ||
344 | @@ -945,11 +946,10 @@ | |||
345 | 945 | name='main', product=self.foo) | 946 | name='main', product=self.foo) |
346 | 946 | removeSecurityProxy(self.foo).official_rosetta = True | 947 | removeSecurityProxy(self.foo).official_rosetta = True |
347 | 947 | 948 | ||
349 | 948 | self.potemplate = self.factory.makePOTemplate( | 949 | template = self.potemplate = self.factory.makePOTemplate( |
350 | 949 | productseries=self.foo_main, name="messages") | 950 | productseries=self.foo_main, name="messages") |
354 | 950 | self.potmsgset = self.factory.makePOTMsgSet(self.potemplate, | 951 | self.potmsgset = self.factory.makePOTMsgSet(template, sequence=1) |
355 | 951 | sequence=1) | 952 | self.pofile = self.factory.makePOFile('eo', template) |
353 | 952 | self.pofile = self.factory.makePOFile('eo', self.potemplate) | ||
356 | 953 | 953 | ||
357 | 954 | def test_resetCurrentTranslation_shared(self): | 954 | def test_resetCurrentTranslation_shared(self): |
358 | 955 | # Resetting a shared current translation will change iscurrent=False | 955 | # Resetting a shared current translation will change iscurrent=False |
359 | @@ -1625,3 +1625,75 @@ | |||
360 | 1625 | 1625 | ||
361 | 1626 | self.assertEqual(message, potmsgset.getImportedTranslationMessage( | 1626 | self.assertEqual(message, potmsgset.getImportedTranslationMessage( |
362 | 1627 | pofile.potemplate, pofile.language)) | 1627 | pofile.potemplate, pofile.language)) |
363 | 1628 | |||
364 | 1629 | def test_detects_conflict(self): | ||
365 | 1630 | pofile, potmsgset = self._makePOFileAndPOTMsgSet() | ||
366 | 1631 | translations = self._makeTranslations(potmsgset) | ||
367 | 1632 | origin = RosettaTranslationOrigin.ROSETTAWEB | ||
368 | 1633 | |||
369 | 1634 | # A translator bases a change on a page view from 5 minutes ago. | ||
370 | 1635 | lock_timestamp = datetime.now(pytz.UTC) - timedelta(minutes=5) | ||
371 | 1636 | |||
372 | 1637 | # Meanwhile someone else changes the same message's translation. | ||
373 | 1638 | newer_translation = self.factory.makeCurrentTranslationMessage( | ||
374 | 1639 | pofile=pofile, potmsgset=potmsgset) | ||
375 | 1640 | |||
376 | 1641 | # This raises a translation conflict. | ||
377 | 1642 | self.assertRaises( | ||
378 | 1643 | TranslationConflict, | ||
379 | 1644 | potmsgset.setCurrentTranslation, | ||
380 | 1645 | pofile, pofile.potemplate.owner, translations, origin, | ||
381 | 1646 | lock_timestamp=lock_timestamp) | ||
382 | 1647 | |||
383 | 1648 | |||
384 | 1649 | class TestCheckForConflict(TestCaseWithFactory): | ||
385 | 1650 | """Test POTMsgSet._checkForConflict.""" | ||
386 | 1651 | |||
387 | 1652 | layer = ZopelessDatabaseLayer | ||
388 | 1653 | |||
389 | 1654 | def test_passes_nonconflict(self): | ||
390 | 1655 | # If there is no conflict, _checkForConflict completes normally. | ||
391 | 1656 | current_tm = self.factory.makeCurrentTranslationMessage() | ||
392 | 1657 | potmsgset = removeSecurityProxy(current_tm.potmsgset) | ||
393 | 1658 | newer = current_tm.date_reviewed + timedelta(days=1) | ||
394 | 1659 | |||
395 | 1660 | potmsgset._checkForConflict(current_tm, newer) | ||
396 | 1661 | |||
397 | 1662 | def test_detects_conflict(self): | ||
398 | 1663 | # If there's been another translation since lock_timestamp, | ||
399 | 1664 | # _checkForConflict raises TranslationConflict. | ||
400 | 1665 | current_tm = self.factory.makeCurrentTranslationMessage() | ||
401 | 1666 | potmsgset = removeSecurityProxy(current_tm.potmsgset) | ||
402 | 1667 | older = current_tm.date_reviewed - timedelta(days=1) | ||
403 | 1668 | |||
404 | 1669 | self.assertRaises( | ||
405 | 1670 | TranslationConflict, | ||
406 | 1671 | potmsgset._checkForConflict, | ||
407 | 1672 | current_tm, older) | ||
408 | 1673 | |||
409 | 1674 | def test_passes_identical_change(self): | ||
410 | 1675 | # When concurrent translations are identical, there is no | ||
411 | 1676 | # conflict. | ||
412 | 1677 | current_tm = self.factory.makeCurrentTranslationMessage() | ||
413 | 1678 | potmsgset = removeSecurityProxy(current_tm.potmsgset) | ||
414 | 1679 | older = current_tm.date_reviewed - timedelta(days=1) | ||
415 | 1680 | |||
416 | 1681 | potmsgset._checkForConflict( | ||
417 | 1682 | current_tm, older, potranslations=current_tm.all_msgstrs) | ||
418 | 1683 | |||
419 | 1684 | def test_quiet_if_no_current_message(self): | ||
420 | 1685 | # If there is no current translation, _checkForConflict accepts | ||
421 | 1686 | # that as conflict-free. | ||
422 | 1687 | potemplate = self.factory.makePOTemplate() | ||
423 | 1688 | potmsgset = self.factory.makePOTMsgSet(potemplate, sequence=1) | ||
424 | 1689 | old = datetime.now(pytz.UTC) - timedelta(days=366) | ||
425 | 1690 | |||
426 | 1691 | removeSecurityProxy(potmsgset)._checkForConflict(None, old) | ||
427 | 1692 | |||
428 | 1693 | def test_quiet_if_no_timestamp(self): | ||
429 | 1694 | # If there is no lock_timestamp, _checkForConflict does not | ||
430 | 1695 | # check for conflicts. | ||
431 | 1696 | current_tm = self.factory.makeCurrentTranslationMessage() | ||
432 | 1697 | potmsgset = removeSecurityProxy(current_tm.potmsgset) | ||
433 | 1698 | |||
434 | 1699 | removeSecurityProxy(potmsgset)._checkForConflict(current_tm, None) | ||
435 | 1628 | 1700 | ||
436 | === modified file 'lib/lp/translations/tests/test_translationmessage.py' | |||
437 | --- lib/lp/translations/tests/test_translationmessage.py 2010-08-17 10:13:25 +0000 | |||
438 | +++ lib/lp/translations/tests/test_translationmessage.py 2010-08-24 07:55:55 +0000 | |||
439 | @@ -18,7 +18,9 @@ | |||
440 | 18 | from lp.translations.model.translationmessage import DummyTranslationMessage | 18 | from lp.translations.model.translationmessage import DummyTranslationMessage |
441 | 19 | from lp.translations.interfaces.side import ITranslationSideTraitsSet | 19 | from lp.translations.interfaces.side import ITranslationSideTraitsSet |
442 | 20 | from lp.translations.interfaces.translationmessage import ( | 20 | from lp.translations.interfaces.translationmessage import ( |
444 | 21 | ITranslationMessage) | 21 | ITranslationMessage, |
445 | 22 | TranslationConflict, | ||
446 | 23 | ) | ||
447 | 22 | from lp.translations.interfaces.translations import TranslationConstants | 24 | from lp.translations.interfaces.translations import TranslationConstants |
448 | 23 | from canonical.testing import ZopelessDatabaseLayer | 25 | from canonical.testing import ZopelessDatabaseLayer |
449 | 24 | 26 | ||
450 | @@ -250,6 +252,19 @@ | |||
451 | 250 | 252 | ||
452 | 251 | self.assertEqual([], karmarecorder.karma_events) | 253 | self.assertEqual([], karmarecorder.karma_events) |
453 | 252 | 254 | ||
454 | 255 | def test_approve_detects_conflict(self): | ||
455 | 256 | pofile = self.factory.makePOFile('bo') | ||
456 | 257 | current = self.factory.makeCurrentTranslationMessage(pofile=pofile) | ||
457 | 258 | potmsgset = current.potmsgset | ||
458 | 259 | suggestion = self.factory.makeSuggestion( | ||
459 | 260 | pofile=pofile, potmsgset=potmsgset) | ||
460 | 261 | old = datetime.now(UTC) - timedelta(days=1) | ||
461 | 262 | |||
462 | 263 | self.assertRaises( | ||
463 | 264 | TranslationConflict, | ||
464 | 265 | suggestion.approve, | ||
465 | 266 | pofile, self.factory.makePerson(), lock_timestamp=old) | ||
466 | 267 | |||
467 | 253 | 268 | ||
468 | 254 | class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory): | 269 | class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory): |
469 | 255 | """Tests for `TranslationMessage.findIdenticalMessage`.""" | 270 | """Tests for `TranslationMessage.findIdenticalMessage`.""" |