Skip to content

Commit

Permalink
Merge pull request #138 from interDist/privateco5
Browse files Browse the repository at this point in the history
Privateco Ⅴ (restarigo de malfunkciigitaj profiloj)
  • Loading branch information
interDist committed Jun 12, 2018
2 parents 053a7df + 0282403 commit b50cd8c
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 13 deletions.
1 change: 1 addition & 0 deletions core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def dispatch(self, request, *args, **kwargs):
if getattr(self, 'exact_role', None) == ANONYMOUS or self.minimum_role == ANONYMOUS:
self.allow_anonymous = True
if not request.user.is_authenticated and not self.allow_anonymous:
self.role = VISITOR
return self.handle_no_permission() # authorization implies a logged-in user
if 'auth_base' in kwargs:
object = kwargs['auth_base']
Expand Down
4 changes: 3 additions & 1 deletion core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def __getitem__(self, index):
)
# ENDTODO
if trouble_view is not None:
if not Agreement.objects.filter(user=request.user, policy_version=policy.values_list('version')).exists():
agreement = Agreement.objects.filter(
user=request.user, policy_version=policy.values_list('version'), withdrawn__isnull=True)
if not agreement.exists():
# Policy will be needed to display the following page anyway,
# so it is immediately fetched from the database.
request.user.consent_required = [policy.first()]
Expand Down
26 changes: 26 additions & 0 deletions core/migrations/0005_agreement_withdrawal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-05-30 11:21
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0004_agreement_model'),
]

operations = [
migrations.AddField(
model_name='agreement',
name='withdrawn',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='withdrawn on'),
),
migrations.AlterUniqueTogether(
name='agreement',
unique_together=set([('user', 'policy_version', 'withdrawn')]),
),
]
5 changes: 4 additions & 1 deletion core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,15 @@ class Agreement(TimeStampedModel):
policy_version = models.CharField(
_("version of policy"),
max_length=50)
withdrawn = models.DateTimeField(
_("withdrawn on"),
default=None, blank=True, null=True)

class Meta:
verbose_name = _("agreement")
verbose_name_plural = _("agreements")
default_permissions = ('delete', )
unique_together = ('user', 'policy_version')
unique_together = ('user', 'policy_version', 'withdrawn')

def __str__(self):
return str(_("User {user} agreed to '{policy}' on {date:%Y-%m-%d}")).format(
Expand Down
13 changes: 9 additions & 4 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def post(self, request, *args, **kwargs):
return HttpResponseRedirect(target_page if target_page and is_safe_url(target_page)
else reverse_lazy('home'))
elif action == 'reject':
request.session['agreement_rejected'] = True
request.session['agreement_rejected'] = self._agreement[1]['version']
return HttpResponseRedirect(reverse_lazy('agreement_reject'))
else:
return HttpResponseRedirect(reverse_lazy('agreement'))
Expand All @@ -150,9 +150,10 @@ def get(self, request, *args, **kwargs):
"""
Show the warning about consequences of not accepting the agreement.
"""
if not request.session.pop('agreement_rejected', None):
agreement = request.session.pop('agreement_rejected', None)
if not agreement:
return HttpResponse()
request.session['agreement_rejected_final'] = True
request.session['agreement_rejected_final'] = agreement
return super().get(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
Expand All @@ -162,7 +163,8 @@ def post(self, request, *args, **kwargs):
deactivate the user account,
and then redirect to home URL.
"""
if not request.session.pop('agreement_rejected_final', None):
agreement = request.session.pop('agreement_rejected_final', None)
if not agreement:
return HttpResponse()
now = timezone.now()
owned_places = Place.all_objects.filter(owner__user=request.user)
Expand All @@ -182,6 +184,9 @@ def post(self, request, *args, **kwargs):
]]
request.user.is_active = False
request.user.save(update_fields=['is_active'])
agreement = Agreement.objects.filter(
user=request.user, policy_version=agreement, withdrawn__isnull=True)
agreement.update(withdrawn=now)
messages.info(request, _("Farewell !"))
return HttpResponseRedirect(reverse_lazy('home'))

Expand Down
11 changes: 9 additions & 2 deletions hosting/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,15 @@ def get_queryset(self, request):
@admin.register(Agreement)
class AgreementAdmin(admin.ModelAdmin):
list_display = (
'id', 'policy_link', 'user_link', 'created', 'modified',
'id', 'policy_link', 'user_link', 'created', 'modified', 'withdrawn',
)
ordering = ['-policy_version', 'user']
search_fields = ['user__username']
list_filter = ('policy_version',)
date_hierarchy = 'created'
fields = (
'user_link', 'policy_link',
'created', 'modified',
'created', 'modified', 'withdrawn',
)
readonly_fields = [f.name for f in Agreement._meta.fields] + ['user_link', 'policy_link']

Expand Down Expand Up @@ -246,6 +246,13 @@ def policy_link(self, obj):
policy_link.short_description = _("version of policy")
policy_link.admin_order_field = 'policy_version'

def get_queryset(self, request):
qs = super().get_queryset(request).select_related('user', 'user__profile')
qs = qs.only(
*[f.name for f in Agreement._meta.fields],
'user__id', 'user__username', 'user__profile__id')
return qs

def has_add_permission(self, request):
return False

Expand Down
7 changes: 5 additions & 2 deletions hosting/templates/hosting/base_confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
<form action="" method="POST" class="pull-right">
{% csrf_token %}
<a id="id_form_cancel" href="#" onclick="history.go(-1)" data-kbdshortcut="Z-out"
class="btn btn-default btn-vert-space" title="{% trans "Cancel operation" %}">
<span class="fa fa-arrow-left" aria-hidden="true"></span> {% trans "Cancel" %}
class="btn btn-default btn-vert-space"
title="{% block form_cancel_button_title %}{% trans "Cancel operation" %}{% endblock %}">
{% block form_cancel_button %}
<span class="fa fa-arrow-left" aria-hidden="true"></span> {% trans "Cancel" %}
{% endblock %}
</a>
<span style="display: inline-block">
{% block confirm_buttons %}
Expand Down
73 changes: 73 additions & 0 deletions hosting/templates/hosting/profile_confirm_restore.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{% extends 'hosting/base_confirm.html' %}
{% load i18n %}

{% block head_title %}
{% trans "Restore profile" %}
{% if view.role >= roles.SUPERVISOR and profile.user %} ({{ profile.name|default:profile.INCOGNITO }}) {% endif %}
{% endblock %}

{% block confirmation %}
{% if not profile.deleted %}
<span class="fa fa-exclamation-circle" aria-label="{% trans "Note " context 'Title' %}" style="transform: rotate(-10deg)"></span>&nbsp;
{% trans "This profile is active and does not require restoration." %}
{% else %}
{% blocktrans trimmed %}
Are you sure you want to re-enable this profile and the data that was linked to it?
{% endblocktrans %}
{% endif %}
{% endblock %}

{% block extra_details %}
{% if profile.deleted %}
{{ profile.get_fullname_always_display }}
{% endif %}
{% endblock %}

{% block extra_details_block %}
{% if profile.deleted %}
<br />
<div class="row">
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
<p class="text-justify">
{% blocktrans trimmed %}
Note that only objects deleted at the same time as the profile will be re-enabled:
{% endblocktrans %}
</p>
<ul class="list-group small">
{% for obj in linked_objects %}
<li class="list-group-item place-list-compact">
{{ obj.icon }} &nbsp; {{ obj }}
</li>
{% empty %}
<li class="list-group-item place-list-compact">
<em>
{% blocktrans with deleted_date=profile.deleted_on|date:"DATE_FORMAT" trimmed %}
No additional objects deleted on {{ deleted_date }} were found.
{% endblocktrans %}
</em>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endblock %}


{% block confirm_buttons %}
{% if not profile.deleted %}{# No restoration is possible. #}{% else %}{{ block.super }}{% endif %}
{% endblock %}

{% block form_confirm_button_title %}{% trans "Restore profile" %}{% endblock %}
{% block form_confirm_button %}
<span class="fa fa-repeat" aria-hidden="true"></span> {% trans "Restore" %}
{% endblock %}

{% block form_cancel_button_title %}{% if not profile.deleted %}{% trans "Back" %}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block form_cancel_button %}
{% if not profile.deleted %}
<span class="fa fa-arrow-left" aria-hidden="true"></span> {% trans "Back" %}
{% else %}
{{ block.super }}
{% endif %}
{% endblock %}
9 changes: 9 additions & 0 deletions hosting/templates/hosting/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ <h4>{% if view.role == roles.OWNER %}{% trans "End my membership" %}{% else %}{%
{% trans "Delete account" %}
</a>
</section>
{% elif view.role >= roles.SUPERVISOR %}
<section class="callout callout-success">
<h4>{% trans "Restore membership" %}</h4>
<a href="{% url 'profile_restore' profile.pk profile.autoslug %}"
class="btn btn-default">
<span class="fa fa-repeat" aria-hidden="true"></span>&nbsp;
{% trans "Restore account" %}
</a>
</section>
{% endif %}

<div class="pull-left">
Expand Down
3 changes: 2 additions & 1 deletion hosting/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .views import ( # isort:skip
ProfileCreateView, ProfileRedirectView, ProfileDetailView,
ProfileEditView, ProfileUpdateView, ProfileDeleteView,
ProfileEditView, ProfileUpdateView, ProfileDeleteView, ProfileRestoreView,
ProfileSettingsRedirectView, ProfileSettingsView,
ProfileEmailUpdateView, ProfilePrivacyUpdateView,
PlaceCreateView, PlaceDetailView, PlaceDetailVerboseView,
Expand All @@ -30,6 +30,7 @@
url(_(r'^update/$'), ProfileUpdateView.as_view(), name='profile_update'),
url(_(r'^email/$'), ProfileEmailUpdateView.as_view(), name='profile_email_update'),
url(_(r'^delete/$'), ProfileDeleteView.as_view(), name='profile_delete'),
url(_(r'^restore/$'), ProfileRestoreView.as_view(), name='profile_restore'),
url(_(r'^settings/$'), ProfileSettingsView.as_view(), name='profile_settings'),
url(_(r'^staff/'), include([
url(_(r'^email/'), include([
Expand Down
61 changes: 60 additions & 1 deletion hosting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
from collections import OrderedDict, namedtuple
from datetime import date
from itertools import chain

from django.conf import settings
from django.contrib import messages
Expand All @@ -11,7 +12,9 @@
from django.core import serializers
from django.core.exceptions import NON_FIELD_ERRORS
from django.core.mail import send_mail
from django.db import transaction
from django.db.models import Q
from django.db.models.functions import Trunc
from django.forms import modelformset_factory
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest,
Expand All @@ -33,7 +36,7 @@
from django_countries.fields import Country

from core.auth import (
ANONYMOUS, OWNER, PERM_SUPERVISOR, SUPERVISOR, VISITOR, AuthMixin,
ADMIN, ANONYMOUS, OWNER, PERM_SUPERVISOR, SUPERVISOR, VISITOR, AuthMixin,
)
from core.forms import UserRegistrationForm
from core.mixins import LoginRequiredMixin
Expand Down Expand Up @@ -122,6 +125,62 @@ def delete(self, request, *args, **kwargs):
return super().delete(request, *args, **kwargs)


class ProfileRestoreView(
AuthMixin, ProfileIsUserMixin, ProfileModifyMixin,
generic.DetailView):
model = Profile
template_name = 'hosting/profile_confirm_restore.html'
exact_role = ADMIN

def get_permission_denied_message(self, object, context_omitted=False):
return _("Only administrators can access this page")

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.object.deleted:
context['linked_objects'] = (p for p in chain(self.linked_places, self.linked_phones))
return context

@property
def deletion_timestamp(self):
return self.object.deleted_on.replace(second=0, microsecond=0)

def _truncate_delete_field(self):
return Trunc('deleted_on', kind='minute')

def _annotated_objects(self, qs):
return qs.annotate(
deleted_when=self._truncate_delete_field()
).filter(
deleted_when=self.deletion_timestamp
)

@cached_property
def linked_places(self):
return self._annotated_objects(self.object.owned_places)

@cached_property
def linked_phones(self):
return self._annotated_objects(self.object.phones)

def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.deleted:
return self.get(request, *args, **kwargs)
with transaction.atomic():
[qs.update(deleted_on=None) for qs in [
self._annotated_objects(Profile.all_objects)
.filter(
pk__in=self.linked_places.values_list('family_members', flat=True),
user_id__isnull=True),
self.linked_places,
self.linked_phones,
Profile.all_objects.filter(pk=self.object.pk),
]]
User.objects.filter(pk=self.object.user_id).update(is_active=True)
return HttpResponseRedirect(self.object.get_edit_url())


class ProfileRedirectView(LoginRequiredMixin, generic.RedirectView):
permanent = False

Expand Down
Loading

0 comments on commit b50cd8c

Please sign in to comment.