diff --git a/src/c3nav/editor/hosters/base.py b/src/c3nav/editor/hosters/base.py index 4e4b12ba..aafe5e14 100644 --- a/src/c3nav/editor/hosters/base.py +++ b/src/c3nav/editor/hosters/base.py @@ -5,7 +5,7 @@ from django.conf import settings from django.urls.base import reverse from django.utils.translation import ugettext_lazy as _ -from c3nav.editor.tasks import check_access_token, request_access_token +from c3nav.editor.tasks import check_access_token, request_access_token, submit_edit from c3nav.mapdata.models import Package @@ -114,6 +114,11 @@ class Hoster(ABC): session_data['checking_progress_id'] = task.id self._handle_checking_task(request, task, session_data) + def submit_edit(self, request, data): + session_data = self._get_session_data(request) + task = submit_edit.apply_async(access_token=session_data['access_token'], data=data) + return task + @abstractmethod def get_auth_uri(self, request): """ @@ -133,7 +138,7 @@ class Hoster(ABC): """ Task method for requesting the access token asynchroniously. Returns a dict with a 'state' key containing the new hoster state, an optional 'error' key containing an - error message and an optional 'access_token' keys containing a new access token. + error message and an optional 'access_token' key containing a new access token. """ pass @@ -144,3 +149,13 @@ class Hoster(ABC): Returns a dict with a 'state' key containing the new hoster state. """ pass + + @abstractmethod + def do_submit_edit(self, access_token, data): + """ + Task method for submitting an edit (e.g. creating a pull request). + + Returns a dict with a 'success' key that contains a boolean, an optional 'error' key containing an error + message and an optional 'url' key containing an URL to the created pull request. + """ + pass diff --git a/src/c3nav/editor/hosters/github.py b/src/c3nav/editor/hosters/github.py index 41b332b6..579b4e32 100644 --- a/src/c3nav/editor/hosters/github.py +++ b/src/c3nav/editor/hosters/github.py @@ -80,3 +80,6 @@ class GithubHoster(Hoster): return {'state': 'missing_permissions'} return {'state': 'logged_in'} + + def do_submit_edit(self, access_token, data): + raise NotImplementedError diff --git a/src/c3nav/editor/hosters/gitlab.py b/src/c3nav/editor/hosters/gitlab.py index d414cb1f..2ebd1947 100644 --- a/src/c3nav/editor/hosters/gitlab.py +++ b/src/c3nav/editor/hosters/gitlab.py @@ -75,3 +75,6 @@ class GitlabHoster(Hoster): return {'state': 'logged_out'} return {'state': 'logged_in'} + + def do_submit_edit(self, access_token, data): + raise NotImplementedError diff --git a/src/c3nav/editor/tasks.py b/src/c3nav/editor/tasks.py index 634c62d2..278f6da4 100644 --- a/src/c3nav/editor/tasks.py +++ b/src/c3nav/editor/tasks.py @@ -11,3 +11,9 @@ def request_access_token(hoster, *args, **kwargs): def check_access_token(hoster, access_token): from c3nav.editor.hosters import hosters return hosters[hoster].do_check_access_token(access_token) + + +@app.task() +def submit_edit(hoster, access_token, data): + from c3nav.editor.hosters import hosters + return hosters[hoster].do_submit_edit(access_token, data) diff --git a/src/c3nav/editor/templates/editor/finalize.html b/src/c3nav/editor/templates/editor/finalize.html index 083b1ada..87dda68b 100644 --- a/src/c3nav/editor/templates/editor/finalize.html +++ b/src/c3nav/editor/templates/editor/finalize.html @@ -3,58 +3,78 @@ {% load bootstrap3 %} {% block content %} {% if hoster %} - {% if hoster_error %} - - {% endif %} - {% if hoster_state == 'logged_in' %} -

Propose Changes

-

Please provide a short helpful title for your change.

-
- {% csrf_token %} - - {% bootstrap_form commit_form %} + {% if not task %} - {% buttons %} -
- - {{ hoster.name }} – {{ hoster.base_url }} - - {% endbuttons %} -
- - {% elif hoster_state == 'checking' %} -

Sign in with {{ hoster.title }}

-
- {% csrf_token %} - -

-

Checking authentication, please wait…

-
- - {% else %} - {% if hoster_state == 'misssing_permissions' %} -

Missing {{ hoster.title }} Permissions

-

c3nav is missing permissions that it needs to propose your edit.

-

Please click the button below to grant the missing permissions.

- {% else %} -

Sign in with {{ hoster.title }}

-

Please sign in to continue and propose your edit.

+ {% if hoster_error %} + + {% endif %} + {% if hoster_state == 'logged_in' %} +

Propose Changes

+

Please provide a short helpful title for your change.

+
+ {% csrf_token %} + + + + {% bootstrap_form commit_form %} + + {% buttons %} +
+ + {{ hoster.name }} – {{ hoster.base_url }} + + {% endbuttons %} +
+ + {% elif hoster_state == 'checking' %} +

Sign in with {{ hoster.title }}

+
+ {% csrf_token %} + +

+

Checking authentication, please wait…

+
+ + {% else %} + {% if hoster_state == 'misssing_permissions' %} +

Missing {{ hoster.title }} Permissions

+

c3nav is missing permissions that it needs to propose your edit.

+

Please click the button below to grant the missing permissions.

+ {% else %} +

Sign in with {{ hoster.title }}

+

Please sign in to continue and propose your edit.

+ {% endif %} +
+ {% csrf_token %} + + +

+
+ + {{ hoster.name }} – {{ hoster.base_url }} + +

+
+ {% endif %} +

Alternatively, you can copy your edit below and send it to the maps maintainer.

+ {% else %} + {% if not task.ready or redirect %} +

Creating Pull Request…

+
+ {% csrf_token %} + + + +

+

Creating Pull Request…

+
+ {% else %} +

Pull Request created

+

You can find it here:

{% endif %} -
- {% csrf_token %} - - -

-
- - {{ hoster.name }} – {{ hoster.base_url }} - -

-
{% endif %} -

Alternatively, you can copy your edit below and send it to the maps maintainer.

{% else %}

Copy your edit

In order to propose your edit, please copy it and send it to the maps maintainer.

diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index e1aa214e..9dd7c584 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -1,12 +1,16 @@ +import string + from django.conf import settings from django.core import signing from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.http.response import Http404 from django.shortcuts import get_object_or_404, redirect, render +from django.utils.crypto import get_random_string from django.views.decorators.http import require_POST from c3nav.editor.forms import CommitForm, FeatureForm from c3nav.editor.hosters import get_hoster_for_package, hosters +from c3nav.editor.tasks import submit_edit from c3nav.mapdata.models.feature import FEATURE_TYPES, Feature from c3nav.mapdata.models.package import Package from c3nav.mapdata.packageio.write import json_encode @@ -112,22 +116,41 @@ def finalize(request): hoster = get_hoster_for_package(package) action = request.POST.get('action') + + if 'commit_msg' in request.POST or action == 'submit': + form = CommitForm(request.POST) + else: + form = CommitForm({'commit_msg': data['commit_msg']}) + + task = None + new_submit_token = False if action == 'check': hoster.check_state(request) elif action == 'oauth': hoster.set_tmp_data(request, raw_data) return redirect(hoster.get_auth_uri(request)) + elif action == 'submit' and hoster.get_state(request) == 'logged_in': + if request.POST.get('editor_submit_token', '') != request.session.get('editor_submit_token', None): + raise SuspiciousOperation('Invalid submit token.') + if form.is_valid(): + new_submit_token = True + data['commit_msg'] = form.cleaned_data['commit_msg'] + task = hoster.submit_edit(request, data) + elif action == 'result': + if 'task' not in request.POST: + raise SuspiciousOperation('Missing task id.') + task = submit_edit.AsyncResult(task_id=request.POST['task']) + try: + task.ready() + except: + raise Http404() + + if 'editor_submit_token' not in request.session or new_submit_token: + request.session['editor_submit_token'] = get_random_string(42, string.ascii_letters + string.digits) hoster_state = hoster.get_state(request) hoster_error = hoster.get_error(request) if hoster_state == 'logged_out' else None - if request.method == 'POST' and 'commit_msg' in request.POST: - form = CommitForm(request.POST) - if form.is_valid() and hoster_state == 'logged_in': - pass - else: - form = CommitForm({'commit_msg': data['commit_msg']}) - return render(request, 'editor/finalize.html', { 'data': raw_data, 'action': data['action'], @@ -137,6 +160,9 @@ def finalize(request): 'hoster': hoster, 'hoster_state': hoster_state, 'hoster_error': hoster_error, + 'redirect': action == 'submit' and not settings.CELERY_ALWAYS_EAGER, + 'editor_submit_token': request.session['editor_submit_token'], + 'task': {'id': task.id, 'ready': task.ready(), 'result': task.result} if task is not None else None, 'file_path': data['file_path'], 'file_contents': data.get('content') })