Commit 3816e0ed authored by frekk's avatar frekk

add cokelog parsing and bulk "refresh payment status" admin action

parent 6150dbb4
......@@ -99,8 +99,12 @@ SQUARE_APP_ID = '${SQUARE_APP_ID}'
# path to the OpenDispense2 client binary
DISPENSE_BIN = '/usr/local/bin/dispense'
# path to the OpenDispense2 logfile
COKELOG_PATH = ROOT_DIR + '/cokelog'
# configure the email backend (see
......@@ -4,6 +4,9 @@ from functools import wraps, singledispatch
from django.db.models import FieldDoesNotExist
from django.http import HttpResponse
from django.contrib import messages
from squarepay.cokelog import try_update_from_dispense
def prep_field(obj, field):
......@@ -120,3 +123,21 @@ def _(description):
return download_as_csv(modeladmin, request, queryset)
wrapped_action.short_description = description
return wrapped_action
def refresh_dispense_payment(modeladmin, request, queryset):
""" update paid status from cokelog, for Membership model """
num_changed = 0
membership_list = list(queryset)
for ms in membership_list:
if ms.date_paid is not None:
if try_update_from_dispense(ms):
num_changed += 1
if num_changed > 0:
messages.success(request, "Updated %d records" % num_changed)
messages.warning(request, "No records updated")
refresh_dispense_payment.short_description = "Update payment status from cokelog"
\ No newline at end of file
......@@ -8,17 +8,15 @@ from django.template.loader import render_to_string
from gms import admin
from memberdb.models import Member, IncAssocMember, Membership
from memberdb.actions import download_as_csv
from memberdb.actions import download_as_csv, refresh_dispense_payment
from memberdb.approve import MembershipApprovalForm, MembershipApprovalAdminView
from memberdb.account import AccountForm, AccountView
def get_model_url(pk, model_name):
return reverse('admin:memberdb_%s_change' % model_name, args=[pk])
helper mixin to make the admin page display only "View" rather than "Change" or "Add"
class ReadOnlyModelAdmin(admin.ModelAdmin):
""" helper mixin to make the admin page display only "View" rather than "Change" or "Add" """
def has_add_permission(self, request):
return False
......@@ -28,10 +26,10 @@ class ReadOnlyModelAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
return False
Define the administrative interface for viewing member details required under the Incorporations Act
class IAMemberAdmin(ReadOnlyModelAdmin):
Define the administrative interface for viewing member details required under the Incorporations Act
readonly_fields = ['__str__', 'updated', 'created']
fields = ['first_name', 'last_name', 'email_address', 'updated', 'created']
search_fields = ['first_name', 'last_name', 'email_address']
......@@ -66,14 +64,11 @@ class MemberAdmin(admin.ModelAdmin):
return custom_urls + urls
# add a "go to member" URL into the template context data
def change_view(self, request, object_id, form_url='', extra_context={}):
extra_context['incassocmember_url'] = get_model_url(object_id, 'incassocmember')
return super().change_view(request, object_id, form_url, extra_context=extra_context)
def process_account(self, request, *args, **kwargs):
inst = Member.objects.get(pk=kwargs['object_id'])
model_dict = {
......@@ -82,17 +77,16 @@ class MemberAdmin(admin.ModelAdmin):
return AccountView.as_view(object=inst,admin=self)(request, *args, **kwargs)
Define the admin page for viewing normal Member records (all details included) and approving them
class MembershipAdmin(admin.ModelAdmin):
Define the admin page for viewing normal Member records (all details included) and approving them
list_display = ['membership_info', 'membership_type', 'payment_method', 'approved', 'date_submitted', 'member_actions', ]
list_display_links = None
list_filter = ['approved']
readonly_fields = ['date_submitted']
radio_fields = {'payment_method': admin.VERTICAL, 'membership_type': admin.VERTICAL}
actions = [refresh_dispense_payment]
# make the admin page queryset preload the parent records (Member)
def get_queryset(self, request):
......@@ -143,10 +137,11 @@ class MembershipAdmin(admin.ModelAdmin):
def process_approve(self, request, *args, **kwargs):
return MembershipApprovalAdminView.as_view(admin=self)(request, *args, **kwargs)
Register multiple ModelAdmins per model. See
class ProxyMembership(Membership):
Register multiple ModelAdmins per model.
class Meta:
proxy = True
# script to check if a user has already paid via dispense (or purchased an item in dispense)
# prints either a date like "Feb 25 17:25:23" or "None"
PURCHASE=$(grep "for $USER" $LOG | grep ": dispense '" | grep "$ITEM")
if [ "x$PURCHASE" == "x" ] || [ $(echo $PURCHASE | wc -l) -gt 1 ]; then
echo None
exit 1
echo $PURCHASE | cut -c1-15
""" functions to deal with the dispense cokelog, primarily to parse and determine usernames and membership type of paid members """
import re
from datetime import datetime
from django.conf import settings
from .payments import log
COKELOG = getattr(settings, "COKELOG_PATH", None)
if COKELOG is None:
log.warning("COKELOG_PATH is not defined, cannot sync payment status from dispense to DB")
ALL_REGEX = r"^(?P<date>[A-Za-z]{3}\s+\d+\s[\d:]{8})\s(\w+)\sodispense2:\sdispense '([^']+)' \((?P<item>(coke|pseudo|snack|door):(\d{1,3}))\) for (?P<for>\w+) by (?P<by>\w+) \[cost\s+(\d+), balance\s+(\d+)\]$"
MEMBERSHIP_REGEX = r"^(?P<date>[A-Za-z]{3}\s+\d+\s[\d:]{8})\s(\w+)\sodispense2:\sdispense '([^']+)' \((?P<item>(pseudo):(\d{1,3}))\) for (?P<for>\w+) by (?P<by>\w+) \[cost\s+(\d+), balance\s+(\d+)\]$"
class CokeLog:
regex = ALL_REGEX
# dictionary (keyed by username) of lists of dispense records (by-user, and date)
dispenses = {}
filename = COKELOG
file = None
last_offset = 0
def __init__(self, **kwargs):
if "filename" in kwargs:
self.filename = kwargs.pop("filename")
if "regex" in kwargs:
self.regex = kwargs.pop("regex")
def is_loaded(self):
return self.file is not None
def open(self):
""" loads the cokelog, trying to avoid parsing the whole thing again """
if not self.is_loaded():
# open the logfile if we haven't already
self.file = open(self.filename, 'r')
def reload(self):
if not self.is_loaded():
return None
def parse(self):
""" read the cokelog, starting where we left off """
pat = re.compile(self.regex)
year =
# set file offset
while True:
line = self.file.readline()
if line == '':
m = pat.match(line)
if m is not None:
data = {
'date': datetime.strptime("date"), "%b %d %H:%M:%S").replace(year=year),
user ="for")
if user in self.dispenses:
self.dispenses[user] += [data]
self.dispenses[user] = [data]
#log.debug("got dispense item for user %s, item %s on date %s" % (user, data['item'], data['date']))
# remember the latest file offset
self.last_offset = self.file.tell()
def get_last_dispense(self, username, item_code=None, dispense_by=None):
if self.dispenses is None:
return None
for r in reversed(self.dispenses[username]):
if item_code is not None and r["item"] != item_code:
if dispense_by is not None and r["by"] != dispense_by:
return r["date"]
return None
# create a "static" instance of cokelog
member_cokelog = CokeLog(regex=MEMBERSHIP_REGEX)
def try_update_from_dispense(membership):
updates the membership with payment details from dispense, if found
Note: this WILL overwrite any existing payment information
# check if anything has happened since last time
if member_cokelog.is_loaded():
# look for entries like "dispense 'membership ...' (pseudo:) for <user> by <user> ..."
ms_disp = member_cokelog.get_last_dispense(
if ms_disp is not None:
membership.date_paid = ms_disp
membership.payment_method = 'dispense'
return True
return False
\ No newline at end of file
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