Merge branch 'develop' into feature/idoklad
This commit is contained in:
@@ -1648,7 +1648,7 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
|||||||
datadf = pl.concat(data)
|
datadf = pl.concat(data)
|
||||||
existing_columns = [col for col in columns if col in datadf.columns]
|
existing_columns = [col for col in columns if col in datadf.columns]
|
||||||
datadf = datadf.select(existing_columns)
|
datadf = datadf.select(existing_columns)
|
||||||
except (ShapeError, SchemaError):
|
except ShapeError:
|
||||||
try:
|
try:
|
||||||
data = [
|
data = [
|
||||||
df.select(columns)
|
df.select(columns)
|
||||||
@@ -1703,7 +1703,61 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
|||||||
datadf = pl.concat(data)
|
datadf = pl.concat(data)
|
||||||
except ShapeError:
|
except ShapeError:
|
||||||
return pl.DataFrame()
|
return pl.DataFrame()
|
||||||
|
except SchemaError:
|
||||||
|
try:
|
||||||
|
data = [
|
||||||
|
df.select(columns)
|
||||||
|
for df in data]
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
existing_columns = [col for col in columns if col in df.columns]
|
||||||
|
df = df.select(existing_columns)
|
||||||
|
|
||||||
|
# float columns
|
||||||
|
floatcolumns = []
|
||||||
|
intcolumns = []
|
||||||
|
stringcolumns = []
|
||||||
|
for c in columns:
|
||||||
|
try:
|
||||||
|
if metricsdicts[c]['numtype'] == 'float':
|
||||||
|
floatcolumns.append(c)
|
||||||
|
if metricsdicts[c]['numtype'] == 'integer':
|
||||||
|
intcolumns.append(c)
|
||||||
|
except KeyError:
|
||||||
|
if c[0] == 'f':
|
||||||
|
stringcolumns.append(c)
|
||||||
|
else:
|
||||||
|
intcolumns.append(c)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = [
|
||||||
|
df.with_columns(
|
||||||
|
cs.float().cast(pl.Float64)
|
||||||
|
).with_columns(
|
||||||
|
cs.integer().cast(pl.Int64)
|
||||||
|
).with_columns(
|
||||||
|
cs.by_name(intcolumns).cast(pl.Int64)
|
||||||
|
).with_columns(
|
||||||
|
cs.by_name(floatcolumns).cast(pl.Float64)
|
||||||
|
).with_columns(
|
||||||
|
cs.by_name(stringcolumns).cast(pl.String)
|
||||||
|
)
|
||||||
|
for df in data
|
||||||
|
]
|
||||||
|
except ComputeError:
|
||||||
|
pass
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
datadf = pl.concat(data)
|
||||||
|
except SchemaError:
|
||||||
|
try:
|
||||||
|
data = [
|
||||||
|
df.with_columns(cs.integer().cast(pl.Float64)) for df in data
|
||||||
|
]
|
||||||
|
datadf = pl.concat(data)
|
||||||
|
except ShapeError:
|
||||||
|
return pl.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
|
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
|
||||||
from rowers.models import Rower, User, Workout, TombStone
|
from rowers.models import Rower, User, Workout, TombStone, PlannedSession
|
||||||
from rowingdata import rowingdata
|
from rowingdata import rowingdata
|
||||||
|
from rowers.rower_rules import user_is_not_basic, user_is_coachee
|
||||||
|
|
||||||
from rowers import mytypes
|
from rowers import mytypes
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ headers = {
|
|||||||
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
|
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
|
||||||
intervals_token_url = 'https://intervals.icu/api/oauth/token'
|
intervals_token_url = 'https://intervals.icu/api/oauth/token'
|
||||||
|
|
||||||
|
webhookverification = 'JA9Vt6RNH10'
|
||||||
|
|
||||||
class IntervalsIntegration(SyncIntegration):
|
class IntervalsIntegration(SyncIntegration):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(IntervalsIntegration, self).__init__(*args, **kwargs)
|
super(IntervalsIntegration, self).__init__(*args, **kwargs)
|
||||||
@@ -336,6 +339,28 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def update_plannedsession(self, ps, data, *args, **kwargs):
|
||||||
|
_ = self.open()
|
||||||
|
r = self.rower
|
||||||
|
|
||||||
|
if data['category'] == 'WORKOUT':
|
||||||
|
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + '/downloadfit'
|
||||||
|
headers = {
|
||||||
|
'Authorization': 'Bearer ' + r.intervals_token,
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
dologging('intervals.icu.log', response.text)
|
||||||
|
else:
|
||||||
|
filename = 'planned_' + str(ps.intervals_icu_id) + '.fit'
|
||||||
|
filename2 = 'media/planned_' + str(ps.intervals_icu_id) + '.fit'
|
||||||
|
with open(filename2, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
data['fitfile'] = filename
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def get_plannedsession(self, id, *args, **kwargs):
|
def get_plannedsession(self, id, *args, **kwargs):
|
||||||
_ = self.open()
|
_ = self.open()
|
||||||
r = self.rower
|
r = self.rower
|
||||||
@@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
"name": ps.name,
|
"name": ps.name,
|
||||||
"description": stepstext,
|
"description": stepstext,
|
||||||
"indoor": ps.sessionsport in mytypes.ergtypes,
|
"indoor": ps.sessionsport in mytypes.ergtypes,
|
||||||
|
'external_id': ps.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ps.sessiontype == 'cycletarget':
|
if ps.sessiontype == 'cycletarget':
|
||||||
@@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
ps.save()
|
ps.save()
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def update_calendar(self, r, event, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
records = event["events"]
|
||||||
|
except KeyError:
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
id = record['id']
|
||||||
|
data = {}
|
||||||
|
try:
|
||||||
|
pss = PlannedSession.objects.filter(intervals_icu_id=id)
|
||||||
|
if pss.count() > 0:
|
||||||
|
ps = pss[0]
|
||||||
|
data = self.update_plannedsession(ps, record)
|
||||||
|
else:
|
||||||
|
data = self.get_plannedsession(id)
|
||||||
|
ps = PlannedSession(
|
||||||
|
manager=r.user,
|
||||||
|
intervals_icu_id=id,
|
||||||
|
)
|
||||||
|
ps.save()
|
||||||
|
ps.rower.add(r)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# got data
|
||||||
|
if data:
|
||||||
|
ps.name = data['name']
|
||||||
|
ps.comment = data['description']
|
||||||
|
ps.startdate = arrow.get(data['start_date_local']).datetime
|
||||||
|
ps.enddate = arrow.get(data['end_date_local']).datetime
|
||||||
|
ps.preferreddate = arrow.get(data['start_date_local']).datetime
|
||||||
|
ps.sessionsport = mytypes.intervalsmappinginv[data['type']]
|
||||||
|
ps.sessiontype = 'session'
|
||||||
|
ps.save()
|
||||||
|
try:
|
||||||
|
timetarget = data['time_target']
|
||||||
|
except KeyError:
|
||||||
|
timetarget = None
|
||||||
|
if timetarget is None:
|
||||||
|
try:
|
||||||
|
timetarget = data['moving_time']
|
||||||
|
except KeyError:
|
||||||
|
timetarget = None
|
||||||
|
if timetarget is None:
|
||||||
|
timetarget = 3600
|
||||||
|
timetarget = int(timetarget)/60.
|
||||||
|
ps.sessionvalue = timetarget
|
||||||
|
ps.save()
|
||||||
|
if data['category'].lower() == 'workout':
|
||||||
|
ps.fitfile = data['fitfile']
|
||||||
|
ps.save()
|
||||||
|
ps.update_steps()
|
||||||
|
if data['category'].lower() == 'target':
|
||||||
|
ps.sessiontype = 'cycletarget'
|
||||||
|
ps.sessionvalue = int(data['time_target'])/60.
|
||||||
|
ps.enddate = ps.startdate + datetime.timedelta(days=6)
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted_records = event["deleted_events"]
|
||||||
|
except KeyError:
|
||||||
|
deleted_records = []
|
||||||
|
|
||||||
|
for record in deleted_records:
|
||||||
|
id = record['id']
|
||||||
|
try:
|
||||||
|
pss = PlannedSession.objects.filter(intervals_icu_id=id)
|
||||||
|
if r.intervals_delete_plannedsession and pss.count() > 0:
|
||||||
|
for ps in pss:
|
||||||
|
ps.delete()
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def import_activities(self, event, *args, **kwargs):
|
||||||
|
if not self.rower.intervals_auto_import:
|
||||||
|
return 0
|
||||||
|
if user_is_not_basic(self.rower.user) or user_is_coachee(self.rower.user):
|
||||||
|
try:
|
||||||
|
record = event["activity"]
|
||||||
|
except KeyError:
|
||||||
|
records = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
id = record['id']
|
||||||
|
result = self.get_workout(id)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def delete_activities(self, event, *args, **kwargs):
|
||||||
|
if not self.rower.intervals_auto_delete:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
record = event["activity"]
|
||||||
|
except KeyError:
|
||||||
|
records = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
id = record['id']
|
||||||
|
try:
|
||||||
|
w = Workout.objects.get(uploadedtointervals=id)
|
||||||
|
if w.user == self.rower:
|
||||||
|
w.delete()
|
||||||
|
except Workout.DoesNotExist:
|
||||||
|
pass
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Command(BaseCommand):
|
|||||||
counter = 0
|
counter = 0
|
||||||
print('----- Workouts ---------')
|
print('----- Workouts ---------')
|
||||||
for w in ws:
|
for w in ws:
|
||||||
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2:
|
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2 or w.uploadedtointervals:
|
||||||
record = SyncRecord(
|
record = SyncRecord(
|
||||||
workout = w,
|
workout = w,
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@ class Command(BaseCommand):
|
|||||||
aantal = ts.count()
|
aantal = ts.count()
|
||||||
counter = 0
|
counter = 0
|
||||||
for w in ts:
|
for w in ts:
|
||||||
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2:
|
if w.uploadedtostrava or w.uploadedtotp or w.uploadedtonk or w.uploadedtosporttracks or w.uploadedtoc2 or w.uploadedtointervals:
|
||||||
record = SyncRecord(
|
record = SyncRecord(
|
||||||
)
|
)
|
||||||
if w.uploadedtostrava:
|
if w.uploadedtostrava:
|
||||||
|
|||||||
@@ -1176,6 +1176,7 @@ class Rower(models.Model):
|
|||||||
intervals_auto_export = models.BooleanField(default=False)
|
intervals_auto_export = models.BooleanField(default=False)
|
||||||
intervals_auto_import = models.BooleanField(default=False)
|
intervals_auto_import = models.BooleanField(default=False)
|
||||||
intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu")
|
intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu")
|
||||||
|
intervals_auto_delete = models.BooleanField(default=True, verbose_name="Deleting an activity on intervals.icu deletes it on both")
|
||||||
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
|
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
|
||||||
sporttrackstoken = models.CharField(
|
sporttrackstoken = models.CharField(
|
||||||
default='', max_length=200, blank=True, null=True)
|
default='', max_length=200, blank=True, null=True)
|
||||||
@@ -3830,6 +3831,36 @@ class Workout(models.Model):
|
|||||||
record.boatclass = self.workouttype
|
record.boatclass = self.workouttype
|
||||||
record.save()
|
record.save()
|
||||||
|
|
||||||
|
if self.uploadedtostrava:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
stravaid=self.uploadedtostrava,
|
||||||
|
)
|
||||||
|
if self.uploadedtointervals:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
intervalsid= self.uploadedtointervals,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.uploadedtotp:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
tpid= self.uploadedtotp,
|
||||||
|
)
|
||||||
|
if self.uploadedtonk:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
nkid= self.uploadedtonk,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.uploadedtosporttracks:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
sporttracksid=self.uploadedtosporttracks,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.uploadedtoc2:
|
||||||
|
record = create_or_update_syncrecord(self.user, self,
|
||||||
|
c2id= self.uploadedtoc2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
super(Workout, self).save(*args, **kwargs)
|
super(Workout, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -4006,7 +4037,7 @@ def create_or_update_syncrecord(rower, workout, **kwargs):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
record.save()
|
record.save()
|
||||||
except IntegrityError:
|
except (IntegrityError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return record
|
return record
|
||||||
@@ -4646,6 +4677,7 @@ class RowerExportForm(ModelForm):
|
|||||||
'intervals_auto_export',
|
'intervals_auto_export',
|
||||||
'intervals_delete_plannedsession',
|
'intervals_delete_plannedsession',
|
||||||
'intervals_resample_to_1s',
|
'intervals_resample_to_1s',
|
||||||
|
'intervals_auto_delete',
|
||||||
'imports_are_private'
|
'imports_are_private'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4673,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm):
|
|||||||
'intervals_auto_import',
|
'intervals_auto_import',
|
||||||
'intervals_auto_export',
|
'intervals_auto_export',
|
||||||
'intervals_resample_to_1s',
|
'intervals_resample_to_1s',
|
||||||
|
'intervals_auto_delete',
|
||||||
'intervals_delete_plannedsession',
|
'intervals_delete_plannedsession',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1070,7 +1070,7 @@ def get_workouts_session(r, ps):
|
|||||||
|
|
||||||
def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False, plan=None,
|
def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False, plan=None,
|
||||||
plan_past_days=False,
|
plan_past_days=False,
|
||||||
asynchronous=False, queue=queue):
|
asynchronous=True, queue=queue):
|
||||||
trainingdays = plansteps['trainingDays']
|
trainingdays = plansteps['trainingDays']
|
||||||
planstartdate = startdate
|
planstartdate = startdate
|
||||||
if not asynchronous:
|
if not asynchronous:
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ def create_sessions_from_json_async(plansteps, rower, startdate, manager, planby
|
|||||||
elif preferreddate >= timezone.now().date():
|
elif preferreddate >= timezone.now().date():
|
||||||
create_session = True
|
create_session = True
|
||||||
|
|
||||||
|
|
||||||
if create_session:
|
if create_session:
|
||||||
ps = PlannedSession(
|
ps = PlannedSession(
|
||||||
startdate=preferreddate -
|
startdate=preferreddate -
|
||||||
|
|||||||
@@ -633,6 +633,7 @@ urlpatterns = [
|
|||||||
name='workout_rojaboimport_view'),
|
name='workout_rojaboimport_view'),
|
||||||
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
|
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
|
||||||
name='plannedsession_intervalsimport_view'),
|
name='plannedsession_intervalsimport_view'),
|
||||||
|
re_path(r'^session/intervals/webhook/$', views.intervals_webhook_view, name='intervals_webhook_view'),
|
||||||
re_path(r'^workout/(?P<source>\w+.*)import/$',
|
re_path(r'^workout/(?P<source>\w+.*)import/$',
|
||||||
views.workout_import_view, name='workout_import_view'),
|
views.workout_import_view, name='workout_import_view'),
|
||||||
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
|
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ from rowers.utils import NoTokenError
|
|||||||
from rowers.models import PlannedSession
|
from rowers.models import PlannedSession
|
||||||
|
|
||||||
import rowers.integrations.strava as strava
|
import rowers.integrations.strava as strava
|
||||||
|
import rowers.integrations.intervals as intervals
|
||||||
from rowers.integrations import importsources
|
from rowers.integrations import importsources
|
||||||
|
from rowers.integrations import IntervalsIntegration
|
||||||
from rowers.utils import NoTokenError
|
from rowers.utils import NoTokenError
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@@ -913,9 +915,62 @@ def workout_rojaboimport_view(request, message="", userid=0): # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def intervals_webhook_view(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
verificationtoken = request.GET.get('secret')
|
||||||
|
if verificationtoken != intervals.webhookverification:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
dologging("intervals_webhooks.log","GET request")
|
||||||
|
dologging("intervals_webhooks.log",request.body)
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
try:
|
||||||
|
verificationtoken = data['secret']
|
||||||
|
except KeyError:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
if verificationtoken != intervals.webhookverification:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
try:
|
||||||
|
events = data['events']
|
||||||
|
except KeyError:
|
||||||
|
# return invalid request if no events
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
webhook_type = None
|
||||||
|
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
try:
|
||||||
|
athlete_id = event['athlete_id']
|
||||||
|
webhook_type = event['type']
|
||||||
|
r = Rower.objects.get(intervals_owner_id=athlete_id)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
except MultipleObjectsReturned:
|
||||||
|
rs = Rower.objects.filter(intervals_owner_id=athlete_id)
|
||||||
|
r = rs[0]
|
||||||
|
except KeyError:
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
integration = IntervalsIntegration(r.user)
|
||||||
|
|
||||||
|
if webhook_type.lower() == 'calendar_updated':
|
||||||
|
integration.update_calendar(r, event)
|
||||||
|
|
||||||
|
if webhook_type.lower() == 'activity_uploaded':
|
||||||
|
integration.import_activities(event)
|
||||||
|
|
||||||
|
if webhook_type.lower() == 'activity_deleted':
|
||||||
|
integration.delete_activities(event)
|
||||||
|
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
|
||||||
# for Strava webhook request validation
|
# for Strava webhook request validation
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def strava_webhook_view(request):
|
def strava_webhook_view(request):
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
|||||||
@@ -2777,7 +2777,7 @@ def rower_view_instantplan(request, id='', userid=0):
|
|||||||
messages.info(request, 'Your Sessions have been added')
|
messages.info(request, 'Your Sessions have been added')
|
||||||
|
|
||||||
url = reverse('plannedsessions_view')
|
url = reverse('plannedsessions_view')
|
||||||
timeperiod = startdate.strftime(
|
timeperiod = timezone.now().date().strftime(
|
||||||
'%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d')
|
'%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d')
|
||||||
url = url+'?when='+timeperiod
|
url = url+'?when='+timeperiod
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user