Skip to content

Commit

Permalink
[front] display promo code on group management page
Browse files Browse the repository at this point in the history
  • Loading branch information
aktiur committed Nov 9, 2017
1 parent c72a810 commit 2048f74
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 7 deletions.
4 changes: 4 additions & 0 deletions src/api/settings.py
Expand Up @@ -382,3 +382,7 @@
# allow insecure transports for OAUTHLIB in DEBUG mode
if DEBUG:
os.environ.setdefault('OAUTHLIB_INSECURE_TRANSPORT', 'y')

# Get the promo
PROMO_CODE_KEY = os.environb.get(b'PROMO_CODE_KEY', b'prout')
PROMO_CODE_TAG = os.environ.get('PROMO_CODE_TAG', 'Groupe certifié')
14 changes: 14 additions & 0 deletions src/front/templates/front/groups/manage.html
Expand Up @@ -33,6 +33,20 @@ <h6 class="subhead">Contact</h6>
</ul>
</div>
</div>

{% if certified %}
<h3>Mon code promo pour ce mois-ci</h3>

<div style="text-align: center; margin: 2em auto;">
<span style="padding: 10px; font-weight: bolder; font-size: 2em; border: 2px solid darkgrey;">{{ group_promo_code }}</span>
</div>

<p>
Ce code peut être utilisé sur le <a href="https://materiel.lafranceinsoumise.fr">site d'achat de matériel</a>.
</p>

{% endif %}

<h3>Les animateurs et autres gestionnaires du groupe</h3>

<h4>Les animateurs du groupe</h4>
Expand Down
17 changes: 10 additions & 7 deletions src/front/views/groups.py
Expand Up @@ -6,9 +6,11 @@
from django.contrib import messages
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect, HttpResponseBadRequest
from django.core.urlresolvers import reverse_lazy, reverse
from django.conf import settings

from groups.models import SupportGroup, Membership
from groups.tasks import send_someone_joined_notification
from groups.actions.promo_codes import get_next_promo_code

from ..forms import SupportGroupForm, AddReferentForm, AddManagerForm, GroupGeocodingForm
from ..view_mixins import (
Expand Down Expand Up @@ -110,17 +112,18 @@ def get_forms(self):
}

def get_context_data(self, **kwargs):
referents = self.object.memberships.filter(is_referent=True).order_by('created')
managers = self.object.memberships.filter(is_manager=True, is_referent=False).order_by('created')
members = self.object.memberships.all().order_by('created')
kwargs['referents'] = self.object.memberships.filter(is_referent=True).order_by('created')
kwargs['managers'] = self.object.memberships.filter(is_manager=True, is_referent=False).order_by('created')
kwargs['members'] = self.object.memberships.all().order_by('created')
kwargs['certified'] = self.object.tags.filter(label=settings.PROMO_CODE_TAG).exists()
if kwargs['certified']:
kwargs['group_promo_code'] = get_next_promo_code(self.object)

return super().get_context_data(
referents=referents,
managers=managers,
members=members,
is_referent=self.user_membership is not None and self.user_membership.is_referent,
is_manager=self.user_membership is not None and (self.user_membership.is_referent or self.user_membership.is_manager),
**self.get_forms()
**self.get_forms(),
**kwargs
)

def get(self, request, *args, **kwargs):
Expand Down
Empty file added src/groups/actions/__init__.py
Empty file.
95 changes: 95 additions & 0 deletions src/groups/actions/promo_codes.py
@@ -0,0 +1,95 @@
import struct
import base64
import hmac
import hashlib
from datetime import date

from django.conf import settings
from django.utils import timezone

REFERENCE_DATE = date(2017, 1, 1)
GROUP_ID_SIZE = 6
SIGNATURE_SIZE = 6
DIGESTMOD = hashlib.sha1
BASE64ENC = base64.urlsafe_b64encode


def generate_date_fragment(expiration_date):
"""Generate a two-character encoding of the expiration date
That encoding is done that way:
* Compute the number of days since the REFERENCE_DATE
* encode it as two bytes (low-endian)
* left shift the second byte by four bits
It should now look that way (second hexdigit of second byte is all zero after left shift) :
xxxx xxxx xxxx 0000
Only twelve bits of data ==> can be encoded by two base64 characters
* Encode it in base64 and keep the two first characters
:param expiration_date: a date corresponding to the day the promo code should expire
:return: base64 encoding of the input date (bytes object)
"""
days = (expiration_date - REFERENCE_DATE).days
assert days < 4096 # 2^12 or the maximum value that can be set in 2 Base64 characters

# use little-endian for packing
b = bytearray(struct.pack('<I', days)[:2])
b[1] <<= 4

return BASE64ENC(b)[:2]


def generate_msg_part_for_group(group, expiration_date):
"""Generate the msg part of the promo code for a specific group
The msg part is made of :
* the expiration date (as 2 base64 characters)
* 6 characters corresponding to the support group id
:param group: the group for which the promo code must be generated
:param expiration_date: the expiration date for the promo code
:return:
"""
date_fragment = generate_date_fragment(expiration_date)

# let's hash the group part to make sure it works whatever the uuid generation mode
# keep only the strictly minimum number of bytes
keep_bytes = (GROUP_ID_SIZE * 3 // 4) + 1
group_bytes = DIGESTMOD(group.pk.bytes).digest()[:keep_bytes]

# let's use the first GROUP_ID_SIZE characters of the base64 encoding
group_fragment = BASE64ENC(group_bytes)[:GROUP_ID_SIZE]

return date_fragment + group_fragment


def sign_code(msg):
keep_bytes = (SIGNATURE_SIZE * 3 // 4) + 1
sig_bytes = hmac.new(
key=settings.PROMO_CODE_KEY,
msg=msg,
digestmod=DIGESTMOD
).digest()[:keep_bytes]

signature_frag = BASE64ENC(sig_bytes)[:SIGNATURE_SIZE]

return msg + signature_frag


def generate_code_for_group(group, expiration_date):
msg = generate_msg_part_for_group(group, expiration_date)
return sign_code(msg)


def get_next_promo_code(group):
today = timezone.now().astimezone(timezone.get_default_timezone())

if today.month == 12:
expiration_date = date(today.year+1, 1, 1)
else:
expiration_date = date(today.year, today.month+1, 1)

return generate_code_for_group(group, expiration_date)

0 comments on commit 2048f74

Please sign in to comment.