Ability to clone a map and overall UI improvements

This commit is contained in:
David Larlet 2024-02-06 14:23:04 -05:00
parent 5d69d3c22f
commit 8a6e992b9c
No known key found for this signature in database
GPG key ID: 3E2953A359E7E7BD
6 changed files with 144 additions and 34 deletions

View file

@ -144,16 +144,19 @@ body.login header {
} }
h2.section { h2.section {
text-transform: uppercase; text-transform: uppercase;
color: #666; color: #263B58;
text-align: center;
padding-top: 28px; padding-top: 28px;
} }
h2.tabs a { h2.tabs a {
color: #666; color: #263B58;
text-decoration: underline;
text-decoration-thickness: 3px;
margin-right: 2rem;
} }
h2.tabs a:not(.selected) { h2.tabs a:not(.selected) {
font-weight: normal; font-weight: normal;
color: #666; color: #263B58;
text-decoration: none;
} }
h2.tabs a:hover { h2.tabs a:hover {
text-decoration: underline; text-decoration: underline;
@ -310,14 +313,81 @@ ul.umap-autocomplete {
/* **************************** */ /* **************************** */
/* Dashboard */ /* Dashboard */
/* **************************** */ /* **************************** */
.table-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 1rem;
}
.table-header form {
display: flex;
align-items: flex-end;
}
.table-header form input {
border: 2px solid #263B58;
border-radius: 0;
padding: .5rem 1rem;
margin-bottom: 0;
line-height: inherit;
height: 2.5rem;
}
.table-header form input[type="search"] {
width: 30ch;
}
.table-header form input[type="submit"] {
background-color: #263B58;
color: white;
font-weight: bold;
}
.table-header .button-download {
width: inherit;
display: inline;
padding: .5rem 1rem;
border: 2px solid #263B58;
color: #263B58;
font-weight: bold;
background-color: initial;
margin-bottom: 0;
line-height: inherit;
height: 2.5rem;
}
table.maps { table.maps {
width: 100%; width: 100%;
border-collapse: collapse;
} }
table.maps .map_fragment { table.maps .map_fragment {
display: block; display: block;
height: 80vh; height: 80vh;
width: 100%; width: 100%;
} }
table.maps a,
table.maps thead {
color: #263B58;
}
table.maps a {
text-decoration: underline;
}
table.maps form {
display: inline;
}
table.maps input[type="submit"] {
display: inline;
background-color: transparent;
color: #263B58;
padding: 0;
width: inherit;
height: 1rem;
margin: 0;
line-height: inherit;
}
table.maps tbody tr {
border-bottom: 1px solid #BDC7D4;
}
table.maps tbody tr td {
padding: 5px 4px;
}
table.maps tbody tr:nth-child(odd) { table.maps tbody tr:nth-child(odd) {
background-color: #f4f4f4; background-color: #f4f4f4;
} }
@ -357,12 +427,14 @@ dialog::backdrop {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
margin: 1rem;
border-top: 1px solid gray;
} }
.pagination > * { .pagination > * {
padding: 1rem; padding: 1rem;
} }
.pagination a {
color: #263B58;
text-decoration: underline;
}
/* ************************************************* */ /* ************************************************* */

View file

@ -1,9 +1,10 @@
{% extends "umap/content.html" %} {% extends "umap/content.html" %}
{% load i18n %} {% load i18n %}
{% block maincontent %} {% block maincontent %}
<div class="col wide"> <div class="row">
<h2 class="section tabs"> <h2 class="section tabs">
<a href="{% url "user_dashboard" %}">{% trans "My Maps" %}</a> | <a class="selected" href="{% url 'user_profile' %}">{% trans "My Profile" %}</a> <a href="{% url "user_dashboard" %}">{% trans "My Maps" %}</a>
<a class="selected" href="{% url 'user_profile' %}">{% trans "My Profile" %}</a>
</h2> </h2>
</div> </div>
<div class="wrapper"> <div class="wrapper">

View file

@ -4,7 +4,8 @@
<tr> <tr>
<th>{% blocktrans %}Name{% endblocktrans %}</th> <th>{% blocktrans %}Name{% endblocktrans %}</th>
<th>{% blocktrans %}Preview{% endblocktrans %}</th> <th>{% blocktrans %}Preview{% endblocktrans %}</th>
<th>{% blocktrans %}Who can see / edit{% endblocktrans %}</th> <th>{% blocktrans %}Who can see{% endblocktrans %}</th>
<th>{% blocktrans %}Who can edit{% endblocktrans %}</th>
<th>{% blocktrans %}Last save{% endblocktrans %}</th> <th>{% blocktrans %}Last save{% endblocktrans %}</th>
<th>{% blocktrans %}Owner{% endblocktrans %}</th> <th>{% blocktrans %}Owner{% endblocktrans %}</th>
<th>{% blocktrans %}Actions{% endblocktrans %}</th> <th>{% blocktrans %}Actions{% endblocktrans %}</th>
@ -13,12 +14,12 @@
<tbody> <tbody>
{% for map_inst in maps %} {% for map_inst in maps %}
{% with unique_id="map_"|addstr:map_inst.pk %} {% with unique_id="map_"|addstr:map_inst.pk %}
{{ map_inst.preview_settings|json_script:unique_id }}
<tr> <tr>
<td> <td>
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a> <a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
</td> </td>
<td> <td>
{{ map_inst.preview_settings|json_script:unique_id }}
<button class="button map-opener neutral" data-map-id="{{ unique_id }}">{% blocktranslate %}Open preview{% endblocktranslate %}</button> <button class="button map-opener neutral" data-map-id="{{ unique_id }}">{% blocktranslate %}Open preview{% endblocktranslate %}</button>
<dialog> <dialog>
<form method="dialog"> <form method="dialog">
@ -29,7 +30,8 @@
</form> </form>
</dialog> </dialog>
</td> </td>
<td>{{ map_inst.get_share_status_display }} / {{ map_inst.get_edit_status_display }}</td> <td>{{ map_inst.get_share_status_display }}</td>
<td>{{ map_inst.get_edit_status_display }}</td>
<td>{{ map_inst.modified_at }}</td> <td>{{ map_inst.modified_at }}</td>
<td> <td>
<a href="{{ map_inst.owner.get_url }}">{{ map_inst.owner }}</a> <a href="{{ map_inst.owner.get_url }}">{{ map_inst.owner }}</a>
@ -38,28 +40,16 @@
<a href="{{ map_inst.get_absolute_url }}?share">{% translate "Share" %}</a> | <a href="{{ map_inst.get_absolute_url }}?share">{% translate "Share" %}</a> |
<a href="{{ map_inst.get_absolute_url }}?edit">{% translate "Edit" %}</a> | <a href="{{ map_inst.get_absolute_url }}?edit">{% translate "Edit" %}</a> |
<a href="{% url 'map_download' map_inst.pk %}">{% translate "Download" %}</a> | <a href="{% url 'map_download' map_inst.pk %}">{% translate "Download" %}</a> |
<form action="{% url 'map_clone' map_inst.pk %}" method="post">
{% csrf_token %}
<input type="submit" value="{% trans "Clone" %}" />
</form> |
<a href="{% url 'map_delete' map_inst.pk %}">{% translate "Delete" %}</a> <a href="{% url 'map_delete' map_inst.pk %}">{% translate "Delete" %}</a>
</td> </td>
</tr> </tr>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<a href="{% url 'user_download' %}?{% spaceless %}
{% for map_inst in maps %}map_id={{ map_inst.pk }}{% if not forloop.last %}&{% endif %}{% endfor %}
{% endspaceless %}" class="button"
>{% blocktranslate count counter=maps.object_list|length %}
Download this map
{% plural %}
Download these {{ counter }} maps
{% endblocktranslate %}
</a>
</td>
</tr>
</tfoot>
</table> </table>
{% if maps.has_other_pages %} {% if maps.has_other_pages %}
<div class="pagination"> <div class="pagination">
@ -88,6 +78,16 @@
<span></span> <span></span>
{# djlint:on #} {# djlint:on #}
{% endif %} {% endif %}
<span>
{% blocktranslate with per_page=maps.paginator.per_page %}
Lines per page: {{ per_page }}
{% endblocktranslate %}
</span>
<span>
{% blocktranslate with count=maps.paginator.count %}
{{ count }} maps
{% endblocktranslate %}
</span>
</div> </div>
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">

View file

@ -5,17 +5,36 @@
{% endblock head_title %} {% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% trans "Search my maps" as placeholder %} {% trans "Search my maps" as placeholder %}
<div class="col wide"> <div class="row">
<h2 class="section tabs"> <h2 class="section tabs">
<a class="selected" href="{% url 'user_dashboard' %}" <a class="selected" href="{% url 'user_dashboard' %}"
>{% blocktranslate with count=maps.paginator.count %}My Maps ({{ count }}){% endblocktranslate %} >{% blocktranslate with count=maps.paginator.count %}My Maps ({{ count }}){% endblocktranslate %}
</a> | <a href="{% url 'user_profile' %}">{% trans "My profile" %}</a> </a>
<a href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
</h2> </h2>
{% include "umap/search_bar.html" with action=request.get_full_path placeholder=placeholder %}
</div> </div>
<div class="wrapper"> <div class="wrapper">
<div class="row"> <div class="row">
{% if maps %} <div class="table-header">
<form action="{{ request.get_full_path }}" method="get">
<span>
<label for="q">{% blocktranslate %}Maps title{% endblocktranslate %}</label>
<input id="q" name="q" type="search"
value="{{ request.GET.q|default:"" }}" />
</span>
<input type="submit" value="{% trans "Search" %}" />
</form>
{% if maps.object_list|length > 1 %}
<a href="{% url 'user_download' %}?{% spaceless %}
{% for map_inst in maps %}map_id={{ map_inst.pk }}{% if not forloop.last %}&{% endif %}{% endfor %}
{% endspaceless %}" class="button button-download"
>{% blocktranslate with count=maps.object_list|length %}
Download {{ count }} maps
{% endblocktranslate %}
</a>
{% endif %}
</div>
{% if maps or request.GET.q %}
{% include "umap/map_table.html" %} {% include "umap/map_table.html" %}
{% else %} {% else %}
<div> <div>

View file

@ -158,9 +158,23 @@ def test_clone_map_should_create_a_new_instance(client, map):
url = reverse("map_clone", kwargs={"map_id": map.pk}) url = reverse("map_clone", kwargs={"map_id": map.pk})
client.login(username=map.owner.username, password="123123") client.login(username=map.owner.username, password="123123")
response = client.post(url) response = client.post(url)
assert response.status_code == 302
assert Map.objects.count() == 2
clone = Map.objects.latest("pk")
assert response["Location"] == clone.get_absolute_url()
assert clone.pk != map.pk
assert clone.name == "Clone of " + map.name
def test_clone_map_should_be_possible_via_ajax(client, map):
assert Map.objects.count() == 1
url = reverse("map_clone", kwargs={"map_id": map.pk})
client.login(username=map.owner.username, password="123123")
response = client.post(url, headers={"X-Requested-With": "XMLHttpRequest"})
assert response.status_code == 200 assert response.status_code == 200
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest("pk") clone = Map.objects.latest("pk")
assert response.json() == {"redirect": clone.get_absolute_url()}
assert clone.pk != map.pk assert clone.pk != map.pk
assert clone.name == "Clone of " + map.name assert clone.name == "Clone of " + map.name
@ -191,7 +205,7 @@ def test_clone_should_set_cloner_as_owner(client, map, user):
map.save() map.save()
client.login(username=user.username, password="123123") client.login(username=user.username, password="123123")
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 302
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest("pk") clone = Map.objects.latest("pk")
assert clone.pk != map.pk assert clone.pk != map.pk
@ -442,9 +456,10 @@ def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonym
anonymap.edit_status = anonymap.ANONYMOUS anonymap.edit_status = anonymap.ANONYMOUS
anonymap.save() anonymap.save()
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 302
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest("pk") clone = Map.objects.latest("pk")
assert response["Location"] == clone.get_absolute_url()
assert clone.pk != anonymap.pk assert clone.pk != anonymap.pk
assert clone.name == "Clone of " + anonymap.name assert clone.name == "Clone of " + anonymap.name
assert clone.owner is None assert clone.owner is None

View file

@ -860,7 +860,10 @@ class MapClone(PermissionsMixin, View):
return HttpResponseForbidden() return HttpResponseForbidden()
owner = self.request.user if self.request.user.is_authenticated else None owner = self.request.user if self.request.user.is_authenticated else None
self.object = kwargs["map_inst"].clone(owner=owner) self.object = kwargs["map_inst"].clone(owner=owner)
response = simple_json_response(redirect=self.object.get_absolute_url()) if is_ajax(self.request):
response = simple_json_response(redirect=self.object.get_absolute_url())
else:
response = HttpResponseRedirect(self.object.get_absolute_url())
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
key, value = self.object.signed_cookie_elements key, value = self.object.signed_cookie_elements
response.set_signed_cookie( response.set_signed_cookie(