示例#1
0
    def UploadActivity(self, serviceRecord, activity):
        # https://ridewithgps.com/trips.json

        fit_file = FITIO.Dump(activity)
        files = {
            "data_file":
            ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit",
             fit_file)
        }
        params = {}
        params['trip[name]'] = activity.Name
        params['trip[description]'] = activity.Notes
        if activity.Private:
            params[
                'trip[visibility]'] = 1  # Yes, this logic seems backwards but it's how it works

        res = requests.post("https://ridewithgps.com/trips.json",
                            files=files,
                            params=self._add_auth_params(params,
                                                         record=serviceRecord))
        if res.status_code % 100 == 4:
            raise APIException("Invalid login",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))
        res.raise_for_status()
        res = res.json()
        if res["success"] != 1:
            raise APIException("Unable to upload activity")
示例#2
0
    def UploadActivity(self, serviceRecord, activity):
        # Upload the workout as a .FIT file
        session = self._prepare_request(self._getUserToken(serviceRecord))
        uploaddata = FITIO.Dump(activity)
        files = {"deviceFile": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit", uploaddata)}
        response = session.post(self._deviceUploadUrl, files=files)

        if response.status_code != 200:
            raise APIException(
                "Error uploading workout",
                block=False)

        responseJson = response.json()

        if not responseJson['Success']:
            raise APIException(
                "Error uploading workout - " + response.Message,
                block=False)

        # The upload didn't return a PK for some reason. The queue might be stuck or some other internal error
        # but that doesn't necessarily warrant a reupload.
        eventId = responseJson["EventId"]
        if eventId == 0:
            return None
        return eventId
示例#3
0
    def UploadActivity(self, serviceRecord, activity):
        #/proxy/upload-service-1.1/json/upload/.fit
        fit_file = FITIO.Dump(activity)
        files = {"data": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit", fit_file)}

        res = self._request_with_reauth(
            lambda session: session.post("https://connect.garmin.com/modern/proxy/upload-service/upload/.fit",
                                         files=files,
                                         headers={"nk": "NT"}),
            serviceRecord)
        try:
            res = res.json()["detailedImportResult"]
        except ValueError:
            raise APIException("Bad response during GC upload: %s %s" % (res.status_code, res.text))

        if len(res["successes"]) == 0:
            if len(res["failures"]) and len(res["failures"][0]["messages"]):
                if res["failures"][0]["messages"][0]["content"] == "Duplicate activity":
                    logger.debug("Duplicate")
                    return # ...cool?
                if res["failures"][0]["messages"][0]["content"] == "The user is from EU location, but upload consent is not yet granted or revoked":
                    raise APIException("EU user with no upload consent", block=True, user_exception=UserException(UserExceptionType.GCUploadConsent, intervention_required=True))
            raise APIException("Unable to upload activity %s" % res)
        if len(res["successes"]) > 1:
            raise APIException("Uploaded succeeded, resulting in too many activities")
        actid = res["successes"][0]["internalId"]

        name = activity.Name # Capture in logs
        notes = activity.Notes

        # Update activity metadata not included in the FIT file.
        metadata_object = {}
        if activity.Name and activity.Name.strip():
            metadata_object["activityName"] = activity.Name
        if activity.Notes and activity.Notes.strip():
            metadata_object["description"] = activity.Notes
        if activity.Type not in [ActivityType.Running, ActivityType.Cycling, ActivityType.Other]:
            # Set the legit activity type - whatever it is, it's not supported by the FIT schema
            acttype = [k for k, v in self._reverseActivityMappings.items() if v == activity.Type]
            if len(acttype) == 0:
                raise APIWarning("GarminConnect does not support activity type " + activity.Type)
            else:
                acttype = acttype[0]
            metadata_object["activityTypeDTO"] = {"typeKey": acttype}
        if activity.Private:
            metadata_object["accessControlRuleDTO"] = {"typeKey": "private"}

        if metadata_object:
            metadata_object["activityId"] = actid
            encoding_headers = {"Content-Type": "application/json; charset=UTF-8"} # GC really, really needs this part, otherwise it throws obscure errors like "Invalid signature for signature method HMAC-SHA1"
            res = self._request_with_reauth(lambda session: session.put("https://connect.garmin.com/proxy/activity-service/activity/" + str(actid), data=json.dumps(metadata_object), headers=encoding_headers), serviceRecord)
            if res.status_code != 204:
                raise APIWarning("Unable to set activity metadata - %d %s" % (res.status_code, res.text))

        return actid
示例#4
0
    def UploadActivity(self, serviceRecord, activity):
        """
        POST a Multipart-Encoded File

        URL: http://app.velohero.com/upload/file
        Parameters:
        user = username
        pass = password
        view = json
        file = multipart-encodes file (fit, tcx, pwx, gpx, srm, hrm...)

        Maximum file size per file is 16 MB.
        """

        fit_file = FITIO.Dump(activity)
        files = {
            "file":
            ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit",
             fit_file)
        }
        params = self._add_auth_params({"view": "json"}, record=serviceRecord)
        res = requests.post(self._urlRoot + "/upload/file",
                            files=files,
                            params=params)

        if res.status_code != 200:
            if res.status_code == 403:
                raise APIException("Invalid login",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))
            raise APIException("Unable to upload activity")

        res.raise_for_status()
        res = res.json()
        if "error" in res:
            raise APIException(res["error"])
示例#5
0
    def UploadActivity(self, serviceRecord, activity, activitySource):
        # Upload the workout as a .FIT file
        uploaddata = FITIO.Dump(activity)

        headers = self._apiHeaders(serviceRecord.Authorization)
        headers['Content-Type'] = 'application/octet-stream'
        resp = requests.post(TRAINASONE_SERVER_URL + "/api/sync/activity/fit", data=uploaddata, headers=headers)

        if resp.status_code != 200:
            raise APIException(
                "Error uploading activity - " + str(resp.status_code),
                block=False)

        responseJson = resp.json()

        if not responseJson["id"]:
            raise APIException(
                "Error uploading activity - " + resp.Message,
                block=False)

        activityId = responseJson["id"]

        return activityId
示例#6
0
文件: strava.py 项目: soalhn/tapiriik
    def UploadActivity(self, serviceRecord, activity):
        logger.info("Activity tz " + str(activity.TZ) + " dt tz " +
                    str(activity.StartTime.tzinfo) + " starttime " +
                    str(activity.StartTime))

        if self.LastUpload is not None:
            while (datetime.now() - self.LastUpload).total_seconds() < 5:
                time.sleep(1)
                logger.debug("Inter-upload cooldown")
        source_svc = None
        if hasattr(activity, "ServiceDataCollection"):
            source_svc = str(list(activity.ServiceDataCollection.keys())[0])

        upload_id = None
        if activity.CountTotalWaypoints():
            req = {
                "data_type": "fit",
                "activity_name": activity.Name,
                "description": activity.Notes,  # Paul Mach said so.
                "activity_type": self._activityTypeMappings[activity.Type],
                "private": 1 if activity.Private else 0
            }

            if "fit" in activity.PrerenderedFormats:
                logger.debug("Using prerendered FIT")
                fitData = activity.PrerenderedFormats["fit"]
            else:
                # TODO: put the fit back into PrerenderedFormats once there's more RAM to go around and there's a possibility of it actually being used.
                fitData = FITIO.Dump(activity)
            files = {
                "file":
                ("tap-sync-" + activity.UID + "-" + str(os.getpid()) +
                 ("-" + source_svc if source_svc else "") + ".fit", fitData)
            }

            self._globalRateLimit()
            response = requests.post("https://www.strava.com/api/v3/uploads",
                                     data=req,
                                     files=files,
                                     headers=self._apiHeaders(serviceRecord))
            if response.status_code != 201:
                if response.status_code == 401:
                    raise APIException("No authorization to upload activity " +
                                       activity.UID + " response " +
                                       response.text + " status " +
                                       str(response.status_code),
                                       block=True,
                                       user_exception=UserException(
                                           UserExceptionType.Authorization,
                                           intervention_required=True))
                if "duplicate of activity" in response.text:
                    logger.debug("Duplicate")
                    self.LastUpload = datetime.now()
                    return  # Fine by me. The majority of these cases were caused by a dumb optimization that meant existing activities on services were never flagged as such if tapiriik didn't have to synchronize them elsewhere.
                raise APIException("Unable to upload activity " +
                                   activity.UID + " response " +
                                   response.text + " status " +
                                   str(response.status_code))

            upload_id = response.json()["id"]
            upload_poll_wait = 8  # The mode of processing times
            while not response.json()["activity_id"]:
                time.sleep(upload_poll_wait)
                self._globalRateLimit()
                response = requests.get(
                    "https://www.strava.com/api/v3/uploads/%s" % upload_id,
                    headers=self._apiHeaders(serviceRecord))
                logger.debug("Waiting for upload - status %s id %s" %
                             (response.json()["status"],
                              response.json()["activity_id"]))
                if response.json()["error"]:
                    error = response.json()["error"]
                    if "duplicate of activity" in error:
                        self.LastUpload = datetime.now()
                        logger.debug("Duplicate")
                        return  # I guess we're done here?
                    raise APIException(
                        "Strava failed while processing activity - last status %s"
                        % response.text)
            upload_id = response.json()["activity_id"]
        else:
            localUploadTS = activity.StartTime.strftime("%Y-%m-%d %H:%M:%S")
            req = {
                "name":
                activity.Name if activity.Name else
                activity.StartTime.strftime("%d/%m/%Y"),  # This is required
                "description":
                activity.Notes,
                "type":
                self._activityTypeMappings[activity.Type],
                "private":
                1 if activity.Private else 0,
                "start_date_local":
                localUploadTS,
                "distance":
                activity.Stats.Distance.asUnits(
                    ActivityStatisticUnit.Meters).Value,
                "elapsed_time":
                round((activity.EndTime - activity.StartTime).total_seconds())
            }
            headers = self._apiHeaders(serviceRecord)
            self._globalRateLimit()
            response = requests.post(
                "https://www.strava.com/api/v3/activities",
                data=req,
                headers=headers)
            # FFR this method returns the same dict as the activity listing, as REST services are wont to do.
            if response.status_code != 201:
                if response.status_code == 401:
                    raise APIException("No authorization to upload activity " +
                                       activity.UID + " response " +
                                       response.text + " status " +
                                       str(response.status_code),
                                       block=True,
                                       user_exception=UserException(
                                           UserExceptionType.Authorization,
                                           intervention_required=True))
                raise APIException("Unable to upload stationary activity " +
                                   activity.UID + " response " +
                                   response.text + " status " +
                                   str(response.status_code))
            upload_id = response.json()["id"]

        self.LastUpload = datetime.now()
        return upload_id
示例#7
0
    def UploadActivity(self, serviceRecord, activity):
        #/proxy/upload-service-1.1/json/upload/.fit
        fit_file = FITIO.Dump(activity)
        files = {
            "data":
            ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit",
             fit_file)
        }
        session = self._get_session(record=serviceRecord)
        self._rate_limit()
        res = session.post(
            "http://connect.garmin.com/proxy/upload-service-1.1/json/upload/.fit",
            files=files)
        res = res.json()["detailedImportResult"]

        if len(res["successes"]) == 0:
            raise APIException("Unable to upload activity %s" % res)
        if len(res["successes"]) > 1:
            raise APIException(
                "Uploaded succeeded, resulting in too many activities")
        actid = res["successes"][0]["internalId"]

        name = activity.Name  # Capture in logs
        notes = activity.Notes
        encoding_headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
        }  # GC really, really needs this part, otherwise it throws obscure errors like "Invalid signature for signature method HMAC-SHA1"
        warnings = []
        try:
            if activity.Name and activity.Name.strip():
                self._rate_limit()
                res = session.post(
                    "http://connect.garmin.com/proxy/activity-service-1.2/json/name/"
                    + str(actid),
                    data=urlencode({
                        "value": activity.Name
                    }).encode("UTF-8"),
                    headers=encoding_headers)
                try:
                    res = res.json()
                except:
                    raise APIWarning("Activity name request failed - %s" %
                                     res.text)
                if "display" not in res or res["display"][
                        "value"] != activity.Name:
                    raise APIWarning("Unable to set activity name")
        except APIWarning as e:
            warnings.append(e)

        try:
            if activity.Notes and activity.Notes.strip():
                self._rate_limit()
                res = session.post(
                    "http://connect.garmin.com/proxy/activity-service-1.2/json/description/"
                    + str(actid),
                    data=urlencode({
                        "value": activity.Notes
                    }).encode("UTF-8"),
                    headers=encoding_headers)
                try:
                    res = res.json()
                except:
                    raise APIWarning("Activity notes request failed - %s" %
                                     res.text)
                if "display" not in res or res["display"][
                        "value"] != activity.Notes:
                    raise APIWarning("Unable to set activity notes")
        except APIWarning as e:
            warnings.append(e)

        try:
            if activity.Type not in [
                    ActivityType.Running, ActivityType.Cycling,
                    ActivityType.Other
            ]:
                # Set the legit activity type - whatever it is, it's not supported by the TCX schema
                acttype = [
                    k for k, v in self._reverseActivityMappings.items()
                    if v == activity.Type
                ]
                if len(acttype) == 0:
                    raise APIWarning(
                        "GarminConnect does not support activity type " +
                        activity.Type)
                else:
                    acttype = acttype[0]
                self._rate_limit()
                res = session.post(
                    "http://connect.garmin.com/proxy/activity-service-1.2/json/type/"
                    + str(actid),
                    data={"value": acttype})
                res = res.json()
                if "activityType" not in res or res["activityType"][
                        "key"] != acttype:
                    raise APIWarning("Unable to set activity type")
        except APIWarning as e:
            warnings.append(e)

        try:
            if activity.Private:
                self._rate_limit()
                res = session.post(
                    "http://connect.garmin.com/proxy/activity-service-1.2/json/privacy/"
                    + str(actid),
                    data={"value": "private"})
                res = res.json()
                if "definition" not in res or res["definition"][
                        "key"] != "private":
                    raise APIWarning("Unable to set activity privacy")
        except APIWarning as e:
            warnings.append(e)

        if len(warnings):
            raise APIWarning(str(warnings))  # Meh
        return actid
示例#8
0
    def UploadActivity(self, serviceRecord, activity):
        """
        POST a Multipart-Encoded File
        
        URL: https://app.velohero.com/upload/file
        Parameters:
        user = username
        pass = password
        view = json
        file = multipart-encodes file (fit, tcx, pwx, gpx, srm, hrm...)
        
        Maximum file size per file is 16 MB.
        """
        
        has_location = has_distance = has_speed = False

        for lap in activity.Laps:
            for wp in lap.Waypoints:
                if wp.Location and wp.Location.Latitude and wp.Location.Longitude:
                    has_location = True
                if wp.Distance:
                    has_distance = True
                if wp.Speed:
                    has_speed = True

        if has_location and has_distance and has_speed:
            format = "fit"
            data = FITIO.Dump(activity)
        elif has_location and has_distance:
            format = "tcx"
            data = TCXIO.Dump(activity)
        elif has_location:
            format = "gpx"
            data = GPXIO.Dump(activity)
        else:
            format = "fit"
            data = FITIO.Dump(activity)

        # Upload
        files = {"file": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + "." + format, data)}
        params = self._add_auth_params({"view":"json"}, record=serviceRecord)
        res = requests.post(self._urlRoot + "/upload/file",
                            headers=self._obligatory_headers,
                            files=files,
                            params=params)

        if res.status_code != 200:
            if res.status_code == 403:
                raise APIException("Invalid login", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))
            raise APIException("Unable to upload activity")

        res.raise_for_status()
        try:
            res = res.json()
        except ValueError:
            raise APIException("Could not decode activity list")
        
        if "error" in res:
            raise APIException(res["error"])

        # Set date, start time, comment and sport
        if "id" in res:
            workoutId = res["id"]
            params = self._add_auth_params({ 
                "workout_date" : activity.StartTime.strftime("%Y-%m-%d"), 
                "workout_start_time" : activity.StartTime.strftime("%H:%M:%S"),
                "workout_comment" : activity.Notes, 
                "sport_id" : self._activityMappings[activity.Type],
                "workout_hide": "yes" if activity.Private else "no"
            }, record=serviceRecord)
            res = requests.get(self._urlRoot + "/workouts/change/{}".format(workoutId),
                               headers=self._obligatory_headers,
                               params=params)
            if res.status_code != 200:
                if res.status_code == 403:
                    raise APIException("No authorization to change activity with workout ID: {}".format(workoutId), block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))
                raise APIException("Unable to change activity with workout ID: {}".format(workoutId))

            return workoutId
示例#9
0
    def UploadActivity(self, serviceRecord, activity):

        cookies = self._get_web_cookies(record=serviceRecord)
        # Wicket sucks sucks sucks sucks sucks sucks.
        # Step 0
        #   http://www.endomondo.com/?wicket:bookmarkablePage=:com.endomondo.web.page.workout.CreateWorkoutPage2
        #   Get URL of file upload
        #       <a href="#" id="id13a" onclick="var wcall=wicketAjaxGet('?wicket:interface=:8:pageContainer:lowerSection:lowerMain:lowerMainContent:importFileLink::IBehaviorListener:0:',function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$('id13a') != null;}.bind(this));return !wcall;">...                    <div class="fileImport"></div>
        upload_select = requests.get(
            "http://www.endomondo.com/?wicket:bookmarkablePage=:com.endomondo.web.page.workout.CreateWorkoutPage2",
            cookies=cookies)
        upload_lightbox_url = re.findall(
            '<a.+?onclick="var wcall=wicketAjaxGet\(\'(.+?)\'',
            upload_select.text)[3]
        logger.debug("Will request upload lightbox from %s" %
                     upload_lightbox_url)
        # Step 1
        #   http://www.endomondo.com/upload-form-url
        #   Get IFrame src
        upload_iframe = requests.get("http://www.endomondo.com/" +
                                     upload_lightbox_url,
                                     cookies=cookies)
        upload_iframe_src = re.findall('src="(.+?)"', upload_iframe.text)[0]
        logger.debug("Will request upload form from %s" % upload_iframe_src)
        # Step 2
        #   http://www.endomondo.com/iframe-url
        #   Follow redirect to upload page
        #   Get form ID
        #   Get form target from <a class="next" name="uploadSumbit" id="id18d" value="Next" onclick="document.getElementById('fileUploadWaitIcon').style.display='block';var wcall=wicketSubmitFormById('id18c', '?wicket:interface=:13:importPanel:wizardStepPanel:uploadForm:uploadSumbit::IActivePageBehaviorListener:0:-1&amp;wicket:ignoreIfNotActive=true', 'uploadSumbit' ,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$$(this)&amp;&amp;Wicket.$$('id18c')}.bind(this));;; return false;">Next</a>
        upload_form_rd = requests.get("http://www.endomondo.com/" +
                                      upload_iframe_src,
                                      cookies=cookies,
                                      allow_redirects=False)
        assert (
            upload_form_rd.status_code == 302
        )  # Need to manually follow the redirect to keep the cookies available
        upload_form = requests.get(upload_form_rd.headers["location"],
                                   cookies=cookies)
        upload_form_id = re.findall('<form.+?id="([^"]+)"',
                                    upload_form.text)[0]
        upload_form_target = re.findall(
            "wicketSubmitFormById\('[^']+', '([^']+)'", upload_form.text)[0]
        logger.debug("Will POST upload form ID %s to %s" %
                     (upload_form_id, upload_form_target))
        # Step 3
        #   http://www.endomondo.com/upload-target
        #   POST
        #       formID_hf_0
        #       file as `uploadFile`
        #       uploadSubmit=1
        #   Get ID from form
        #   Get confirm target <a class="next" name="reviewSumbit" id="id191" value="Save" onclick="document.getElementById('fileSaveWaitIcon').style.display='block';var wcall=wicketSubmitFormById('id190', '?wicket:interface=:13:importPanel:wizardStepPanel:reviewForm:reviewSumbit::IActivePageBehaviorListener:0:-1&amp;wicket:ignoreIfNotActive=true', 'reviewSumbit' ,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$$(this)&amp;&amp;Wicket.$$('id190')}.bind(this));;; return false;">Save</a>
        fit_file = FITIO.Dump(activity)
        files = {
            "uploadFile":
            ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit",
             fit_file)
        }
        data = {"uploadSumbit": 1, upload_form_id + "_hf_0": ""}
        upload_result = requests.post("http://www.endomondo.com/" +
                                      upload_form_target,
                                      data=data,
                                      files=files,
                                      cookies=cookies)
        confirm_form_id = re.findall('<form.+?id="([^"]+)"',
                                     upload_result.text)[0]
        confirm_form_target = re.findall(
            "wicketSubmitFormById\('[^']+', '([^']+)'", upload_result.text)[0]
        logger.debug("Will POST confirm form ID %s to %s" %
                     (confirm_form_id, confirm_form_target))
        # Step 4
        #   http://www.endomondo.com/confirm-target
        #   POST
        #       formID_hf_0
        #       workoutRow:0:mark=on
        #       workoutRow:0:sport=X
        #       reviewSumbit=1
        sportId = [
            k for k, v in self._reverseActivityMappings.items()
            if v == activity.Type
        ]
        if len(sportId) == 0:
            raise ValueError(
                "Endomondo service does not support activity type " +
                activity.Type)
        else:
            sportId = sportId[0]

        data = {
            confirm_form_id + "_hf_0": "",
            "workoutRow:0:mark": "on",
            "workoutRow:0:sport": sportId,
            "reviewSumbit": 1
        }
        confirm_result = requests.post("http://www.endomondo.com" +
                                       confirm_form_target,
                                       data=data,
                                       cookies=cookies)
        assert (confirm_result.status_code == 200)
        # Step 5
        #   http://api.mobile.endomondo.com/mobile/api/workout/list
        #   GET
        #       authToken=xyz
        #       maxResults=1
        #       before=utcTS+1
        #   Get activity ID
        before = (activity.StartTime + timedelta(seconds=90)).astimezone(
            pytz.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
        params = {
            "authToken": serviceRecord.Authorization["AuthToken"],
            "maxResults": 1,
            "before": before
        }
        id_result = requests.get(
            "http://api.mobile.endomondo.com/mobile/api/workout/list",
            params=params)
        act_id = id_result.json()["data"][0]["id"]
        logger.debug("Retrieved activity ID %s" % act_id)

        # Step 6
        #   http://www.endomondo.com/workouts/xyz
        #   Get edit URL <a class="enabled button edit" href="#" id="id171" onclick="var wcall=wicketAjaxGet('../?wicket:interface=:10:pageContainer:lowerSection:lowerMain:lowerMainContent:workout:details:actions:ownActions:editButton::IBehaviorListener:0:1',function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$('id171') != null;}.bind(this));return !wcall;">Edit</a>
        summary_page = requests.get("http://www.endomondo.com/workouts/%s" %
                                    act_id,
                                    cookies=cookies)
        edit_url = re.findall(
            '<a.+class="enabled button edit".+?onclick="var wcall=wicketAjaxGet\(\'../(.+?)\'',
            summary_page.text)[0]
        logger.debug("Will request edit form from %s" % edit_url)
        # Step 7
        #   http://www.endomondo.com/edit-url
        #   Get form ID
        #   Get form target from <a class="halfbutton" href="#" style="float:left;" name="saveButton" id="id1d5" value="Save" onclick="var wcall=wicketSubmitFormById('id1d4', '../?wicket:interface=:14:pageContainer:lightboxContainer:lightboxContent:panel:detailsContainer:workoutForm:saveButton::IActivePageBehaviorListener:0:1&amp;wicket:ignoreIfNotActive=true', 'saveButton' ,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$$(this)&amp;&amp;Wicket.$$('id1d4')}.bind(this));;; return false;">Save</a>
        edit_page = requests.get("http://www.endomondo.com/" + edit_url,
                                 cookies=cookies)
        edit_form_id = re.findall('<form.+?id="([^"]+)"', edit_page.text)[0]
        edit_form_target = re.findall(
            "wicketSubmitFormById\('[^']+', '([^']+)'", edit_page.text)[0]
        logger.debug("Will POST edit form ID %s to %s" %
                     (edit_form_id, edit_form_target))
        # Step 8
        #   http://www.endomondo.com/edit-finish-url
        #   POST
        #       id34e_hf_0
        #       sport: X
        #       name: name123
        #       startTime:YYYY-MM-DD HH:MM
        #       distance:1.00 km
        #       duration:0h:10m:00s
        #       metersAscent:
        #       metersDescent:
        #       averageHeartRate:30
        #       maximumHeartRate:100
        #       validityToggle:on ("include in statistics")
        #       calorieRecomputeToggle:on
        #       notes:asdasdasd
        #       saveButton:1
        duration = (activity.EndTime - activity.StartTime)
        duration_formatted = "%dh:%dm:%ds" % (duration.seconds / 3600,
                                              duration.seconds % 3600 / 60,
                                              duration.seconds % (60))
        data = {
            edit_form_id + "_hf_0":
            "",
            "saveButton":
            "1",
            "validityToggle":
            "on",
            "calorieRecomputeToggle":
            "on",
            "startTime":
            activity.StartTime.strftime("%Y-%m-%d %H:%M"),
            "distance":
            "%s km" % activity.Stats.Distance.asUnits(
                ActivityStatisticUnit.Kilometers).Value,
            "sport":
            sportId,
            "duration":
            duration_formatted,
            "name":
            activity.Name,
        }
        if activity.Stats.Elevation.Gain is not None:
            data["metersAscent"] = int(round(activity.Stats.Elevation.Gain))
        if activity.Stats.Elevation.Gain is not None:
            data["metersDescent"] = int(round(activity.Stats.Elevation.Loss))
        if activity.Stats.HR.Average is not None:
            data["averageHeartRate"] = int(round(activity.Stats.HR.Average))
        if activity.Stats.HR.Max is not None:
            data["maximumHeartRate"] = int(round(activity.Stats.HR.Max))
        edit_result = requests.post("http://www.endomondo.com/" +
                                    edit_form_target,
                                    data=data,
                                    cookies=cookies)
        assert edit_result.status_code == 200 and "feedbackPanelERROR" not in edit_result.text