From 8e3eb1e2bf515f7595cc5f36f7c1b257452f39d5 Mon Sep 17 00:00:00 2001 From: Skia Date: Mon, 22 Aug 2016 02:56:27 +0200 Subject: [PATCH] Some templating and add webcam support for profile editing --- core/static/core/js/webcam.js | 399 ++++++++++++++++++ core/templates/core/base.jinja | 1 + core/templates/core/user_edit.jinja | 47 ++- core/urls.py | 1 + core/utils.py | 20 + core/views/forms.py | 18 +- core/views/user.py | 34 +- counter/migrations/0020_auto_20160821_0307.py | 19 + counter/templates/counter/counter_click.jinja | 15 +- migrate.py | 45 +- subscription/models.py | 2 +- .../templates/subscription/subscription.jinja | 2 +- 12 files changed, 551 insertions(+), 52 deletions(-) create mode 100644 core/static/core/js/webcam.js create mode 100644 core/utils.py create mode 100644 counter/migrations/0020_auto_20160821_0307.py diff --git a/core/static/core/js/webcam.js b/core/static/core/js/webcam.js new file mode 100644 index 00000000..c4ba7092 --- /dev/null +++ b/core/static/core/js/webcam.js @@ -0,0 +1,399 @@ +// WebcamJS v1.0 +// Webcam library for capturing JPEG/PNG images in JavaScript +// Attempts getUserMedia, falls back to Flash +// Author: Joseph Huckaby: http://github.com/jhuckaby +// Based on JPEGCam: http://code.google.com/p/jpegcam/ +// Copyright (c) 2012 Joseph Huckaby +// Licensed under the MIT License + +/* Usage: +
+
+ + + + Take Snapshot +*/ + +var Webcam = { + version: '1.0.0', + + // globals + protocol: location.protocol.match(/https/i) ? 'https' : 'http', + swfURL: '', // URI to webcam.swf movie (defaults to cwd) + loaded: false, // true when webcam movie finishes loading + live: false, // true when webcam is initialized and ready to snap + userMedia: true, // true when getUserMedia is supported natively + + params: { + width: 0, + height: 0, + dest_width: 0, // size of captured image + dest_height: 0, // these default to width/height + image_format: 'jpeg', // image format (may be jpeg or png) + jpeg_quality: 90, // jpeg image quality from 0 (worst) to 100 (best) + force_flash: false // force flash mode + }, + + hooks: { + load: null, + live: null, + uploadcomplete: null, + uploadprogress: null, + error: function(msg) { alert("Webcam.js Error: " + msg); } + }, // callback hook functions + + init: function() { + // initialize, check for getUserMedia support + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL; + + this.userMedia = this.userMedia && !!navigator.getUserMedia && !!window.URL; + + // Older versions of firefox (< 21) apparently claim support but user media does not actually work + if (navigator.userAgent.match(/Firefox\D+(\d+)/)) { + if (parseInt(RegExp.$1, 10) < 21) this.userMedia = null; + } + }, + + attach: function(elem) { + // create webcam preview and attach to DOM element + // pass in actual DOM reference, ID, or CSS selector + if (typeof(elem) == 'string') { + elem = document.getElementById(elem) || document.querySelector(elem); + } + if (!elem) { + return this.dispatch('error', "Could not locate DOM element to attach to."); + } + + this.container = elem; + if (!this.params.width) this.params.width = elem.offsetWidth; + if (!this.params.height) this.params.height = elem.offsetHeight; + + // set defaults for dest_width / dest_height if not set + if (!this.params.dest_width) this.params.dest_width = this.params.width; + if (!this.params.dest_height) this.params.dest_height = this.params.height; + + // if force_flash is set, disable userMedia + if (this.params.force_flash) this.userMedia = null; + + if (this.userMedia) { + // setup webcam video container + var video = document.createElement('video'); + video.setAttribute('autoplay', 'autoplay'); + video.style.width = '' + this.params.dest_width + 'px'; + video.style.height = '' + this.params.dest_height + 'px'; + + // adjust scale if dest_width or dest_height is different + var scaleX = this.params.width / this.params.dest_width; + var scaleY = this.params.height / this.params.dest_height; + + if ((scaleX != 1.0) || (scaleY != 1.0)) { + elem.style.overflow = 'visible'; + video.style.webkitTransformOrigin = '0px 0px'; + video.style.mozTransformOrigin = '0px 0px'; + video.style.msTransformOrigin = '0px 0px'; + video.style.oTransformOrigin = '0px 0px'; + video.style.transformOrigin = '0px 0px'; + video.style.webkitTransform = 'scaleX('+scaleX+') scaleY('+scaleY+')'; + video.style.mozTransform = 'scaleX('+scaleX+') scaleY('+scaleY+')'; + video.style.msTransform = 'scaleX('+scaleX+') scaleY('+scaleY+')'; + video.style.oTransform = 'scaleX('+scaleX+') scaleY('+scaleY+')'; + video.style.transform = 'scaleX('+scaleX+') scaleY('+scaleY+')'; + } + + // add video element to dom + elem.appendChild( video ); + this.video = video; + + // create offscreen canvas element to hold pixels later on + var canvas = document.createElement('canvas'); + canvas.width = this.params.dest_width; + canvas.height = this.params.dest_height; + var context = canvas.getContext('2d'); + this.context = context; + this.canvas = canvas; + + // ask user for access to their camera + var self = this; + navigator.getUserMedia({ + "audio": false, + "video": true + }, + function(stream) { + // got access, attach stream to video + video.src = window.URL.createObjectURL( stream ) || stream; + Webcam.stream = stream; + Webcam.loaded = true; + Webcam.live = true; + Webcam.dispatch('load'); + Webcam.dispatch('live'); + }, + function(err) { + return self.dispatch('error', "Could not access webcam."); + }); + } + else { + // flash fallback + elem.innerHTML = this.getSWFHTML(); + } + }, + + reset: function() { + // shutdown camera, reset to potentially attach again + if (this.userMedia) { + try { this.stream.stop(); } catch (e) {;} + delete this.stream; + delete this.canvas; + delete this.context; + delete this.video; + } + + this.container.innerHTML = ''; + delete this.container; + + this.loaded = false; + this.live = false; + }, + + set: function() { + // set one or more params + // variable argument list: 1 param = hash, 2 params = key, value + if (arguments.length == 1) { + for (var key in arguments[0]) { + this.params[key] = arguments[0][key]; + } + } + else { + this.params[ arguments[0] ] = arguments[1]; + } + }, + + on: function(name, callback) { + // set callback hook + // supported hooks: onLoad, onError, onLive + name = name.replace(/^on/i, '').toLowerCase(); + + if (typeof(this.hooks[name]) == 'undefined') + throw "Event type not supported: " + name; + + this.hooks[name] = callback; + }, + + dispatch: function() { + // fire hook callback, passing optional value to it + var name = arguments[0].replace(/^on/i, '').toLowerCase(); + var args = Array.prototype.slice.call(arguments, 1); + + if (this.hooks[name]) { + if (typeof(this.hooks[name]) == 'function') { + // callback is function reference, call directly + this.hooks[name].apply(this, args); + } + else if (typeof(this.hooks[name]) == 'array') { + // callback is PHP-style object instance method + this.hooks[name][0][this.hooks[name][1]].apply(this.hooks[name][0], args); + } + else if (window[this.hooks[name]]) { + // callback is global function name + window[ this.hooks[name] ].apply(window, args); + } + return true; + } + return false; // no hook defined + }, + + setSWFLocation: function(url) { + // set location of SWF movie (defaults to webcam.swf in cwd) + this.swfURL = url; + }, + + getSWFHTML: function() { + // Return HTML for embedding flash based webcam capture movie + var html = ''; + + // make sure we aren't running locally (flash doesn't work) + if (location.protocol.match(/file/)) { + return '

Sorry, the Webcam.js Flash fallback does not work from local disk. Please upload it to a web server first.

'; + } + + // set default swfURL if not explicitly set + if (!this.swfURL) { + // find our script tag, and use that base URL + var base_url = ''; + var scpts = document.getElementsByTagName('script'); + for (var idx = 0, len = scpts.length; idx < len; idx++) { + var src = scpts[idx].getAttribute('src'); + if (src && src.match(/\/webcam(\.min)?\.js/)) { + base_url = src.replace(/\/webcam(\.min)?\.js.*$/, ''); + idx = len; + } + } + if (base_url) this.swfURL = base_url + '/webcam.swf'; + else this.swfURL = 'webcam.swf'; + } + + // if this is the user's first visit, set flashvar so flash privacy settings panel is shown first + if (window.localStorage && !localStorage.getItem('visited')) { + this.params.new_user = 1; + localStorage.setItem('visited', 1); + } + + // construct flashvars string + var flashvars = ''; + for (var key in this.params) { + if (flashvars) flashvars += '&'; + flashvars += key + '=' + escape(this.params[key]); + } + + html += ''; + + return html; + }, + + getMovie: function() { + // get reference to movie object/embed in DOM + if (!this.loaded) return this.dispatch('error', "Flash Movie is not loaded yet"); + var movie = document.getElementById('webcam_movie_obj'); + if (!movie || !movie._snap) movie = document.getElementById('webcam_movie_embed'); + if (!movie) this.dispatch('error', "Cannot locate Flash movie in DOM"); + return movie; + }, + + snap: function() { + // take snapshot and return image data uri + if (!this.loaded) return this.dispatch('error', "Webcam is not loaded yet"); + if (!this.live) return this.dispatch('error', "Webcam is not live yet"); + + if (this.userMedia) { + // native implementation + this.context.drawImage(this.video, 0, 0, this.params.dest_width, this.params.dest_height); + return this.canvas.toDataURL('image/' + this.params.image_format, this.params.jpeg_quality / 100 ); + } + else { + // flash fallback + var raw_data = this.getMovie()._snap(); + return 'data:image/'+this.params.image_format+';base64,' + raw_data; + } + }, + + configure: function(panel) { + // open flash configuration panel -- specify tab name: + // "camera", "privacy", "default", "localStorage", "microphone", "settingsManager" + if (!panel) panel = "camera"; + this.getMovie()._configure(panel); + }, + + flashNotify: function(type, msg) { + // receive notification from flash about event + switch (type) { + case 'flashLoadComplete': + // movie loaded successfully + this.loaded = true; + this.dispatch('load'); + break; + + case 'cameraLive': + // camera is live and ready to snap + this.live = true; + this.dispatch('live'); + break; + + case 'error': + // Flash error + this.dispatch('error', msg); + break; + + default: + // catch-all event, just in case + // console.log("webcam flash_notify: " + type + ": " + msg); + break; + } + }, + + b64ToUint6: function(nChr) { + // convert base64 encoded character to 6-bit integer + // from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + return nChr > 64 && nChr < 91 ? nChr - 65 + : nChr > 96 && nChr < 123 ? nChr - 71 + : nChr > 47 && nChr < 58 ? nChr + 4 + : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; + }, + + base64DecToArr: function(sBase64, nBlocksSize) { + // convert base64 encoded string to Uintarray + // from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, + taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= this.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + return taBytes; + }, + + upload: function(image_data_uri, target_url, callback, form_elem_name='webcam', csrf=null) { + // submit image data to server using binary AJAX + if (callback) Webcam.on('uploadComplete', callback); + + // detect image format from within image_data_uri + var image_fmt = ''; + if (image_data_uri.match(/^data\:image\/(\w+)/)) + image_fmt = RegExp.$1; + else + throw "Cannot locate image format in Data URI"; + + // extract raw base64 data from Data URI + var raw_image_data = image_data_uri.replace(/^data\:image\/\w+\;base64\,/, ''); + + // contruct use AJAX object + var http = new XMLHttpRequest(); + http.open("POST", target_url, true); + + // setup progress events + if (http.upload && http.upload.addEventListener) { + http.upload.addEventListener( 'progress', function(e) { + if (e.lengthComputable) { + var progress = e.loaded / e.total; + Webcam.dispatch('uploadProgress', progress, e); + } + }, false ); + } + + // completion handler + http.onload = function() { + Webcam.dispatch('uploadComplete', http.status, http.responseText, http.statusText); + }; + + // create a blob and decode our base64 to binary + var blob = new Blob( [ this.base64DecToArr(raw_image_data) ], {type: 'image/'+image_fmt} ); + + // stuff into a form, so servers can easily receive it as a standard file upload + var form = new FormData(); + if (csrf) + form.append(csrf.name, csrf.value); + form.append( form_elem_name, blob, form_elem_name+"."+image_fmt.replace(/e/, '') ); + + // send data to server + http.send(form); + } + +}; + +Webcam.init(); diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index f6677ef8..6c46225f 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -96,6 +96,7 @@ $('.select_date').datepicker({ }).datepicker( $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}"] ); $(document).keydown(function (e) { if ($(e.target).is('input')) { return } + if ($(e.target).is('textarea')) { return } if (e.keyCode == 83) { $("#search").focus(); return false; diff --git a/core/templates/core/user_edit.jinja b/core/templates/core/user_edit.jinja index d7fa3b56..5610f02d 100644 --- a/core/templates/core/user_edit.jinja +++ b/core/templates/core/user_edit.jinja @@ -10,9 +10,21 @@ {% csrf_token %} {% for field in form %}

{{ field.errors }}

+
+ {% trans %}Take picture{% endtrans %} +
+

+ {% endif %}
{%- elif field.name == "avatar_pict" and form.instance.avatar_pict -%}
{% trans %}Current avatar: {% endtrans %}
@@ -34,6 +46,39 @@ {% endblock %} +{% block script %} + {{ super() }} + {% if not form.instance.profile_pict %} + + + {% endif %} +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 9c6c5d29..d4093f55 100644 --- a/core/urls.py +++ b/core/urls.py @@ -31,6 +31,7 @@ urlpatterns = [ url(r'^user/(?P[0-9]+)/mini$', UserMiniView.as_view(), name='user_profile_mini'), url(r'^user/(?P[0-9]+)/$', UserView.as_view(), name='user_profile'), url(r'^user/(?P[0-9]+)/edit$', UserUpdateProfileView.as_view(), name='user_edit'), + url(r'^user/(?P[0-9]+)/profile_upload$', UserUploadProfilePictView.as_view(), name='user_profile_upload'), url(r'^user/(?P[0-9]+)/groups$', UserUpdateGroupView.as_view(), name='user_groups'), url(r'^user/tools/$', UserToolsView.as_view(), name='user_tools'), url(r'^user/(?P[0-9]+)/account$', UserAccountView.as_view(), name='user_account'), diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 00000000..db6f7f82 --- /dev/null +++ b/core/utils.py @@ -0,0 +1,20 @@ +# Image utils + +from io import BytesIO +from PIL import Image + +def scale_dimension(width, height, long_edge): + if width > height: + ratio = long_edge * 1. / width + else: + ratio = long_edge * 1. / height + return int(width * ratio), int(height * ratio) + +def resize_image(im, edge, format): # TODO move that into a utils file + from django.core.files.base import ContentFile + (w, h) = im.size + (width, height) = scale_dimension(w, h, long_edge=edge) + content = BytesIO() + im.resize((width, height), Image.ANTIALIAS).save(fp=content, format=format, dpi=[72, 72]) + return ContentFile(content.getvalue()) + diff --git a/core/views/forms.py b/core/views/forms.py index d7b8107a..227ed121 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -83,26 +83,10 @@ class RegisteringForm(UserCreationForm): user.save() return user -# Image utils - +from core.utils import resize_image from io import BytesIO from PIL import Image -def scale_dimension(width, height, long_edge): - if width > height: - ratio = long_edge * 1. / width - else: - ratio = long_edge * 1. / height - return int(width * ratio), int(height * ratio) - -def resize_image(im, edge, format): - from django.core.files.base import ContentFile - (w, h) = im.size - (width, height) = scale_dimension(w, h, long_edge=edge) - content = BytesIO() - im.resize((width, height), Image.ANTIALIAS).save(fp=content, format=format, dpi=[72, 72]) - return ContentFile(content.getvalue()) - class UserProfileForm(forms.ModelForm): """ Form handling the user profile, managing the files diff --git a/core/views/user.py b/core/views/user.py index 6cdf6812..77873458 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -1,8 +1,9 @@ # This file contains all the views that concern the user model from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import logout as auth_logout, views +from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist, ValidationError from django.http import Http404 from django.views.generic.edit import UpdateView from django.views.generic import ListView, DetailView, TemplateView @@ -16,7 +17,7 @@ import logging from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin from core.views.forms import RegisteringForm, UserPropForm, UserProfileForm -from core.models import User +from core.models import User, SithFile def login(request): """ @@ -160,6 +161,35 @@ class UserListView(ListView): model = User template_name = "core/user_list.jinja" +class UserUploadProfilePictView(CanEditMixin, DetailView): + """ + Handle the upload of the profile picture taken with webcam in navigator + """ + model = User + pk_url_kwarg = "user_id" + template_name = "core/user_edit.jinja" + + def post(self, request, *args, **kwargs): + from core.utils import resize_image + from io import BytesIO + from PIL import Image + self.object = self.get_object() + if self.object.profile_pict: + raise ValidationError(_("User already has a profile picture")) + print(request.FILES['new_profile_pict']) + f = request.FILES['new_profile_pict'] + parent = SithFile.objects.filter(parent=None, name="profiles").first() + name = str(self.object.id) + "_profile.jpg" # Webcamejs uploads JPGs + im = Image.open(BytesIO(f.read())) + new_file = SithFile(parent=parent, name=name, + file=resize_image(im, 400, f.content_type.split('/')[-1]), + owner=self.object, is_folder=False, mime_type=f.content_type, size=f._size) + new_file.file.name = name + new_file.save() + self.object.profile_pict = new_file + self.object.save() + return redirect("core:user_edit", user_id=self.object.id) + class UserUpdateProfileView(CanEditMixin, UpdateView): """ Edit a user's profile diff --git a/counter/migrations/0020_auto_20160821_0307.py b/counter/migrations/0020_auto_20160821_0307.py new file mode 100644 index 00000000..768fcd30 --- /dev/null +++ b/counter/migrations/0020_auto_20160821_0307.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('counter', '0019_auto_20160820_2053'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='buying_groups', + field=models.ManyToManyField(to='core.Group', related_name='products', verbose_name='buying groups'), + ), + ] diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 38ba8a85..3a13c531 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -86,10 +86,17 @@ {% endif %}

+ {% for t in categories -%} {%- if counter.products.filter(product_type=t).exists() -%} -
{{ t }}
-
+
+
{{ t }}
{% for p in counter.products.filter(product_type=t).all() -%} {% set file = None %} {% if p.icon %} @@ -148,9 +155,7 @@ $( function() { $("#bar_ui").accordion({ heightStyle: "content", }); - $("#products").accordion({ - heightStyle: "content", - }); + $("#products").tabs(); }); {% endblock %} diff --git a/migrate.py b/migrate.py index ee435a07..688a7bc8 100644 --- a/migrate.py +++ b/migrate.py @@ -375,15 +375,10 @@ def migrate_refillings(): """) root_cust = Customer.objects.filter(user__id=0).first() mde = Counter.objects.filter(id=1).first() - threshold_date = datetime.datetime(year=2006, month=3, day=21, hour=22, minute=17) - Refilling.objects.filter(payment_method="CASH").delete() - Refilling.objects.filter(payment_method="CHECK").delete() - Refilling.objects.filter(date__lte=threshold_date.replace(tzinfo=timezone('Europe/Paris'))).delete() - print("Sith account refillings deleted") + Refilling.objects.all().delete() + print("Refillings deleted") fail = 100 for r in cur: - if r['type_paiement_rech'] == 2 and r['date_rech'] > threshold_date: - continue # There are no invoices before threshold_date when paid with credit card try: cust = Customer.objects.filter(user__id=r['id_utilisateur']).first() user = User.objects.filter(id=r['id_utilisateur']).first() @@ -607,24 +602,24 @@ def migrate_permanencies(): cur.close() def main(): - migrate_users() - migrate_profile_pict() - migrate_clubs() - migrate_club_memberships() - migrate_subscriptions() - update_customer_account() - migrate_counters() - migrate_permanencies() - migrate_typeproducts() - migrate_products() - migrate_product_pict() - migrate_products_to_counter() - reset_customer_amount() - migrate_refillings() - reset_index('counter') - migrate_invoices() - migrate_sellings() - reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter') + # migrate_users() + # migrate_profile_pict() + # migrate_clubs() + # migrate_club_memberships() + # migrate_subscriptions() + # update_customer_account() + # migrate_counters() + # migrate_permanencies() + # migrate_typeproducts() + # migrate_products() + # migrate_product_pict() + # migrate_products_to_counter() + # reset_customer_amount() + # migrate_invoices() + # migrate_refillings() + # migrate_sellings() + reset_index('core') + # reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter') if __name__ == "__main__": main() diff --git a/subscription/models.py b/subscription/models.py index 8ed0f555..4a57cb04 100644 --- a/subscription/models.py +++ b/subscription/models.py @@ -58,7 +58,7 @@ class Subscription(models.Model): self.member.make_home() def get_absolute_url(self): - return reverse('core:user_profile', kwargs={'user_id': self.member.pk}) + return reverse('core:user_edit', kwargs={'user_id': self.member.pk}) def __str__(self): if hasattr(self, "member") and self.member is not None: diff --git a/subscription/templates/subscription/subscription.jinja b/subscription/templates/subscription/subscription.jinja index 4568d655..dc1a33b8 100644 --- a/subscription/templates/subscription/subscription.jinja +++ b/subscription/templates/subscription/subscription.jinja @@ -11,8 +11,8 @@ {% csrf_token %}

{{ form.member.errors }} {{ form.member }}

-

{{ form.last_name.errors }} {{ form.last_name }}

{{ form.first_name.errors }} {{ form.first_name }}

+

{{ form.last_name.errors }} {{ form.last_name }}

{{ form.email.errors }} {{ form.email }}

{{ form.subscription_type.errors }} {{ form.subscription_type }}