API & Client - sport administration & refactor - #15
This commit is contained in:
parent
a10128f13e
commit
1f8de2eccc
@ -4,4 +4,5 @@ Sports
|
||||
.. autoflask:: fittrackee_api:create_app()
|
||||
:endpoints:
|
||||
sports.get_sports,
|
||||
sports.get_sport
|
||||
sports.get_sport,
|
||||
sports.update_sport
|
||||
|
@ -142,39 +142,45 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-transport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Transport)"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/hiking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Hiking"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/mountain-biking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Mountain Biking"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/running.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Running"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/walking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Walking"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
@ -228,9 +234,10 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_deleted"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
@ -280,6 +287,86 @@
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="patch">
|
||||
<dt id="patch--api-sports-(int-sport_id)">
|
||||
<code class="sig-name descname">PATCH </code><code class="sig-name descname">/api/sports/</code><span class="sig-paren">(</span><em class="property">int: </em><em class="sig-param">sport_id</em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-sports-(int-sport_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Update a sport</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/sports/1</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>success</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"_can_be_disabled"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"success"</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>sport not found</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">NOT FOUND</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[]</span>
|
||||
<span class="p">},</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"not found"</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>auth_user_id</strong> (<em>integer</em>) – authenticate user id (from JSON Web Token)</p></li>
|
||||
<li><p><strong>sport_id</strong> (<em>integer</em>) – sport id</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request JSON Object</dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><strong>is_active</strong> (<em>string</em>) – sport active status</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Request Headers</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Status Codes</dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a> – sport updated</p></li>
|
||||
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> – invalid payload</p></li>
|
||||
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> – <ul>
|
||||
<li><p>Provide a valid auth token.</p></li>
|
||||
<li><p>Signature expired. Please log in again.</p></li>
|
||||
<li><p>Invalid token. Please log in again.</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> – sport not found</p></li>
|
||||
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> – </p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -259,6 +259,11 @@
|
||||
<td>
|
||||
<a href="api/activities.html#patch--api-activities-(int-activity_id)"><code class="xref">PATCH /api/activities/(int:activity_id)</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/sports.html#patch--api-sports-(int-sport_id)"><code class="xref">PATCH /api/sports/(int:sport_id)</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -4,4 +4,5 @@ Sports
|
||||
.. autoflask:: fittrackee_api:create_app()
|
||||
:endpoints:
|
||||
sports.get_sports,
|
||||
sports.get_sport
|
||||
sports.get_sport,
|
||||
sports.update_sport
|
||||
|
@ -60,7 +60,7 @@ class Sport(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
label = db.Column(db.String(50), unique=True, nullable=False)
|
||||
img = db.Column(db.String(255), unique=True, nullable=True)
|
||||
is_default = db.Column(db.Boolean, default=False, nullable=False)
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
activities = db.relationship(
|
||||
'Activity', lazy=True, backref=db.backref('sports', lazy='joined')
|
||||
)
|
||||
@ -79,8 +79,10 @@ class Sport(db.Model):
|
||||
'id': self.id,
|
||||
'label': self.label,
|
||||
'img': self.img,
|
||||
'_can_be_deleted': len(self.activities) == 0
|
||||
and not self.is_default,
|
||||
'is_active': self.is_active,
|
||||
'_can_be_disabled': not (
|
||||
len(self.activities) > 0 and self.is_active
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,39 +32,45 @@ def get_sports(auth_user_id):
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
}
|
||||
]
|
||||
@ -117,9 +123,10 @@ def get_sport(auth_user_id, sport_id):
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
@ -168,64 +175,101 @@ def get_sport(auth_user_id, sport_id):
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
# no administration - no documentation for now
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports', methods=['POST'])
|
||||
@authenticate_as_admin
|
||||
def post_sport(auth_user_id):
|
||||
"""Post a sport"""
|
||||
sport_data = request.get_json()
|
||||
if not sport_data or sport_data.get('label') is None:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
try:
|
||||
new_sport = Sport(label=sport_data.get('label'))
|
||||
db.session.add(new_sport)
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'created',
|
||||
'data': {'sports': [new_sport.serialize()]},
|
||||
}
|
||||
code = 201
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
def update_sport(auth_user_id, sport_id):
|
||||
"""Update a sport"""
|
||||
"""Update a sport
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/sports/1 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": false,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- sport not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": []
|
||||
},
|
||||
"status": "not found"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer sport_id: sport id
|
||||
|
||||
:<json string is_active: sport active status
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: sport updated
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 404: sport not found
|
||||
:statuscode 500:
|
||||
|
||||
"""
|
||||
sport_data = request.get_json()
|
||||
if not sport_data or sport_data.get('label') is None:
|
||||
if not sport_data or sport_data.get('is_active') is None:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
sports_list = []
|
||||
try:
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
sport.label = sport_data.get('label')
|
||||
db.session.commit()
|
||||
sports_list.append({'id': sport.id, 'label': sport.label})
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': sports_list},
|
||||
}
|
||||
code = 200
|
||||
if not (
|
||||
not sport_data.get('is_active')
|
||||
and sport.is_active
|
||||
and len(sport.activities) > 0
|
||||
):
|
||||
sport.is_active = sport_data.get('is_active')
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize()]},
|
||||
}
|
||||
code = 200
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Sport can not be disabled, activities exist.',
|
||||
}
|
||||
code = 400
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'not found',
|
||||
'data': {'sports': sports_list},
|
||||
}
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
@ -236,36 +280,3 @@ def update_sport(auth_user_id, sport_id):
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['DELETE'])
|
||||
@authenticate_as_admin
|
||||
def delete_sport(auth_user_id, sport_id):
|
||||
"""Delete a sport"""
|
||||
try:
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
db.session.delete(sport)
|
||||
db.session.commit()
|
||||
response_object = {'status': 'no content'}
|
||||
code = 204
|
||||
else:
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
except exc.IntegrityError as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Associated activities exist.',
|
||||
}
|
||||
code = 500
|
||||
except (exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
@ -4,14 +4,16 @@ expected_sport_1_cycling_result = {
|
||||
'id': 1,
|
||||
'label': 'Cycling',
|
||||
'img': None,
|
||||
'_can_be_deleted': True,
|
||||
'is_active': True,
|
||||
'_can_be_disabled': True,
|
||||
}
|
||||
|
||||
expected_sport_2_running_result = {
|
||||
'id': 2,
|
||||
'label': 'Running',
|
||||
'img': None,
|
||||
'_can_be_deleted': True,
|
||||
'is_active': True,
|
||||
'_can_be_disabled': True,
|
||||
}
|
||||
|
||||
|
||||
@ -83,78 +85,6 @@ def test_get_a_sport_invalid(app, user_1):
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_add_a_sport(app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='Cycling')),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 201
|
||||
assert 'created' in data['status']
|
||||
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert data['data']['sports'][0] == expected_sport_1_cycling_result
|
||||
|
||||
|
||||
def test_add_a_sport_not_admin(app, user_1):
|
||||
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.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='surfing')),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 403
|
||||
assert 'created' not in data['status']
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
|
||||
def test_add_a_sport_invalid_payload(app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'error' in data['status']
|
||||
assert 'Invalid payload.' in data['message']
|
||||
|
||||
|
||||
def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
@ -165,7 +95,7 @@ def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -177,7 +107,49 @@ def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
assert 'success' in data['status']
|
||||
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert 'cycling updated' in data['data']['sports'][0]['label']
|
||||
assert data['data']['sports'][0]['is_active'] is False
|
||||
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=True)),
|
||||
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']['sports']) == 1
|
||||
assert data['data']['sports'][0]['is_active'] is True
|
||||
|
||||
|
||||
def test_disable_a_sport_with_activities(
|
||||
app, user_1_admin, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'fail' in data['status']
|
||||
assert 'Sport can not be disabled, activities exist.' in data['message']
|
||||
|
||||
|
||||
def test_update_a_sport_not_admin(app, user_1, sport_1_cycling):
|
||||
@ -190,7 +162,7 @@ def test_update_a_sport_not_admin(app, user_1, sport_1_cycling):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -237,7 +209,7 @@ def test_update_a_sport_invalid_id(app, user_1_admin):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -248,91 +220,3 @@ def test_update_a_sport_invalid_id(app, user_1_admin):
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_delete_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_delete_a_sport_not_admin(app, user_1, sport_1_cycling):
|
||||
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.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
|
||||
def test_delete_a_sport_invalid_id(app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_delete_a_sport_with_an_activity(
|
||||
app, user_1_admin, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'Error. Associated activities exist.' in data['message']
|
||||
|
@ -6,7 +6,8 @@ def test_sport_model(app, sport_1_cycling):
|
||||
serialized_sport = sport_1_cycling.serialize()
|
||||
assert 1 == serialized_sport['id']
|
||||
assert 'Cycling' == serialized_sport['label']
|
||||
assert serialized_sport['_can_be_deleted'] is True
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['_can_be_disabled'] is True
|
||||
|
||||
|
||||
def test_sport_model_with_activity(
|
||||
@ -19,4 +20,5 @@ def test_sport_model_with_activity(
|
||||
serialized_sport = sport_1_cycling.serialize()
|
||||
assert 1 == serialized_sport['id']
|
||||
assert 'Cycling' == serialized_sport['label']
|
||||
assert serialized_sport['_can_be_deleted'] is False
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['_can_be_disabled'] is False
|
||||
|
@ -0,0 +1,36 @@
|
||||
"""replace 'is_default' with 'is_active' in 'Sports' table
|
||||
|
||||
Revision ID: 1345afe3b11d
|
||||
Revises: f69f1e413bde
|
||||
Create Date: 2019-09-22 17:57:00.595775
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1345afe3b11d'
|
||||
down_revision = 'f69f1e413bde'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_active',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_active = true")
|
||||
op.alter_column('sports', 'is_active', nullable=False)
|
||||
op.drop_column('sports', 'is_default')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_default',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_default = true")
|
||||
op.alter_column('sports', 'is_default', nullable=False)
|
||||
op.drop_column('sports', 'is_active')
|
@ -24,14 +24,29 @@ export const setLoading = loading => ({
|
||||
loading,
|
||||
})
|
||||
|
||||
export const getOrUpdateData = (action, target, data) => dispatch => {
|
||||
export const updateSportsData = data => ({
|
||||
type: 'UPDATE_SPORT_DATA',
|
||||
data,
|
||||
})
|
||||
|
||||
export const getOrUpdateData = (
|
||||
action,
|
||||
target,
|
||||
data,
|
||||
canDispatch = true
|
||||
) => dispatch => {
|
||||
if (data && data.id && isNaN(data.id)) {
|
||||
return dispatch(setError(`${target}|Incorrect id`))
|
||||
}
|
||||
dispatch(setError(''))
|
||||
return FitTrackeeApi[action](target, data)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(setData(target, ret.data))
|
||||
if (canDispatch) {
|
||||
dispatch(setData(target, ret.data))
|
||||
} else if (action === 'updateData' && target === 'sports') {
|
||||
dispatch(updateSportsData(ret.data.sports[0]))
|
||||
}
|
||||
} else {
|
||||
dispatch(setError(`${target}|${ret.message || ret.status}`))
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class ActivityDisplay extends React.Component {
|
||||
} = this.props
|
||||
const { coordinates, displayModal } = this.state
|
||||
const [activity] = activities
|
||||
const title = activity ? activity.title : 'Activity'
|
||||
const title = activity ? activity.title : t('activities:Activity')
|
||||
const [sport] = activity
|
||||
? sports.filter(s => s.id === activity.sport_id)
|
||||
: []
|
||||
|
@ -5,7 +5,8 @@ import { capitalize } from '../../utils/index'
|
||||
|
||||
const menuItems = ['application', 'sports', 'users']
|
||||
|
||||
export default function AdminMenu() {
|
||||
export default function AdminMenu(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div>
|
||||
<ul className="admin-items">
|
||||
@ -16,7 +17,7 @@ export default function AdminMenu() {
|
||||
pathname: `/admin/${item}`,
|
||||
}}
|
||||
>
|
||||
{capitalize(item)}
|
||||
{t(`administration:${capitalize(item)}`)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getOrUpdateData } from '../../../actions'
|
||||
import AdminDetail from '../generic/AdminDetail'
|
||||
|
||||
class AdminSports extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadSport(this.props.match.params.sportId)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// reload all Sports
|
||||
this.props.loadSport(null)
|
||||
}
|
||||
render() {
|
||||
const { sports } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminDetail results={sports} target="sports" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadSport: sportId => {
|
||||
dispatch(getOrUpdateData('getData', 'sports', { id: sportId }))
|
||||
},
|
||||
})
|
||||
)(AdminSports)
|
@ -1,42 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
|
||||
import { getOrUpdateData } from '../../../actions'
|
||||
import AdminPage from '../generic/AdminPage'
|
||||
import AdminSport from './AdminSport'
|
||||
import AdminSportsAdd from './AdminSportsAdd'
|
||||
import NotFound from '../../Others/NotFound'
|
||||
|
||||
class AdminSports extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadSports()
|
||||
}
|
||||
render() {
|
||||
const { sports } = this.props
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/admin/sports"
|
||||
render={() => <AdminPage data={sports} target="sports" />}
|
||||
/>
|
||||
<Route exact path="/admin/sports/add" component={AdminSportsAdd} />
|
||||
<Route exact path="/admin/sports/:sportId" component={AdminSport} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadSports: () => {
|
||||
dispatch(getOrUpdateData('getData', 'sports'))
|
||||
},
|
||||
})
|
||||
)(AdminSports)
|
@ -1,75 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { addData } from '../../../actions/index'
|
||||
import { history } from '../../../index'
|
||||
|
||||
class AdminSportsAdd extends React.Component {
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
const { message, onAddSport } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin - Add Sport</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">Administration - Sport</h1>
|
||||
{message && <code>{message}</code>}
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<div className="card-header">Add a sport</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={event => event.preventDefault()}>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Label:
|
||||
<input
|
||||
name="label"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={event => onAddSport(event)}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/admin/sports')}
|
||||
value="Cancel"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddSport: e => {
|
||||
const data = { label: e.target.form.label.value }
|
||||
dispatch(addData('sports', data))
|
||||
},
|
||||
})
|
||||
)(AdminSportsAdd)
|
121
fittrackee_client/src/components/Admin/Sports/index.jsx
Normal file
121
fittrackee_client/src/components/Admin/Sports/index.jsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
import Message from '../../Common/Message'
|
||||
import { getOrUpdateData } from '../../../actions'
|
||||
import { history } from '../../../index'
|
||||
|
||||
class AdminSports extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadSports()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, sports, t, updateSport } = this.props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - {t('administration:Administration')}</title>
|
||||
</Helmet>
|
||||
{message && <Message message={message} t={t} />}
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col card">
|
||||
<div className="card-body">
|
||||
{sports.length > 0 && (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('administration:id')}</th>
|
||||
<th>{t('administration:Image')}</th>
|
||||
<th>{t('administration:Label')}</th>
|
||||
<th>{t('administration:Active')}</th>
|
||||
<th>{t('administration:Actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sports.map(sport => (
|
||||
<tr key={sport.id}>
|
||||
<th scope="row">{sport.id}</th>
|
||||
<td>
|
||||
<img
|
||||
className="admin-img"
|
||||
src={sport.img ? sport.img : '/img/photo.png'}
|
||||
alt="sport logo"
|
||||
/>
|
||||
</td>
|
||||
<td>{t(`sports:${sport.label}`)}</td>
|
||||
<td>
|
||||
{sport.is_active ? (
|
||||
<i
|
||||
className="fa fa-check-square-o custom-fa"
|
||||
aria-hidden="true"
|
||||
data-toggle="tooltip"
|
||||
/>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-square-o custom-fa"
|
||||
aria-hidden="true"
|
||||
data-toggle="tooltip"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{sport._can_be_disabled ? (
|
||||
<input
|
||||
type="submit"
|
||||
className={`btn btn-${
|
||||
sport.is_active ? 'dark' : 'primary'
|
||||
} btn-sm`}
|
||||
value={
|
||||
sport.is_active
|
||||
? t('administration:Disable')
|
||||
: t('administration:Enable')
|
||||
}
|
||||
onClick={() =>
|
||||
updateSport(sport.id, !sport.is_active)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="admin-message">
|
||||
{t('administration:activities exist')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/admin/')}
|
||||
value={t('administration:Back')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadSports: () => {
|
||||
dispatch(getOrUpdateData('getData', 'sports'))
|
||||
},
|
||||
updateSport: (sportId, isActive) => {
|
||||
const data = { id: sportId, is_active: isActive }
|
||||
dispatch(getOrUpdateData('updateData', 'sports', data, false))
|
||||
},
|
||||
})
|
||||
)(AdminSports)
|
@ -1,141 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { deleteData, getOrUpdateData } from '../../../actions/index'
|
||||
import { history } from '../../../index'
|
||||
|
||||
class AdminDetail extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
isInEdition: false,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, onDataUpdate, onDataDelete, results, target } = this.props
|
||||
const { isInEdition } = this.state
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
{message ? (
|
||||
<code>{message}</code>
|
||||
) : (
|
||||
results.length === 1 && (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col card">
|
||||
<div className="card-body">
|
||||
<form onSubmit={event => event.preventDefault()}>
|
||||
{Object.keys(results[0])
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => (
|
||||
<div className="form-group" key={key}>
|
||||
<label>
|
||||
{key}:
|
||||
{key === 'img' ? (
|
||||
<img
|
||||
src={
|
||||
results[0][key]
|
||||
? results[0][key]
|
||||
: '/img/photo.png'
|
||||
}
|
||||
alt="property"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name={key}
|
||||
readOnly={key === 'id' || !isInEdition}
|
||||
defaultValue={results[0][key]}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{isInEdition ? (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={event => {
|
||||
onDataUpdate(event, target)
|
||||
this.setState({ isInEdition: false })
|
||||
}}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={event => {
|
||||
event.target.form.reset()
|
||||
this.setState({ isInEdition: false })
|
||||
}}
|
||||
value="Cancel"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => this.setState({ isInEdition: true })}
|
||||
value="Edit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-danger btn-lg btn-block"
|
||||
disabled={!results[0]._can_be_deleted}
|
||||
onClick={event => onDataDelete(event, target)}
|
||||
title={
|
||||
results[0]._can_be_deleted
|
||||
? ''
|
||||
: "Can't be deleted, associated data exist"
|
||||
}
|
||||
value="Delete"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push(`/admin/${target}`)}
|
||||
value="Back to the list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
}),
|
||||
dispatch => ({
|
||||
onDataDelete: (e, target) => {
|
||||
const id = e.target.form.id.value
|
||||
dispatch(deleteData(target, id))
|
||||
},
|
||||
onDataUpdate: (e, target) => {
|
||||
const data = [].slice
|
||||
.call(e.target.form.elements)
|
||||
.reduce(function(map, obj) {
|
||||
if (obj.name) {
|
||||
map[obj.name] = obj.value
|
||||
}
|
||||
return map
|
||||
}, {})
|
||||
dispatch(getOrUpdateData('updateData', target, data))
|
||||
},
|
||||
})
|
||||
)(AdminDetail)
|
@ -1,94 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { history } from '../../../index'
|
||||
|
||||
export default function AdminPage(props) {
|
||||
const { data, target } = props
|
||||
const { error } = data
|
||||
const results = data.data
|
||||
const tbKeys = []
|
||||
if (results.length > 0) {
|
||||
Object.keys(results[0])
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => tbKeys.push(key))
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
{error ? (
|
||||
<code>{error}</code>
|
||||
) : (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col card">
|
||||
<div className="card-body">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
{tbKeys.map(tbKey => (
|
||||
<th key={tbKey} scope="col">
|
||||
{tbKey}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<tr key={idx}>
|
||||
{Object.keys(result)
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => {
|
||||
if (key === 'id') {
|
||||
return (
|
||||
<th key={key} scope="row">
|
||||
<Link to={`/admin/${target}/${result[key]}`}>
|
||||
{result[key]}
|
||||
</Link>
|
||||
</th>
|
||||
)
|
||||
} else if (key === 'img') {
|
||||
return (
|
||||
<td key={key}>
|
||||
<img
|
||||
className="admin-img"
|
||||
src={
|
||||
result[key]
|
||||
? result[key]
|
||||
: '/img/photo.png'
|
||||
}
|
||||
alt="logo"
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
return <td key={key}>{result[key]}</td>
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => history.push(`/admin/${target}/add`)}
|
||||
value="Add a new item"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/admin/')}
|
||||
value="Back"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import AdminDashboard from './AdminDashboard'
|
||||
import AdminMenu from './AdminMenu'
|
||||
import AdminSports from './Sports/AdminSports'
|
||||
import AdminSports from './Sports'
|
||||
import AccessDenied from './../Others/AccessDenied'
|
||||
import NotFound from './../Others/NotFound'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Admin(props) {
|
||||
const { user } = props
|
||||
const { t, user } = props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
<title>FitTrackee - {t('administration:Administration')}</title>
|
||||
</Helmet>
|
||||
<div className="container dashboard">
|
||||
<div className="row">
|
||||
@ -27,11 +28,11 @@ function Admin(props) {
|
||||
pathname: '/admin/',
|
||||
}}
|
||||
>
|
||||
Administration
|
||||
{t('administration:Administration')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<AdminMenu />
|
||||
<AdminMenu t={t} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,8 +40,16 @@ function Admin(props) {
|
||||
{isLoggedIn() ? (
|
||||
user.admin ? (
|
||||
<Switch>
|
||||
<Route exact path="/admin" component={AdminDashboard} />
|
||||
<Route path="/admin/sports" component={AdminSports} />
|
||||
<Route
|
||||
exact
|
||||
path="/admin"
|
||||
render={() => <AdminDashboard t={t} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/admin/sports"
|
||||
render={() => <AdminSports t={t} />}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
@ -56,6 +65,8 @@ function Admin(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
user: state.user,
|
||||
}))(Admin)
|
||||
export default withTranslation()(
|
||||
connect(state => ({
|
||||
user: state.user,
|
||||
}))(Admin)
|
||||
)
|
||||
|
@ -170,6 +170,12 @@ label {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.admin-message {
|
||||
color: #7c7c7d;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import XHR from 'i18next-xhr-backend'
|
||||
|
||||
import EnActivitiesTranslations from './locales/en/activities.json'
|
||||
import EnAdministrationTranslations from './locales/en/administration.json'
|
||||
import EnCommonTranslations from './locales/en/common.json'
|
||||
import EnDashboardTranslations from './locales/en/dashboard.json'
|
||||
import EnMessagesTranslations from './locales/en/messages.json'
|
||||
@ -10,6 +11,7 @@ import EnSportsTranslations from './locales/en/sports.json'
|
||||
import EnStatisticsTranslations from './locales/en/statistics.json'
|
||||
import EnUserTranslations from './locales/en/user.json'
|
||||
import FrActivitiesTranslations from './locales/fr/activities.json'
|
||||
import FrAdministrationTranslations from './locales/fr/administration.json'
|
||||
import FrCommonTranslations from './locales/fr/common.json'
|
||||
import FrDashboardTranslations from './locales/fr/dashboard.json'
|
||||
import FrMessagesTranslations from './locales/fr/messages.json'
|
||||
@ -31,6 +33,7 @@ i18n
|
||||
resources: {
|
||||
en: {
|
||||
activities: EnActivitiesTranslations,
|
||||
administration: EnAdministrationTranslations,
|
||||
common: EnCommonTranslations,
|
||||
dashboard: EnDashboardTranslations,
|
||||
messages: EnMessagesTranslations,
|
||||
@ -40,6 +43,7 @@ i18n
|
||||
},
|
||||
fr: {
|
||||
activities: FrActivitiesTranslations,
|
||||
administration: FrAdministrationTranslations,
|
||||
common: FrCommonTranslations,
|
||||
dashboard: FrDashboardTranslations,
|
||||
messages: FrMessagesTranslations,
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"Activity": "Activity",
|
||||
"Activity Date": "Activity Date",
|
||||
"Add a workout": "Add a workout",
|
||||
"Are you sure you want to delete this activity?": "Are you sure you want to delete this activity?",
|
||||
|
15
fittrackee_client/src/locales/en/administration.json
Normal file
15
fittrackee_client/src/locales/en/administration.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"activities exist": "activities exist",
|
||||
"Administration": "Administration",
|
||||
"Application": "Application",
|
||||
"Back": "Back",
|
||||
"Disable": "Disable",
|
||||
"Enable": "Enable",
|
||||
"id": "id",
|
||||
"Image": "Image",
|
||||
"Label": "Label",
|
||||
"Sports": "Sports",
|
||||
"Users": "Users"
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
"records": "records",
|
||||
"Signature expired. Please log in again.": "Signature expired. Please log in again.",
|
||||
"Sorry. That user already exists.": "Sorry. That user already exists.",
|
||||
"Sport can not be disabled, activities exist." : "Sport can not be disabled, activities exist.",
|
||||
"Sport does not exist.": "Sport does not exist.",
|
||||
"sports": "sports",
|
||||
"statistics": "statistiques",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"Activity": "Activité",
|
||||
"Activity Date": "Date de l'activité",
|
||||
"Add a workout": "Ajouter une activité",
|
||||
"Are you sure you want to delete this activity?": "Etes-vous sûr de vouloir supprimer cette activité ?",
|
||||
|
15
fittrackee_client/src/locales/fr/administration.json
Normal file
15
fittrackee_client/src/locales/fr/administration.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"Administration": "Administration",
|
||||
"activities exist": "des activités existent",
|
||||
"Application": "Application",
|
||||
"Back": "Retour",
|
||||
"Disable": "désactiver",
|
||||
"Enable": "activer",
|
||||
"id": "id",
|
||||
"Image": "Image",
|
||||
"Label": "Label",
|
||||
"Sports": "Sports",
|
||||
"Users": "Utilisateurs"
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
"records": "records",
|
||||
"Signature expired. Please log in again.": "Signature expirée. Merci de vous reconnecter.",
|
||||
"Sorry. That user already exists.": "Désolé. Cet utilisateur existe déjà.",
|
||||
"Sport can not be disabled, activities exist." : "Le sport ne peut être désactivé, des activitées existent",
|
||||
"Sport does not exist.": "Le sport n'existe pas.",
|
||||
"sports": "sports",
|
||||
"statistics": "statistics",
|
||||
|
@ -105,8 +105,20 @@ const messages = (state = initial.messages, action) => {
|
||||
const records = (state = initial.records, action) =>
|
||||
handleDataAndError(state, 'records', action)
|
||||
|
||||
const sports = (state = initial.sports, action) =>
|
||||
handleDataAndError(state, 'sports', action)
|
||||
const sports = (state = initial.sports, action) => {
|
||||
if (action.type === 'UPDATE_SPORT_DATA') {
|
||||
return {
|
||||
...state,
|
||||
data: state.data.map(sport => {
|
||||
if (sport.id === action.data.id) {
|
||||
sport.is_active = action.data.is_active
|
||||
}
|
||||
return sport
|
||||
}),
|
||||
}
|
||||
}
|
||||
return handleDataAndError(state, 'sports', action)
|
||||
}
|
||||
|
||||
const user = (state = initial.user, action) => {
|
||||
switch (action.type) {
|
||||
|
Loading…
Reference in New Issue
Block a user