Repository: spiral-project/ihatemoney

Issue:
Deleting a project in admin-dashboard
I’m still experiencing the same issue as #978. I cannot delete any project from the Admin dashboard.
I’m using docker which works perfectly otherwise.
Problem occurs with SESSION_COOKIE_SECURE set to True or False. No difference.

Comments:

Patch:
diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css
index 1700fec..713e706 100644
--- a/ihatemoney/static/css/main.css
+++ b/ihatemoney/static/css/main.css
@@ -14,18 +14,22 @@ body {
 .navbar-brand span {
   color: #abe128;
 }
+
 .navbar h1 {
   font-size: 1rem;
   margin: 0;
   padding: 0;
 }
+
 .navbar .secondary-nav {
   text-align: right;
   flex-direction: row-reverse;
 }
+
 .secondary-nav .nav-link {
   padding: 0.5rem 1rem;
 }
+
 .navbar-brand {
   font-family: "Lobster", arial, serif;
   font-size: 1.5rem;
@@ -51,7 +55,8 @@ body {
   font-size: 2.4em;
 }
 
-#header .tryout, #header .showcase {
+#header .tryout,
+#header .showcase {
   color: #fff;
   background-color: #414141;
   border-color: #414141;
@@ -59,7 +64,8 @@ body {
   margin-right: 3px;
 }
 
-#header .tryout:hover, #header .showcase:hover {
+#header .tryout:hover,
+#header .showcase:hover {
   background-color: #606060;
   border-color: #606060;
   cursor: pointer;
@@ -91,9 +97,11 @@ body {
 .balance tr td {
   font-weight: bold;
 }
+
 .positive {
   color: green;
 }
+
 .negative {
   color: red;
 }
@@ -113,6 +121,7 @@ body {
   flex: 1 1 auto;
   overflow-y: auto;
 }
+
 .identifier {
   margin-bottom: 15px;
 }
@@ -143,10 +152,12 @@ body {
 .home-container {
   background: linear-gradient(150deg, #abe128 0%, #43ca61 100%);
 }
+
 .home {
   padding-top: 1em;
   padding-bottom: 3em;
 }
+
 #authentication-form legend {
   text-align: right;
 }
@@ -160,11 +171,13 @@ body {
   margin-bottom: 20px;
   margin-left: 25px;
 }
+
 @media (max-width: 450px) {
   .home .card {
     min-width: unset;
   }
 }
+
 /* Other */
 
 #bills {
@@ -174,6 +187,7 @@ body {
 .empty-bill {
   margin-top: 5rem !important;
 }
+
 .empty-bill .card {
   border: 0;
 }
@@ -250,12 +264,15 @@ footer .footer-right a {
   border-radius: 50%;
   opacity: 0.5;
 }
+
 @-moz-document url-prefix() {
+
   /** Firefox style fix **/
   footer .footer-right a {
     padding-top: 2px;
   }
 }
+
 footer .footer-right a:hover {
   opacity: 1;
 }
@@ -268,6 +285,7 @@ footer .footer-left {
 /* If you don't want the footer to be responsive, remove these media queries */
 
 @media (max-width: 600px) {
+
   footer .footer-left,
   footer .footer-right {
     text-align: center;
@@ -298,9 +316,9 @@ footer .footer-left {
   position: relative;
 }
 
-.bill-actions > form > .delete,
-.bill-actions > .edit,
-.bill-actions > .show {
+.bill-actions>form>.delete,
+.bill-actions>.edit,
+.bill-actions>.show {
   font-size: 0px;
   display: block;
   width: 16px;
@@ -316,15 +334,15 @@ footer .footer-left {
   height: calc(100% - 8px);
 }
 
-.bill-actions > form > .delete {
+.bill-actions>form>.delete {
   background: url("../images/delete.png") no-repeat right;
 }
 
-.bill-actions > .edit {
+.bill-actions>.edit {
   background: url("../images/edit.png") no-repeat right;
 }
 
-.bill-actions > .show {
+.bill-actions>.show {
   background: url("../images/show.png") no-repeat right;
 }
 
@@ -344,6 +362,7 @@ footer .footer-left {
 }
 
 @media (min-width: 768px) {
+
   .split_bills,
   #table_overflow.statistics {
     /* The table is shifted to left, so add the spacer width on the right to match */
@@ -351,9 +370,9 @@ footer .footer-left {
   }
 }
 
-.project-actions > .delete,
-.project-actions > .edit,
-.project-actions > .show {
+.project-actions>form>.delete,
+.project-actions>.edit,
+.project-actions>.show {
   font-size: 0px;
   display: block;
   width: 16px;
@@ -363,21 +382,22 @@ footer .footer-left {
   float: left;
 }
 
-.project-actions > .delete {
+.project-actions>form>.delete {
   background: url("../images/delete.png") no-repeat right;
+  border: 0px !important;
 }
 
-.project-actions > .edit {
+.project-actions>.edit {
   background: url("../images/edit.png") no-repeat right;
 }
 
-.project-actions > .show {
+.project-actions>.show {
   background: url("../images/show.png") no-repeat right;
 }
 
-.history_icon > .delete,
-.history_icon > .add,
-.history_icon > .edit {
+.history_icon>.delete,
+.history_icon>.add,
+.history_icon>.edit {
   font-size: 0px;
   display: block;
   width: 16px;
@@ -388,15 +408,15 @@ footer .footer-left {
   float: left;
 }
 
-.history_icon > .delete {
+.history_icon>.delete {
   background: url("../images/delete.png") no-repeat right;
 }
 
-.history_icon > .edit {
+.history_icon>.edit {
   background: url("../images/edit.png") no-repeat right;
 }
 
-.history_icon > .add {
+.history_icon>.add {
   background: url("../images/add.png") no-repeat right;
 }
 
@@ -461,7 +481,7 @@ tr.payer_line .balance-name {
   margin-top: 30px;
 }
 
-#bill-form > fieldset {
+#bill-form>fieldset {
   margin-top: 10px;
 }
 
@@ -485,64 +505,81 @@ tr:hover .extra-info {
 
 /* Fluid Offsets for Boostrap */
 
-.row-fluid > [class*="span"]:not([class*="offset"]):first-child {
+.row-fluid>[class*="span"]:not([class*="offset"]):first-child {
   margin-left: 0;
 }
-.row-fluid > .offset12 {
+
+.row-fluid>.offset12 {
   margin-left: 100%;
 }
-.row-fluid > .offset11 {
+
+.row-fluid>.offset11 {
   margin-left: 93.5%;
 }
-.row-fluid > .offset10 {
+
+.row-fluid>.offset10 {
   margin-left: 85%;
 }
-.row-fluid > .offset9 {
+
+.row-fluid>.offset9 {
   margin-left: 76.5%;
 }
-.row-fluid > .offset8 {
+
+.row-fluid>.offset8 {
   margin-left: 68%;
 }
-.row-fluid > .offset7 {
+
+.row-fluid>.offset7 {
   margin-left: 59.5%;
 }
-.row-fluid > .offset6 {
+
+.row-fluid>.offset6 {
   margin-left: 51%;
 }
-.row-fluid > .offset5 {
+
+.row-fluid>.offset5 {
   margin-left: 42.5%;
 }
-.row-fluid > .offset4 {
+
+.row-fluid>.offset4 {
   margin-left: 34%;
 }
-.row-fluid > .offset3 {
+
+.row-fluid>.offset3 {
   margin-left: 25.5%;
 }
-.row-fluid > .offset2 {
+
+.row-fluid>.offset2 {
   margin-left: 17%;
 }
-.row-fluid > .offset1 {
+
+.row-fluid>.offset1 {
   margin-left: 8.5%;
 }
 
 .globe-europe svg {
   fill: rgba(255, 255, 255, 0.5);
 }
+
 .navbar-nav .dropdown-toggle:hover .globe-europe svg {
   fill: rgba(255, 255, 255, 0.75);
 }
-.navbar-dark .navbar-nav .show > .nav-link svg {
+
+.navbar-dark .navbar-nav .show>.nav-link svg {
   fill: white;
 }
+
 .navbar-dark .dropdown-menu {
   max-height: 50vh;
   overflow-y: auto;
 }
+
 .icon svg {
   display: inline-block;
   border-bottom: 0.2em solid transparent;
   height: 1.2em;
-  width: 1.2em; /* protection for IE11 */
+  width: 1.2em;
+  /* protection for IE11 */
 }
 
 .icon.high svg {
@@ -558,9 +595,11 @@ tr:hover .extra-info {
 .icon.before-text svg {
   margin-right: 3px;
 }
+
 footer .icon svg {
   fill: white;
 }
+
 .icon.icon-white {
   fill: white;
 }
@@ -568,7 +607,8 @@ footer .icon svg {
 .icon.icon-red {
   fill: #dc3545;
 }
-.btn:hover .icon.icon-red  {
+
+.btn:hover .icon.icon-red {
   fill: white !important;
 }
 
@@ -592,6 +632,7 @@ footer .icon svg {
   margin-top: 1em;
   margin-bottom: 3em;
 }
+
 .edit-project .custom-file {
   margin-bottom: 2em;
-}
+}
\ No newline at end of file
diff --git a/ihatemoney/templates/dashboard.html b/ihatemoney/templates/dashboard.html
index 6036de9..15baf96 100644
--- a/ihatemoney/templates/dashboard.html
+++ b/ihatemoney/templates/dashboard.html
@@ -2,34 +2,53 @@
 {% block content %}
 {% if is_admin_dashboard_activated %}
 <table id="bill_table" class="table table-striped">
-    <thead><tr><th>{{ _("Project") }}</th><th>{{ _("Number of participants") }}</th><th>{{ _("Number of bills") }}</th><th>{{_("Newest bill")}}</th><th>{{_("Oldest bill")}}</th><th>{{_("Actions")}}</th></tr></thead>
+    <thead>
+        <tr>
+            <th>{{ _("Project") }}</th>
+            <th>{{ _("Number of participants") }}</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>
-        <td><a href="{{ url_for(".list_bills", project_id=project.id) }}" title="{{ project.name }}">{{ project.name }}</a></td><td>{{ project.members | count }}</td><td>{{ project.get_bills_unordered().count() }}</td>
-        {% if project.has_bills() %}
-        <td>{{ project.get_bills().all()[0].date }}</td>
-        <td>{{ project.get_bills().all()[-1].date }}</td>
-        {% else %}
-        <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>
-            <a class="show" href="{{ url_for(".list_bills", project_id=project.id) }}" title="{{ _("show") }}">{{ _('show') }}</a>
-        </td>
+            <td><a href="{{ url_for('.list_bills', project_id=project.id) }}"
+                    title="{{ project.name }}">{{project.name}}</a></td>
+            <td>{{ project.members | count }}</td>
+            <td>{{ project.get_bills_unordered().count() }}</td>
+            {% if project.has_bills() %}
+            <td>{{ project.get_bills().all()[0].date }}</td>
+            <td>{{ project.get_bills().all()[-1].date }}</td>
+            {% else %}
+            <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>
+
+                <form id="delete-project" class="form-horizontal" action="{{ url_for('.dashboard_delete_project',
+                    project_id=project.id) }}" method="post">
+
+                    <button class="delete">{{ _("Delete project") }}</button>
+                </form>
+                <a class="show" href="{{ url_for('.list_bills', project_id=project.id) }}" title="{{ _(" show") }}">{{
+                    _('show') }}</a>
+            </td>
         </tr>
-    {% endfor %}
+        {% endfor %}
     </tbody>
 </table>
 <script language="JavaScript">
-$(document).ready(function() {
-    $('#bill_table').DataTable({
-        paging: true
-    });
-})
+    $(document).ready(function () {
+        $('#bill_table').DataTable({
+            paging: true
+        });
+    })
 </script>
 {% else %}
 <div class="alert alert-danger">{{ _("The Dashboard is currently deactivated.") }}</div>
 {% endif %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index b94a66d..9417ace 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -888,10 +888,19 @@ def dashboard():
     return render_template(
         "dashboard.html",
         projects=Project.query.all(),
+        delete_project_form=DestructiveActionProjectForm,
         is_admin_dashboard_activated=is_admin_dashboard_activated,
     )
 
 
+@main.route("/dashboard/<project_id>/delete", methods=["POST"])
+@requires_admin()
+def dashboard_delete_project():
+    g.project.remove_project()
+    flash(_("Project successfully deleted"))
+    return redirect(request.headers.get("Referer") or url_for(".home"))
+
+
 @main.route("/favicon.ico")
 def favicon():
     return send_from_directory(


Test patch:
diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py
index ee10319..a4dcf9e 100644
--- a/ihatemoney/tests/budget_test.py
+++ b/ihatemoney/tests/budget_test.py
@@ -926,18 +926,30 @@ class BudgetTestCase(IhatemoneyTestCase):
         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.enable_admin()
+        resp = self.client.get("/dashboard")
         self.assertIn(
-            "<thead><tr><th>Project</th><th>Number of participants",
+            """<thead>
+        <tr>
+            <th>Project</th>
+            <th>Number of participants</th>""",
             resp.data.decode("utf-8"),
         )
 
+    def test_dashboard_project_deletion(self):
+        self.post_project("raclette")
+        self.enable_admin()
+        resp = self.client.get("/dashboard")
+        pattern = re.compile(r"<form id=\"delete-project\" [^>]*?action=\"(.*?)\"")
+        match = pattern.search(resp.data.decode("utf-8"))
+        assert match is not None
+        assert match.group(1) is not None
+
+        resp = self.client.post(match.group(1))
+
+        # project removed
+        assert len(models.Project.query.all()) == 0
+
     def test_statistics_page(self):
         self.post_project("raclette")
         response = self.client.get("/raclette/statistics")
diff --git a/ihatemoney/tests/common/ihatemoney_testcase.py b/ihatemoney/tests/common/ihatemoney_testcase.py
index 4b11d47..7047537 100644
--- a/ihatemoney/tests/common/ihatemoney_testcase.py
+++ b/ihatemoney/tests/common/ihatemoney_testcase.py
@@ -110,3 +110,12 @@ class IhatemoneyTestCase(BaseTestCase):
             resp.status_code,
             f"{url} expected {expected}, got {resp.status_code}",
         )
+
+    def enable_admin(self, password="adminpass"):
+        self.app.config["ACTIVATE_ADMIN_DASHBOARD"] = True
+        self.app.config["ADMIN_PASSWORD"] = generate_password_hash(password)
+        return self.client.post(
+            "/admin?goto=%2Fdashboard",
+            data={"admin_password": password},
+            follow_redirects=True,
+        )