Feature 'Import libravatar export (mails + photos)'

This commit is contained in:
Oliver Falk
2018-07-12 15:00:59 +02:00
parent 06d86e991d
commit f4524961cd
7 changed files with 206 additions and 21 deletions

View File

@@ -30,5 +30,5 @@ def basepage(request):
unconfirmed = request.user.unconfirmedemail_set.count() unconfirmed = request.user.unconfirmedemail_set.count()
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS: if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
context['max_emails'] = True context['max_emails'] = True
return context return context

View File

@@ -2,21 +2,26 @@
Classes for our ivatar.ivataraccount.forms Classes for our ivatar.ivataraccount.forms
''' '''
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
from io import BytesIO
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse from django.urls import reverse
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.mail import send_mail from django.core.mail import send_mail
from django.contrib import messages
from ipware import get_client_ip from ipware import get_client_ip
from ivatar import settings from ivatar import settings
from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL
from ivatar.settings import JPEG_QUALITY
from . models import UnconfirmedEmail, ConfirmedEmail, Photo from . models import UnconfirmedEmail, ConfirmedEmail, Photo
from . models import UnconfirmedOpenId, ConfirmedOpenId from . models import UnconfirmedOpenId, ConfirmedOpenId
from . models import UserPreference from . models import UserPreference
from . models import pil_format, file_format
from . read_libravatar_export import read_gzdata as libravatar_read_gzdata
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5 MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
@@ -76,22 +81,7 @@ class AddEmailForm(forms.Form):
unconfirmed.email = self.cleaned_data['email'] unconfirmed.email = self.cleaned_data['email']
unconfirmed.user = user unconfirmed.user = user
unconfirmed.save() unconfirmed.save()
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1])
link = request.build_absolute_uri('/')[:-1] + \
reverse(
'confirm_email',
kwargs={'verification_key': unconfirmed.verification_key})
email_subject = _('Confirm your email address on %s') % \
settings.SITE_NAME
email_body = render_to_string('email_confirmation.txt', {
'verification_link': link,
'site_name': settings.SITE_NAME,
})
# if settings.DEBUG:
# print('DEBUG: %s' % link)
send_mail(
email_subject, email_body, settings.SERVER_EMAIL,
[unconfirmed.email])
return True return True
@@ -182,6 +172,7 @@ class AddOpenIDForm(forms.Form):
return unconfirmed.pk return unconfirmed.pk
class UpdatePreferenceForm(forms.ModelForm): class UpdatePreferenceForm(forms.ModelForm):
''' '''
Form for updating user preferences Form for updating user preferences
@@ -193,3 +184,68 @@ class UpdatePreferenceForm(forms.ModelForm):
''' '''
model = UserPreference model = UserPreference
fields = ['theme'] fields = ['theme']
class UploadLibravatarExportForm(forms.Form):
'''
Form handling libravatar user export upload
'''
export_file = forms.FileField(
label=_('Export file'),
error_messages={'required': _('You must choose an export file to upload.')})
not_porn = forms.BooleanField(
label=_('suitable for all ages (i.e. no offensive content)'),
required=True,
error_messages={
'required':
_('We only host "G-rated" images and so this field must\
be checked.')
})
can_distribute = forms.BooleanField(
label=_('can be freely copied'),
required=True,
error_messages={
'required':
_('This field must be checked since we need to be able to\
distribute photos to third parties.')
})
@staticmethod
def save(request, data):
'''
Save the models and assign it to the current user
'''
items = libravatar_read_gzdata(data.read())
for email in items['emails']:
if not ConfirmedEmail.objects.filter(email=email) and \
not UnconfirmedEmail.objects.filter(email=email): # pylint: disable=no-member
try:
unconfirmed = UnconfirmedEmail.objects.create( # pylint: disable=no-member
user=request.user,
email=email
)
unconfirmed.save()
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1])
except Exception as e: # pylint: disable=broad-except,invalid-name
# Debugging only
print('Exception while trying to import mail addresses: %s' % e)
if 'openids' in items:
messages.warning(
request,
_('You have OpenIDs in your export file, but we cannot\
import those at the moment'))
for pilobj in items['photos']:
# Is there a reasonable way to check if that photo already exists!?
try:
data = BytesIO()
pilobj.save(data, pilobj.format, quality=JPEG_QUALITY)
data.seek(0)
photo = Photo()
photo.user = request.user
photo.ip_address = get_client_ip(request)[0]
photo.format = file_format(pilobj.format)
photo.data = data.read()
photo.save()
except Exception: # pylint: disable=broad-except
pass

View File

@@ -17,9 +17,11 @@ from django.contrib import messages
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.template.loader import render_to_string
from openid.association import Association as OIDAssociation from openid.association import Association as OIDAssociation
from openid.store import nonce as oidnonce from openid.store import nonce as oidnonce
from openid.store.interface import OpenIDStore from openid.store.interface import OpenIDStore
@@ -29,6 +31,7 @@ from libravatar import libravatar_url
from ivatar.settings import MAX_LENGTH_EMAIL, logger from ivatar.settings import MAX_LENGTH_EMAIL, logger
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
from ivatar.settings import MAX_LENGTH_URL from ivatar.settings import MAX_LENGTH_URL
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, SERVER_EMAIL
from .gravatar import get_photo as get_gravatar_photo from .gravatar import get_photo as get_gravatar_photo
@@ -257,7 +260,7 @@ class Photo(BaseAccountModel):
return HttpResponseRedirect(reverse_lazy('profile')) return HttpResponseRedirect(reverse_lazy('profile'))
def __str__(self): def __str__(self):
return '%s (%i) from %s' % (self.format, self.pk, self.user) return '%s (%i) from %s' % (self.format, self.pk or 0, self.user)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@@ -361,6 +364,27 @@ class UnconfirmedEmail(BaseAccountModel):
using, using,
update_fields) update_fields)
def send_confirmation_mail(self, url=SECURE_BASE_URL):
'''
Send confirmation mail to that mail address
'''
link = url + \
reverse(
'confirm_email',
kwargs={'verification_key': self.verification_key})
email_subject = _('Confirm your email address on %s') % \
SITE_NAME
email_body = render_to_string('email_confirmation.txt', {
'verification_link': link,
'site_name': SITE_NAME,
})
# if settings.DEBUG:
# print('DEBUG: %s' % link)
send_mail(
email_subject, email_body, SERVER_EMAIL,
[self.email])
return True
def __str__(self): def __str__(self):
return '%s (%i) from %s' % (self.email, self.pk, self.user) return '%s (%i) from %s' % (self.email, self.pk, self.user)

View File

@@ -0,0 +1,64 @@
'''
Reading libravatar export
'''
from io import BytesIO
import gzip
import xml.etree.ElementTree
import base64
from PIL import Image
SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2'
def read_gzdata(gzdata=None):
'''
Read gzipped data file
'''
emails = [] # pylint: disable=invalid-name
openids = [] # pylint: disable=invalid-name
photos = [] # pylint: disable=invalid-name
if not gzdata:
return False
fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name
content = fh.read()
fh.close()
root = xml.etree.ElementTree.fromstring(content)
if not root.tag == '{%s}user' % SCHEMAROOT:
print('Unknown export format: %s' % root.tag)
exit(-1)
# Emails
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
if email.tag == '{%s}email' % SCHEMAROOT:
emails.append(email.text)
# OpenIDs
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
if openid.tag == '{%s}openid' % SCHEMAROOT:
openids.append(openid.text)
# Photos
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
if photo.tag == '{%s}photo' % SCHEMAROOT:
try:
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
except Exception as e: # pylint: disable=broad-except,invalid-name
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], e))
continue
try:
img = Image.open(BytesIO(data))
except Exception as e: # pylint: disable=broad-except,invalid-name
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], e))
continue
photos.append(img)
return {
'emails': emails,
'openids': openids,
'photos': photos,
}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap4 %}
{% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %}
{% block content %}
<h1>{% trans 'Upload an export from libravatar' %}</h1>
<div style="max-width:600px;">
<form enctype="multipart/form-data" method="post">{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">{% trans 'Upload' %}</button>
<button type="cancel" class="btn btn-danger">{% trans 'Cancel' %}</button>
{% endbuttons %}
</form>
</div>
{% endblock content %}

View File

@@ -16,7 +16,7 @@ from . views import ImportPhotoView, RawImageView, DeletePhotoView
from . views import UploadPhotoView, AssignPhotoOpenIDView from . views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from . views import CropPhotoView from . views import CropPhotoView
from . views import UserPreferenceView from . views import UserPreferenceView, UploadLibravatarExportView
# Define URL patterns, self documenting # Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use: # To see the fancy, colorful evaluation of these use:
@@ -84,4 +84,5 @@ urlpatterns = [ # pylint: disable=invalid-name
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'), url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'), url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'), url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'),
url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'),
] ]

View File

@@ -5,6 +5,7 @@ import io
from urllib.request import urlopen from urllib.request import urlopen
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.contrib import messages from django.contrib import messages
@@ -31,7 +32,7 @@ from .gravatar import get_photo as get_gravatar_photo
from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE
from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm
from .forms import UpdatePreferenceForm from .forms import UpdatePreferenceForm, UploadLibravatarExportForm
from .models import UnconfirmedEmail, ConfirmedEmail, Photo from .models import UnconfirmedEmail, ConfirmedEmail, Photo
from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore
from .models import UserPreference from .models import UserPreference
@@ -690,6 +691,7 @@ class CropPhotoView(TemplateView):
return photo.perform_crop(request, dimensions, email, openid) return photo.perform_crop(request, dimensions, email, openid)
@method_decorator(login_required, name='dispatch') # pylint: disable=too-many-ancestors @method_decorator(login_required, name='dispatch') # pylint: disable=too-many-ancestors
class UserPreferenceView(FormView, UpdateView): class UserPreferenceView(FormView, UpdateView):
''' '''
@@ -702,3 +704,21 @@ class UserPreferenceView(FormView, UpdateView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
return self.request.user.userpreference return self.request.user.userpreference
@method_decorator(login_required, name='dispatch')
class UploadLibravatarExportView(SuccessMessageMixin, FormView):
'''
View class responsible for libravatar user data export upload
'''
template_name = 'upload_libravatar_export.html'
form_class = UploadLibravatarExportForm
success_message = _('Successfully uploaded')
success_url = reverse_lazy('profile')
model = User
def form_valid(self, form):
data = self.request.FILES['export_file']
form.save(self.request, data)
return super().form_valid(form)