Issues (87)

tcms/rpc/api/testcase.py (2 issues)

1
# -*- coding: utf-8 -*-
2
3
from datetime import timedelta
4
5
from django.db.models.functions import Coalesce
6
from django.forms import EmailField, ValidationError
7
from django.forms.models import model_to_dict
8
from modernrpc.core import REQUEST_KEY, rpc_method
9
10
from tcms.core import helpers
11
from tcms.core.utils import form_errors_to_list
12
from tcms.management.models import Component, Tag
13
from tcms.rpc import utils
14
from tcms.rpc.api.forms.testcase import NewForm, UpdateForm
15
from tcms.rpc.decorators import permissions_required
16
from tcms.testcases.models import Property, TestCase, TestCasePlan
17
18
__all__ = (
19
    "create",
20
    "update",
21
    "filter",
22
    "history",
23
    "sortkeys",
24
    "remove",
25
    "add_comment",
26
    "remove_comment",
27
    "add_component",
28
    "comments",
29
    "remove_component",
30
    "add_notification_cc",
31
    "get_notification_cc",
32
    "remove_notification_cc",
33
    "add_tag",
34
    "remove_tag",
35
    "add_attachment",
36
    "list_attachments",
37
    "properties",
38
    "remove_property",
39
    "add_property",
40
)
41
42
43
@permissions_required("testcases.add_testcasecomponent")
44
@rpc_method(name="TestCase.add_component")
45
def add_component(case_id, component):
46
    """
47
    .. function:: RPC TestCase.add_component(case_id, component)
48
49
        Add component to the selected test case.
50
51
        :param case_id: PK of TestCase to modify
52
        :type case_id: int
53
        :param component: Name of Component to add
54
        :type component: str
55
        :return: Serialized :class:`tcms.management.models.Component` object
56
        :rtype: dict
57
        :raises PermissionDenied: if missing the *testcases.add_testcasecomponent*
58
                 permission
59
        :raises DoesNotExist: if missing test case or component that match the
60
                 specified PKs
61
    """
62
    case = TestCase.objects.get(pk=case_id)
63
    component_obj = Component.objects.get(name=component, product=case.category.product)
64
    case.add_component(component_obj)
65
    return model_to_dict(component_obj)
66
67
68
@permissions_required("testcases.delete_testcasecomponent")
69
@rpc_method(name="TestCase.remove_component")
70
def remove_component(case_id, component_id):
71
    """
72
    .. function:: RPC TestCase.remove_component(case_id, component_id)
73
74
        Remove selected component from the selected test case.
75
76
        :param case_id: PK of TestCase to modify
77
        :type case_id: int
78
        :param component_id: PK of Component to remove
79
        :type component_id: int
80
        :raises PermissionDenied: if missing the *testcases.delete_testcasecomponent*
81
                 permission
82
        :raises DoesNotExist: if missing test case or component that match the
83
                 specified PKs
84
    """
85
    TestCase.objects.get(pk=case_id).remove_component(
86
        Component.objects.get(pk=component_id)
87
    )
88
89
90
def _validate_cc_list(cc_list):
91
    """
92
    Validate each email address given in argument. Called by
93
    notification RPC methods.
94
95
    :param cc_list: List of email addresses
96
    :type cc_list: list
97
    :raises TypeError or ValidationError: if addresses are not valid.
98
    """
99
100
    if not isinstance(cc_list, list):
101
        raise TypeError("cc_list should be a list object.")
102
103
    field = EmailField(
104
        required=True,
105
        error_messages={"invalid": "Following email address(es) are invalid: %s"},
106
    )
107
    invalid_emails = []
108
109
    for item in cc_list:
110
        try:
111
            field.clean(item)
112
        except ValidationError:
113
            invalid_emails.append(item)
114
115
    if invalid_emails:
116
        raise ValidationError(
117
            field.error_messages["invalid"] % ", ".join(invalid_emails)
118
        )
119
120
121
@permissions_required("testcases.change_testcase")
122
@rpc_method(name="TestCase.add_notification_cc")
123
def add_notification_cc(case_id, cc_list):
124
    """
125
    .. function:: RPC TestCase.add_notification_cc(case_id, cc_list)
126
127
        Add email addresses to the notification list of specified TestCase
128
129
        :param case_id: PK of TestCase to be modified
130
        :type case_id: int
131
        :param cc_list: List of email addresses
132
        :type cc_list: list(str)
133
        :raises TypeError or ValidationError: if email validation fails
134
        :raises PermissionDenied: if missing *testcases.change_testcase* permission
135
        :raises TestCase.DoesNotExist: if object with case_id doesn't exist
136
    """
137
138
    _validate_cc_list(cc_list)
139
140
    test_case = TestCase.objects.get(pk=case_id)
141
    test_case.emailing.add_cc(cc_list)
142
143
144
@permissions_required("testcases.change_testcase")
145
@rpc_method(name="TestCase.remove_notification_cc")
146
def remove_notification_cc(case_id, cc_list):
147
    """
148
    .. function:: RPC TestCase.remove_notification_cc(case_id, cc_list)
149
150
        Remove email addresses from the notification list of specified TestCase
151
152
        :param case_id: PK of TestCase to modify
153
        :type case_id: int
154
        :param cc_list: List of email addresses
155
        :type cc_list: list(str)
156
        :raises TypeError or ValidationError: if email validation fails
157
        :raises PermissionDenied: if missing *testcases.change_testcase* permission
158
        :raises TestCase.DoesNotExist: if object with case_id doesn't exist
159
    """
160
161
    _validate_cc_list(cc_list)
162
163
    TestCase.objects.get(pk=case_id).emailing.remove_cc(cc_list)
164
165
166
@permissions_required("testcases.view_testcase")
167
@rpc_method(name="TestCase.get_notification_cc")
168
def get_notification_cc(case_id):
169
    """
170
    .. function:: RPC TestCase.get_notification_cc(case_id)
171
172
        Return notification list for specified TestCase
173
174
        :param case_id: PK of TestCase
175
        :type case_id: int
176
        :return: List of email addresses
177
        :rtype: list(str)
178
        :raises TestCase.DoesNotExist: if object with case_id doesn't exist
179
    """
180
    return TestCase.objects.get(pk=case_id).emailing.get_cc_list()
181
182
183
@permissions_required("testcases.add_testcasetag")
184
@rpc_method(name="TestCase.add_tag")
185
def add_tag(case_id, tag, **kwargs):
186
    """
187
    .. function:: RPC TestCase.add_tag(case_id, tag)
188
189
        Add one tag to the specified test case.
190
191
        :param case_id: PK of TestCase to modify
192
        :type case_id: int
193
        :param tag: Tag name to add
194
        :type tag: str
195
        :param \\**kwargs: Dict providing access to the current request, protocol,
196
                entry point name and handler instance from the rpc method
197
        :raises PermissionDenied: if missing *testcases.add_testcasetag* permission
198
        :raises TestCase.DoesNotExist: if object specified by PK doesn't exist
199
        :raises Tag.DoesNotExist: if missing *management.add_tag* permission and *tag*
200
                 doesn't exist in the database!
201
    """
202
    request = kwargs.get(REQUEST_KEY)
203
    tag, _ = Tag.get_or_create(request.user, tag)
204
    TestCase.objects.get(pk=case_id).add_tag(tag)
205
206
207
@permissions_required("testcases.delete_testcasetag")
208
@rpc_method(name="TestCase.remove_tag")
209
def remove_tag(case_id, tag):
210
    """
211
    .. function:: RPC TestCase.remove_tag(case_id, tag)
212
213
        Remove tag from a test case.
214
215
        :param case_id: PK of TestCase to modify
216
        :type case_id: int
217
        :param tag: Tag name to remove
218
        :type tag: str
219
        :raises PermissionDenied: if missing *testcases.delete_testcasetag* permission
220
        :raises DoesNotExist: if objects specified don't exist
221
    """
222
    TestCase.objects.get(pk=case_id).remove_tag(Tag.objects.get(name=tag))
223
224
225
@permissions_required("testcases.add_testcase")
226
@rpc_method(name="TestCase.create")
227
def create(values, **kwargs):
228
    """
229
    .. function:: RPC TestCase.create(values)
230
231
        Create a new TestCase object and store it in the database.
232
233
        :param values: Field values for :class:`tcms.testcases.models.TestCase`
234
        :type values: dict
235
        :param \\**kwargs: Dict providing access to the current request, protocol,
236
                entry point name and handler instance from the rpc method
237
        :return: Serialized :class:`tcms.testcases.models.TestCase` object
238
        :rtype: dict
239
        :raises ValueError: if form is not valid
240
        :raises PermissionDenied: if missing *testcases.add_testcase* permission
241
242
        Minimal test case parameters::
243
244
            >>> values = {
245
                'category': 135,
246
                'product': 61,
247
            'summary': 'Testing XML-RPC',
248
            'priority': 1,
249
            }
250
            >>> TestCase.create(values)
251
    """
252
    request = kwargs.get(REQUEST_KEY)
253
254
    if not (values.get("author") or values.get("author_id")):
255
        values["author"] = request.user.pk
256
257
    form = NewForm(values)
258
259
    if form.is_valid():
260
        test_case = form.save()
261
        result = model_to_dict(test_case, exclude=["component", "plan", "tag"])
262
        # b/c date is added in the DB layer and model_to_dict() doesn't return it
263
        result["create_date"] = test_case.create_date
264
        result["setup_duration"] = str(result["setup_duration"])
265
        result["testing_duration"] = str(result["testing_duration"])
266
        return result
267
268
    raise ValueError(form_errors_to_list(form))
269
270
271
@permissions_required("testcases.view_testcase")
272
@rpc_method(name="TestCase.filter")
273
def filter(query=None):  # pylint: disable=redefined-builtin
274
    """
275
    .. function:: RPC TestCase.filter(query)
276
277
        Perform a search and return the resulting list of test cases
278
        augmented with their latest ``text``.
279
280
        :param query: Field lookups for :class:`tcms.testcases.models.TestCase`
281
        :type query: dict
282
        :return: Serialized list of :class:`tcms.testcases.models.TestCase` objects.
283
        :rtype: list(dict)
284
    """
285
    if query is None:
286
        query = {}
287
288
    qs = (
289
        TestCase.objects.annotate(
290
            expected_duration=Coalesce("setup_duration", timedelta(0))
291
            + Coalesce("testing_duration", timedelta(0))
292
        )
293
        .filter(**query)
294
        .values(
295
            "id",
296
            "create_date",
297
            "is_automated",
298
            "script",
299
            "arguments",
300
            "extra_link",
301
            "summary",
302
            "requirement",
303
            "notes",
304
            "text",
305
            "case_status",
306
            "case_status__name",
307
            "category",
308
            "category__name",
309
            "priority",
310
            "priority__value",
311
            "author",
312
            "author__username",
313
            "default_tester",
314
            "default_tester__username",
315
            "reviewer",
316
            "reviewer__username",
317
            "setup_duration",
318
            "testing_duration",
319
            "expected_duration",
320
        )
321
        .distinct()
322
    )
323
324
    return list(qs)
325
326
327
@permissions_required("testcases.view_testcase")
328
@rpc_method(name="TestCase.history")
329
def history(case_id, query=None):
330
    """
331
    .. function:: RPC TestCase.history(case_id, query)
332
333
        Return the history for a specified test case.
334
335
        :param case_id: TestCase PK
336
        :type case_id: int
337
        :param query: Field lookups for :class:`tcms.testcases.models.TestCase`
338
        :type query: dict
339
        :return: Serialized list of HistoricalTestCase objects.
340
        :rtype: list(dict)
341
    """
342
    if query is None:
343
        query = {}
344
345
    return list(TestCase.objects.get(pk=case_id).history.filter(**query).values())
346
347
348
@permissions_required("testcases.view_testcase")
349
@rpc_method(name="TestCase.sortkeys")
350
def sortkeys(query=None):
351
    """
352
    .. function:: RPC TestCase.sortkeys(query)
353
354
        Return information about TestCase position inside TestPlan.
355
356
        For example `TestCase.sortkeys({'plan': 3})`
357
358
        :param query: Field lookups for :class:`tcms.testcases.models.TestCasePlan`
359
        :type query: dict
360
        :return: Dictionary of (case_id, sortkey) pairs!
361
        :rtype: dict(case_id, sortkey)
362
    """
363
    if query is None:
364
        query = {}
365
366
    result = {}
367
    for record in TestCasePlan.objects.filter(**query):
368
        # NOTE: convert to str() otherwise we get:
369
        # Unable to serialize result as valid XML: dictionary key must be string
370
        result[str(record.case_id)] = record.sortkey
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable str does not seem to be defined.
Loading history...
371
372
    return result
373
374
375
@permissions_required("testcases.change_testcase")
376
@rpc_method(name="TestCase.update")
377
def update(case_id, values):
378
    """
379
    .. function:: RPC TestCase.update(case_id, values)
380
381
        Update the fields of the selected test case.
382
383
        :param case_id: PK of TestCase to be modified
384
        :type case_id: int
385
        :param values: Field values for :class:`tcms.testcases.models.TestCase`.
386
        :type values: dict
387
        :return: Serialized :class:`tcms.testcases.models.TestCase` object
388
        :rtype: dict
389
        :raises ValueError: if form is not valid
390
        :raises TestCase.DoesNotExist: if object specified by PK doesn't exist
391
        :raises PermissionDenied: if missing *testcases.change_testcase* permission
392
    """
393
    test_case = TestCase.objects.get(pk=case_id)
394
    form = UpdateForm(values, instance=test_case)
395
396
    if form.is_valid():
397
        test_case = form.save()
398
        result = model_to_dict(test_case, exclude=["component", "plan", "tag"])
399
        # b/c date may be None and model_to_dict() doesn't return it
400
        result["create_date"] = test_case.create_date
401
402
        # additional information
403
        result["case_status__name"] = test_case.case_status.name
404
        result["category__name"] = test_case.category.name
405
        result["priority__value"] = test_case.priority.value
406
        result["author__username"] = (
407
            test_case.author.username if test_case.author else None
408
        )
409
        result["default_tester__username"] = (
410
            test_case.default_tester.username if test_case.default_tester else None
411
        )
412
        result["reviewer__username"] = (
413
            test_case.reviewer.username if test_case.reviewer else None
414
        )
415
        result["setup_duration"] = str(result["setup_duration"])
416
        result["testing_duration"] = str(result["testing_duration"])
417
418
        return result
419
420
    raise ValueError(form_errors_to_list(form))
421
422
423
@permissions_required("testcases.delete_testcase")
424
@rpc_method(name="TestCase.remove")
425
def remove(query):
426
    """
427
    .. function:: RPC TestCase.remove(query)
428
429
        Remove TestCase object(s).
430
431
        :param query: Field lookups for :class:`tcms.testcases.models.TestCase`
432
        :type query: dict
433
        :raises PermissionDenied: if missing the *testcases.delete_testcase* permission
434
        :return: The number of objects deleted and a dictionary with the
435
                 number of deletions per object type.
436
        :rtype: int, dict
437
438
        Example - removing bug from TestCase::
439
440
            >>> TestCase.remove({
441
                'pk__in': [1, 2, 3, 4],
442
            })
443
    """
444
    return TestCase.objects.filter(**query).delete()
445
446
447
@permissions_required("attachments.view_attachment")
448
@rpc_method(name="TestCase.list_attachments")
449
def list_attachments(case_id, **kwargs):
450
    """
451
    .. function:: RPC TestCase.list_attachments(case_id)
452
453
        List attachments for the given TestCase.
454
455
        :param case_id: PK of TestCase to inspect
456
        :type case_id: int
457
        :param \\**kwargs: Dict providing access to the current request, protocol,
458
                entry point name and handler instance from the rpc method
459
        :return: A list containing information and download URLs for attachements
460
        :rtype: list
461
        :raises TestCase.DoesNotExist: if object specified by PK is missing
462
    """
463
    case = TestCase.objects.get(pk=case_id)
464
    request = kwargs.get(REQUEST_KEY)
465
    return utils.get_attachments_for(request, case)
466
467
468
@permissions_required("attachments.add_attachment")
469
@rpc_method(name="TestCase.add_attachment")
470
def add_attachment(case_id, filename, b64content, **kwargs):
471
    """
472
    .. function:: RPC TestCase.add_attachment(case_id, filename, b64content)
473
474
        Add attachment to the given TestCase.
475
476
        :param case_id: PK of TestCase
477
        :type case_id: int
478
        :param filename: File name of attachment, e.g. 'logs.txt'
479
        :type filename: str
480
        :param b64content: Base64 encoded content
481
        :type b64content: str
482
        :param \\**kwargs: Dict providing access to the current request, protocol,
483
                entry point name and handler instance from the rpc method
484
    """
485
    utils.add_attachment(
486
        case_id,
487
        "testcases.TestCase",
488
        kwargs.get(REQUEST_KEY).user,
489
        filename,
490
        b64content,
491
    )
492
493
494
@permissions_required("django_comments.add_comment")
495
@rpc_method(name="TestCase.add_comment")
496
def add_comment(case_id, comment, **kwargs):
497
    """
498
    .. function:: TestCase.add_comment(case_id, comment)
499
500
        Add comment to selected test case.
501
502
        :param case_id: PK of a TestCase object
503
        :type case_id: int
504
        :param comment: The text to add as a comment
505
        :type comment: str
506
        :param \\**kwargs: Dict providing access to the current request, protocol,
507
                entry point name and handler instance from the rpc method
508
        :return: Serialized :class:`django_comments.models.Comment` object
509
        :rtype: dict
510
        :raises PermissionDenied: if missing *django_comments.add_comment* permission
511
        :raises TestCase.DoesNotExist: if object specified by PK is missing
512
513
        .. important::
514
515
            In webUI comments are only shown **only** during test case review!
516
    """
517
    case = TestCase.objects.get(pk=case_id)
518
    created = helpers.comments.add_comment(
519
        [case], comment, kwargs.get(REQUEST_KEY).user
520
    )
521
    # we always create only one comment
522
    return model_to_dict(created[0])
523
524
525
@permissions_required("django_comments.delete_comment")
526
@rpc_method(name="TestCase.remove_comment")
527
def remove_comment(case_id, comment_id=None):
528
    """
529
    .. function:: TestCase.remove_comment(case_id, comment_id)
530
531
        Remove all or specified comment(s) from selected test case.
532
533
        :param case_id: PK of a TestCase object
534
        :type case_id: int
535
        :param comment_id: PK of a Comment object or None
536
        :type comment_id: int
537
        :raises PermissionDenied: if missing *django_comments.delete_comment* permission
538
        :raises TestCase.DoesNotExist: if object specified by PK is missing
539
    """
540
    case = TestCase.objects.get(pk=case_id)
541
    to_be_deleted = helpers.comments.get_comments(case)
542
    if comment_id:
543
        to_be_deleted = to_be_deleted.filter(pk=comment_id)
544
545
    to_be_deleted.delete()
546
547
548
@permissions_required("django_comments.view_comment")
549
@rpc_method(name="TestCase.comments")
550
def comments(case_id):
551
    """
552
    .. function:: TestCase.comments(case_id)
553
554
        Return all comment(s) for the specified test case.
555
556
        :param case_id: PK of a TestCase object
557
        :type case_id: int
558
        :return: Serialized list of :class:`django_comments.models.Comment` objects
559
        :rtype: list
560
        :raises PermissionDenied: if missing *django_comments.view_comment* permission
561
        :raises TestCase.DoesNotExist: if object specified by PK is missing
562
    """
563
    case = TestCase.objects.get(pk=case_id)
564
    result = []
565
    for comment in helpers.comments.get_comments(case):
566
        result.append(model_to_dict(comment))
567
568
    return result
569
570
571 View Code Duplication
@permissions_required("testcases.view_property")
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
572
@rpc_method(name="TestCase.properties")
573
def properties(query=None):
574
    """
575
    .. function:: TestCase.properties(query)
576
577
        Return all properties for the specified test case(s).
578
579
        :param query: Field lookups for :class:`tcms.testcases.models.Property`
580
        :type query: dict
581
        :return: Serialized list of :class:`tcms.testcases.models.Property` objects.
582
        :rtype: list(dict)
583
        :raises PermissionDenied: if missing *testcases.view_property* permission
584
    """
585
    if query is None:
586
        query = {}
587
588
    return list(
589
        Property.objects.filter(**query)
590
        .values(
591
            "id",
592
            "case",
593
            "name",
594
            "value",
595
        )
596
        .order_by("case", "name", "value")
597
        .distinct()
598
    )
599
600
601
@permissions_required("testcases.delete_property")
602
@rpc_method(name="TestCase.remove_property")
603
def remove_property(query):
604
    """
605
    .. function:: TestCase.remove_property(query)
606
607
        Remove selected properties.
608
609
        :param query: Field lookups for :class:`tcms.testcases.models.Property`
610
        :type query: dict
611
        :raises PermissionDenied: if missing *testcases.delete_property* permission
612
    """
613
    Property.objects.filter(**query).delete()
614
615
616
@permissions_required("testcases.add_property")
617
@rpc_method(name="TestCase.add_property")
618
def add_property(case_id, name, value):
619
    """
620
    .. function:: TestCase.add_property(case_id, name, value)
621
622
        Add property to test case! Duplicates are skipped without errors.
623
624
        :param case_id: Primary key for :class:`tcms.testcases.models.TestCase`
625
        :type case_id: int
626
        :param name: Name of the property
627
        :type name: str
628
        :param value: Value of the property
629
        :type value: str
630
        :return: Serialized :class:`tcms.testcases.models.Property` object.
631
        :rtype: dict
632
        :raises PermissionDenied: if missing *testcases.add_property* permission
633
    """
634
    prop, _ = Property.objects.get_or_create(case_id=case_id, name=name, value=value)
635
    return model_to_dict(prop)
636