Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anonymous calendar links with user tokens #36

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 187 additions & 84 deletions src/locale/de/LC_MESSAGES/django.po

Large diffs are not rendered by default.

220 changes: 146 additions & 74 deletions src/locale/en/LC_MESSAGES/django.po

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.0.9 on 2024-07-06 17:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('accounts', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='user',
name='ical_token',
field=models.CharField(max_length=64, null=True, unique=True, verbose_name='iCalendar Token'),
),
migrations.AddIndex(
model_name='user',
index=models.Index(fields=['ical_token'], name='user_ical_token_idx'),
),
]
17 changes: 17 additions & 0 deletions src/shiftings/accounts/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from shiftings import local_settings

if TYPE_CHECKING:
from shiftings.events.models import Event
Expand All @@ -31,10 +32,14 @@ def display(self) -> str:
class User(BaseUser):
display_name = models.CharField(max_length=150, verbose_name=_('Display Name'), null=True, blank=True)
phone_number = PhoneNumberField(verbose_name=_('Telephone Number'), blank=True, null=True)
ical_token = models.CharField(max_length=64, verbose_name=_('iCalendar Token'), null=True, unique=True)

class Meta:
default_permissions = ()
ordering = ['username']
indexes = [
models.Index(fields=['ical_token'], name='user_ical_token_idx')
]

def __str__(self):
return self.display
Expand Down Expand Up @@ -65,5 +70,17 @@ def shift_count(self) -> int:
total += Shift.objects.filter(participants__user=claimed_user).count()
return total

@property
def all_shifts_url(self):
git if self.ical_token is None:
return None
return local_settings.SITE + '/user/calendar?token=' + self.ical_token
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was tut das?
Einfach Dinge aus local_settings importieren bringt nix das gibts bei dir aber das wars
Mal abgesehen davon das SITE vom Request abhängt

Ein fstring ist mMn ebenfalls wesentlich lesbarer:
f'{SITE}/user../{self.ical_token}'


@property
def my_shifts_url(self):
if self.ical_token is None:
return None
return local_settings.SITE + '/user/participation_calendar?token=' + self.ical_token
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.o.


def get_absolute_url(self):
return reverse('user_profile')
38 changes: 38 additions & 0 deletions src/shiftings/accounts/templates/accounts/user_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ <h4 class="text-danger">Your data cannot be restored.</h4>
</form>
</div>
{% endsimpledisplaymodal %}
{% simpledisplaymodal 'resetCalendarToken' _('Reset Calendar URL') %}
<div>
{% trans "Do you want to reset your personal calendar access token? All previous tokens will stop working." %}
</div>
<div class="mt-3">
<form action="{% url "user_ical_token_reset" %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-info float-end" title="{% trans "Reset iCalendar Link Token" %}">
{% trans "Reset Token" %} <i class="fa-solid fa-repeat"></i>
</button>
</form>
</div>
{% endsimpledisplaymodal %}
{% endblock %}
{% block left %}
<div class="sticky-top">
Expand Down Expand Up @@ -115,6 +128,31 @@ <h5>{% trans "Organizations" %}</h5>
</div>
</div>
</div>
<div class="card bg-dark mt-2">
<div class="card-header center-items justify-content-between">
<h4>{% trans "Calendar URL" %}</h4>
<button class="btn btn-info" title="{% trans "Reset iCalendar Link Token" %}" data-bs-toggle="modal"
data-bs-target="#resetCalendarToken">
{% trans "Reset Token" %} <i class="fa-solid fa-repeat"></i>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ist das ein Reset?

</button>
</div>
<div class="card-body pt-0">
{% if user.ical_token is None %}
<div class="text-bg-dark text-center text-secondary p-2">{% trans "(Re-)Generate your personal Token in order to use persistent iCalendar links." %}</div>
{% else %}
<span class="input-group mb-2">
<label for="user_all_shifts_url" class="input-group-text">{% trans "Overview" %}</label>
<input id="user_all_shifts_url" class="form-control" value="{{ user.all_shifts_url }}" />
{% include 'widgets/copy_input.html' with input_selector='#user_all_shifts_url' %}
</span>
<span class="input-group mb-2">
<label for="user_my_shifts_url" class="input-group-text">{% trans "My Shifts" %}</label>
<input id="user_my_shifts_url" class="form-control" value="{{ user.my_shifts_url }}" />
{% include 'widgets/copy_input.html' with input_selector='#user_all_shifts_url' %}
</span>
{% endif %}
</div>
</div>
{% endblock %}

{% block right %}
Expand Down
3 changes: 2 additions & 1 deletion src/shiftings/accounts/urls/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from shiftings.accounts.views.auth import UserLoginView, UserLogoutView, UserReLoginView
from shiftings.accounts.views.password import PasswordResetConfirmView, PasswordResetView
from shiftings.accounts.views.user import (ConfirmEMailView, UserDeleteSelfView, UserEditView, UserProfileView,
UserRegisterView)
UserRegisterView, UserRegenerateCalendarTokenView)
from shiftings.cal.feed.user import OwnShiftsFeed, UserFeed
from shiftings.utils.converters import AlphaNumericConverter

Expand All @@ -30,6 +30,7 @@
name='password_reset_success'),
path('calendar/', UserFeed(), name='user_calendar'),
path('participation_calendar/', OwnShiftsFeed(), name='user_participation_calendar'),
path('ical_token_reset/', UserRegenerateCalendarTokenView.as_view(), name='user_ical_token_reset')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich fände rengenerate oder renew da besser da es ja kein reset ist

]
if settings.FEATURES.get('registration', False):
urlpatterns.append(path('register/', UserRegisterView.as_view(), name='register'))
Expand Down
13 changes: 13 additions & 0 deletions src/shiftings/accounts/views/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import date
from secrets import token_urlsafe
from typing import Any, Dict

from django import template
Expand All @@ -10,6 +11,7 @@
from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes, force_str
Expand Down Expand Up @@ -131,3 +133,14 @@ def delete(self, request, *args, **kwargs):

def post(self, request, *args, **kwargs):
return self.delete(request, *args, **kwargs)


class UserRegenerateCalendarTokenView(BaseLoginMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('user_profile'))

def post(self, request, *args, **kwargs):
user = self.request.user
user.ical_token = token_urlsafe(64)
user.save()
return redirect(reverse('user_profile'))
9 changes: 6 additions & 3 deletions src/shiftings/cal/feed/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@

class UserFeed(ShiftFeed[User]):
def get_object(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Optional[User]:
if not request.user.is_authenticated:
raise Http403()
return request.user
if request.user.is_authenticated:
return request.user

# noinspection all
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sowas würde ich eher nicht verwenden meistens hat es einen Grund wenn die inspection meckert

user = User.objects.get(ical_token=request.GET.get('token', ''))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das reicht nicht ganz aus, vor allem für SSO-User müsste man das einmal im Monat checken ob der die Gruppen überhaupt noch sehen darf und dazu bräuchte man nen SSO Login.

return user

def file_name(self, obj: User) -> str:
return f'{obj.display.lower().replace(" ", "_")}_shifts.ics'
Expand Down
7 changes: 7 additions & 0 deletions src/shiftings/templates/widgets/copy_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<button onclick="
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format
Was tut dieses
Auch wenn das inline im Button geht wäre es vmtl besser das irgendwo als inkludierbares Skript schnipsel zu haben.

var x = document.querySelector('{{ input_selector }}');
x.select();
x.setSelectionRange(0, 99999);
navigator.clipboard.writeText(x.value);" class="btn btn-secondary">
{% trans "Copy" %} <i class="fa-solid fa-clipboard"></i></button>
</span>