Skip to content

Commit

Permalink
[api] Add TestPlan.add_attachment(), TestCase.add_attachment()
Browse files Browse the repository at this point in the history
to allow adding attachments via RPC. Fixes #446
  • Loading branch information
atodorov committed Mar 17, 2019
1 parent b2a67ca commit 4f39825
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 5 deletions.
31 changes: 28 additions & 3 deletions tcms/xmlrpc/api/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from tcms.management.models import Tag
from tcms.management.models import Component
from tcms.testcases.models import TestCase
from tcms.xmlrpc.utils import get_attachments_for

from tcms.xmlrpc import utils
from tcms.xmlrpc.forms import UpdateCaseForm, NewCaseForm
from tcms.xmlrpc.decorators import permissions_required


__all__ = (
'create',
'update',
Expand All @@ -30,6 +30,7 @@
'add_tag',
'remove_tag',

'add_attachment',
'list_attachments',
)

Expand Down Expand Up @@ -378,4 +379,28 @@ def list_attachments(case_id, **kwargs):
"""
case = TestCase.objects.get(pk=case_id)
request = kwargs.get(REQUEST_KEY)
return get_attachments_for(request, case)
return utils.get_attachments_for(request, case)


@permissions_required('attachments.add_attachment')
@rpc_method(name='TestCase.add_attachment')
def add_attachment(case_id, filename, b64content, **kwargs):
"""
.. function:: XML-RPC TestCase.add_attachment(case_id, filename, b64content)
Add attachment to the given TestCase.
:param case_id: PK of TestCase
:type case_id: int
:param filename: File name of attachment, e.g. 'logs.txt'
:type filename: str
:param b64content: Base64 encoded content
:type b64content: str
:return: None
"""
utils.add_attachment(
case_id,
'testcases.TestCase',
kwargs.get(REQUEST_KEY).user,
filename,
b64content)
29 changes: 27 additions & 2 deletions tcms/xmlrpc/api/testplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from tcms.testplans.models import TestPlan
from tcms.testcases.models import TestCase, TestCasePlan

from tcms.xmlrpc.utils import get_attachments_for
from tcms.xmlrpc import utils
from tcms.xmlrpc.api.forms.testplan import EditPlanForm, NewPlanForm
from tcms.xmlrpc.decorators import permissions_required

Expand All @@ -22,6 +22,7 @@
'add_tag',
'remove_tag',

'add_attachment',
'list_attachments',
)

Expand Down Expand Up @@ -264,4 +265,28 @@ def list_attachments(plan_id, **kwargs):
"""
plan = TestPlan.objects.get(pk=plan_id)
request = kwargs.get(REQUEST_KEY)
return get_attachments_for(request, plan)
return utils.get_attachments_for(request, plan)


@permissions_required('attachments.add_attachment')
@rpc_method(name='TestPlan.add_attachment')
def add_attachment(plan_id, filename, b64content, **kwargs):
"""
.. function:: XML-RPC TestPlan.add_attachment(plan_id, filename, b64content)
Add attachment to the given TestPlan.
:param plan_id: PK of TestPlan
:type plan_id: int
:param filename: File name of attachment, e.g. 'logs.txt'
:type filename: str
:param b64content: Base64 encoded content
:type b64content: str
:return: None
"""
utils.add_attachment(
plan_id,
'testplans.TestPlan',
kwargs.get(REQUEST_KEY).user,
filename,
b64content)
79 changes: 79 additions & 0 deletions tcms/xmlrpc/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# -*- coding: utf-8 -*-

import io
import time
from mock import MagicMock

from attachments.models import Attachment
from attachments import views as attachment_views

from django.http import HttpRequest
from django.middleware.csrf import get_token
from django.db.models import FieldDoesNotExist

from tcms.management.models import Product
Expand Down Expand Up @@ -133,3 +141,74 @@ def get_attachments_for(request, obj):
'date': attachment.created.isoformat(),
})
return result


def encode_multipart(csrf_token, filename, b64content):
"""
Build a multipart/form-data body with generated random boundary
suitable for parsing by django.http.request.HttpRequest and
the parser classes related to it!
.. note::
``\r\n`` are expected! Do not change!
"""
boundary = '----------%s' % int(time.time() * 1000)
data = ['--%s' % boundary]

data.append('Content-Disposition: form-data; name="csrfmiddlewaretoken"\r\n')
data.append(csrf_token)
data.append('--%s' % boundary)

data.append('Content-Disposition: form-data; name="attachment_file"; filename="%s"' % filename)
data.append('Content-Type: application/octet-stream')
data.append('Content-Transfer-Encoding: base64')
data.append('Content-Length: %d\r\n' % len(b64content))
data.append(b64content)

data.append('--%s--\r\n' % boundary)
return '\r\n'.join(data), boundary


def request_for_upload(user, filename, b64content):
"""
Return a request object containing all fields necessary for file
upload as if it was sent by the browser.
"""
request = HttpRequest()
request.user = user
request.method = 'POST'
request.content_type = 'multipart/form-data'
# because attachment.views.add_attachment() calls messages.success()
request._messages = MagicMock()

data, boundary = encode_multipart(
get_token(request),
filename,
b64content
)

request.META['CONTENT_TYPE'] = 'multipart/form-data; boundary=%s' % boundary
request.META['CONTENT_LENGTH'] = len(data)
request._stream = io.BytesIO(data.encode())

# manually parse the input data and populate data attributes
request._read_started = False
request._load_post_and_files()
request.POST = request._post
request.FILES = request._files

return request


def add_attachment(obj_id, app_model, user, filename, b64content):
"""
High-level function which performs the attachment process
by constructing an HttpRequest object and passing it to
attachments.views.add_attachment() as if it came from the browser.
"""
request = request_for_upload(user, filename, b64content)
app, model = app_model.split('.')
response = attachment_views.add_attachment(request, app, model, obj_id)
if response.status_code == 404:
raise Exception("Adding attachment to %s(%d) failed" % (app_model, obj_id))

0 comments on commit 4f39825

Please sign in to comment.