Merge pull request #65 from SamR1/fix-workouts-display
Fix workouts display on calendar
This commit is contained in:
commit
8796e718a6
@ -42,7 +42,7 @@ Workouts
|
||||
- Montain Biking
|
||||
- Running
|
||||
- Walking
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
|
||||
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed
|
||||
- Workout edition and deletion. User can add a note
|
||||
|
@ -249,7 +249,7 @@
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><strong>page</strong> (<em>integer</em>) – page if using pagination (default: 1)</p></li>
|
||||
<li><p><strong>per_page</strong> (<em>integer</em>) – number of workouts per page
|
||||
(default: 5, max: 50)</p></li>
|
||||
(default: 5, max: 100)</p></li>
|
||||
<li><p><strong>sport_id</strong> (<em>integer</em>) – sport id</p></li>
|
||||
<li><p><strong>from</strong> (<em>string</em>) – start date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li>
|
||||
<li><p><strong>to</strong> (<em>string</em>) – end date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li>
|
||||
|
@ -191,7 +191,7 @@
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)</p></li>
|
||||
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.</p></li>
|
||||
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li>
|
||||
<li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed</p></li>
|
||||
<li><p>Workout edition and deletion. User can add a note</p></li>
|
||||
|
File diff suppressed because one or more lines are too long
@ -42,7 +42,7 @@ Workouts
|
||||
- Montain Biking
|
||||
- Running
|
||||
- Walking
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
|
||||
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed
|
||||
- Workout edition and deletion. User can add a note
|
||||
|
6
fittrackee/dist/asset-manifest.json
vendored
6
fittrackee/dist/asset-manifest.json
vendored
@ -1,8 +1,8 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.376b8924.chunk.css",
|
||||
"main.js": "/static/js/main.b00026d7.chunk.js",
|
||||
"main.js.map": "/static/js/main.b00026d7.chunk.js.map",
|
||||
"main.js": "/static/js/main.75e5c894.chunk.js",
|
||||
"main.js.map": "/static/js/main.75e5c894.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.1240af94.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.1240af94.js.map",
|
||||
"static/js/2.1ec1bf1c.chunk.js": "/static/js/2.1ec1bf1c.chunk.js",
|
||||
@ -19,6 +19,6 @@
|
||||
"static/js/runtime-main.1240af94.js",
|
||||
"static/js/2.1ec1bf1c.chunk.js",
|
||||
"static/css/main.376b8924.chunk.css",
|
||||
"static/js/main.b00026d7.chunk.js"
|
||||
"static/js/main.75e5c894.chunk.js"
|
||||
]
|
||||
}
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1ec1bf1c.chunk.js"></script><script src="/static/js/main.b00026d7.chunk.js"></script></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1ec1bf1c.chunk.js"></script><script src="/static/js/main.75e5c894.chunk.js"></script></body></html>
|
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/main.75e5c894.chunk.js.map
vendored
Normal file
1
fittrackee/dist/static/js/main.75e5c894.chunk.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,5 @@
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import Flask
|
||||
@ -278,7 +279,8 @@ class TestGetWorkoutsWithPagination:
|
||||
in data['message']
|
||||
)
|
||||
|
||||
def test_it_gets_5_workouts_per_page(
|
||||
@patch('fittrackee.workouts.workouts.MAX_WORKOUTS_PER_PAGE', 6)
|
||||
def test_it_gets_max_workouts_per_page_if_per_page_exceeds_max(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -303,17 +305,18 @@ class TestGetWorkoutsWithPagination:
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['workouts']) == 7
|
||||
assert len(data['data']['workouts']) == 6
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][0]['workout_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['workouts'][6]['workout_date']
|
||||
'Thu, 01 Jun 2017 00:00:00 GMT'
|
||||
== data['data']['workouts'][5]['workout_date']
|
||||
)
|
||||
|
||||
def test_it_gets_3_workouts_per_page(
|
||||
@patch('fittrackee.workouts.workouts.MAX_WORKOUTS_PER_PAGE', 6)
|
||||
def test_it_gets_given_number_of_workouts_per_page(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -349,6 +352,113 @@ class TestGetWorkoutsWithPagination:
|
||||
)
|
||||
|
||||
|
||||
class TestGetWorkoutsWithOrder:
|
||||
def test_it_gets_workouts_with_default_order(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
seven_workouts_user_1: Workout,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/workouts',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['workouts']) == 5
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][0]['workout_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][4]['workout_date']
|
||||
)
|
||||
|
||||
def test_it_gets_workouts_with_ascending_order(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
seven_workouts_user_1: Workout,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/workouts?order=asc',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['workouts']) == 5
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['workouts'][0]['workout_date']
|
||||
)
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][4]['workout_date']
|
||||
)
|
||||
|
||||
def test_it_gets_workouts_with_descending_order(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
seven_workouts_user_1: Workout,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/workouts?order=desc',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['workouts']) == 5
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][0]['workout_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][4]['workout_date']
|
||||
)
|
||||
|
||||
|
||||
class TestGetWorkoutsWithFilters:
|
||||
def test_it_gets_workouts_with_date_filter(
|
||||
self,
|
||||
@ -487,41 +597,6 @@ class TestGetWorkoutsWithFilters:
|
||||
== data['data']['workouts'][1]['workout_date']
|
||||
)
|
||||
|
||||
def test_it_gets_workouts_with_ascending_order(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
seven_workouts_user_1: Workout,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/workouts?order=asc',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['workouts']) == 5
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['workouts'][0]['workout_date']
|
||||
)
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['workouts'][4]['workout_date']
|
||||
)
|
||||
|
||||
def test_it_gets_workouts_with_distance_filter(
|
||||
self,
|
||||
app: Flask,
|
||||
|
@ -44,7 +44,8 @@ from .utils_id import decode_short_id
|
||||
|
||||
workouts_blueprint = Blueprint('workouts', __name__)
|
||||
|
||||
WORKOUTS_PER_PAGE = 5
|
||||
DEFAULT_WORKOUTS_PER_PAGE = 5
|
||||
MAX_WORKOUTS_PER_PAGE = 100
|
||||
|
||||
|
||||
@workouts_blueprint.route('/workouts', methods=['GET'])
|
||||
@ -168,7 +169,7 @@ def get_workouts(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
|
||||
:query integer page: page if using pagination (default: 1)
|
||||
:query integer per_page: number of workouts per page
|
||||
(default: 5, max: 50)
|
||||
(default: 5, max: 100)
|
||||
:query integer sport_id: sport id
|
||||
:query string from: start date (format: ``%Y-%m-%d``)
|
||||
:query string to: end date (format: ``%Y-%m-%d``)
|
||||
@ -219,10 +220,10 @@ def get_workouts(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
per_page = (
|
||||
int(params.get('per_page'))
|
||||
if params.get('per_page')
|
||||
else WORKOUTS_PER_PAGE
|
||||
else DEFAULT_WORKOUTS_PER_PAGE
|
||||
)
|
||||
if per_page > 50:
|
||||
per_page = 50
|
||||
if per_page > MAX_WORKOUTS_PER_PAGE:
|
||||
per_page = MAX_WORKOUTS_PER_PAGE
|
||||
workouts = (
|
||||
Workout.query.filter(
|
||||
Workout.user_id == auth_user_id,
|
||||
|
@ -179,7 +179,7 @@ export const getMonthWorkouts = (from, to) => dispatch =>
|
||||
FitTrackeeGenericApi.getData('workouts', {
|
||||
from,
|
||||
to,
|
||||
order: 'asc',
|
||||
order: 'desc',
|
||||
per_page: 100,
|
||||
})
|
||||
.then(ret => {
|
||||
|
Loading…
Reference in New Issue
Block a user