How can I refresh admin_urls

58 views
Skip to first unread message

Mike Dewhirst

unread,
Feb 22, 2019, 1:42:54 AM2/22/19
to Django users
In a 'Substance' app I have a Stripe payment facility working from within the Admin. The problem is it spoils admin_urls so the user cannot get back to the Substance Admin. When the Stripe work is done, how do I re-initialise the admin urls?

I want to display a success page with a link back to the substance page. I can provide exactly the correct url but all it shows is an admin page with no content. The correct url is /admin/substance/substance/<pk>/ and that works only if the Stripe mechanism has not been called since the dev server reloaded itself.

I probably have an incorrect diagnosis here. The problem is most likely my misunderstanding of how to use the Admin properly. Any help will be very much appreciated.

Thanks

Mike

The process uses ModelAdmin.change_view()[1] to call my billing_payment_view()[2] which uses PaymentForm[3] and payment.html[4] template to pass the necessary hidden vars.

When the view executes (depending on whether payment is required) it pops up a (Stripe js) credit card collection form with a [Pay] button and that submits the detail to the Stripe API which in turn generates a token if successful or an error message and re-executes the billing_payment_view with request.POST including that detail plus all the hidden fields in the form. The view goes on to process the detail, generate a receipt, send an email to the payer and launch the billing_success_view()[5] and success.html[6] template.

[1] SubstanceAdmin.change_view()
def change_view(self, request, object_id, form_url='', extra_context=None):
    """
    self = SubstanceAdmin
    request = wsgi request object
    object_id = substance
    form_url = no idea!
    extra_context = dict of apps, models, admin_urls and permissions

    sm2mi stands for substance m2m ingredient. Ingredients are substances in
    a many-to-many relationship with another substance (ie a mixture). The 
    through table is called Substance_Ingredients
    """
    ingredients = Substance_Ingredients.objects.filter(substance_id=object_id)
    subscription = None
    for sm2mi in ingredients:
        payable, fee_type = sm2mi.fee_payable()  # eg., True, PAID_DATA
        if payable:
            subscription = billing_subscribe(sm2mi, fee_type)
            if subscription:    # we need to collect money for the owner
                self.change_form_template = 'payment.html'
                context = billing_collect_context(
                    sm2mi,
                    subscription,
                )
                # get everything into the payment_view context
                if not extra_context:
                    extra_context = dict()
                extra_context.update(self.admin_site.each_context(request))
                extra_context.update(context)
                self.admin_site.admin_view(
                    # now call the Stripe mechanism
                    billing_payment_view(
                        request,
                        sm2mi,
                        subscription,
                        context=extra_context,
                    )
                )
                # only one sm2mi at a time
                break
    return super(SubstanceAdmin, self).change_view(
        request, object_id, form_url, extra_context
    )


[2] billing_payment_view()
@ensure_csrf_cookie
def payment_view(request, sm2mi=None, subscription=None, context=None):
    display_message = ''
    display_insert = ''
    email_message = ''
    subscription_message = ''
    subscription_insert = ''
    charge = None
    context = context or {}
    templt = 'payment.html'
    form = PaymentForm()
    if request.method == "POST":
        resp = dict(request.POST)
        # data has been submitted to Stripe via Stripe js
        # stripe submits the js form which comes back without identifying args
        form = PaymentForm(request.POST)
        if form.is_valid():
            go_back = '/admin/substance/substance/'
            try:
                token = form.cleaned_data['stripeToken']
                if token:
                    email = form.cleaned_data['stripeEmail']
                    sm2mi_id = form.cleaned_data['sm2mi_id']
                    if sm2mi_id and sm2mi is None:
                        sm2mi = Substance_Ingredients.objects.get(pk=int(sm2mi_id))

                    subscription_id = form.cleaned_data['subscription_id']
                    if subscription_id and subscription is None:
                        subscription = Subscription.objects.get(pk=int(subscription_id))

                    if sm2mi and subscription:
                        ingredient = sm2mi.ingredient
                        cents = subscription.get_cents()
                        fee_total = subscription.calc_fee(total=True)  # gst in
                        charge = stripe.Charge.create(
                            amount=int(float(fee_total) * cents),
                            currency=subscription.fee_currency,
                            description='{0}'.format(subscription.ingredient.name),
                            source=token,
                        )
                    if charge:
                        go_back = '/admin/substance/substance/%s/' % sm2mi.substance.id
                        subscription.token = token     # see above
                        try:
                            with transaction.atomic():
                                subscription.save()
                        except Exception:
                            raise
                        # transaction is done now make a receipt
                        amount_paid = charge['amount']
                        if cents > 1:  # cents may be zero eg', JPY
                            amount_paid = round(charge['amount'] / cents, 2)
                        receipt = Receipt.objects.create(
                            licensee=sm2mi.substance.division.company,
                            ingredient=ingredient,
                            currency=subscription.fee_currency,
                            amount_paid=amount_paid,
                            charge=charge,
                        )
                        receipt.save()  # defaults need all methods to execute
                        # make a received message for display and emailing
                        para = '<p>'
                        endpara = '</p>'
                        strong = '<strong>'
                        endstrong = '</strong>'
                        display_message = '<p>Thank you for your {0} {1}{2} '\
                        'payment to {3} for <strong>{4}</strong></p>'.format(
                            subscription.fee_currency,
                            subscription.fee_symbol,
                            amount_paid,
                            settings.BRANDING,
                            ingredient.name.capitalize(),
                        )
                        display_insert = '<p>A receipt will be emailed to you '\
                        'shortly</p>'
                        subscription_message = '<p>You are now '\
                        'subscribed to {0} until 30 September. You may include '\
                        'it in as many mixtures as desired.</p>'.format(
                            ingredient.name.capitalize(),
                        )
                        subscription_insert = '<p>If you have difficulty '\
                        'accessing <a href="{0}">{1}</a> or you have any '\
                        'questions please get in touch via {2}. Easy questions '\
                        'might have an answer on the <a href="https://{3}/user-'\
                        'docs/sharing/">sharing page</a>.</p>'.format(
                            ingredient.get_absolute_url(),
                            ingredient.name.capitalize(),
                            settings.MANAGERS[0][1],
                            settings.WEBSITE,
                        )

                        email_message = '{0}{1}{2}'.format(
                            display_message,
                            '\n',
                            subscription_message,
                        ).replace(
                            para, ''
                        ).replace(
                            endpara, '\n'
                        ).replace(
                            strong, '*'
                        ).replace(
                            endstrong, '*'
                        )

                        display_message = '{0}{1}{2}{3}'.format(
                            display_message,
                            display_insert,
                            subscription_message,
                            subscription_insert,
                        )
                        from_address = get_admin_email(full=True)
                        to_address = [charge['source']['name']]
                        subject = 'Subscription receipt for {0}'.format(
                            ingredient.name.capitalize()
                        )

                        try:
                            send_mail(
                                subject,
                                email_message,
                                from_address,
                                to_address,
                                fail_silently=False,
                            )
                        except Exception as err:
                            print('\n315 billing.views %s' % err)
                            pass

                        context['sm2mi'] = sm2mi
                        context['receipt'] = receipt
                        context['subscription'] = subscription
                        context['message'] = mark_safe(display_message)
                        #context['link'] = sm2mi.substance.get_substance_url()
                        context['link'] = go_back
                        return success_view(request, context)
                    else:
                        raise Exception('Gateway error')
            except Exception as err:
                err = '%s' % err
                if 'cannot use a Stripe token' in err:
                    backto = ''
                    if sm2mi:
                        backto = '<p><a href="{0}">Back to {1}</a>'\
                        '</p>'.format(
                            go_back,
                            sm2mi.substance.name.capitalize()
                        )
                    display_message = '<p>Cannot process the same payment '\
                    'twice.</p><p>If an emailed receipt is not received at the '\
                    'address just specified in the payment popup please get in '\
                    'touch with <em>{0}</em> to ascertain whether payment was '\
                    'successful. Please copy and paste the token below as a '\
                    'reference.</p><p>{1}</p><p>{2}<p>'.format(
                        get_manager_email(),
                        token or 'No token at this point (375)',
                        backto
                    )
                else:
                    display_message = '{0}. Please report this error message to '\
                    '{1}'.format(err, get_admin_email())
                context['message'] = mark_safe(display_message)
                # report failure to the card payer
                form.add_error(None, '%s' % err)
            return success_view(request, context)
    context['form'] = form
    return render(
        request=request,
        template_name=templt,
        context=context
    )


[3] PaymentForm
class PaymentForm(forms.Form):
    sm2mi_id = forms.CharField(widget=forms.HiddenInput, required=False)
    ingredient_id = forms.CharField(widget=forms.HiddenInput, required=False)
    subscription_id = forms.CharField(widget=forms.HiddenInput, required=False)

    stripeToken = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeEmail = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingName = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingAddressLine1 = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingAddressZip = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingAddressState = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingAddressCity = forms.CharField(widget=forms.HiddenInput, required=False)
    stripeBillingAddressCountry = forms.CharField(widget=forms.HiddenInput, required=False)



[4] payment.html
{% extends "admin/base_site.html" %}
{% load admin_urls %}
{% load i18n %}
{% load common_tags %}
{% block title %}{{ original }}{% endblock %}
{% block content %}
{% if subscription %}
  <div class="payment">
    <p>{{ prepay_message }}</p>
    <p>Please proceed to payment.</p>
    <blockquote><strong>
    We do not store your payment detail here. Our PCI DSS compliant gateway
    service provider interfaces your card directly with your bank. Links to
    their terms and privacy policy will be visible when you click the <em>Pay
    with Card</em> button. See also our own privacy policy (link above).
    </strong> &nbsp; (PCI DSS = Payment Card Industry Data Security Standard)
    </blockquote>
    <p>&nbsp;</p>
    <form action="payment" method="POST">
        {% csrf_token %}
        {% if form.errors or form.non_field_errors %}
            {{ form.errors }}
            {{ form.non_field_errors }}
        {% endif %}
        {% for field in form %}
            {% if field.name == "sm2mi_id" %}
                <input type="hidden" name="sm2mi_id" id="id_sm2mi_id" value="{{ sm2mi.id }}" />
            {% elif field.name == "ingredient_id" %}
                <input type="hidden" name="ingredient_id" id="id_ingredient_id" value="{{ ingredient.id }}" />
            {% elif field.name == "subscription_id" %}
                <input type="hidden" name="subscription_id" id="id_subscription_id" value="{{ subscription.id }}" />
            {% else %}
                {{ field }}
            {% endif %}
        {% endfor %}
        <script
          src="https://checkout.stripe.com/checkout.js" class="stripe-button"
          data-key="{{ data_key }}"
          data-amount="{{ data_amount }}"
          data-name="{{ data_name }}"
          data-description="{{ data_description }}"
          data-image="{{ data_image }}"
          data-locale="{{ data_locale }}"
          data-currency="{{ data_currency | lower}}"
          data-zip-code="true"
          data-allow-remember-me="true"
          data-panel-label="Pay">
        </script>
    </form>
  </div>
{% endif %}
{{ block.super }}
{% endblock %}




[5] billing_success_view
def success_view(request, context=None):
    """ launched after a Stripe token is received by PaymentForm

    context is passed in by payment_view and has context['message']
    """
    if context is None:
        context = dict()
        context['message'] = 'Sorry - payment details cannot be reproduced'

    if 'Thank you for your' in context['message']:
        pass

    return render(
        request=request,
        template_name='success.html',
        context=context,
    )


[6] success.html
{% extends "admin/base_site.html" %}
{% load admin_urls %}
{% load i18n %}
{% load common_tags %}
{% block title %}{{ original }}{% endblock %}
{% block content %}
  <div id="container">
    <div id="content" class="colM">
    {% if message %}{{ message }}{% endif %}
    {% if link %}
        <p>Please do not refresh this page but rather click the 'Home' link above or
        <a href="{{ link }}">go back to {{ sm2mi.substance.name }}</a></p>
    {% endif %}
    </div>
  </div>
{{ block.super }}
{% endblock %}


Python 3.6, Django 1.11.20, dev server on Windows 10.

Haven't been *able* to get it working yet on Ubuntu 16.04 with Python 2.7 and haven't tried it yet on Python 2.7 on the dev server.

Getting Python 3.x running with Apache2 on Ubuntu is non-trivial due to co-existence with Trac (2.7 only) which also requires my SVN to cohabit on the same machine.



Message has been deleted

Scot Hacker

unread,
Feb 23, 2019, 7:25:13 PM2/23/19
to Django users
Django Admin is customizable to an extent, but at a certain point those customizations can cost you more time than they save, and it makes more sense to build your business logic in a standard app/view. Without digging into your code, my spidey sense tells me your app is at that point - stop trying to do it in the admin and you'll have all the control you need or want.

./s

Mike Dewhirst

unread,
Feb 25, 2019, 8:13:29 PM2/25/19
to Django users

On Sunday, February 24, 2019 at 11:25:13 AM UTC+11, Scot Hacker wrote:
Django Admin is customizable to an extent, but at a certain point those customizations can cost you more time than they save, and it makes more sense to build your business logic in a standard app/view. Without digging into your code, my spidey sense tells me your app is at that point - stop trying to do it in the admin and you'll have all the control you need or want.

./s

Scot

I agree. How do I do that?

The entire system is in the Admin except for some trivial search and non-is_staff stuff. If I was to redo it without the Admin I would have to reinvent that entire wheel.

Specifically, how do I call an external view from within the admin? I only want to call that view if the model instance attributes demand it. That means it cannot be an optional/clickable link. It must be automatic.

Currently the admin url is (e.g.) http://localhost:8000/admin/substance/substance/1450/change/ while the substance is being edited. That url doesn't change while adding an ingredient. If the ingredient hasn't been paid for by *that* user the payment screen has to pop up. Currently it does this via modelAdmin.change_view() calling billing_payment_view()

That url doesn't change until after a successful payment when billing_payment_view() calls billing_success_view() directly. At that point the url becomes http://localhost:8000/admin/substance/substance/1450/change/payment (without a trailing slash). Thereafter valid admin urls fail to show any content..

If I make settings APPEND_SLASH False, it says 404.

Mike

Mike Dewhirst

unread,
Feb 26, 2019, 12:20:22 AM2/26/19
to django...@googlegroups.com
Thank you all for your attention. Kept me going.

The fix is to initialise the self.change_form_template to None *every*
time ModelAdmin.change_form() is called. If it is None the
ModelAdmin.render_change_form() method will dynamically create the model
instance change form. Otherwise it uses the form I give it. My problem
was it remembered that instead of doing its own thing when I had
finished with it.

I reckon it was designed to swap out the default form and completely
replace it forever. Thank heavens for open source.

Here is the only change to my code ...

def change_view(self, request, object_id, form_url='',
extra_context=None): """ self = SubstanceAdmin request = wsgi request
object object_id = substance form_url = no idea! extra_context = dict of
apps, models, admin_urls and permissions """ self.change_form_template =
None ############## this works ############# ingredients =
Substance_Ingredients.objects.filter(substance_id=object_id)
subscription = None for sm2mi in ingredients: payable, fee_type =
sm2mi.fee_payable() # eg., True, PAID_DATA if payable: subscription =
billing_subscribe(sm2mi, fee_type) if subscription: # we need to collect
money for the owner self.change_form_template = 'payment.html' context =
billing_collect_context( sm2mi, subscription, ) # get everything into
the payment_view context if not extra_context: extra_context = dict()
extra_context.update(self.admin_site.each_context(request))
extra_context.update(context) self.admin_site.admin_view( # call the
Stripe mechanism billing_payment_view( request, sm2mi, subscription,
context=extra_context, ) ) # only one sm2mi at a time break return
super(SubstanceAdmin, self).change_view( request, object_id, form_url,
extra_context )


 I reckon the Admin can do anything. It just takes some head scratching

:)

Mike

On 23/02/2019 6:58 pm, Mike Dewhirst wrote:
> (Resend with urls which I previously omitted)

Mike Dewhirst

unread,
Feb 26, 2019, 12:35:51 AM2/26/19
to django...@googlegroups.com
Thank you all for your attention. Kept me going. (another resend this time hopefully it will be laid out better)
        #if extra_context:
        #    logging.log(logging.DEBUG, '205 admin {0}'.format(extra_context))

Dylan Reinhold

unread,
Feb 26, 2019, 12:40:49 AM2/26/19
to django...@googlegroups.com
Thanks for sharing Mike.

Dylan

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users...@googlegroups.com.
To post to this group, send email to django...@googlegroups.com.
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/4cf8ad94-da46-35e1-ba5a-d168de645561%40dewhirst.com.au.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages