Changeset - d4b0b52253ae
[Not reviewed]
0 9 0
Joel Addison - 4 years ago 2020-01-04 14:34:17
joel@addison.net.au
Registrasion updates

Show applied vouchers on attendee registration admin page.
Add head title and page title to more areas.
Update the dashboard to show warning for empty categories and have
button to open voucher page.
Fix exception on attendee page when user does not have attendee object.
9 files changed with 79 insertions and 23 deletions:
0 comments (0 inline, 0 general)
pinaxcon/templates/registrasion/_items_list.html
Show inline comments
 
{% if items %}
 
  <ul>
 
    {% for item in items %}
 
      <li>{{ item.quantity }} &times; {{ item.product }} {{ suffix }}</li>
 
    {% endfor %}
 
  </ul>
 
{% else %}
 
  <p>No items.</p>
 
{% endif %}
pinaxcon/templates/registrasion/amend_registration.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load registrasion_tags %}
 

	
 
{% block content %}
 

	
 
<h2>Item summary for {{ user.attendee.attendeeprofilebase.attendee_name }}
 
  (id={{user.id}})</h2>
 

	
 
<h3>Paid Items</h3>
 

	
 
<p>You cannot remove paid items from someone's registration. You must first
 
  cancel the invoice that added those items. You will need to re-add the items
 
  from that invoice for the user to have them available again.</p>
 

	
 
{% include "registrasion/_items_list.html" with items=paid %}
 

	
 
<br />
 

	
 
<h3>Cancelled Items</h3>
 

	
 
{% include "registrasion/_items_list.html" with items=cancelled %}
 

	
 
<br />
 

	
 
<h3>Amend pending items</h3>
 

	
 
<form class="form-horizontal" method="POST">
 
  {% csrf_token %}
 
  {% include "_form_snippet.html" with form=form %}
 
  <br/>
 
  <input class="btn btn-primary" type="submit">
 
</form>
 

	
 
<br />
 

	
 
<h3>Generate invoice</h3>
 

	
 
<div class="btn-group">
 
  <a class="btn btn-primary" href="{% url "checkout" user.id %}">Check out cart and view invoice</a>
 
</div>
 

	
 
<br />
 

	
 
<h3>Currently applied vouchers</h3>
 

	
 
{% if vouchers %}
 
<ul>
 
  {% for voucher in vouchers %}
 
    <li>{{ voucher.code }}</li>
 
  {% endfor %}
 
</ul>
 
{% else %}
 
<p>No vouchers applied.</p>
 
{% endif %}
 

	
 
<br />
 

	
 
<h3>Apply voucher</h3>
 

	
 
<form class="form-horizontal" method="POST">
 
  {% csrf_token %}
 
  {% include "_form_snippet.html" with form=voucher_form %}
 
  <br/>
 
  <input class="btn btn-primary" type="submit">
 
</form>
 

	
 
{% endblock %}
pinaxcon/templates/registrasion/profile_form.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load lca2018_tags %}
 

	
 

	
 
{% block head_title %}Your Profile{% endblock %}
 
{% block page_title %}Your Profile{% endblock %}
 

	
 
{% block scripts_extra %}
 
{{ form.media.js }}
 
<script type="text/javascript">
 
    postcode_label = $("label[for='id_profile-state']");
 
    postcode_help = $("#id_profile-state + p");
 
      $('#id_profile-country').change(function () {
 
        if ($(this).val() == 'AU' )  {
 
          postcode_label.addClass('label-required');
 
          postcode_help.show();
 
        } else {
 
          postcode_label.removeClass('label-required');
 
          postcode_help.hide();
 
        } });
 
        $("#id_profile-country").change();
 

	
 
      </script>
 
{% endblock %}
 

	
 
{% block proposals_body %}
 
  <h1>Your Profile</h1>
 
  <p>These details will appear on your badge, your invoices, and will be used to order catered food at the conference.</p>
 

	
 
  <form class="form-horizontal" method="post" action="">
 
    {% csrf_token %}
 
    <fieldset>
 
      {% include "_form_snippet.html" with form=form %}
 
      <br />
 
      <input class="btn btn-primary" type="submit" value="Save Profile" />
 
      {% if user.attendee %}
 
      <a class="btn btn-light" href="{% url "dashboard" %}">Cancel</a>
 
      {% endif %}
 
    </fieldset>
 
  </form>
 
{% endblock %}
pinaxcon/templates/registrasion/review.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 

	
 
{% block page_title %}Review your selection{% endblock %}
 
{% block page_lead %}
 
  Please ensure that you have selected all of the products you require, including
 
  t-shirts and social event tickets.
 
{% endblock %}
 

	
 
{% block scripts_extra %}
 
  {{ voucher_form.media.js }}
 
  {{ form.media.js }}
 
{% endblock %}
 

	
 

	
 
{% block proposals_body %}
 
  <h1 class="mb-4">Order Review</h1>
 
  <h2 class="mb-4">Order Review</h2>
 

	
 
  {% items_pending as pending %}
 
  {% if pending %}
 
  <div class="my-4">
 
    <h2>Current selection</h2>
 
    <h3>Current selection</h3>
 
    <p>You've selected the following items, which will be in your invoice when you check out:<p>
 
    {% include "registrasion/_items_list.html" with items=pending %}
 
  </div>
 
  {% endif %}
 

	
 
  {% items_purchased as purchased %}
 
  {% if purchased %}
 
  <div class="my-4">
 
    <h2>Previously purchased</h2>
 
    <h3>Previously purchased</h3>
 
    <p>You've already paid for the following items:</p>
 
    {% include "registrasion/_items_list.html" with items=purchased suffix="<em>(PAID)</em>" %}
 
  </div>
 
  {% endif %}
 

	
 
  <div class="my-4">
 
    <h2>Modify your selection</h2>
 
    <h3>Modify your selection</h3>
 

	
 
    {% missing_categories as missing %}
 
    {% if missing %}
 
    <div class="alert alert-warning my-4 pb-4">
 
      <h4 class="alert-heading">You have empty categories</h4>
 
      <p>You have <em>not</em> selected anything from the following
 
        categories. If your ticket includes any of these, you still need to
 
        make a selection:
 
      </p>
 

	
 
      {% include "registrasion/_category_list.html" with categories=missing %}
 
    </div>
 
    {% endif %}
 

	
 
    <p>
 
      <strong>You can change your selection from these categories:</strong>
 
      {% available_categories as available %}
 
      {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
    </p>
 
    {% available_categories as available %}
 
    {% if available|contains_items_not_in:missing %}
 
    <p><strong>You can change your selection from these categories:</strong></p>
 
    {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
    {% endif %}
 
  </div>
 

	
 
  <div class="my-4">
 
    <h2>Voucher</h2>
 
    <h3>Voucher</h3>
 
    <p>If you have been given a voucher, please <a id="voucher-form-button" href="{% url "voucher_code" %}">enter your voucher code</a> now.
 
    </p>
 
  </div>
 

	
 
  <div class="my-4">
 
    <h2>What next?</h2>
 
    <h3>What next?</h3>
 
    {% if pending %}
 
    <p>You can either check out an invoice and pay for your selections, or return to
 
        the dashboard.</p>
 

	
 
    <a class="btn btn-primary" href="{% url "checkout" %}">
 
      <i class="fa fa-credit-card"></i> Check out and pay
 
    </a>
 

	
 
    <a class="btn btn-light" href="{% url "dashboard" %}">Return to dashboard</a>
 

	
 
    {% else %}
 

	
 
    <p>You have no items that need to be paid.</p>
 

	
 
    <div class="form-actions">
 
      <a class="btn btn-light" href="{% url "dashboard" %}">Return to dashboard</a>
 
    </div>
 

	
 
    {% endif %}
 
  </div>
 

	
 
{% endblock %}
pinaxcon/templates/registrasion/voucher_code.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load crispy_forms_tags %}
 

	
 
{% block head_title %}Apply Voucher{% endblock %}
 
{% block page_title %}Apply Voucher{% endblock %}
 

	
 
{% block proposals_body %}
 
  <form method="post" action="">
 
    {% csrf_token %}
 

	
 
    <div class="panel panel-primary">
 
      <div class="panel-body">
 
        {{ voucher_form|crispy}}
 
      </div>
 

	
 
      <div class="panel-footer">
 
        <input class="btn btn-primary" type="submit" value="Apply voucher code" />
 
        <a href="{% url "dashboard" %}" class="btn btn-default">Back to dashboard</a>
 
      </div>
 
    </div>
 
  </form>
 

	
 
{% endblock %}
pinaxcon/templates/symposion/dashboard/_categories.html
Show inline comments
 
{% load i18n %}
 
{% load proposal_tags %}
 
{% load review_tags %}
 
{% load teams_tags %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 
{% load staticfiles %}
 
{% load waffle_tags %}
 

	
 
{% if user.is_staff %}
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Administration" %}</h2>
 
      <p>The following administrative tools are available to you:
 
        <ul class="list-unstyled">
 
          <li><a href="{% url "reports_list" %}">Reports</a></li>
 
        </ul>
 
      </p>
 
    </div>
 
  </div>
 
</div>
 
{% endif %}
 

	
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Attend" %} {% conference_name %}</h2>
 
    </div>
 
  </div>
 

	
 
  {% if not user.attendee.completed_registration %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3>Register</h3>
 
      <p>To attend the conference, you must create an attendee profile and purchase your ticket</p>
 
      <div>
 
        <a class="btn btn-lg btn-primary" role="button" href="{% url "guided_registration" %}">Get your ticket</a>
 
      </div>
 
    </div>
 
  </div>
 
  {% else %}
 
  <div class="row">
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Attendee Profile</h3>
 
      <p>If you would like to change the details on your badge or your attendee statistics, you may edit your attendee profile here.</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="{% url "attendee_edit" %}">Edit attendee profile</a>
 
        {% flag "badge_preview" %}
 
        <a class="btn btn-info" role="button" href="{% url "user_badge" %}">Preview my badge</a>
 
        {% endflag %}
 
      </div>
 
    </div>
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Account Management</h3>
 
      <p>If you would like to change your registered email address or password, you can use our self-service account management portal</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="https://login.linux.conf.au/manage/">Account Management</a>
 
      </div>
 
    </div>
 
  </div>
 

	
 
  {% items_pending as pending %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3 class="my-3">Account</h3>
 
    </div>
 
  </div>
 

	
 
  <div class="row">
 
    {% if pending %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Items pending payment</h4>
 
      {% include "registrasion/_items_list.html" with items=pending %}
 
      <a class="btn btn-lg btn-primary" role="button" href="{% url "checkout" %}">Check out and pay</a>
 
      <a class="btn btn-primary" role="button" href="{% url "checkout" %}"><i class="fa fa-credit-card"></i> Check out and pay</a>
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
    </div>
 
    {% endif %}
 

	
 
    {% items_purchased as purchased %}
 
    {% if purchased %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Paid Items</h4>
 
      {% include "registrasion/_items_list.html" with items=purchased %}
 

	
 
      {% if not pending %}
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
      {% endif %}
 
    </div>
 
    {% endif %}
 

	
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Add/Update Items</h4>
 
      {% include "registrasion/_category_list.html" with categories=categories %}
 
      {% missing_categories as missing %}
 
      {% if missing %}
 
      <div class="alert alert-warning my-4 pb-4">
 
        <h5 class="alert-heading">You have empty categories</h5>
 
        <p>You have <em>not</em> selected anything from the following
 
          categories. If your ticket includes any of these, you still need to
 
          make a selection:
 
        </p>
 

	
 
        {% include "registrasion/_category_list.html" with categories=missing %}
 
      </div>
 
      {% endif %}
 

	
 
      {% available_categories as available %}
 
      {% if available|contains_items_not_in:missing %}
 
      <p><strong>You can change your selection from these categories:</strong></p>
 
      {% include "registrasion/_category_list.html" with categories=available exclude=missing %}
 
      {% endif %}
 
    </div>
 

	
 
    {% invoices as invoices %}
 
    {% if invoices %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Invoices</h4>
 
      <ul>
 
        {% for invoice in invoices %}
 
        <li{% if invoice.is_void %} class="void-invoice" style="display: none;"{% endif %}>
 
          <a href="{% url "invoice" invoice.id %}" >Invoice {{ invoice.id }}</a> - ${{ invoice.value }} ({{ invoice.get_status_display }})
 
        </li>
 
        {% endfor %}
 
      </ul>
 
      {% if invoices|any_is_void %}
 
      <div class="mt-auto">
 
        <button id="toggle-void-invoices" onclick="toggleVoidInvoices();" class="btn btn-lg btn-default">Show void invoices</button>
 
        <button type="button" class="btn btn-sm btn-outline-dark" id="toggle-void-invoices" onclick="toggleVoidInvoices();">Show void invoices</button>
 
      </div>
 
      {% endif %}
 
    </div>
 
    {% endif %}
 

	
 
    {% if false %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Raffle Tickets</h4>
 

	
 
      <p><a href="/raffle/tickets/">View all my raffle tickets</a></p>
 
      {# REMOVE HARDCODED CATEGORY NUMBER!!!! #}
 
      <p><a href="/tickets/category/8">Buy raffle tickets</a></p>
 
    </div>
 
    {% endif %}
 

	
 
    {% available_credit as credit %}
 
    {% if credit %}
 
    <div class="col-md-6 mb-3 mb-md-0">
 
    <div class="col-md-6 mb-3">
 
      <h4>Credit</h4>
 
      <p>You have ${{ credit }} leftover from refunded invoices. This credit will be automatically applied to new invoices. Contact the conference organisers if you wish to arrange a refund to your original payment source.</p>
 
    </div>
 
    {% endif %}
 
  </div>
 
  {% endif %} {# user.attendee.completed_registration #}
 
</div>
pinaxcon/templatetags/lca2019_tags.py
Show inline comments
...
 
@@ -3,66 +3,71 @@ from django.forms import Form
 
import re
 

	
 

	
 
register = template.Library()
 

	
 

	
 
@register.filter
 
def has_required_fields(form):
 
    for field in form:
 
        if isinstance(field, Form):
 
            if has_required_fields(field):
 
                return True
 
        if field.field.required:
 
            return True
 
    return False
 

	
 

	
 
@register.filter
 
def has_price_fields(form):
 
    for field in form:
 
        if isinstance(field, Form):
 
            return has_price_fields(field)
 

	
 
        help_text = field.field.help_text or ''
 
        if '$' in help_text:
 
            return True
 

	
 
        label = field.field.label or ''
 
        if '$' in label:
 
            return True
 

	
 
        choices = getattr(field.field, 'choices', [])
 
        if choices:
 
            for choice_id, choice_text in choices:
 
                if '$' in choice_text:
 
                    return True
 

	
 
    return False
 

	
 

	
 
@register.filter
 
def any_is_void(invoices):
 
    for invoice in invoices:
 
        if invoice.is_void:
 
            return True
 
    return False
 

	
 

	
 
@register.filter
 
def contains_items_not_in(list1, list2):
 
    return len(set(list1).difference(list2)) > 0
 

	
 

	
 
@register.filter
 
def listlookup(lookup, target):
 
    try:
 
        return lookup[target]
 
    except IndexError:
 
        return None
 

	
 

	
 
@register.filter
 
def clean_text(txt):
 
    # Remove double/triple/+ spaces from `txt` and replace with single space
 
    return re.sub(r' {2,}' , ' ', txt)
 

	
 

	
 
@register.filter
 
def twitter_handle(txt):
 
    # Add @ to twitter handle if not present
 
    return txt if txt.startswith('@') else '@{}'.format(txt)
vendor/registrasion/registrasion/reporting/views.py
Show inline comments
...
 
@@ -517,99 +517,103 @@ def credit_notes(request, form):
 
        "invoice__user__attendee__attendeeprofilebase",
 
    )
 

	
 
    return QuerysetReport(
 
        "Credit Notes",
 
        ["id",
 
         "invoice__user__attendee__attendeeprofilebase__invoice_recipient",
 
         "status", "value"],
 
        notes,
 
        headings=["id", "Owner", "Status", "Value"],
 
        link_view=views.credit_note,
 
    )
 

	
 

	
 
@report_view("Invoices")
 
def invoices(request, form):
 
    ''' Shows all of the invoices in the system. '''
 

	
 
    invoices = commerce.Invoice.objects.all().order_by("status", "id")
 

	
 
    return QuerysetReport(
 
        "Invoices",
 
        ["id", "recipient", "value", "get_status_display"],
 
        invoices,
 
        headings=["id", "Recipient", "Value", "Status"],
 
        link_view=views.invoice,
 
    )
 

	
 

	
 
class AttendeeListReport(ListReport):
 

	
 
    def get_link(self, argument):
 
        return reverse(self._link_view) + "?user=%d" % int(argument)
 

	
 

	
 
@report_view("Attendee", form_type=forms.UserIdForm)
 
def attendee(request, form, user_id=None):
 
    ''' Returns a list of all manifested attendees if no attendee is specified,
 
    else displays the attendee manifest. '''
 

	
 
    if user_id is None and form.cleaned_data["user"] is not None:
 
        user_id = form.cleaned_data["user"]
 

	
 
    if user_id is None:
 
        return attendee_list(request)
 

	
 
    reports = []
 

	
 
    profile_data = []
 
    try:
 
        attendee = people.Attendee.objects.get(user__id=user_id)
 
    except people.DoesNotExist:
 
        return reports
 

	
 
    profile_data = []
 
    try:
 
        name = attendee.attendeeprofilebase.attendee_name()
 

	
 
        profile = people.AttendeeProfileBase.objects.get_subclass(
 
            attendee=attendee
 
        )
 
        fields = profile._meta.get_fields()
 
    except people.AttendeeProfileBase.DoesNotExist:
 
        name = attendee.user.username
 
        fields = []
 

	
 
    exclude = set(["attendeeprofilebase_ptr", "id"])
 
    for field in fields:
 
        if field.name in exclude:
 
            # Not actually important
 
            continue
 
        if not hasattr(field, "verbose_name"):
 
            continue  # Not a publicly visible field
 
        value = getattr(profile, field.name)
 

	
 
        if isinstance(field, models.ManyToManyField):
 
            value = ", ".join(str(i) for i in value.all())
 
        elif isinstance(field, CharField):
 
            try:
 
                value = bleach.clean(str(value))
 
            except TypeError:
 
                value = "Bad value for %s" % field.name
 

	
 
        profile_data.append((field.verbose_name, value))
 

	
 
    cart = CartController.for_user(attendee.user)
 
    try:
 
        reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
 
    except AttributeError:  # No reservation_duration set -- default to 24h
 
        reservation = datetime.datetime.now() + datetime.timedelta(hours=24)
 

	
 
    profile_data.append(("Current cart reserved until", reservation))
 

	
 
    reports.append(ListReport("Profile", ["", ""], profile_data))
 

	
 
    links = []
 
    links.append((
 
        reverse(views.badge, args=[user_id]),
 
        "View badge",
 
    ))
 
    links.append((
 
        reverse(views.amend_registration, args=[user_id]),
 
        "Amend current cart",
 
    ))
vendor/registrasion/registrasion/views.py
Show inline comments
...
 
@@ -1032,96 +1032,97 @@ def amend_registration(request, user_id):
 
            voucher_form.add_error(None, ve)
 

	
 

	
 
    items = commerce.ProductItem.objects.filter(
 
        cart=current_cart.cart,
 
    ).select_related("product")
 
    initial = [{"product": i.product, "quantity": i.quantity} for i in items]
 

	
 
    StaffProductsFormSet = forms.staff_products_formset_factory(user)
 
    formset = StaffProductsFormSet(
 
        request.POST or None,
 
        initial=initial,
 
        prefix="products",
 
    )
 

	
 
    for item, form in zip(items, formset):
 
        queryset = inventory.Product.objects.filter(id=item.product.id)
 
        form.fields["product"].queryset = queryset
 

	
 
    if request.POST and formset.is_valid():
 

	
 
        pq = [
 
            (f.cleaned_data["product"], f.cleaned_data["quantity"])
 
            for f in formset
 
            if "product" in f.cleaned_data and
 
            f.cleaned_data["product"] is not None
 
        ]
 

	
 
        try:
 
            current_cart.set_quantities(pq)
 
            return redirect(amend_registration, user_id)
 
        except ValidationError as ve:
 
            for ve_field in ve.error_list:
 
                product, message = ve_field.message
 
                for form in formset:
 
                    if "product" not in form.cleaned_data:
 
                        # This is the empty form.
 
                        continue
 
                    if form.cleaned_data["product"] == product:
 
                        form.add_error("quantity", message)
 

	
 
    ic = ItemController(user)
 
    data = {
 
        "user": user,
 
        "paid": ic.items_purchased(),
 
        "cancelled": ic.items_released(),
 
        "form": formset,
 
        "voucher_form": voucher_form,
 
        "vouchers": current_cart.cart.vouchers.all(),
 
    }
 

	
 
    return render(request, "registrasion/amend_registration.html", data)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def extend_reservation(request, user_id, days=7):
 
    ''' Allows staff to extend the reservation on a given user's cart.
 
    '''
 

	
 
    user = User.objects.get(id=int(user_id))
 
    cart = CartController.for_user(user)
 
    cart.extend_reservation(datetime.timedelta(days=days))
 

	
 
    return redirect(request.META["HTTP_REFERER"])
 

	
 

	
 
Email = namedtuple(
 
    "Email",
 
    ("subject", "body", "from_email", "recipient_list"),
 
)
 

	
 

	
 
@user_passes_test(_staff_only)
 
def invoice_mailout(request):
 
    ''' Allows staff to send emails to users based on their invoice status. '''
 

	
 
    category = request.GET.getlist("category", [])
 
    product = request.GET.getlist("product", [])
 
    status = request.GET.get("status")
 

	
 
    form = forms.InvoiceEmailForm(
 
        request.POST or None,
 
        category=category,
 
        product=product,
 
        status=status,
 
    )
 

	
 
    emails = []
 

	
 
    if form.is_valid():
 
        emails = []
 
        for invoice in form.cleaned_data["invoice"]:
 
            # datatuple = (subject, message, from_email, recipient_list)
 
            from_email = form.cleaned_data["from_email"]
 
            subject = form.cleaned_data["subject"]
 
            body = Template(form.cleaned_data["body"]).render(
 
                Context({
0 comments (0 inline, 0 general)