Skip to content

Commit 9c6c5f1

Browse files
committed
api.views: move pull request views to own module (bug 2039059)
1 parent 47ca83c commit 9c6c5f1

3 files changed

Lines changed: 160 additions & 148 deletions

File tree

src/lando/api/views/__init__.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import json
2+
from functools import wraps
3+
from typing import Callable
4+
5+
from django import forms
6+
from django.core.handlers.wsgi import WSGIRequest
7+
from django.http import JsonResponse
8+
from django.utils.decorators import method_decorator
9+
from django.views import View
10+
from django.views.decorators.csrf import csrf_exempt
11+
12+
from lando.main.models import (
13+
CommitMap,
14+
)
15+
from lando.main.models.revision import DiffWarning, DiffWarningStatus
16+
from lando.main.scm import SCMType
17+
from lando.utils.phabricator import PHABRICATOR_API_KEY_HEADER, get_phabricator_client
18+
19+
20+
class APIView(View):
21+
"""A base class for API views."""
22+
23+
pass
24+
25+
26+
def phabricator_api_key_required(func: Callable) -> Callable:
27+
"""A simple wrapper that checks for a valid Phabricator API token."""
28+
29+
@wraps(func)
30+
def _wrapper(self: View, request: WSGIRequest, *args, **kwargs) -> Callable:
31+
if PHABRICATOR_API_KEY_HEADER not in request.headers:
32+
return JsonResponse(
33+
{"error": f"{PHABRICATOR_API_KEY_HEADER} missing."}, status=400
34+
)
35+
36+
api_key = request.headers[PHABRICATOR_API_KEY_HEADER]
37+
client = get_phabricator_client(api_key=api_key)
38+
if not client.verify_api_token():
39+
return JsonResponse({"error": "Invalid Phabricator API token."}, status=401)
40+
41+
return func(self, request, *args, **kwargs)
42+
43+
return _wrapper
44+
45+
46+
@method_decorator(csrf_exempt, name="dispatch")
47+
class LegacyDiffWarningView(View):
48+
"""
49+
This class provides the API controllers for the legacy `DiffWarning` model.
50+
51+
These API endpoints can be used by clients (such as Code Review bot) to
52+
get, create, or archive warnings.
53+
"""
54+
55+
@phabricator_api_key_required
56+
def post(self, request: WSGIRequest) -> JsonResponse:
57+
"""Create a new `DiffWarning` based on provided revision and diff IDs.
58+
59+
Args:
60+
data (dict): A dictionary containing data to store in the warning. `data`
61+
should contain at least a `message` key that contains the message to
62+
show in the warning.
63+
64+
Returns:
65+
dict: a dictionary representation of the object that was created.
66+
"""
67+
68+
class Form(forms.Form):
69+
def data_validator(data):
70+
if not data or "message" not in data:
71+
raise forms.ValidationError(
72+
"Provided data is missing the message value"
73+
)
74+
75+
revision_id = forms.IntegerField()
76+
diff_id = forms.IntegerField()
77+
group = forms.CharField()
78+
data = forms.JSONField(validators=[data_validator])
79+
80+
# TODO: validate whether revision/diff exist or not.
81+
form = Form(json.loads(request.body))
82+
if form.is_valid():
83+
data = form.cleaned_data
84+
warning = DiffWarning.objects.create(**data)
85+
return JsonResponse(warning.serialize(), status=201)
86+
87+
return JsonResponse({"errors": dict(form.errors)}, status=400)
88+
89+
@phabricator_api_key_required
90+
def delete(self, request: WSGIRequest, diff_warning_id: int) -> JsonResponse:
91+
"""Archive a `DiffWarning` based on provided pk."""
92+
warning = DiffWarning.objects.get(pk=diff_warning_id)
93+
if not warning:
94+
return JsonResponse({}, status=404)
95+
96+
warning.status = DiffWarningStatus.ARCHIVED
97+
warning.save()
98+
return JsonResponse(warning.serialize(), status=200)
99+
100+
@phabricator_api_key_required
101+
def get(self, request: WSGIRequest, **kwargs) -> JsonResponse:
102+
"""Return a list of active revision diff warnings, if any."""
103+
104+
class Form(forms.Form):
105+
revision_id = forms.IntegerField()
106+
diff_id = forms.IntegerField()
107+
group = forms.CharField()
108+
109+
form = Form(request.GET)
110+
if form.is_valid():
111+
warnings = DiffWarning.objects.filter(**form.cleaned_data).all()
112+
return JsonResponse(
113+
[warning.serialize() for warning in warnings], status=200, safe=False
114+
)
115+
116+
return JsonResponse({"errors": dict(form.errors)}, status=400)
117+
118+
119+
class CommitMapBaseView(View):
120+
"""CommitMap base view to be extended for bidirectional git - hg mapping."""
121+
122+
scm: str
123+
124+
def get(
125+
self, request: WSGIRequest, git_repo_name: str, commit_hash: str
126+
) -> JsonResponse:
127+
try:
128+
commit = CommitMap.map_hash_from(self.scm, git_repo_name, commit_hash)
129+
except CommitMap.DoesNotExist as exc:
130+
error_detail = f"No commit found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
131+
return JsonResponse(
132+
{"error": "No commits found", "detail": error_detail}, status=404
133+
)
134+
except CommitMap.MultipleObjectsReturned as exc:
135+
error_detail = f"Multiple commits found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
136+
return JsonResponse(
137+
{"error": "Multiple commits found", "detail": error_detail}, status=400
138+
)
139+
140+
return JsonResponse(commit.serialize(), status=200)
141+
142+
143+
@method_decorator(csrf_exempt, name="dispatch")
144+
class git2hgCommitMapView(CommitMapBaseView):
145+
"""Return corresponding CommitMap given a git hash."""
146+
147+
scm = SCMType.GIT
148+
149+
150+
@method_decorator(csrf_exempt, name="dispatch")
151+
class hg2gitCommitMapView(CommitMapBaseView):
152+
"""Return corresponding CommitMap given an hg hash."""
153+
154+
scm = SCMType.HG
Lines changed: 1 addition & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,31 @@
11
import json
22
from collections import defaultdict
33
from datetime import datetime
4-
from functools import wraps
5-
from typing import Callable
64

75
from django import forms
86
from django.core.handlers.wsgi import WSGIRequest
97
from django.http import HttpRequest, JsonResponse
108
from django.utils.decorators import method_decorator
119
from django.utils.html import escape
1210
from django.views import View
13-
from django.views.decorators.csrf import csrf_exempt
1411

1512
from lando.api.legacy.commit_message import replace_reviewers
1613
from lando.main.auth import require_authenticated_user
1714
from lando.main.models import (
18-
CommitMap,
1915
JobStatus,
2016
LandingJob,
2117
Repo,
2218
Revision,
2319
add_revisions_to_job,
2420
)
2521
from lando.main.models.landing_job import get_jobs_for_pull
26-
from lando.main.models.revision import DiffWarning, DiffWarningStatus
27-
from lando.main.scm import SCMType
2822
from lando.utils.github import GitHubAPIClient, PullRequest, PullRequestPatchHelper
2923
from lando.utils.github_checks import (
3024
ALL_PULL_REQUEST_BLOCKERS,
3125
ALL_PULL_REQUEST_WARNINGS,
3226
PullRequestChecks,
3327
)
3428
from lando.utils.landing_checks import LandingChecks
35-
from lando.utils.phabricator import PHABRICATOR_API_KEY_HEADER, get_phabricator_client
36-
37-
38-
class APIView(View):
39-
"""A base class for API views."""
40-
41-
pass
42-
43-
44-
def phabricator_api_key_required(func: Callable) -> Callable:
45-
"""A simple wrapper that checks for a valid Phabricator API token."""
46-
47-
@wraps(func)
48-
def _wrapper(self: View, request: WSGIRequest, *args, **kwargs) -> Callable:
49-
if PHABRICATOR_API_KEY_HEADER not in request.headers:
50-
return JsonResponse(
51-
{"error": f"{PHABRICATOR_API_KEY_HEADER} missing."}, status=400
52-
)
53-
54-
api_key = request.headers[PHABRICATOR_API_KEY_HEADER]
55-
client = get_phabricator_client(api_key=api_key)
56-
if not client.verify_api_token():
57-
return JsonResponse({"error": "Invalid Phabricator API token."}, status=401)
58-
59-
return func(self, request, *args, **kwargs)
60-
61-
return _wrapper
6229

6330

6431
def generate_warnings_and_blockers(
@@ -92,117 +59,6 @@ def generate_warnings_and_blockers(
9259
return {"warnings": warnings, "blockers": blockers}
9360

9461

95-
@method_decorator(csrf_exempt, name="dispatch")
96-
class LegacyDiffWarningView(View):
97-
"""
98-
This class provides the API controllers for the legacy `DiffWarning` model.
99-
100-
These API endpoints can be used by clients (such as Code Review bot) to
101-
get, create, or archive warnings.
102-
"""
103-
104-
@phabricator_api_key_required
105-
def post(self, request: WSGIRequest) -> JsonResponse:
106-
"""Create a new `DiffWarning` based on provided revision and diff IDs.
107-
108-
Args:
109-
data (dict): A dictionary containing data to store in the warning. `data`
110-
should contain at least a `message` key that contains the message to
111-
show in the warning.
112-
113-
Returns:
114-
dict: a dictionary representation of the object that was created.
115-
"""
116-
117-
class Form(forms.Form):
118-
def data_validator(data):
119-
if not data or "message" not in data:
120-
raise forms.ValidationError(
121-
"Provided data is missing the message value"
122-
)
123-
124-
revision_id = forms.IntegerField()
125-
diff_id = forms.IntegerField()
126-
group = forms.CharField()
127-
data = forms.JSONField(validators=[data_validator])
128-
129-
# TODO: validate whether revision/diff exist or not.
130-
form = Form(json.loads(request.body))
131-
if form.is_valid():
132-
data = form.cleaned_data
133-
warning = DiffWarning.objects.create(**data)
134-
return JsonResponse(warning.serialize(), status=201)
135-
136-
return JsonResponse({"errors": dict(form.errors)}, status=400)
137-
138-
@phabricator_api_key_required
139-
def delete(self, request: WSGIRequest, diff_warning_id: int) -> JsonResponse:
140-
"""Archive a `DiffWarning` based on provided pk."""
141-
warning = DiffWarning.objects.get(pk=diff_warning_id)
142-
if not warning:
143-
return JsonResponse({}, status=404)
144-
145-
warning.status = DiffWarningStatus.ARCHIVED
146-
warning.save()
147-
return JsonResponse(warning.serialize(), status=200)
148-
149-
@phabricator_api_key_required
150-
def get(self, request: WSGIRequest, **kwargs) -> JsonResponse:
151-
"""Return a list of active revision diff warnings, if any."""
152-
153-
class Form(forms.Form):
154-
revision_id = forms.IntegerField()
155-
diff_id = forms.IntegerField()
156-
group = forms.CharField()
157-
158-
form = Form(request.GET)
159-
if form.is_valid():
160-
warnings = DiffWarning.objects.filter(**form.cleaned_data).all()
161-
return JsonResponse(
162-
[warning.serialize() for warning in warnings], status=200, safe=False
163-
)
164-
165-
return JsonResponse({"errors": dict(form.errors)}, status=400)
166-
167-
168-
class CommitMapBaseView(View):
169-
"""CommitMap base view to be extended for bidirectional git - hg mapping."""
170-
171-
scm: str
172-
173-
def get(
174-
self, request: WSGIRequest, git_repo_name: str, commit_hash: str
175-
) -> JsonResponse:
176-
try:
177-
commit = CommitMap.map_hash_from(self.scm, git_repo_name, commit_hash)
178-
except CommitMap.DoesNotExist as exc:
179-
error_detail = f"No commit found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
180-
return JsonResponse(
181-
{"error": "No commits found", "detail": error_detail}, status=404
182-
)
183-
except CommitMap.MultipleObjectsReturned as exc:
184-
error_detail = f"Multiple commits found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
185-
return JsonResponse(
186-
{"error": "Multiple commits found", "detail": error_detail}, status=400
187-
)
188-
189-
return JsonResponse(commit.serialize(), status=200)
190-
191-
192-
@method_decorator(csrf_exempt, name="dispatch")
193-
class git2hgCommitMapView(CommitMapBaseView):
194-
"""Return corresponding CommitMap given a git hash."""
195-
196-
scm = SCMType.GIT
197-
198-
199-
@method_decorator(csrf_exempt, name="dispatch")
200-
class hg2gitCommitMapView(CommitMapBaseView):
201-
"""Return corresponding CommitMap given an hg hash."""
202-
203-
scm = SCMType.HG
204-
205-
20662
class PullRequestAPIView(View):
20763
"""Set various common attributes for views that extend this one."""
20864

@@ -314,7 +170,7 @@ class Form(forms.Form):
314170
return JsonResponse({"id": job.id}, status=201)
315171

316172

317-
class PullRequestChecksAPIView(PullRequestAPIView):
173+
class ChecksPullRequestAPIView(PullRequestAPIView):
318174
def get(
319175
self, request: WSGIRequest, repo_name: str, pull_number: int
320176
) -> JsonResponse:

src/lando/urls.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
from lando.api.legacy.api import landing_jobs
2323
from lando.api.uplift_api import api as uplift_api
2424
from lando.api.views import (
25-
LandingJobPullRequestAPIView,
2625
LegacyDiffWarningView,
27-
PullRequestChecksAPIView,
2826
git2hgCommitMapView,
2927
hg2gitCommitMapView,
3028
)
29+
from lando.api.views.pull_requests import (
30+
LandingJobPullRequestAPIView,
31+
ChecksPullRequestAPIView,
32+
)
3133
from lando.headless_api.api import (
3234
api as headless_api,
3335
)
@@ -122,7 +124,7 @@
122124
),
123125
path(
124126
"api/pulls/<str:repo_name>/<int:pull_number>/checks",
125-
PullRequestChecksAPIView.as_view(),
127+
ChecksPullRequestAPIView.as_view(),
126128
name="api-pull-request-checks",
127129
),
128130
]

0 commit comments

Comments
 (0)