diff --git a/publications/templates/user_settings.html b/publications/templates/user_settings.html
index a5e6a48e..f326676f 100644
--- a/publications/templates/user_settings.html
+++ b/publications/templates/user_settings.html
@@ -1,9 +1,11 @@
{% extends "base.html" %} {% load static %} {% block navbar %}
- {% if request.user.is_authenticated %} {% include
- "authenticated_menu_snippet.html" %} {% else %} {% include "menu_snippet.html"
- %} {% endif %}
+ {% if request.user.is_authenticated %}
+ {% include "authenticated_menu_snippet.html" %}
+ {% else %}
+ {% include "menu_snippet.html" %}
+ {% endif %}
{% endblock %} {% block content %}
diff --git a/publications/urls.py b/publications/urls.py
index 267a1db9..aadb6a03 100644
--- a/publications/urls.py
+++ b/publications/urls.py
@@ -11,34 +11,35 @@
urlpatterns = [
path('', views.main, name="main"),
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
+ path('api/schema/ui/',SpectacularRedocView.as_view(url_name='optimap:schema'),name='redoc'),
+ path('download/geojson/', views.download_geojson, name='download_geojson'),
+ path('download/geopackage/', views.download_geopackage, name='download_geopackage'),
path('favicon.ico', lambda request: redirect('static/favicon.ico', permanent=True)),
+ path('feed/', RedirectView.as_view(pattern_name='optimap:georss_feed', permanent=True)),
+ path('feed/geoatom/', GeoFeed(feed_type_variant="geoatom"), name='geoatom_feed'),
+ path('feed/georss/', GeoFeed(feed_type_variant="georss"), name='georss_feed'),
+ path('feed/w3cgeo/', GeoFeed(feed_type_variant="w3cgeo"), name='w3cgeo_feed'),
+ path("about/", views.about, name="about"),
+ path("addsubscriptions/", views.add_subscriptions, name="addsubscriptions"),
path("api", lambda request: redirect('/api/v1/', permanent=False), name="api"),
path("api/", lambda request: redirect('/api/v1/', permanent=False)),
path("api/v1", lambda request: redirect('/api/v1/', permanent=False)),
path("api/v1/", include("publications.api")),
- path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
- path('api/schema/ui/sitemap',SpectacularRedocView.as_view(url_name='optimap:schema'),name='redoc'),
+ path("changeuser/", views.change_useremail, name="changeuser"),
+ path("confirm-delete/
/", views.confirm_account_deletion, name="confirm_delete"),
+ path("confirm-email///", views.confirm_email_change, name="confirm-email-change"),
+ path("contact/", RedirectView.as_view(pattern_name='about', permanent=True)),
path("data/", views.data, name="data"),
- path('feed/georss/', GeoFeed(feed_type_variant="georss"), name='georss_feed'),
- path('feed/geoatom/', GeoFeed(feed_type_variant="geoatom"), name='geoatom_feed'),
- path('feed/w3cgeo/', GeoFeed(feed_type_variant="w3cgeo"), name='w3cgeo_feed'),
- path('feed/', RedirectView.as_view(pattern_name='optimap:georss_feed', permanent=True)),
- path("loginres/", views.loginres, name="loginres"),
- path("privacy/", views.privacy, name="privacy"),
- path("contact/", RedirectView.as_view(pattern_name='optimap:privacy', permanent=True)),
- path("imprint/", RedirectView.as_view(pattern_name='optimap:privacy', permanent=True)),
- path("loginconfirm/", views.Confirmationlogin, name="loginconfirm"),
+ path("finalize-delete/", views.finalize_account_deletion, name="finalize_delete"),
+ path("imprint/", RedirectView.as_view(pattern_name='about', permanent=True)),
path("login/", views.authenticate_via_magic_link, name="magic_link"),
+ path("loginconfirm/", views.confirmation_login, name="loginconfirm"),
+ path("loginres/", views.loginres, name="loginres"),
path("logout/", views.customlogout, name="logout"),
- path("usersettings/", views.user_settings, name="usersettings"),
+ path("privacy/", views.privacy, name="privacy"),
+ path("request-delete/", views.request_delete, name="request_delete"),
path("subscriptions/", views.user_subscriptions, name="subscriptions"),
path("unsubscribe/", views.unsubscribe, name="unsubscribe"),
- path("addsubscriptions/", views.add_subscriptions, name="addsubscriptions"),
- path("request-delete/", views.request_delete, name="request_delete"),
- path("confirm-delete//", views.confirm_account_deletion, name="confirm_delete"),
- path("finalize-delete/", views.finalize_account_deletion, name="finalize_delete"),
- path("changeuser/", views.change_useremail, name="changeuser"),
- path("confirm-email///", views.confirm_email_change, name="confirm-email-change"),
- path('download/geojson/', views.download_geojson, name='download_geojson'),
- path('download/geopackage/', views.download_geopackage, name='download_geopackage'),
+ path("usersettings/", views.user_settings, name="usersettings"),
]
diff --git a/publications/views.py b/publications/views.py
index 1e0f30c2..384bd917 100644
--- a/publications/views.py
+++ b/publications/views.py
@@ -38,6 +38,7 @@
EMAIL_CONFIRMATION_TIMEOUT_SECONDS = 10 * 60
ACCOUNT_DELETE_TOKEN_TIMEOUT_SECONDS = 10 * 60
USER_DELETE_TOKEN_PREFIX = "user_delete_token"
+EMAIL_CONFIRMATION_TOKEN_PREFIX = "email_confirmation_"
# ------------------------------
# Download Endpoints
@@ -129,6 +130,9 @@ def download_geopackage(request):
def main(request):
return render(request, "main.html")
+def about(request):
+ return render(request, 'about.html')
+
def loginres(request):
email = request.POST.get('email', False)
@@ -138,14 +142,14 @@ def loginres(request):
'error': {
'class': 'danger',
'title': 'Login failed!',
- 'text': 'You attempted to login using an email that is blocked. Please contact support for assistance.'
- }
+ 'text': f"You attempted to login using an email that is blocked. Please contact support for assistance: {request.site.domain}/contact"
+ }
})
-
- subject = 'OPTIMAP Login'
- link = get_login_link(request, email)
- valid = floor(LOGIN_TOKEN_TIMEOUT_SECONDS / 60)
- body = f"""Hello {email} !
+ else:
+ subject = 'OPTIMAP Login'
+ link = get_login_link(request, email)
+ valid = floor(LOGIN_TOKEN_TIMEOUT_SECONDS / 60)
+ body = f"""Hello {email} !
You requested that we send you a link to log in to OPTIMAP at {request.site.domain}:
@@ -154,36 +158,41 @@ def loginres(request):
Please click on the link to log in.
The link is valid for {valid} minutes.
"""
- logger.info('Login process started for user %s', email)
- try:
- email_message = EmailMessage(
- subject=subject,
- body=body,
- from_email=settings.EMAIL_HOST_USER,
- to=[email],
- headers={'OPTIMAP': request.site.domain}
- )
- result = email_message.send()
- logger.info('%s sent login email to %s with the result: %s', settings.EMAIL_HOST_USER, email_message.recipients(), result)
- if str(get_connection().__class__.__module__).endswith("smtp"):
- with imaplib.IMAP4_SSL(settings.EMAIL_HOST_IMAP, port=settings.EMAIL_PORT_IMAP) as imap:
- message = str(email_message.message()).encode()
- imap.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD)
- folder = settings.EMAIL_IMAP_SENT_FOLDER
- imap.append(folder, '\\Seen', imaplib.Time2Internaldate(time.time()), message)
- logger.debug('Saved email to IMAP folder {folder}')
- return render(request, 'login_response.html', {'email': email, 'valid_minutes': valid})
- except Exception as ex:
- logger.exception('Error sending login email to %s from %s', email, settings.EMAIL_HOST_USER)
- logger.error(ex)
- return render(request, "error.html", {
- 'error': {
- 'class': 'danger',
- 'title': 'Login failed!',
- 'text': 'Error sending the login email. Please try again or contact us!'
- }
- })
+ logger.info('Login process started for user %s', email)
+ try:
+ email_message = EmailMessage(
+ subject=subject,
+ body=body,
+ from_email=settings.EMAIL_HOST_USER,
+ to=[email],
+ headers={'OPTIMAP': request.site.domain}
+ )
+ result = email_message.send()
+ logger.info('%s sent login email to %s with the result: %s', settings.EMAIL_HOST_USER, email_message.recipients(), result)
+ except Exception as ex:
+ logger.exception('Error sending login email to %s from %s', email, settings.EMAIL_HOST_USER)
+ logger.error(ex)
+ return render(request, "error.html", {
+ 'error': {
+ 'class': 'danger',
+ 'title': 'Login failed!',
+ 'text': 'Error sending the login email. Please try again or contact us!'
+ }
+ })
+
+ try:
+ if str(get_connection().__class__.__module__).endswith("smtp"):
+ with imaplib.IMAP4_SSL(settings.EMAIL_HOST_IMAP, port=settings.EMAIL_PORT_IMAP) as imap:
+ message = str(email_message.message()).encode()
+ imap.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD)
+ folder = settings.EMAIL_IMAP_SENT_FOLDER
+ imap.append('"{folder}"', '\\Seen', imaplib.Time2Internaldate(time.time()), str(message).encode('utf-8'))
+ logger.debug('Saved email to IMAP folder "%s"', folder)
+ except Exception as ex:
+ logger.exception('Error saving sent email to %s for %s', email, settings.EMAIL_HOST_USER)
+ logger.error(ex)
+ return render(request, 'login_response.html', {'email': email, 'valid_minutes': valid})
def privacy(request):
return render(request, 'privacy.html')
@@ -236,7 +245,7 @@ def data(request):
'last_gpkg': last_gpkg.name if last_gpkg else None,
})
-def Confirmationlogin(request):
+def confirmation_login(request):
return render(request, 'confirmation_login.html')
@@ -259,13 +268,19 @@ def authenticate_via_magic_link(request, token):
})
user = User.objects.filter(email=email).first()
if user:
- is_new = False
- else:
+ is_new = False
+ needs_confirmation = False
+ login_user(request, user)
+ elif request.GET.get('confirmed', None) == 'true':
user = User.objects.create_user(username=email, email=email)
- is_new = True
- login_user(request, user)
- cache.delete(token)
- return render(request, "confirmation_login.html", {'is_new': is_new})
+ is_new = True
+ needs_confirmation = False
+ login_user(request, user)
+ else:
+ is_new = True
+ needs_confirmation = True
+
+ return render(request, "confirmation_login.html", {'email': email, 'token': token, 'is_new': is_new, 'needs_confirmation': needs_confirmation})
@login_required
def customlogout(request):
@@ -302,7 +317,7 @@ def add_subscriptions(request):
user_name = currentuser.username if currentuser.is_authenticated else None
start_date_object = datetime.strptime(start_date, '%m/%d/%Y')
end_date_object = datetime.strptime(end_date, '%m/%d/%Y')
-
+
subscription = Subscription(
search_term=search_term,
timeperiod_startdate=start_date_object,
@@ -313,7 +328,7 @@ def add_subscriptions(request):
subscription.save()
return HttpResponseRedirect('/subscriptions/')
-@login_required
+@login_required
def unsubscribe(request):
"""Handles unsubscription requests from emails."""
user = request.user
@@ -323,17 +338,17 @@ def unsubscribe(request):
if unsubscribe_all:
Subscription.objects.filter(user=user).update(subscribed=False)
messages.success(request, "You have been unsubscribed from all subscriptions.")
- return redirect("/")
+ return redirect("/")
if search_term:
- exact_search_term = unquote(search_term).strip()
+ exact_search_term = unquote(search_term).strip()
subscription = get_object_or_404(Subscription, user=user, search_term=exact_search_term)
if not subscription:
messages.warning(request, f"No subscription found for '{search_term}'.")
- return redirect("/")
+ return redirect("/")
subscription.subscribed = False
subscription.save()
messages.success(request, f"You have unsubscribed from '{search_term}'.")
- return redirect("/")
+ return redirect("/")
return HttpResponse("Invalid request.", status=400)
@@ -368,8 +383,8 @@ def change_useremail(request):
return render(request, 'changeuser.html')
token = secrets.token_urlsafe(32)
cache.set(
- f"email_confirmation_{email_new}",
- {"token": token, "old_email": request.user.email},
+ f"{EMAIL_CONFIRMATION_TOKEN_PREFIX}_{email_new}",
+ {"token": token, "old_email": request.user.email},
timeout=EMAIL_CONFIRMATION_TIMEOUT_SECONDS,
)
confirm_url = request.build_absolute_uri(
@@ -393,11 +408,11 @@ def change_useremail(request):
return render(request, 'changeuser.html')
def confirm_email_change(request, token, email_new):
- cached_data = cache.get(f"email_confirmation_{email_new}")
+ cached_data = cache.get(f"{EMAIL_CONFIRMATION_TOKEN_PREFIX}_{email_new}")
if not cached_data:
messages.error(request, "Invalid or expired confirmation link.")
return HttpResponseRedirect("/")
- if isinstance(cached_data, str):
+ if isinstance(cached_data, str):
messages.error(request, "Cache error: Expected dictionary, got string.")
return HttpResponseRedirect("/")
stored_token = cached_data.get("token")
@@ -410,7 +425,7 @@ def confirm_email_change(request, token, email_new):
messages.error(request, "User not found.")
return HttpResponseRedirect("/")
user.email = email_new
- user.username = email_new
+ user.username = email_new
user.save()
contactURL = f"{settings.BASE_URL}/contact"
notify_subject = 'Your OPTIMAP Email Was Changed'
@@ -474,8 +489,8 @@ def confirm_account_deletion(request, token):
messages.error(request, "You are not authorized to delete this account.")
return redirect(reverse('optimap:main'))
request.session[USER_DELETE_TOKEN_PREFIX] = token
- request.session.modified = True
- request.session.save()
+ request.session.modified = True
+ request.session.save()
messages.warning(request, "Please confirm your account deletion. Your contributed data will remain on the platform.")
return redirect(reverse('optimap:usersettings'))
except Exception as e:
@@ -509,7 +524,7 @@ def finalize_account_deletion(request):
cache.delete(f"{USER_DELETE_TOKEN_PREFIX}_{token}")
if USER_DELETE_TOKEN_PREFIX in request.session:
del request.session[USER_DELETE_TOKEN_PREFIX]
- request.session.modified = True
+ request.session.modified = True
class RobotsView(View):
http_method_names = ['get']
diff --git a/publications/viewsets.py b/publications/viewsets.py
index 9f9ae2b5..bc1ce513 100644
--- a/publications/viewsets.py
+++ b/publications/viewsets.py
@@ -21,5 +21,5 @@ class SubscriptionViewset(viewsets.ModelViewSet):
def get_queryset(self):
user = self.request.user
- queryset = Subscription.objects.filter(user_name=user)
+ queryset = Subscription.objects.filter(user=user)
return queryset
\ No newline at end of file
diff --git a/release_command.sh b/release_command.sh
index c85d1262..1ca8cbec 100755
--- a/release_command.sh
+++ b/release_command.sh
@@ -1,3 +1,5 @@
#!/bin/sh -e
-python manage.py migrate
-python manage.py createcachetable
+#python manage.py migrate
+#python manage.py createcachetable
+echo "Release does nothing anymore, all in etc/manage-and-run.sh"
+
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 7e020e82..d5d2a90e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -3,3 +3,4 @@ pyvirtualdisplay
responses
xmldiff
fiona
+coverage