Commit 9b8caa7f authored by ynerant's avatar ynerant
Browse files

Merge branch 'beta' into 'master'

Add animated profile picture support

See merge request !116
parents f1dac73c fa3c7231
Pipeline #8699 passed with stages
in 13 minutes and 19 seconds
......@@ -188,6 +188,12 @@ class Entry(models.Model):
verbose_name = _("entry")
verbose_name_plural = _("entries")
def __str__(self):
return _("Entry for {guest}, invited by {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity)) if self.guest \
else _("Entry for {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity))
def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
......
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('logs', '0001_initial'),
]
operations = [
migrations.RunSQL(
"UPDATE logs_changelog SET previous = '' WHERE previous IS NULL;"
),
migrations.RunSQL(
"UPDATE logs_changelog SET data = '' WHERE data IS NULL;"
),
]
# Generated by Django 2.2.16 on 2020-09-06 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0002_replace_null_by_blank'),
]
operations = [
migrations.AlterField(
model_name='changelog',
name='data',
field=models.TextField(blank=True, default='', verbose_name='new data'),
),
migrations.AlterField(
model_name='changelog',
name='previous',
field=models.TextField(blank=True, default='', verbose_name='previous data'),
),
]
......@@ -44,12 +44,14 @@ class Changelog(models.Model):
)
previous = models.TextField(
null=True,
blank=True,
default="",
verbose_name=_('previous data'),
)
data = models.TextField(
null=True,
blank=True,
default="",
verbose_name=_('new data'),
)
......@@ -80,3 +82,7 @@ class Changelog(models.Model):
class Meta:
verbose_name = _("changelog")
verbose_name_plural = _("changelogs")
def __str__(self):
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
......@@ -50,7 +50,7 @@ def save_object(sender, instance, **kwargs):
in order to store each modification made
"""
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
# noinspection PyProtectedMember
......@@ -99,7 +99,7 @@ def save_object(sender, instance, **kwargs):
model = instance.__class__
fields = changed_fields
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else ""
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
Changelog.objects.create(user=user,
......@@ -117,7 +117,7 @@ def delete_object(sender, instance, **kwargs):
Each time a model is deleted, an entry in the table `Changelog` is added in the database
"""
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
......@@ -149,6 +149,6 @@ def delete_object(sender, instance, **kwargs):
model=ContentType.objects.get_for_model(instance),
instance_pk=instance.pk,
previous=instance_json,
data=None,
data="",
action="delete"
).save()
......@@ -3,7 +3,7 @@
import io
from PIL import Image
from PIL import Image, ImageSequence
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
......@@ -20,7 +20,7 @@ from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField(
label="Masque de permissions",
label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("rank"),
empty_label=None,
)
......@@ -82,13 +82,19 @@ class ImageForm(forms.Form):
height = forms.FloatField(widget=forms.HiddenInput())
def clean(self):
"""Load image and crop"""
"""
Load image and crop
In the future, when Pillow will support APNG we will be able to
simplify this code to save only PNG/APNG.
"""
cleaned_data = super().clean()
# Image size is limited by Django DATA_UPLOAD_MAX_MEMORY_SIZE
image = cleaned_data.get('image')
if image:
# Let Pillow detect and load image
# If it is an animation, then there will be multiple frames
try:
im = Image.open(image)
except OSError:
......@@ -96,20 +102,30 @@ class ImageForm(forms.Form):
# but Pil is unable to load it
raise forms.ValidationError(_('This image cannot be loaded.'))
# Crop image
# Crop each frame
x = cleaned_data.get('x', 0)
y = cleaned_data.get('y', 0)
w = cleaned_data.get('width', 200)
h = cleaned_data.get('height', 200)
im = im.crop((x, y, x + w, y + h))
im = im.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.ANTIALIAS,
)
frames = []
for frame in ImageSequence.Iterator(im):
frame = frame.crop((x, y, x + w, y + h))
frame = frame.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.ANTIALIAS,
)
frames.append(frame)
# Save
om = frames.pop(0) # Get first frame
om.info = im.info # Copy metadata
image.file = io.BytesIO()
im.save(image.file, "PNG")
if len(frames) > 1:
# Save as GIF
om.save(image.file, "GIF", save_all=True, append_images=list(frames), loop=0)
else:
# Save as PNG
om.save(image.file, "PNG")
return cleaned_data
......
......@@ -27,8 +27,8 @@ def create_bde_and_kfet(apps, schema_editor):
parent_club_id=1,
email="tresorerie.bde@example.com",
require_memberships=True,
membership_fee_paid=500,
membership_fee_unpaid=500,
membership_fee_paid=3500,
membership_fee_unpaid=3500,
membership_duration=396,
membership_start="2020-08-01",
membership_end="2021-09-30",
......
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('member', '0003_create_bde_and_kfet'),
]
operations = [
migrations.RunSQL(
"UPDATE member_profile SET address = '' WHERE address IS NULL;",
),
migrations.RunSQL(
"UPDATE member_profile SET ml_events_registration = '' WHERE ml_events_registration IS NULL;",
),
migrations.RunSQL(
"UPDATE member_profile SET section = '' WHERE section IS NULL;",
),
]
# Generated by Django 2.2.16 on 2020-09-06 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0004_replace_null_by_blank'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='address',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='address'),
),
migrations.AlterField(
model_name='profile',
name='ml_events_registration',
field=models.CharField(blank=True, choices=[('', 'No'), ('fr', 'Yes (receive them in french)'), ('en', 'Yes (receive them in english)')], default='', max_length=2, verbose_name='Register on the mailing list to stay informed of the events of the campus (1 mail/week)'),
),
migrations.AlterField(
model_name='profile',
name='section',
field=models.CharField(blank=True, default='', help_text='e.g. "1A0", "9A♥", "SAPHIRE"', max_length=255, verbose_name='section'),
),
]
......@@ -46,7 +46,7 @@ class Profile(models.Model):
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
max_length=255,
blank=True,
null=True,
default="",
)
department = models.CharField(
......@@ -83,7 +83,7 @@ class Profile(models.Model):
verbose_name=_('address'),
max_length=255,
blank=True,
null=True,
default="",
)
paid = models.BooleanField(
......@@ -94,11 +94,10 @@ class Profile(models.Model):
ml_events_registration = models.CharField(
blank=True,
null=True,
default=None,
default='',
max_length=2,
choices=[
(None, _("No")),
('', _("No")),
('fr', _("Yes (receive them in french)")),
('en', _("Yes (receive them in english)")),
],
......
......@@ -6,7 +6,7 @@ def save_user_profile(instance, created, raw, **_kwargs):
"""
Hook to create and save a profile when an user is updated if it is not registered with the signup form
"""
if not raw and created and instance.is_active:
if not raw and created and instance.is_active and not hasattr(instance, "_no_signal"):
from .models import Profile
Profile.objects.get_or_create(user=instance)
if instance.is_superuser:
......
......@@ -48,10 +48,10 @@
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }})
{% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }})
</a>
</dd>
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-8"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
</dl>
\ No newline at end of file
</dl>
......@@ -138,7 +138,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
We can't display information of a not registered user.
"""
return super().get_queryset().filter(profile__registration_valid=True)
return super().get_queryset(**kwargs).filter(profile__registration_valid=True)
def get_context_data(self, **kwargs):
"""
......@@ -271,9 +271,17 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
def form_valid(self, form):
"""Save image to note"""
image_field = form.cleaned_data['image']
image_field.name = "{}_pic.png".format(self.object.note.pk)
self.object.note.display_image = image_field
image = form.cleaned_data['image']
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else:
image.name = "{}_pic.png".format(self.object.note.pk)
# Save
self.object.note.display_image = image
self.object.note.save()
return super().form_valid(form)
......
......@@ -3,7 +3,7 @@
from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_delete
from django.utils.translation import gettext_lazy as _
from . import signals
......@@ -25,3 +25,8 @@ class NoteConfig(AppConfig):
signals.save_club_note,
sender='member.Club',
)
pre_delete.connect(
signals.delete_transaction,
sender='note.transaction',
)
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('note', '0002_create_special_notes'),
]
operations = [
migrations.RunSQL(
"UPDATE note_note SET inactivity_reason = '' WHERE inactivity_reason IS NULL;"
),
migrations.RunSQL(
"UPDATE note_transaction SET invalidity_reason = '' WHERE invalidity_reason IS NULL;"
),
]
# Generated by Django 2.2.16 on 2020-09-06 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('note', '0003_replace_null_by_blank'),
]
operations = [
migrations.AlterField(
model_name='note',
name='inactivity_reason',
field=models.CharField(blank=True, choices=[('manual', 'The user blocked his/her note manually, eg. when he/she left the school for holidays. It can be reactivated at any time.'), ('forced', "The note is blocked by the the BDE and can't be manually reactivated.")], default='', max_length=255),
),
migrations.AlterField(
model_name='transaction',
name='invalidity_reason',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='invalidity reason'),
),
]
......@@ -70,8 +70,8 @@ class Note(PolymorphicModel):
"It can be reactivated at any time.")),
('forced', _("The note is blocked by the the BDE and can't be manually reactivated.")),
],
null=True,
default=None,
blank=True,
default="",
)
class Meta:
......
......@@ -90,6 +90,9 @@ class TransactionTemplate(models.Model):
def get_absolute_url(self):
return reverse('note:template_update', args=(self.pk,))
def __str__(self):
return self.name
class Transaction(PolymorphicModel):
"""
......@@ -150,8 +153,7 @@ class Transaction(PolymorphicModel):
invalidity_reason = models.CharField(
verbose_name=_('invalidity reason'),
max_length=255,
default=None,
null=True,
default='',
blank=True,
)
......@@ -173,7 +175,7 @@ class Transaction(PolymorphicModel):
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
if not created and not self.valid and not hasattr(self, "_force_save"):
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
# Check that nothing important changed
......@@ -195,7 +197,7 @@ class Transaction(PolymorphicModel):
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
self.invalidity_reason = ""
if source_balance > 9223372036854775807 or source_balance < -9223372036854775808\
or dest_balance > 9223372036854775807 or dest_balance < -9223372036854775808:
......@@ -242,14 +244,6 @@ class Transaction(PolymorphicModel):
self.destination._force_save = True
self.destination.save()
def delete(self, **kwargs):
"""
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
"""
self.valid = False
self.save(**kwargs)
super().delete(**kwargs)
@property
def total(self):
return self.amount * self.quantity
......
......@@ -6,7 +6,8 @@ def save_user_note(instance, raw, **_kwargs):
"""
Hook to create and save a note when an user is updated
"""
if not raw and (instance.is_superuser or instance.profile.registration_valid):
if not raw and (instance.is_superuser or instance.profile.registration_valid)\
and not hasattr(instance, "_no_signal"):
# Create note only when the registration is validated
from note.models import NoteUser
NoteUser.objects.get_or_create(user=instance)
......@@ -17,10 +18,17 @@ def save_club_note(instance, raw, **_kwargs):
"""
Hook to create and save a note when a club is updated
"""
if raw:
# When provisionning data, do not try to autocreate
return
# When provisionning data, do not try to autocreate
if not raw and not hasattr(instance, "_no_signal"):
from .models import NoteClub
NoteClub.objects.get_or_create(club=instance)
instance.note.save()
from .models import NoteClub
NoteClub.objects.get_or_create(club=instance)
instance.note.save()
def delete_transaction(instance, **_kwargs):
"""
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
"""
if not hasattr(instance, "_no_signal"):
instance.valid = False
instance.save()
{% load pretty_money %}
{% load getenv %}
{% load render_table from django_tables2 %}
{% load static %}
{% load i18n %}
<!DOCTYPE html>
......@@ -8,13 +10,8 @@
<meta charset="UTF-8">
<title>[Note Kfet] Rapport de la Note Kfet</title>
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://{{ "NOTE_URL"|getenv }}{% static "bootstrap4/css/bootstrap.min.css" %}">
<script src="https://{{ "NOTE_URL"|getenv }}{% static "bootstrap4/js/bootstrap.min.js" %}"></script>
</head>
<body>
<p>
......@@ -27,7 +24,7 @@
Ensuite, un rapport vous est envoyé à la fréquence demandée seulement si vous avez consommé
depuis le dernier rapport.<br>
Pour arrêter de recevoir des rapports, il vous suffit de modifier votre profil Note et de
mettre la fréquence des rapports à 0 ou -1.<br>
mettre la fréquence des rapports à 0.<br>
Pour toutes suggestions par rapport à ce service, contactez
<a href="mailto:notekfet2020@lists.crans.org">notekfet2020@lists.crans.org</a>.
</p>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment