Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import warnings
  2. from urllib.parse import urlparse, urlunparse
  3. from django.conf import settings
  4. # Avoid shadowing the login() and logout() views below.
  5. from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
  6. from django.contrib.auth import login as auth_login
  7. from django.contrib.auth import logout as auth_logout
  8. from django.contrib.auth import update_session_auth_hash
  9. from django.contrib.auth.decorators import login_required
  10. from django.contrib.auth.forms import (
  11. AuthenticationForm,
  12. PasswordChangeForm,
  13. PasswordResetForm,
  14. SetPasswordForm,
  15. )
  16. from django.contrib.auth.tokens import default_token_generator
  17. from django.contrib.sites.shortcuts import get_current_site
  18. from django.core.exceptions import ImproperlyConfigured, ValidationError
  19. from django.http import HttpResponseRedirect, QueryDict
  20. from django.shortcuts import resolve_url
  21. from django.urls import reverse_lazy
  22. from django.utils.decorators import method_decorator
  23. from django.utils.deprecation import RemovedInDjango50Warning
  24. from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_decode
  25. from django.utils.translation import gettext_lazy as _
  26. from django.views.decorators.cache import never_cache
  27. from django.views.decorators.csrf import csrf_protect
  28. from django.views.decorators.debug import sensitive_post_parameters
  29. from django.views.generic.base import TemplateView
  30. from django.views.generic.edit import FormView
  31. UserModel = get_user_model()
  32. class RedirectURLMixin:
  33. next_page = None
  34. redirect_field_name = REDIRECT_FIELD_NAME
  35. success_url_allowed_hosts = set()
  36. def get_success_url(self):
  37. return self.get_redirect_url() or self.get_default_redirect_url()
  38. def get_redirect_url(self):
  39. """Return the user-originating redirect URL if it's safe."""
  40. redirect_to = self.request.POST.get(
  41. self.redirect_field_name, self.request.GET.get(self.redirect_field_name)
  42. )
  43. url_is_safe = url_has_allowed_host_and_scheme(
  44. url=redirect_to,
  45. allowed_hosts=self.get_success_url_allowed_hosts(),
  46. require_https=self.request.is_secure(),
  47. )
  48. return redirect_to if url_is_safe else ""
  49. def get_success_url_allowed_hosts(self):
  50. return {self.request.get_host(), *self.success_url_allowed_hosts}
  51. def get_default_redirect_url(self):
  52. """Return the default redirect URL."""
  53. if self.next_page:
  54. return resolve_url(self.next_page)
  55. raise ImproperlyConfigured("No URL to redirect to. Provide a next_page.")
  56. class LoginView(RedirectURLMixin, FormView):
  57. """
  58. Display the login form and handle the login action.
  59. """
  60. form_class = AuthenticationForm
  61. authentication_form = None
  62. template_name = "registration/login.html"
  63. redirect_authenticated_user = False
  64. extra_context = None
  65. @method_decorator(sensitive_post_parameters())
  66. @method_decorator(csrf_protect)
  67. @method_decorator(never_cache)
  68. def dispatch(self, request, *args, **kwargs):
  69. if self.redirect_authenticated_user and self.request.user.is_authenticated:
  70. redirect_to = self.get_success_url()
  71. if redirect_to == self.request.path:
  72. raise ValueError(
  73. "Redirection loop for authenticated user detected. Check that "
  74. "your LOGIN_REDIRECT_URL doesn't point to a login page."
  75. )
  76. return HttpResponseRedirect(redirect_to)
  77. return super().dispatch(request, *args, **kwargs)
  78. def get_default_redirect_url(self):
  79. """Return the default redirect URL."""
  80. if self.next_page:
  81. return resolve_url(self.next_page)
  82. else:
  83. return resolve_url(settings.LOGIN_REDIRECT_URL)
  84. def get_form_class(self):
  85. return self.authentication_form or self.form_class
  86. def get_form_kwargs(self):
  87. kwargs = super().get_form_kwargs()
  88. kwargs["request"] = self.request
  89. return kwargs
  90. def form_valid(self, form):
  91. """Security check complete. Log the user in."""
  92. auth_login(self.request, form.get_user())
  93. return HttpResponseRedirect(self.get_success_url())
  94. def get_context_data(self, **kwargs):
  95. context = super().get_context_data(**kwargs)
  96. current_site = get_current_site(self.request)
  97. context.update(
  98. {
  99. self.redirect_field_name: self.get_redirect_url(),
  100. "site": current_site,
  101. "site_name": current_site.name,
  102. **(self.extra_context or {}),
  103. }
  104. )
  105. return context
  106. class LogoutView(RedirectURLMixin, TemplateView):
  107. """
  108. Log out the user and display the 'You are logged out' message.
  109. """
  110. # RemovedInDjango50Warning: when the deprecation ends, remove "get" and
  111. # "head" from http_method_names.
  112. http_method_names = ["get", "head", "post", "options"]
  113. template_name = "registration/logged_out.html"
  114. extra_context = None
  115. # RemovedInDjango50Warning: when the deprecation ends, move
  116. # @method_decorator(csrf_protect) from post() to dispatch().
  117. @method_decorator(never_cache)
  118. def dispatch(self, request, *args, **kwargs):
  119. if request.method.lower() == "get":
  120. warnings.warn(
  121. "Log out via GET requests is deprecated and will be removed in Django "
  122. "5.0. Use POST requests for logging out.",
  123. RemovedInDjango50Warning,
  124. )
  125. return super().dispatch(request, *args, **kwargs)
  126. @method_decorator(csrf_protect)
  127. def post(self, request, *args, **kwargs):
  128. """Logout may be done via POST."""
  129. auth_logout(request)
  130. redirect_to = self.get_success_url()
  131. if redirect_to != request.get_full_path():
  132. # Redirect to target page once the session has been cleared.
  133. return HttpResponseRedirect(redirect_to)
  134. return super().get(request, *args, **kwargs)
  135. # RemovedInDjango50Warning.
  136. get = post
  137. def get_default_redirect_url(self):
  138. """Return the default redirect URL."""
  139. if self.next_page:
  140. return resolve_url(self.next_page)
  141. elif settings.LOGOUT_REDIRECT_URL:
  142. return resolve_url(settings.LOGOUT_REDIRECT_URL)
  143. else:
  144. return self.request.path
  145. def get_context_data(self, **kwargs):
  146. context = super().get_context_data(**kwargs)
  147. current_site = get_current_site(self.request)
  148. context.update(
  149. {
  150. "site": current_site,
  151. "site_name": current_site.name,
  152. "title": _("Logged out"),
  153. "subtitle": None,
  154. **(self.extra_context or {}),
  155. }
  156. )
  157. return context
  158. def logout_then_login(request, login_url=None):
  159. """
  160. Log out the user if they are logged in. Then redirect to the login page.
  161. """
  162. login_url = resolve_url(login_url or settings.LOGIN_URL)
  163. return LogoutView.as_view(next_page=login_url)(request)
  164. def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
  165. """
  166. Redirect the user to the login page, passing the given 'next' page.
  167. """
  168. resolved_url = resolve_url(login_url or settings.LOGIN_URL)
  169. login_url_parts = list(urlparse(resolved_url))
  170. if redirect_field_name:
  171. querystring = QueryDict(login_url_parts[4], mutable=True)
  172. querystring[redirect_field_name] = next
  173. login_url_parts[4] = querystring.urlencode(safe="/")
  174. return HttpResponseRedirect(urlunparse(login_url_parts))
  175. # Class-based password reset views
  176. # - PasswordResetView sends the mail
  177. # - PasswordResetDoneView shows a success message for the above
  178. # - PasswordResetConfirmView checks the link the user clicked and
  179. # prompts for a new password
  180. # - PasswordResetCompleteView shows a success message for the above
  181. class PasswordContextMixin:
  182. extra_context = None
  183. def get_context_data(self, **kwargs):
  184. context = super().get_context_data(**kwargs)
  185. context.update(
  186. {"title": self.title, "subtitle": None, **(self.extra_context or {})}
  187. )
  188. return context
  189. class PasswordResetView(PasswordContextMixin, FormView):
  190. email_template_name = "registration/password_reset_email.html"
  191. extra_email_context = None
  192. form_class = PasswordResetForm
  193. from_email = None
  194. html_email_template_name = None
  195. subject_template_name = "registration/password_reset_subject.txt"
  196. success_url = reverse_lazy("password_reset_done")
  197. template_name = "registration/password_reset_form.html"
  198. title = _("Password reset")
  199. token_generator = default_token_generator
  200. @method_decorator(csrf_protect)
  201. def dispatch(self, *args, **kwargs):
  202. return super().dispatch(*args, **kwargs)
  203. def form_valid(self, form):
  204. opts = {
  205. "use_https": self.request.is_secure(),
  206. "token_generator": self.token_generator,
  207. "from_email": self.from_email,
  208. "email_template_name": self.email_template_name,
  209. "subject_template_name": self.subject_template_name,
  210. "request": self.request,
  211. "html_email_template_name": self.html_email_template_name,
  212. "extra_email_context": self.extra_email_context,
  213. }
  214. form.save(**opts)
  215. return super().form_valid(form)
  216. INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token"
  217. class PasswordResetDoneView(PasswordContextMixin, TemplateView):
  218. template_name = "registration/password_reset_done.html"
  219. title = _("Password reset sent")
  220. class PasswordResetConfirmView(PasswordContextMixin, FormView):
  221. form_class = SetPasswordForm
  222. post_reset_login = False
  223. post_reset_login_backend = None
  224. reset_url_token = "set-password"
  225. success_url = reverse_lazy("password_reset_complete")
  226. template_name = "registration/password_reset_confirm.html"
  227. title = _("Enter new password")
  228. token_generator = default_token_generator
  229. @method_decorator(sensitive_post_parameters())
  230. @method_decorator(never_cache)
  231. def dispatch(self, *args, **kwargs):
  232. if "uidb64" not in kwargs or "token" not in kwargs:
  233. raise ImproperlyConfigured(
  234. "The URL path must contain 'uidb64' and 'token' parameters."
  235. )
  236. self.validlink = False
  237. self.user = self.get_user(kwargs["uidb64"])
  238. if self.user is not None:
  239. token = kwargs["token"]
  240. if token == self.reset_url_token:
  241. session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
  242. if self.token_generator.check_token(self.user, session_token):
  243. # If the token is valid, display the password reset form.
  244. self.validlink = True
  245. return super().dispatch(*args, **kwargs)
  246. else:
  247. if self.token_generator.check_token(self.user, token):
  248. # Store the token in the session and redirect to the
  249. # password reset form at a URL without the token. That
  250. # avoids the possibility of leaking the token in the
  251. # HTTP Referer header.
  252. self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
  253. redirect_url = self.request.path.replace(
  254. token, self.reset_url_token
  255. )
  256. return HttpResponseRedirect(redirect_url)
  257. # Display the "Password reset unsuccessful" page.
  258. return self.render_to_response(self.get_context_data())
  259. def get_user(self, uidb64):
  260. try:
  261. # urlsafe_base64_decode() decodes to bytestring
  262. uid = urlsafe_base64_decode(uidb64).decode()
  263. user = UserModel._default_manager.get(pk=uid)
  264. except (
  265. TypeError,
  266. ValueError,
  267. OverflowError,
  268. UserModel.DoesNotExist,
  269. ValidationError,
  270. ):
  271. user = None
  272. return user
  273. def get_form_kwargs(self):
  274. kwargs = super().get_form_kwargs()
  275. kwargs["user"] = self.user
  276. return kwargs
  277. def form_valid(self, form):
  278. user = form.save()
  279. del self.request.session[INTERNAL_RESET_SESSION_TOKEN]
  280. if self.post_reset_login:
  281. auth_login(self.request, user, self.post_reset_login_backend)
  282. return super().form_valid(form)
  283. def get_context_data(self, **kwargs):
  284. context = super().get_context_data(**kwargs)
  285. if self.validlink:
  286. context["validlink"] = True
  287. else:
  288. context.update(
  289. {
  290. "form": None,
  291. "title": _("Password reset unsuccessful"),
  292. "validlink": False,
  293. }
  294. )
  295. return context
  296. class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
  297. template_name = "registration/password_reset_complete.html"
  298. title = _("Password reset complete")
  299. def get_context_data(self, **kwargs):
  300. context = super().get_context_data(**kwargs)
  301. context["login_url"] = resolve_url(settings.LOGIN_URL)
  302. return context
  303. class PasswordChangeView(PasswordContextMixin, FormView):
  304. form_class = PasswordChangeForm
  305. success_url = reverse_lazy("password_change_done")
  306. template_name = "registration/password_change_form.html"
  307. title = _("Password change")
  308. @method_decorator(sensitive_post_parameters())
  309. @method_decorator(csrf_protect)
  310. @method_decorator(login_required)
  311. def dispatch(self, *args, **kwargs):
  312. return super().dispatch(*args, **kwargs)
  313. def get_form_kwargs(self):
  314. kwargs = super().get_form_kwargs()
  315. kwargs["user"] = self.request.user
  316. return kwargs
  317. def form_valid(self, form):
  318. form.save()
  319. # Updating the password logs out all other sessions for the user
  320. # except the current one.
  321. update_session_auth_hash(self.request, form.user)
  322. return super().form_valid(form)
  323. class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
  324. template_name = "registration/password_change_done.html"
  325. title = _("Password change successful")
  326. @method_decorator(login_required)
  327. def dispatch(self, *args, **kwargs):
  328. return super().dispatch(*args, **kwargs)