xmlrpc.py 22 KB
Newer Older
1
# Patchwork - automated patch tracking system
Jeremy Kerr's avatar
Jeremy Kerr committed
2
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#
# This file is part of the Patchwork package.
#
# Patchwork is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Patchwork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
Jeremy Kerr's avatar
Jeremy Kerr committed
20
21
# Patchwork XMLRPC interface
#
22

23
24
from __future__ import absolute_import

25
26
27
import base64
import sys

28
import patchwork.threadlocalrequest
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
29
import xmlrpc.client as xmlrpc_client
30

31
from django.urls import reverse
Jeremy Kerr's avatar
Jeremy Kerr committed
32
from django.contrib.auth import authenticate
33
34
35
from django.http import (
    HttpResponse, HttpResponseRedirect, HttpResponseServerError)
from django.views.decorators.csrf import csrf_exempt
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
36
from xmlrpc.server import SimpleXMLRPCDispatcher, XMLRPCDocGenerator
37

Guilherme Salgado's avatar
Guilherme Salgado committed
38
from patchwork.models import Patch, Project, Person, State
39
from patchwork.views import patch_to_mbox
40
from patchwork.permissions import Can
41

Jeremy Kerr's avatar
Jeremy Kerr committed
42

43
44
45
46
47
class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher,
                                XMLRPCDocGenerator):

    server_name = 'Patchwork XML-RPC API'
    server_title = 'Patchwork XML-RPC API v1 Documentation'
48

Jeremy Kerr's avatar
Jeremy Kerr committed
49
    def __init__(self):
50
51
        SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
                                        encoding=None)
52
        XMLRPCDocGenerator.__init__(self)
53
54
55
56

        def _dumps(obj, *args, **kwargs):
            kwargs['allow_none'] = self.allow_none
            kwargs['encoding'] = self.encoding
Stephen Finucane's avatar
Stephen Finucane committed
57
            return xmlrpc_client.dumps(obj, *args, **kwargs)
Jeremy Kerr's avatar
Jeremy Kerr committed
58

59
60
        self.dumps = _dumps

Jeremy Kerr's avatar
Jeremy Kerr committed
61
62
63
        # map of name => (auth, func)
        self.func_map = {}

64

Jeremy Kerr's avatar
Jeremy Kerr committed
65
    def register_function(self, fn, auth_required):
66
        self.funcs[fn.__name__] = fn  # needed by superclass methods
Jeremy Kerr's avatar
Jeremy Kerr committed
67
68
69
        self.func_map[fn.__name__] = (auth_required, fn)

    def _user_for_request(self, request):
70
        auth_header = None
71

72
        if 'HTTP_AUTHORIZATION' in request.META:
73
            auth_header = request.META.get('HTTP_AUTHORIZATION')
74
        elif 'Authorization' in request.META:
75
            auth_header = request.META.get('Authorization')
76

77
        if auth_header is None or auth_header == '':
78
            raise Exception('No authentication credentials given')
Jeremy Kerr's avatar
Jeremy Kerr committed
79

80
        header = auth_header.strip()
81

82
83
        if not header.startswith('Basic '):
            raise Exception('Authentication scheme not supported')
Jeremy Kerr's avatar
Jeremy Kerr committed
84

85
        header = header[len('Basic '):].strip()
Jeremy Kerr's avatar
Jeremy Kerr committed
86
87

        try:
88
            decoded = base64.decodebytes(header.encode())
89
            username, password = decoded.decode().split(':', 1)
90
        except Exception:
91
            raise Exception('Invalid authentication credentials')
Jeremy Kerr's avatar
Jeremy Kerr committed
92

93
        return authenticate(username=username, password=password)
Jeremy Kerr's avatar
Jeremy Kerr committed
94
95

    def _dispatch(self, request, method, params):
Stephen Finucane's avatar
Stephen Finucane committed
96
        if method not in list(self.func_map.keys()):
Jeremy Kerr's avatar
Jeremy Kerr committed
97
98
99
100
101
102
103
            raise Exception('method "%s" is not supported' % method)

        auth_required, fn = self.func_map[method]

        if auth_required:
            user = self._user_for_request(request)
            if not user:
104
                raise Exception('Invalid username/password')
Jeremy Kerr's avatar
Jeremy Kerr committed
105

106
            request.user = user
107
            patchwork.threadlocalrequest.set_request(request)
Jeremy Kerr's avatar
Jeremy Kerr committed
108
109
110
111
112
113
            params = (user,) + params

        return fn(*params)

    def _marshaled_dispatch(self, request):
        try:
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
114
            params, method = xmlrpc_client.loads(request.body)
Jeremy Kerr's avatar
Jeremy Kerr committed
115
116
117
118

            response = self._dispatch(request, method, params)
            # wrap response in a singleton tuple
            response = (response,)
119
            response = self.dumps(response, methodresponse=1)
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
120
        except xmlrpc_client.Fault as fault:
121
            response = self.dumps(fault)
122
        except Exception:
Jeremy Kerr's avatar
Jeremy Kerr committed
123
            # report exception back to server
124
            response = self.dumps(
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
125
                xmlrpc_client.Fault(1,
126
                        '%s:%s' % (sys.exc_info()[0], sys.exc_info()[1])),
127
            )
Jeremy Kerr's avatar
Jeremy Kerr committed
128
129
130

        return response

131

Jeremy Kerr's avatar
Jeremy Kerr committed
132
133
134
dispatcher = PatchworkXMLRPCDispatcher()

# XMLRPC view function
135
136


Jeremy Kerr's avatar
Jeremy Kerr committed
137
@csrf_exempt
Jeremy Kerr's avatar
Jeremy Kerr committed
138
def xmlrpc(request):
139
    if request.method not in ['POST', 'GET']:
140
        return HttpResponseRedirect(reverse(
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
141
            'help', kwargs={'path': 'pwclient/'}))
Jeremy Kerr's avatar
Jeremy Kerr committed
142
143

    response = HttpResponse()
144
145
146
147
148
149
150
151
152
153

    if request.method == 'POST':
        try:
            ret = dispatcher._marshaled_dispatch(request)
        except Exception:
            return HttpResponseServerError()
    else:
        ret = dispatcher.generate_html_documentation()

    response.write(ret)
Jeremy Kerr's avatar
Jeremy Kerr committed
154
155
156
157
158

    return response

# decorator for XMLRPC methods. Setting login_required to true will call
# the decorated function with a non-optional user as the first argument.
159
160
161


def xmlrpc_method(login_required=False):
Jeremy Kerr's avatar
Jeremy Kerr committed
162
163
164
165
166
167
168
    def wrap(f):
        dispatcher.register_function(f, login_required)
        return f

    return wrap


169
# We allow most of the Django field lookup types for remote queries
170
171
172
173
LOOKUP_TYPES = ['iexact', 'contains', 'icontains', 'gt', 'gte', 'lt',
                'in', 'startswith', 'istartswith', 'endswith',
                'iendswith', 'range', 'year', 'month', 'day', 'isnull']

174
175
176
177
178
179

#######################################################################
# Helper functions
#######################################################################

def project_to_dict(obj):
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
    """Serialize a project object.

    Return a trimmed down dictionary representation of a Project
    object which is safe to send to the client. For example:

    {
        'id': 1,
        'linkname': 'my-project',
        'name': 'My Project',
    }

    Args:
        Project object to serialize.

    Returns:
        Serialized Project object.
    """
197
198
199
200
201
202
    return {
        'id': obj.id,
        'linkname': obj.linkname,
        'name': obj.name,
    }

203
204

def person_to_dict(obj):
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
    """Serialize a person object.

    Return a trimmed down dictionary representation of a Person
    object which is safe to send to the client. For example:

    {
        'id': 1,
        'email': 'joe.bloggs@example.com',
        'name': 'Joe Bloggs',
        'user': None,
    }

    Args:
        Person object to serialize.

    Returns:
        Serialized Person object.
    """
223
224
225
226
227
228
229
230

    # Make sure we don't return None even if the user submitted a patch
    # with no real name.  XMLRPC can't marshall None.
    if obj.name is not None:
        name = obj.name
    else:
        name = obj.email

231
232
233
234
    return {
        'id': obj.id,
        'email': obj.email,
        'name': name,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
235
        'user': str(obj.user).encode('utf-8'),
236
237
    }

238
239

def patch_to_dict(obj):
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
    """Serialize a patch object.

    Return a trimmed down dictionary representation of a Patch
    object which is safe to send to the client. For example:

    {
        'id': 1
        'date': '2000-12-31 00:11:22',
        'filename': 'Fix-all-the-bugs.patch',
        'msgid': '<BLU438-SMTP36690BBDD2CE71A7138B082511A@phx.gbl>',
        'name': "Fix all the bugs",
        'project': 'my-project',
        'project_id': 1,
        'state': 'New',
        'state_id': 1,
        'archived': False,
        'submitter': 'Joe Bloggs <joe.bloggs at example.com>',
        'submitter_id': 1,
        'delegate': 'admin',
        'delegate_id': 1,
        'commit_ref': '',
    }

    Args:
        Patch object to serialize.

    Returns:
        Serialized Patch object.
    """
269
270
    return {
        'id': obj.id,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
271
        'date': str(obj.date).encode('utf-8'),
272
273
274
        'filename': obj.filename(),
        'msgid': obj.msgid,
        'name': obj.name,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
275
        'project': str(obj.project).encode('utf-8'),
276
        'project_id': obj.project_id,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
277
        'state': str(obj.state).encode('utf-8'),
278
279
        'state_id': obj.state_id,
        'archived': obj.archived,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
280
        'submitter': str(obj.submitter).encode('utf-8'),
281
        'submitter_id': obj.submitter_id,
Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
282
        'delegate': str(obj.delegate).encode('utf-8'),
283
284
        'delegate_id': obj.delegate_id or 0,
        'commit_ref': obj.commit_ref or '',
285
286
    }

287
288

def bundle_to_dict(obj):
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
    """Serialize a bundle object.

    Return a trimmed down dictionary representation of a Bundle
    object which is safe to send to the client. For example:

    {
        'id': 1,
        'name': 'New',
        'n_patches': 2,
        'public_url': 'http://patchwork.example.com/bundle/admin/stuff/mbox/',
    }

    Args:
        Bundle object to serialize.

    Returns:
        Serialized Bundle object.
    """
307
308
309
310
311
312
313
    return {
        'id': obj.id,
        'name': obj.name,
        'n_patches': obj.n_patches(),
        'public_url': obj.public_url(),
    }

314
315

def state_to_dict(obj):
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
    """Serialize a state object.

    Return a trimmed down dictionary representation of a State
    object which is safe to send to the client. For example:

    {
        'id': 1,
        'name': 'New',
    }

    Args:
        State object to serialize.

    Returns:
        Serialized State object.
    """
332
333
334
335
336
    return {
        'id': obj.id,
        'name': obj.name,
    }

337
338
339
340
341

#######################################################################
# Public XML-RPC methods
#######################################################################

342
@xmlrpc_method()
343
def pw_rpc_version():
344
345
346
347
348
349
350
351
352
353
354
    """Return Patchwork XML-RPC interface version.

    The API is versioned separately from patchwork itself. The API
    version only changes when the API itself changes. As these changes
    can include the removal or modification of methods, it is highly
    recommended that one first test the API version for compatibility
    before making method calls.

    Returns:
        Version of the API.
    """
355
356
    return 1

357
358

@xmlrpc_method()
359
def project_list(search_str=None, max_count=0):
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    """List projects matching a given linkname filter.

    Filter projects by linkname. Projects are compared to the search
    string via a case-insensitive containment test, a.k.a. a partial
    match.

    Args:
        search_str: The string to compare project names against. If
            blank, all projects will be returned.
        max_count (int): The maximum number of projects to return.

    Returns:
        A serialized list of projects matching filter, if any. A list
        of all projects if no filter given.
    """
375
    try:
376
        if search_str:
377
            projects = Project.objects.filter(linkname__icontains=search_str)
378
379
380
381
        else:
            projects = Project.objects.all()

        if max_count > 0:
382
            return list(map(project_to_dict, projects[:max_count]))
383
        elif max_count < 0:
384
385
            query = projects.reverse()[:-max_count]
            return [project_to_dict(project) for project in reversed(query)]
386
        else:
Stephen Finucane's avatar
Stephen Finucane committed
387
            return list(map(project_to_dict, projects))
388
    except Project.DoesNotExist:
389
390
        return []

391
392

@xmlrpc_method()
393
def project_get(project_id):
394
395
396
397
398
399
400
401
402
403
404
    """Get a project by its ID.

    Retrieve a project matching a given project ID, if any exists.

    Args:
        project_id (int): The ID of the project to retrieve.

    Returns:
        The serialized project matching the ID, if any, else an empty
        dict.
    """
405
    try:
406
        project = Project.objects.filter(id=project_id)[0]
407
        return project_to_dict(project)
408
    except Project.DoesNotExist:
409
410
        return {}

411
412

@xmlrpc_method()
413
def person_list(search_str=None, max_count=0):
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
    """List persons matching a given name or email filter.

    Filter persons by name and email. Persons are compared to the
    search string via a case-insensitive containment test, a.k.a. a
    partial match.

    Args:
        search_str: The string to compare person names or emails
            against. If blank, all persons will be returned.
        max_count (int): The maximum number of persons to return.

    Returns:
        A serialized list of persons matching filter, if any. A list
        of all persons if no filter given.
    """
429
    try:
430
        if search_str:
431
432
            people = (Person.objects.filter(name__icontains=search_str) |
                      Person.objects.filter(email__icontains=search_str))
433
434
435
436
        else:
            people = Person.objects.all()

        if max_count > 0:
437
            return list(map(person_to_dict, people[:max_count]))
438
        elif max_count < 0:
439
440
            query = people.reverse()[:-max_count]
            return [person_to_dict(person) for person in reversed(query)]
441
        else:
Stephen Finucane's avatar
Stephen Finucane committed
442
            return list(map(person_to_dict, people))
443
    except Person.DoesNotExist:
444
445
        return []

446
447

@xmlrpc_method()
448
def person_get(person_id):
449
450
451
452
453
454
455
456
457
458
459
    """Get a person by its ID.

    Retrieve a person matching a given person ID, if any exists.

    Args:
        person_id (int): The ID of the person to retrieve.

    Returns:
        The serialized person matching the ID, if any, else an empty
        dict.
    """
460
    try:
461
        person = Person.objects.filter(id=person_id)[0]
462
        return person_to_dict(person)
463
    except Person.DoesNotExist:
464
465
        return {}

466
467
468

@xmlrpc_method()
def patch_list(filt=None):
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
    """List patches matching all of a given set of filters.

    Filter patches by one or more of the below fields:

     * id
     * name
     * project_id
     * submitter_id
     * delegate_id
     * archived
     * state_id
     * date
     * commit_ref
     * hash
     * msgid

    It is also possible to specify the number of patches returned via
    a ``max_count`` filter.

     * max_count

    With the exception of ``max_count``, the specified field of the
    patches are compared to the search string using a provided
    field lookup type, which can be one of:

     * iexact
     * contains
     * icontains
     * gt
     * gte
     * lt
     * in
     * startswith
     * istartswith
     * endswith
     * iendswith
     * range
     * year
     * month
     * day
     * isnull

    Please refer to the Django documentation for more information on
    these field lookup types.

    An example filter would look like so:

    {
        'name__icontains': 'Joe Bloggs',
        'max_count': 1,
    }

    Args:
        filt (dict): The filters specifying the field to compare, the
            lookup type and the value to compare against. Keys are of
            format ``[FIELD_NAME]`` or ``[FIELD_NAME]__[LOOKUP_TYPE]``.
            Example: ``name__icontains``. Values are plain strings to
            compare against.

    Returns:
        A serialized list of patches matching filters, if any. A list
        of all patches if no filter given.
    """
532
533
534
    if filt is None:
        filt = {}

535
536
537
538
539
    try:
        # We allow access to many of the fields.  But, some fields are
        # filtered by raw object so we must lookup by ID instead over
        # XML-RPC.
        ok_fields = [
540
541
542
543
544
545
546
547
548
549
550
551
552
            'id',
            'name',
            'project_id',
            'submitter_id',
            'delegate_id',
            'archived',
            'state_id',
            'date',
            'commit_ref',
            'hash',
            'msgid',
            'max_count',
        ]
553
554
555
556

        dfilter = {}
        max_count = 0

557
558
        for key in filt:
            parts = key.split('__')
Jeremy Kerr's avatar
Jeremy Kerr committed
559
            if parts[0] not in ok_fields:
560
561
562
563
564
565
566
567
                # Invalid field given
                return []
            if len(parts) > 1:
                if LOOKUP_TYPES.count(parts[1]) == 0:
                    # Invalid lookup type given
                    return []

            if parts[0] == 'project_id':
568
                dfilter['project'] = Project.objects.filter(id=filt[key])[0]
569
            elif parts[0] == 'submitter_id':
570
                dfilter['submitter'] = Person.objects.filter(id=filt[key])[0]
571
            elif parts[0] == 'delegate_id':
572
                dfilter['delegate'] = Person.objects.filter(id=filt[key])[0]
573
            elif parts[0] == 'state_id':
574
                dfilter['state'] = State.objects.filter(id=filt[key])[0]
575
            elif parts[0] == 'max_count':
576
                max_count = filt[key]
577
            else:
578
                dfilter[key] = filt[key]
579
580
581
582

        patches = Patch.objects.filter(**dfilter)

        if max_count > 0:
583
            return list(map(patch_to_dict, patches[:max_count]))
584
        elif max_count < 0:
585
586
            query = patches.reverse()[:-max_count]
            return [patch_to_dict(patch) for patch in reversed(query)]
587
        else:
588
            return list(map(patch_to_dict, patches))
589
    except Patch.DoesNotExist:
590
591
        return []

592
593

@xmlrpc_method()
594
def patch_get(patch_id):
595
596
597
598
599
600
601
602
603
604
605
    """Get a patch by its ID.

    Retrieve a patch matching a given patch ID, if any exists.

    Args:
        patch_id (int): The ID of the patch to retrieve

    Returns:
        The serialized patch matching the ID, if any, else an empty
        dict.
    """
606
    try:
607
        patch = Patch.objects.filter(id=patch_id)[0]
608
        return patch_to_dict(patch)
609
    except Patch.DoesNotExist:
610
611
        return {}

612
613

@xmlrpc_method()
614
def patch_get_by_hash(hash):
615
616
617
618
619
620
621
622
623
624
625
    """Get a patch by its hash.

    Retrieve a patch matching a given patch hash, if any exists.

    Args:
        hash: The hash of the patch to retrieve

    Returns:
        The serialized patch matching the hash, if any, else an empty
        dict.
    """
626
    try:
627
        patch = Patch.objects.filter(hash=hash)[0]
628
        return patch_to_dict(patch)
629
    except Patch.DoesNotExist:
630
631
        return {}

632
633

@xmlrpc_method()
634
def patch_get_by_project_hash(project, hash):
635
636
637
638
639
640
641
642
643
644
645
646
647
    """Get a patch by its project and hash.

    Retrieve a patch matching a given project and patch hash, if any
    exists.

    Args:
        project (str): The project of the patch to retrieve.
        hash: The hash of the patch to retrieve.

    Returns:
        The serialized patch matching both the project and the hash,
        if any, else an empty dict.
    """
648
    try:
649
650
        patch = Patch.objects.filter(project__linkname=project,
                                     hash=hash)[0]
651
        return patch_to_dict(patch)
652
    except Patch.DoesNotExist:
653
654
        return {}

655
656

@xmlrpc_method()
657
def patch_get_mbox(patch_id):
658
659
660
661
662
663
664
665
666
667
668
669
    """Get a patch by its ID in mbox format.

    Retrieve a patch matching a given patch ID, if any exists, and
    return in mbox format.

    Args:
        patch_id (int): The ID of the patch to retrieve.

    Returns:
        The serialized patch matching the ID, if any, in mbox format,
        else an empty string.
    """
670
    try:
671
        patch = Patch.objects.filter(id=patch_id)[0]
672
        return patch_to_mbox(patch).as_string(True)
673
674
    except Patch.DoesNotExist:
        return ''
675

676
677

@xmlrpc_method()
678
def patch_get_diff(patch_id):
679
680
681
682
683
684
685
686
687
688
689
690
    """Get a patch by its ID in diff format.

    Retrieve a patch matching a given patch ID, if any exists, and
    return in diff format.

    Args:
        patch_id (int): The ID of the patch to retrieve.

    Returns:
        The serialized patch matching the ID, if any, in diff format,
        else an empty string.
    """
691
    try:
692
        patch = Patch.objects.filter(id=patch_id)[0]
693
        return patch.content
694
695
696
    except Patch.DoesNotExist:
        return ''

697

698
@xmlrpc_method(login_required=True)
Jeremy Kerr's avatar
Jeremy Kerr committed
699
def patch_set(user, patch_id, params):
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
    """Set fields of a patch.

    Modify a patch matching a given patch ID, if any exists, and using
    the provided ``key,value`` pairs. Only the following parameters may
    be set:

     * state
     * commit_ref
     * archived

    Any other field will be rejected.

    **NOTE:** Authentication is required for this method.

    Args:
        user (User): The user making the request. This will be
            populated from HTTP Basic Auth.
        patch_id (int): The ID of the patch to modify.
        params (dict): A dictionary of keys corresponding to patch
            object fields and the values that said fields should be
            set to.

    Returns:
        True, if successful else raise exception.

    Raises:
        Exception: User did not have necessary permissions to edit this
            patch
        Patch.DoesNotExist: The patch did not exist.
    """
Jeremy Kerr's avatar
Jeremy Kerr committed
730
731
732
    try:
        ok_params = ['state', 'commit_ref', 'archived']

733
        patch = Patch.objects.get(id=patch_id)
Jeremy Kerr's avatar
Jeremy Kerr committed
734

735
        if not Can(user).edit(patch):
Jeremy Kerr's avatar
Jeremy Kerr committed
736
737
            raise Exception('No permissions to edit this patch')

Stephen Finucane's avatar
Stephen Finucane committed
738
        for (k, v) in params.items():
Jeremy Kerr's avatar
Jeremy Kerr committed
739
740
741
742
            if k not in ok_params:
                continue

            if k == 'state':
743
                patch.state = State.objects.get(id=v)
Jeremy Kerr's avatar
Jeremy Kerr committed
744
745
746
747
748
749
750
751

            else:
                setattr(patch, k, v)

        patch.save()

        return True

752
    except Patch.DoesNotExist:
Jeremy Kerr's avatar
Jeremy Kerr committed
753
754
        raise

755
756

@xmlrpc_method()
757
def state_list(search_str=None, max_count=0):
758
759
760
761
762
763
764
765
766
767
768
769
770
771
    """List states matching a given name filter.

    Filter states by name. States are compared to the search string
    via a case-insensitive containment test, a.k.a. a partial match.

    Args:
        search_str: The string to compare state names against. If
            blank, all states will be returned.
        max_count (int): The maximum number of states to return.

    Returns:
        A serialized list of states matching filter, if any. A list
        of all states if no filter given.
    """
772
    try:
773
        if search_str:
774
            states = State.objects.filter(name__icontains=search_str)
775
776
777
778
        else:
            states = State.objects.all()

        if max_count > 0:
779
            return list(map(state_to_dict, states[:max_count]))
780
        elif max_count < 0:
781
782
            query = states.reverse()[:-max_count]
            return [state_to_dict(state) for state in reversed(query)]
783
        else:
Stephen Finucane's avatar
Stephen Finucane committed
784
            return list(map(state_to_dict, states))
785
    except State.DoesNotExist:
786
787
        return []

788
789

@xmlrpc_method()
790
def state_get(state_id):
791
792
793
794
795
796
797
798
799
800
801
    """Get a state by its ID.

    Retrieve a state matching a given state ID, if any exists.

    Args:
        state_id: The ID of the state to retrieve.

    Returns:
        The serialized state matching the ID, if any, else an empty
        dict.
    """
802
    try:
803
        state = State.objects.filter(id=state_id)[0]
804
        return state_to_dict(state)
805
    except State.DoesNotExist:
806
        return {}