Merge lp:~cjohnston/ubuntu-ci-services-itself/ticket-complete into lp:ubuntu-ci-services-itself

Proposed by Chris Johnston
Status: Merged
Approved by: Chris Johnston
Approved revision: 131
Merged at revision: 144
Proposed branch: lp:~cjohnston/ubuntu-ci-services-itself/ticket-complete
Merge into: lp:ubuntu-ci-services-itself
Diff against target: 470 lines (+220/-33)
9 files modified
docs/components/ticket-system.rst (+10/-1)
ticket_system/project/admin.py (+2/-2)
ticket_system/project/api.py (+0/-5)
ticket_system/project/migrations/0001_initial.py (+1/-3)
ticket_system/project/models.py (+0/-1)
ticket_system/project/tests.py (+4/-15)
ticket_system/ticket/api.py (+37/-0)
ticket_system/ticket/models.py (+12/-0)
ticket_system/ticket/tests/test_write_api.py (+154/-6)
To merge this branch: bzr merge lp:~cjohnston/ubuntu-ci-services-itself/ticket-complete
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Chris Johnston (community) Needs Resubmitting
Review via email: mp+202454@code.launchpad.net

Commit message

Add ticket complete API call to the TS

Description of the change

- Update docs
- Remove sourcepackage from BinaryPackage object as it isn't needed
- Add a updateticketstatus/complete/ api url.

What this new url does:

This URL can be called by a get api call. When called it does a few things.

- It takes all binaries that the owner of the ticket added as 'added binaries' and adds them to the 'master list' of binaries.
- It takes all binaries that the owner of the ticket added as 'removed binaries' and removes them from the 'master list' of binaries.
- Marks the ticket workflow step and status as complete.

To post a comment you must log in.
Revision history for this message
Andy Doan (doanac) wrote :

do we really want this API to be an HTTP GET and not a PATCH?

Revision history for this message
Chris Johnston (cjohnston) wrote :

On Wed, Jan 22, 2014 at 11:36 AM, Andy Doan <email address hidden>wrote:

> do we really want this API to be an HTTP GET and not a PATCH?
>
You aren't actually patching anything. I'm not sure though?

Revision history for this message
Andy Doan (doanac) wrote :

On 01/22/2014 10:54 AM, Chris Johnston wrote:
> On Wed, Jan 22, 2014 at 11:36 AM, Andy Doan <email address hidden>wrote:
>
>> do we really want this API to be an HTTP GET and not a PATCH?
>>
> You aren't actually patching anything. I'm not sure though?

Things are being changed:

219 + queryset.current_workflow_step = TicketWorkflowStep.COMPLETED.value
220 + queryset.status = TicketWorkflowStepStatus.COMPLETED.value
221 + queryset.save()

Normally a GET implies a read-only operation. This is doing writes. If
we move to a secure model someday - we could easily forget about this
method.

126. By Chris Johnston

Update per review

127. By Chris Johnston

Merge trunk

Revision history for this message
Chris Johnston (cjohnston) :
review: Needs Resubmitting
128. By Chris Johnston

Fix tests

Revision history for this message
Andy Doan (doanac) wrote :

191 + if (
192 + update["current_workflow_step"] == "1000" and
193 + update["status"] == "1000"
194 + ):

you forgot to update to using the new str functions you implemented

24 + curl --dump-header - http://localhost:8000/api/v1/updateticketstatus/1/complete/

you need to update the doc for the updated usage

129. By Chris Johnston

Update per review

Revision history for this message
Chris Johnston (cjohnston) :
review: Needs Resubmitting
130. By Chris Johnston

Merge trunk

131. By Chris Johnston

Merge trunk

Revision history for this message
Andy Doan (doanac) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/components/ticket-system.rst'
2--- docs/components/ticket-system.rst 2014-01-24 20:33:49 +0000
3+++ docs/components/ticket-system.rst 2014-01-24 20:49:45 +0000
4@@ -204,7 +204,7 @@
5
6 curl --dump-header - http://localhost:8000/api/v1/ticketstatus/
7
8-*New*
9+*Queued*
10
11 ::
12
13@@ -256,6 +256,15 @@
14
15 curl --dump-header - -H "Content-Type: application/json" -X PATCH --data '{"current_workflow_step": "100", "status": "000"}' http://localhost:8000/api/v1/updateticketstatus/1/
16
17+Mark ticket complete
18+~~~~~~~~~~~~~~~~~~~~
19+
20+To be used by the lander. The act of marking a ticket complete will modify the master list of binary packages which are tested by adding to the list any of the 'added_binaries' from the ticket and removing any of the 'removed_binaries' from the ticket.
21+
22+::
23+
24+ curl --dump-header - -H "Content-Type: application/json" -X PATCH --data '{"current_workflow_step": "1000", "status": "1000"}' http://localhost:8000/api/v1/updateticketstatus/1/
25+
26 Update subticket status
27 ~~~~~~~~~~~~~~~~~~~~~~~
28
29
30=== modified file 'ticket_system/project/admin.py'
31--- ticket_system/project/admin.py 2013-12-20 19:41:18 +0000
32+++ ticket_system/project/admin.py 2014-01-24 20:49:45 +0000
33@@ -23,8 +23,8 @@
34
35
36 class BinaryPackageAdmin(admin.ModelAdmin):
37- search_fields = ['name', 'sourcepackage']
38- list_display = ('name', 'sourcepackage')
39+ search_fields = ['name']
40+ list_display = ('name',)
41
42 admin.site.register(SourcePackage, SourcePackageAdmin)
43 admin.site.register(BinaryPackage, BinaryPackageAdmin)
44
45=== modified file 'ticket_system/project/api.py'
46--- ticket_system/project/api.py 2013-12-16 18:52:37 +0000
47+++ ticket_system/project/api.py 2014-01-24 20:49:45 +0000
48@@ -13,10 +13,8 @@
49 # You should have received a copy of the GNU Affero General Public License
50 # along with this program. If not, see <http://www.gnu.org/licenses/>.
51
52-from tastypie import fields
53 from tastypie.authorization import Authorization
54 from tastypie.resources import ModelResource
55-from tastypie.constants import ALL
56 from project.models import SourcePackage, BinaryPackage
57
58
59@@ -32,8 +30,6 @@
60
61
62 class BinaryPackageResource(ModelResource):
63- sourcepackage = fields.ToOneField(SourcePackageResource, 'sourcepackage',
64- full=True)
65
66 class Meta:
67 queryset = BinaryPackage.objects.all()
68@@ -41,5 +37,4 @@
69 authorization = Authorization()
70 filtering = {
71 "name": ('exact', 'iexact', 'startswith', 'istartswith'),
72- "seeded": ALL,
73 }
74
75=== modified file 'ticket_system/project/migrations/0001_initial.py'
76--- ticket_system/project/migrations/0001_initial.py 2014-01-22 16:33:53 +0000
77+++ ticket_system/project/migrations/0001_initial.py 2014-01-24 20:49:45 +0000
78@@ -19,7 +19,6 @@
79 db.create_table('binarypackage', (
80 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
81 ('name', self.gf('django.db.models.fields.CharField')(max_length=4096)),
82- ('sourcepackage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['project.SourcePackage'])),
83 ))
84 db.send_create_signal(u'project', ['BinaryPackage'])
85
86@@ -36,8 +35,7 @@
87 u'project.binarypackage': {
88 'Meta': {'object_name': 'BinaryPackage', 'db_table': "'binarypackage'"},
89 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90- 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
91- 'sourcepackage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['project.SourcePackage']"})
92+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
93 },
94 u'project.sourcepackage': {
95 'Meta': {'object_name': 'SourcePackage', 'db_table': "'sourcepackage'"},
96
97=== modified file 'ticket_system/project/models.py'
98--- ticket_system/project/models.py 2014-01-23 17:31:52 +0000
99+++ ticket_system/project/models.py 2014-01-24 20:49:45 +0000
100@@ -31,7 +31,6 @@
101 db_table = "binarypackage"
102
103 name = models.CharField(max_length=4096)
104- sourcepackage = models.ForeignKey("SourcePackage")
105
106 def __unicode__(self):
107 return self.name
108
109=== modified file 'ticket_system/project/tests.py'
110--- ticket_system/project/tests.py 2014-01-24 15:10:17 +0000
111+++ ticket_system/project/tests.py 2014-01-24 20:49:45 +0000
112@@ -43,8 +43,7 @@
113
114 def setUp(self):
115 self.sourcepackage = sourcepackage_recipe.make()
116- self.binarypackage = mommy.make('BinaryPackage',
117- sourcepackage=self.sourcepackage)
118+ self.binarypackage = mommy.make('BinaryPackage')
119
120 def test_creating_binarypackage(self):
121 binarypackage_in_database = BinaryPackage.objects.all()
122@@ -122,25 +121,15 @@
123 def setUp(self):
124 super(BinaryPackageResourceTest, self).setUp('/api/v1')
125 self.sourcepackage = sourcepackage_recipe.make()
126- self.binarypackage = mommy.make('BinaryPackage',
127- sourcepackage=self.sourcepackage)
128+ self.binarypackage = mommy.make('BinaryPackage')
129 self.detail_url = 'binarypackage/{0}/'.format(self.binarypackage.pk)
130- self.post_binarypackage_data = {
131- 'name': 'ubuntu-ci',
132- 'sourcepackage': '/api/v1/sourcepackage/{0}/'.format(
133- self.sourcepackage.pk)
134- }
135+ self.post_binarypackage_data = {'name': 'ubuntu-ci'}
136
137 def test_get_binarypackage_list_json(self):
138 obj = self.getResource('binarypackage/')
139 self.assertEqual(obj['objects'][0], {
140 u'id': self.binarypackage.pk,
141 u'name': unicode(self.binarypackage.name),
142- u'sourcepackage': {
143- u'id': self.sourcepackage.pk,
144- u'name': unicode(self.sourcepackage.name),
145- u'resource_uri': u'/api/v1/sourcepackage/{0}/'.format(
146- self.sourcepackage.pk)},
147 u'resource_uri': u'/api/v1/binarypackage/{0}/'.format(
148 self.binarypackage.pk)
149 })
150@@ -149,7 +138,7 @@
151 obj = self.getResource(self.detail_url)
152
153 # We use ``assertKeys`` here to just verify the keys, not all the data.
154- self.assertKeys(obj, ['id', 'name', 'resource_uri', 'sourcepackage'])
155+ self.assertKeys(obj, ['id', 'name', 'resource_uri'])
156 self.assertEqual(obj['name'], self.binarypackage.name)
157
158 def test_post_binarypackage(self):
159
160=== modified file 'ticket_system/ticket/api.py'
161--- ticket_system/ticket/api.py 2014-01-24 16:39:35 +0000
162+++ ticket_system/ticket/api.py 2014-01-24 20:49:45 +0000
163@@ -13,6 +13,10 @@
164 # You should have received a copy of the GNU Affero General Public License
165 # along with this program. If not, see <http://www.gnu.org/licenses/>.
166
167+import json
168+
169+from django.shortcuts import get_object_or_404
170+
171 from tastypie import fields
172 from tastypie.constants import ALL
173 from tastypie.authorization import Authorization
174@@ -23,6 +27,7 @@
175 TicketWorkflowStep, TicketWorkflowStepStatus,
176 get_enum_title)
177
178+from project.models import BinaryPackage
179 from project.api import SourcePackageResource
180
181
182@@ -161,6 +166,38 @@
183 "id": ('exact'),
184 }
185
186+ def patch_detail(self, request, **kwargs):
187+ update = json.loads(request.body)
188+ if (
189+ update["current_workflow_step"] == str(
190+ TicketWorkflowStep.COMPLETED.value) and
191+ update["status"] == str(TicketWorkflowStepStatus.COMPLETED.value)
192+ ):
193+ queryset = get_object_or_404(Ticket, pk=kwargs['pk'])
194+ added_binaries = queryset.added_binaries
195+ if added_binaries is not None:
196+ added_binaries = added_binaries.split(",")
197+ else:
198+ added_binaries = []
199+ removed_binaries = queryset.removed_binaries
200+ if removed_binaries is not None:
201+ removed_binaries = removed_binaries.split(",")
202+ else:
203+ removed_binaries = []
204+ for binary in added_binaries:
205+ #Use get or create incase the binary already exists in the list
206+ BinaryPackage.objects.get_or_create(name=binary)
207+ for binary in removed_binaries:
208+ try:
209+ # If the binary specified exists in the list, delete it
210+ bp = BinaryPackage.objects.get(name=binary)
211+ bp.delete()
212+ except BinaryPackage.DoesNotExist:
213+ # If the binary specified does not exist, move on
214+ pass
215+ return super(TicketUpdateStatusResource, self).patch_detail(request,
216+ **kwargs)
217+
218
219 class TicketStatusResource(TicketTranslatedResource):
220
221
222=== modified file 'ticket_system/ticket/models.py'
223--- ticket_system/ticket/models.py 2014-01-24 20:33:49 +0000
224+++ ticket_system/ticket/models.py 2014-01-24 20:49:45 +0000
225@@ -31,6 +31,9 @@
226 PKG_BUILDING = Item(200, "Package building")
227 COMPLETED = Item(1000, "Completed")
228
229+ def __str__(self):
230+ return str(self.value)
231+
232
233 class SubTicketWorkflowStepStatus(DBEnumeratedType):
234
235@@ -40,6 +43,9 @@
236 PKG_BUILDING_COMPLETED = Item(230, "Completed")
237 PKG_BUILDING_FAILED = Item(240, "Failed")
238
239+ def __str__(self):
240+ return str(self.value)
241+
242
243 class TicketWorkflowStep(DBEnumeratedType):
244
245@@ -52,6 +58,9 @@
246 FAILED = Item(999, "Failed")
247 COMPLETED = Item(1000, "Completed")
248
249+ def __str__(self):
250+ return str(self.value)
251+
252
253 class TicketWorkflowStepStatus(DBEnumeratedType):
254
255@@ -74,6 +83,9 @@
256 PKG_PUBLISHING_FAILED = Item(540, "Package Publishing Failed")
257 COMPLETED = Item(1000, "Completed")
258
259+ def __str__(self):
260+ return str(self.value)
261+
262
263 def _choices(enum):
264 return [
265
266=== modified file 'ticket_system/ticket/tests/test_write_api.py'
267--- ticket_system/ticket/tests/test_write_api.py 2014-01-24 17:44:14 +0000
268+++ ticket_system/ticket/tests/test_write_api.py 2014-01-24 20:49:45 +0000
269@@ -21,6 +21,11 @@
270 SubTicketWorkflowStep, SubTicketWorkflowStepStatus,
271 Ticket, TicketWorkflowStep, SubTicketArtifact,
272 TicketWorkflowStepStatus)
273+from project.models import BinaryPackage
274+
275+
276+def create_ticket(added=None, removed=None):
277+ return mommy.make('Ticket', removed_binaries=removed, added_binaries=added)
278
279
280 sourcepackage_recipe = Recipe(SourcePackage, name=seq('foobar'))
281@@ -30,7 +35,7 @@
282
283 def setUp(self):
284 super(APICreateTicketResourceTest, self).setUp('/api/v1')
285- self.ticket = mommy.make('Ticket')
286+ self.ticket = create_ticket()
287 self.resource = 'ticket/'
288 self.detail_url = self.resource + '{0}/'.format(self.ticket.pk)
289 self.post_ticket_data = {
290@@ -264,21 +269,38 @@
291
292 def setUp(self):
293 super(APIUpdateTicketStatuses, self).setUp('/api/v1')
294- self.ticket = mommy.make('Ticket')
295+ mommy.make('BinaryPackage', name='binary3')
296+ mommy.make('BinaryPackage', name='binary4')
297+ mommy.make('BinaryPackage', name='binary5')
298+ self.ticket = create_ticket(added='binary1,binary2,binary6',
299+ removed='binary3,binary4')
300 self.ticket_detail_url = 'updateticketstatus/{0}/'.format(
301 self.ticket.pk)
302
303 def test_patch_ticket_status(self):
304 new_data = {
305- 'current_workflow_step': int(TicketWorkflowStep.IMAGE_BUILDING),
306- 'status': int(TicketWorkflowStepStatus.IMAGE_BUILDING_COMPLETED),
307+ 'current_workflow_step': str(
308+ TicketWorkflowStep.IMAGE_BUILDING.value),
309+ 'status': str(
310+ TicketWorkflowStepStatus.IMAGE_BUILDING_COMPLETED.value),
311 }
312 self.assertEqual(Ticket.objects.count(), 1)
313- self.patch('/api/v1/' + self.ticket_detail_url,
314- new_data)
315+ self.assertEqual(BinaryPackage.objects.count(), 3)
316+ binaries = BinaryPackage.objects.all()
317+ self.assertQuerysetEqual(binaries,
318+ ["<BinaryPackage: binary3>",
319+ "<BinaryPackage: binary4>",
320+ "<BinaryPackage: binary5>"], ordered=False)
321+ self.patch('/api/v1/' + self.ticket_detail_url, new_data)
322 # Make sure the count hasn't changed & we did an update.
323 self.assertEqual(Ticket.objects.count(), 1)
324 # Check for updated data.
325+ self.assertEqual(BinaryPackage.objects.count(), 3)
326+ binaries = BinaryPackage.objects.all()
327+ self.assertQuerysetEqual(binaries,
328+ ["<BinaryPackage: binary3>",
329+ "<BinaryPackage: binary4>",
330+ "<BinaryPackage: binary5>"], ordered=False)
331 self.assertEqual(Ticket.objects.get(
332 pk=self.ticket.pk).current_workflow_step,
333 int(TicketWorkflowStep.IMAGE_BUILDING))
334@@ -286,6 +308,128 @@
335 Ticket.objects.get(pk=self.ticket.pk).status,
336 int(TicketWorkflowStepStatus.IMAGE_BUILDING_COMPLETED))
337
338+ def test_update_ticket_status_complete(self):
339+ new_data = {
340+ "current_workflow_step": str(TicketWorkflowStep.COMPLETED.value),
341+ "status": str(TicketWorkflowStepStatus.COMPLETED.value),
342+ }
343+ self.assertEqual(Ticket.objects.count(), 1)
344+ self.assertEqual(BinaryPackage.objects.count(), 3)
345+ binaries = BinaryPackage.objects.all()
346+ self.assertQuerysetEqual(binaries,
347+ ["<BinaryPackage: binary3>",
348+ "<BinaryPackage: binary4>",
349+ "<BinaryPackage: binary5>"], ordered=False)
350+ self.assertEqual(Ticket.objects.get(
351+ pk=self.ticket.pk).current_workflow_step,
352+ self.ticket.current_workflow_step)
353+ self.assertEqual(Ticket.objects.get(
354+ pk=self.ticket.pk).status,
355+ self.ticket.status)
356+ self.patch('/api/v1/' + self.ticket_detail_url, new_data)
357+ self.assertEqual(Ticket.objects.count(), 1)
358+ self.assertEqual(BinaryPackage.objects.count(), 4)
359+ binaries = BinaryPackage.objects.all()
360+ self.assertQuerysetEqual(binaries,
361+ ["<BinaryPackage: binary1>",
362+ "<BinaryPackage: binary2>",
363+ "<BinaryPackage: binary5>",
364+ "<BinaryPackage: binary6>"], ordered=False)
365+ self.assertEqual(Ticket.objects.get(
366+ pk=self.ticket.pk).current_workflow_step,
367+ int(TicketWorkflowStep.COMPLETED))
368+ self.assertEqual(Ticket.objects.get(
369+ pk=self.ticket.pk).status,
370+ int(TicketWorkflowStepStatus.COMPLETED))
371+
372+ def test_try_adding_binary_already_in_list(self):
373+ """
374+ Test what happens when a ticket attempts to add a binary to the master
375+ list that is already in the master list
376+ """
377+ new_data = {
378+ "current_workflow_step": str(TicketWorkflowStep.COMPLETED.value),
379+ "status": str(TicketWorkflowStepStatus.COMPLETED.value),
380+ }
381+ self.ticket_2 = create_ticket(added='binary1,binary2,binary3')
382+ self.assertEqual(Ticket.objects.count(), 2)
383+ self.assertEqual(BinaryPackage.objects.count(), 3)
384+ binaries = BinaryPackage.objects.all()
385+ self.assertQuerysetEqual(binaries,
386+ ["<BinaryPackage: binary3>",
387+ "<BinaryPackage: binary4>",
388+ "<BinaryPackage: binary5>"], ordered=False)
389+ self.client.patch('/api/v1/updateticketstatus/{0}/'.format(
390+ self.ticket_2.pk), data=new_data)
391+ self.assertEqual(BinaryPackage.objects.count(), 5)
392+ binaries = BinaryPackage.objects.all()
393+ self.assertQuerysetEqual(binaries,
394+ ["<BinaryPackage: binary1>",
395+ "<BinaryPackage: binary2>",
396+ "<BinaryPackage: binary3>",
397+ "<BinaryPackage: binary4>",
398+ "<BinaryPackage: binary5>"], ordered=False)
399+
400+ def test_try_removing_binary_not_in_list(self):
401+ """
402+ Test what happens when a ticket attempts to remove a binary from the
403+ master list when it isn't in the master list
404+ """
405+ new_data = {
406+ "current_workflow_step": str(TicketWorkflowStep.COMPLETED.value),
407+ "status": str(TicketWorkflowStepStatus.COMPLETED.value),
408+ }
409+ self.ticket_2 = create_ticket(removed='binary2,binary3')
410+ self.assertEqual(Ticket.objects.count(), 2)
411+ self.assertEqual(BinaryPackage.objects.count(), 3)
412+ binaries = BinaryPackage.objects.all()
413+ self.assertQuerysetEqual(binaries,
414+ ["<BinaryPackage: binary3>",
415+ "<BinaryPackage: binary4>",
416+ "<BinaryPackage: binary5>"], ordered=False)
417+ self.client.patch('/api/v1/updateticketstatus/{0}/'.format(
418+ self.ticket_2.pk), data=new_data)
419+ binaries = BinaryPackage.objects.all()
420+ self.assertEqual(BinaryPackage.objects.count(), 2)
421+ self.assertQuerysetEqual(binaries,
422+ ["<BinaryPackage: binary4>",
423+ "<BinaryPackage: binary5>"], ordered=False)
424+
425+ def test_update_ticket_status_complete_404(self):
426+ """
427+ Test trying to complete the status of a ticket that doesn't exist
428+ """
429+ new_data = {
430+ "current_workflow_step": str(TicketWorkflowStep.COMPLETED.value),
431+ "status": str(TicketWorkflowStepStatus.COMPLETED.value),
432+ }
433+ self.assertEqual(Ticket.objects.count(), 1)
434+ self.assertEqual(BinaryPackage.objects.count(), 3)
435+ binaries = BinaryPackage.objects.all()
436+ self.assertQuerysetEqual(binaries,
437+ ["<BinaryPackage: binary3>",
438+ "<BinaryPackage: binary4>",
439+ "<BinaryPackage: binary5>"], ordered=False)
440+ self.assertEqual(Ticket.objects.get(
441+ pk=self.ticket.pk).current_workflow_step,
442+ self.ticket.current_workflow_step)
443+ self.assertEqual(Ticket.objects.get(
444+ pk=self.ticket.pk).status,
445+ self.ticket.status)
446+ resp = self.client.patch('updateticketstatus/17394/', data=new_data)
447+ self.assertEqual(Ticket.objects.count(), 1)
448+ self.assertEqual(BinaryPackage.objects.count(), 3)
449+ binaries = BinaryPackage.objects.all()
450+ self.assertQuerysetEqual(binaries,
451+ ["<BinaryPackage: binary3>",
452+ "<BinaryPackage: binary4>",
453+ "<BinaryPackage: binary5>"], ordered=False)
454+ self.assertEqual(resp.status_code, 404)
455+
456+ def test_delete_ticket_not_allowed(self):
457+ resp = self.delete(resource=self.ticket_detail_url)
458+ self.assertHttpMethodNotAllowed(resp)
459+
460
461 class APIUpdateSubTicketStatuses(TastypieTestCase):
462
463@@ -316,3 +460,7 @@
464 int(SubTicketWorkflowStep.PKG_BUILDING))
465 self.assertEqual(SubTicket.objects.get(pk=self.subticket.pk).status,
466 int(SubTicketWorkflowStepStatus.PKG_BUILDING_WAITING))
467+
468+ def test_delete_subticket_not_allowed(self):
469+ resp = self.delete(resource=self.subticket_detail_url)
470+ self.assertHttpMethodNotAllowed(resp)

Subscribers

People subscribed via source and target branches