mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
Enhance the dashboard. (#262)
* Update to a more flexible admin authentication * Admin can now access any project * Add delete and edit options in the dashboard * Add a link to the dashboard in the nav bar This is a rework of the changes proposed by @Olivd, so they can apply on top of the latest master without trouble. All credit goes to him for the code.
This commit is contained in:
parent
ee1ecbf3e7
commit
7a918c9349
13 changed files with 193 additions and 58 deletions
|
@ -6,16 +6,25 @@ This document describes changes between each past release.
|
|||
2.0 (unreleased)
|
||||
----------------
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- ``ADMIN_PASSWORD`` is now hashed rather than plain text. The ``ihatemoney generate_password_hash`` command can now be used to generate a proper password HASH (#236)
|
||||
- Turn the WSGI file into a python module, renamed from budget/ihatemoney.wsgi to ihatemoney/wsgi.py. Please update your Apache configuration!
|
||||
- Admin privileges are now required to access the dashboard
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING CHANGE** Use a hashed ``ADMIN_PASSWORD`` instead of a clear text one, ``./budget/manage.py generate_password_hash`` can be used to generate a proper password HASH (#236)
|
||||
- **BREAKING CHANGE** Turn the WSGI file into a python module, renamed from budget/ihatemoney.wsgi to budget/wsgi.py. Please update your Apache configuration!
|
||||
- Changed the recommended gunicorn configuration to use the wsgi module as an entrypoint
|
||||
|
||||
### Added
|
||||
|
||||
- Add a statistics tab (#257)
|
||||
- Add python3.6 support (#259)
|
||||
- Public project creation can now be deactivated using the ALLOW_PUBLIC_PROJECT_CREATION setting.
|
||||
- If admin credentials are defined, they can be used to access any project.
|
||||
- It is now possible to edit and delete projects directly from the dashboard.
|
||||
- The dashboard can now be deactivated using the ACTIVATE_ADMIN_DASHBOARD setting.
|
||||
- When activated, a link to the dashboard appears in the navigation bar.
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
@ -92,27 +92,35 @@ properly.
|
|||
|
||||
.. warning:: You **must** customize the ``SECRET_KEY`` on a production installation.
|
||||
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| Setting name | Default | What does it do? |
|
||||
+============================+===========================+========================================================================================+
|
||||
| SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information |
|
||||
| | | on the format used can be found on `the SQLAlchemy documentation |
|
||||
| | | <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_. |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email adress to use when sending |
|
||||
| | "budget@notmyidea.org")`` | emails. |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| | ``""`` | If not empty, the specified password must be entered to create new projects. |
|
||||
| ADMIN_PASSWORD | | To generate the proper password HASH, use ``ihatemoney generate_password_hash`` |
|
||||
| | | and copy its output into the value of *ADMIN_PASSWORD*. |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| APPLICATION_ROOT | ``""`` | If empty, ihatemoney will be served at domain root (e.g: *http://domain.tld*), if set |
|
||||
| | | to ``"foo"``, it will be served from a "folder" (e.g: *http://domain.tld/foo*) |
|
||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| Setting name | Default | What does it do? |
|
||||
+===============================+===========================+========================================================================================+
|
||||
| SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information |
|
||||
| | | on the format used can be found on `the SQLAlchemy documentation |
|
||||
| | | <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_. |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email adress to use when sending |
|
||||
| | "budget@notmyidea.org")`` | emails. |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| | | Hashed password to access protected endpoints. If left empty, all administrative |
|
||||
| ADMIN_PASSWORD | ``""`` | tasks are disabled. |
|
||||
| | | To generate the proper password HASH, use ``ihatemoney generate_password_hash`` |
|
||||
| | | and copy the output into the value of *ADMIN_PASSWORD*. |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| ALLOW_PUBLIC_PROJECT_CREATION | ``True`` | If set to `True`, everyone can create a project without entering the admin password |
|
||||
| | | If set to `False`, the password needs to be entered (and as such, defined in the |
|
||||
| | | settings). |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| ACTIVATE_ADMIN_DASHBOARD | ``False`` | If set to `True`, the dashboard will become accessible entering the admin password |
|
||||
| | | If set to `True`, a non empty ADMIN_PASSWORD needs to be set |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
| APPLICATION_ROOT | ``""`` | If empty, ihatemoney will be served at domain root (e.g: *http://domain.tld*), if set |
|
||||
| | | to ``"foo"``, it will be served from a "folder" (e.g: *http://domain.tld/foo*) |
|
||||
+-------------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||
|
||||
In a production environment
|
||||
---------------------------
|
||||
|
|
|
@ -29,3 +29,9 @@ ACTIVATE_DEMO_PROJECT = True
|
|||
# DO NOT enter the password in cleartext. Generate a password hash with
|
||||
# "ihatemoney generate_password_hash" instead.
|
||||
ADMIN_PASSWORD = ""
|
||||
|
||||
# If set to True (default value) anyone can create a new project.
|
||||
ALLOW_PUBLIC_PROJECT_CREATION = True
|
||||
|
||||
# If set to True, an administration dashboard is available.
|
||||
ACTIVATE_ADMIN_DASHBOARD = False
|
||||
|
|
|
@ -169,6 +169,30 @@ footer{
|
|||
background: url('../images/edit.png') no-repeat right;
|
||||
}
|
||||
|
||||
project-actions {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.project-actions > .delete, .project-actions > .edit {
|
||||
font-size: 0px;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 2px;
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.project-actions > .delete{
|
||||
background: url('../images/delete.png') no-repeat right;
|
||||
}
|
||||
|
||||
.project-actions > .edit{
|
||||
background: url('../images/edit.png') no-repeat right;
|
||||
}
|
||||
|
||||
|
||||
.balance .balance-value{
|
||||
text-align:right;
|
||||
}
|
||||
|
|
12
ihatemoney/templates/admin.html
Normal file
12
ihatemoney/templates/admin.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<h2>Authentication</h2>
|
||||
|
||||
{% if is_admin_auth_enabled %}
|
||||
<form class="form-horizontal" method="POST" accept-charset="utf-8">
|
||||
{{ forms.admin(form) }}
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">{{ _("Administration tasks are currently disabled.") }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -7,13 +7,7 @@
|
|||
to") }} <a href="{{ url_for(".create_project", project_id=create_project) }}">{{ _("create it") }}</a>{{ _("?") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if admin_auth %}
|
||||
<form class="form-horizontal" method="POST" accept-charset="utf-8">
|
||||
{{ forms.admin(form) }}
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="form-horizontal" method="POST" accept-charset="utf-8">
|
||||
{{ forms.authenticate(form) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if is_admin_dashboard_activated %}
|
||||
<table id="bill_table" class="table table-striped">
|
||||
<thead><tr><th>{{ _("Project") }}</th><th>{{ _("Number of members") }}</th><th>{{ _("Number of bills") }}</th><th>{{_("Newest bill")}}</th><th>{{_("Oldest bill")}}</th></tr></thead>
|
||||
<thead><tr><th>{{ _("Project") }}</th><th>{{ _("Number of members") }}</th><th>{{ _("Number of bills") }}</th><th>{{_("Newest bill")}}</th><th>{{_("Oldest bill")}}</th><th>{{_("Actions")}}</th></tr></thead>
|
||||
<tbody>{% for project in projects|sort(attribute='name') %}
|
||||
<tr class="{{ loop.cycle("odd", "even") }}">
|
||||
<td>{{ project.name }}</td><td>{{ project.members | count }}</td><td>{{ project.get_bills().count() }}</td>
|
||||
|
@ -13,9 +13,15 @@
|
|||
<td></td>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td class="project-actions">
|
||||
<a class="edit" href="{{ url_for(".edit_project", project_id=project.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||
<a class="delete" href="{{ url_for(".delete_project", project_id=project.id) }}" title="{{ _("delete") }}">{{ _('delete') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">{{ _("The Dashboard is currently deactivated.") }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -28,9 +28,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-5 col-md-3 offset-sm-1">
|
||||
{% if is_admin_mode_enabled %}
|
||||
<a href="{{ url_for(".create_project") }}">...{{ _("or create a new one") }}</a>
|
||||
{% else %}
|
||||
{% if is_public_project_creation_allowed %}
|
||||
<form id="creation-form" class="form-horizontal" action="{{ url_for(".create_project") }}" method="post">
|
||||
<fieldset class="form-group">
|
||||
<legend>...{{ _("or create a new one") }}</legend>
|
||||
|
@ -40,6 +38,8 @@
|
|||
<button class="btn" type="submit">{{ _("let's get started") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for(".create_project") }}">...{{ _("or create a new one") }}</a>
|
||||
{% endif %}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -64,6 +64,9 @@
|
|||
{% endif %}
|
||||
<li class="nav-item{% if g.lang == "fr" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="fr") }}">fr</a></li>
|
||||
<li class="nav-item{% if g.lang == "en" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="en") }}">en</a></li>
|
||||
{% if g.show_admin_dashboard_link %}
|
||||
<li class="nav-item{% if request.url_rule.endpoint == "main.dashboard" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".dashboard") }}">{{ _("Dashboard") }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -379,8 +379,17 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
c.get("/exit")
|
||||
self.assertNotIn('raclette', session)
|
||||
|
||||
# test that whith admin credentials, one can access every project
|
||||
self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
|
||||
with self.app.test_client() as c:
|
||||
resp = c.post("/admin?goto=%2Fraclette", data={'admin_password': 'pass'})
|
||||
self.assertNotIn("Authentication", resp.data.decode('utf-8'))
|
||||
self.assertTrue(session['is_admin'])
|
||||
|
||||
def test_admin_authentication(self):
|
||||
self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
|
||||
# Disable public project creation so we have an admin endpoint to test
|
||||
self.app.config['ALLOW_PUBLIC_PROJECT_CREATION'] = False
|
||||
|
||||
# test the redirection to the authentication page when trying to access admin endpoints
|
||||
resp = self.client.get("/create")
|
||||
|
@ -401,7 +410,8 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
def test_login_throttler(self):
|
||||
self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
|
||||
|
||||
# Authenticate 3 times with a wrong passsword
|
||||
# Activate admin login throttling by authenticating 4 times with a wrong passsword
|
||||
self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
|
||||
self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
|
||||
self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
|
||||
resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
|
||||
|
@ -624,8 +634,23 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
self.assertIn("Invalid email address", resp.data.decode('utf-8'))
|
||||
|
||||
def test_dashboard(self):
|
||||
response = self.client.get("/dashboard")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# test that the dashboard is deactivated by default
|
||||
resp = self.client.post(
|
||||
"/admin?goto=%2Fdashboard",
|
||||
data={'admin_password': 'adminpass'},
|
||||
follow_redirects=True
|
||||
)
|
||||
self.assertIn('<div class="alert alert-danger">', resp.data.decode('utf-8'))
|
||||
|
||||
# test access to the dashboard when it is activated
|
||||
self.app.config['ACTIVATE_ADMIN_DASHBOARD'] = True
|
||||
self.app.config['ADMIN_PASSWORD'] = generate_password_hash("adminpass")
|
||||
resp = self.client.post(
|
||||
"/admin?goto=%2Fdashboard",
|
||||
data={'admin_password': 'adminpass'},
|
||||
follow_redirects=True
|
||||
)
|
||||
self.assertIn('<thead><tr><th>Project</th><th>Number of members', resp.data.decode('utf-8'))
|
||||
|
||||
def test_statistics_page(self):
|
||||
self.post_project("raclette")
|
||||
|
|
Binary file not shown.
|
@ -251,6 +251,10 @@ msgstr "le créer"
|
|||
msgid "?"
|
||||
msgstr " ?"
|
||||
|
||||
#: templates/authenticate.html:7
|
||||
msgid "Administration tasks are currently disabled."
|
||||
msgstr "Les tâches d'administration sont actuellement désactivées."
|
||||
|
||||
#: templates/create_project.html:4
|
||||
msgid "Create a new project"
|
||||
msgstr "Créer un nouveau projet"
|
||||
|
@ -275,6 +279,10 @@ msgstr "Facture la plus récente"
|
|||
msgid "Oldest bill"
|
||||
msgstr "Facture la plus ancienne"
|
||||
|
||||
#: templates/dashboard.html:25
|
||||
msgid "The Dashboard is currently deactivated."
|
||||
msgstr "La page d'administration est actuellement désactivée."
|
||||
|
||||
#: templates/edit_project.html:6 templates/list_bills.html:24
|
||||
msgid "you sure?"
|
||||
msgstr "c'est sûr ?"
|
||||
|
|
|
@ -34,17 +34,30 @@ main = Blueprint("main", __name__)
|
|||
login_throttler = LoginThrottler(max_attempts=3, delay=1)
|
||||
|
||||
|
||||
def requires_admin(f):
|
||||
def requires_admin(bypass=None):
|
||||
"""Require admin permissions for @requires_admin decorated endpoints.
|
||||
Has no effect if ADMIN_PASSWORD is empty (default value)
|
||||
|
||||
This has no effect if ADMIN_PASSWORD is empty.
|
||||
|
||||
:param bypass: Used to conditionnaly bypass the admin authentication.
|
||||
It expects a tuple containing the name of an application
|
||||
setting and its expected value.
|
||||
e.g. if you use @require_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
|
||||
Admin authentication will be bypassed when ALLOW_PUBLIC_PROJECT_CREATION is
|
||||
set to True.
|
||||
"""
|
||||
@wraps(f)
|
||||
def admin_auth(*args, **kws):
|
||||
is_admin = session.get('is_admin')
|
||||
if is_admin or not current_app.config['ADMIN_PASSWORD']:
|
||||
return f(*args, **kws)
|
||||
raise Redirect303(url_for('.admin', goto=request.path))
|
||||
return admin_auth
|
||||
def check_admin(f):
|
||||
@wraps(f)
|
||||
def admin_auth(*args, **kws):
|
||||
is_admin_auth_bypassed = False
|
||||
if bypass is not None and current_app.config.get(bypass[0]) == bypass[1]:
|
||||
is_admin_auth_bypassed = True
|
||||
is_admin = session.get('is_admin')
|
||||
if is_admin or is_admin_auth_bypassed:
|
||||
return f(*args, **kws)
|
||||
raise Redirect303(url_for('.admin', goto=request.path))
|
||||
return admin_auth
|
||||
return check_admin
|
||||
|
||||
|
||||
@main.url_defaults
|
||||
|
@ -59,10 +72,24 @@ def add_project_id(endpoint, values):
|
|||
values['project_id'] = g.project.id
|
||||
|
||||
|
||||
@main.url_value_preprocessor
|
||||
def set_show_admin_dashboard_link(endpoint, values):
|
||||
"""Sets the "show_admin_dashboard_link" variable application wide
|
||||
in order to use it in the layout template.
|
||||
"""
|
||||
|
||||
g.show_admin_dashboard_link = (
|
||||
current_app.config["ACTIVATE_ADMIN_DASHBOARD"]
|
||||
and current_app.config["ADMIN_PASSWORD"]
|
||||
)
|
||||
|
||||
|
||||
@main.url_value_preprocessor
|
||||
def pull_project(endpoint, values):
|
||||
"""When a request contains a project_id value, transform it directly
|
||||
into a project by checking the credentials are stored in session.
|
||||
into a project by checking the credentials stored in the session.
|
||||
|
||||
With administration credentials, one can access any project.
|
||||
|
||||
If not, redirect the user to an authentication form
|
||||
"""
|
||||
|
@ -76,7 +103,9 @@ def pull_project(endpoint, values):
|
|||
if not project:
|
||||
raise Redirect303(url_for(".create_project",
|
||||
project_id=project_id))
|
||||
if project.id in session and session[project.id] == project.password:
|
||||
|
||||
is_admin = session.get('is_admin')
|
||||
if (project.id in session and session[project.id] == project.password) or is_admin:
|
||||
# add project into kwargs and call the original function
|
||||
g.project = project
|
||||
else:
|
||||
|
@ -87,15 +116,20 @@ def pull_project(endpoint, values):
|
|||
|
||||
@main.route("/admin", methods=["GET", "POST"])
|
||||
def admin():
|
||||
"""Admin authentication"""
|
||||
"""Admin authentication.
|
||||
|
||||
When ADMIN_PASSWORD is empty, admin authentication is deactivated.
|
||||
"""
|
||||
form = AdminAuthenticationForm()
|
||||
goto = request.args.get('goto', url_for('.home'))
|
||||
is_admin_auth_enabled = bool(current_app.config['ADMIN_PASSWORD'])
|
||||
if request.method == "POST":
|
||||
client_ip = request.remote_addr
|
||||
if not login_throttler.is_login_allowed(client_ip):
|
||||
msg = _("Too many failed login attempts, please retry later.")
|
||||
form.errors['admin_password'] = [msg]
|
||||
return render_template("authenticate.html", form=form, admin_auth=True)
|
||||
return render_template("admin.html", form=form, admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled)
|
||||
if form.validate():
|
||||
# Valid password
|
||||
if (check_password_hash(current_app.config['ADMIN_PASSWORD'],
|
||||
|
@ -109,7 +143,8 @@ def admin():
|
|||
msg = _("This admin password is not the right one. Only %(num)d attempts left.",
|
||||
num=login_throttler.get_remaining_attempts(client_ip))
|
||||
form.errors['admin_password'] = [msg]
|
||||
return render_template("authenticate.html", form=form, admin_auth=True)
|
||||
return render_template("admin.html", form=form, admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled)
|
||||
|
||||
|
||||
@main.route("/authenticate", methods=["GET", "POST"])
|
||||
|
@ -167,18 +202,17 @@ def authenticate(project_id=None):
|
|||
def home():
|
||||
project_form = ProjectForm()
|
||||
auth_form = AuthenticationForm()
|
||||
# If ADMIN_PASSWORD is empty we consider that admin mode is disabled
|
||||
is_admin_mode_enabled = bool(current_app.config['ADMIN_PASSWORD'])
|
||||
is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
|
||||
is_public_project_creation_allowed = current_app.config['ALLOW_PUBLIC_PROJECT_CREATION']
|
||||
|
||||
return render_template("home.html", project_form=project_form,
|
||||
is_demo_project_activated=is_demo_project_activated,
|
||||
is_admin_mode_enabled=is_admin_mode_enabled,
|
||||
is_public_project_creation_allowed=is_public_project_creation_allowed,
|
||||
auth_form=auth_form, session=session)
|
||||
|
||||
|
||||
@main.route("/create", methods=["GET", "POST"])
|
||||
@requires_admin
|
||||
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
|
||||
def create_project():
|
||||
form = ProjectForm()
|
||||
if request.method == "GET" and 'project_id' in request.values:
|
||||
|
@ -295,7 +329,7 @@ def delete_project():
|
|||
g.project.remove_project()
|
||||
flash(_('Project successfully deleted'))
|
||||
|
||||
return redirect(url_for(".home"))
|
||||
return redirect(request.headers.get('Referer') or url_for('.home'))
|
||||
|
||||
|
||||
@main.route("/exit")
|
||||
|
@ -530,5 +564,11 @@ def statistics():
|
|||
|
||||
|
||||
@main.route("/dashboard")
|
||||
@requires_admin()
|
||||
def dashboard():
|
||||
return render_template("dashboard.html", projects=Project.query.all())
|
||||
is_admin_dashboard_activated = current_app.config['ACTIVATE_ADMIN_DASHBOARD']
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
projects=Project.query.all(),
|
||||
is_admin_dashboard_activated=is_admin_dashboard_activated
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue