Commit 3dfe4355 by Clinton Blackburn

Merge pull request #100 from edx/refund-list-view

Refund List View
parents 9aa9c49a 539b839a
from django.conf.urls import url, include
from oscar import app
......@@ -6,19 +5,5 @@ class EdxShop(app.Shop):
# URLs are only visible to users with staff permissions
default_permissions = 'is_staff'
def get_urls(self):
urls = [
# Make management dashboard accessible at the root
url(r'', include(self.dashboard_app.urls)),
url(r'^promotions/', include(self.promotions_app.urls)),
url(r'^catalogue/', include(self.catalogue_app.urls)),
url(r'^basket/', include(self.basket_app.urls)),
url(r'^checkout/', include(self.checkout_app.urls)),
url(r'^accounts/', include(self.customer_app.urls)),
url(r'^search/', include(self.search_app.urls)),
url(r'^offers/', include(self.offer_app.urls)),
]
return urls
application = EdxShop()
default_app_config = 'ecommerce.extensions.dashboard.config.DashboardConfig' # pragma: no cover
from django.conf.urls import url, include
from oscar.apps.dashboard import app
from oscar.core.loading import get_class
class DashboardApplication(app.DashboardApplication):
refunds_app = get_class('dashboard.refunds.app', 'application')
def get_urls(self):
urls = [
url(r'^$', self.index_view.as_view(), name='index'),
url(r'^catalogue/', include(self.catalogue_app.urls)),
url(r'^reports/', include(self.reports_app.urls)),
url(r'^orders/', include(self.orders_app.urls)),
url(r'^users/', include(self.users_app.urls)),
url(r'^content-blocks/', include(self.promotions_app.urls)),
url(r'^pages/', include(self.pages_app.urls)),
url(r'^partners/', include(self.partners_app.urls)),
url(r'^offers/', include(self.offers_app.urls)),
url(r'^ranges/', include(self.ranges_app.urls)),
url(r'^reviews/', include(self.reviews_app.urls)),
url(r'^vouchers/', include(self.vouchers_app.urls)),
url(r'^comms/', include(self.comms_app.urls)),
url(r'^shipping/', include(self.shipping_app.urls)),
url(r'^refunds/', include(self.refunds_app.urls)),
]
return self.post_process_urls(urls)
application = DashboardApplication()
from oscar.apps.dashboard import config
class DashboardConfig(config.DashboardConfig):
name = 'ecommerce.extensions.dashboard'
# noinspection PyUnresolvedReferences
from oscar.apps.dashboard.models import * # pragma: no cover noqa pylint: disable=wildcard-import,unused-wildcard-import
default_app_config = 'oscar.apps.dashboard.orders.config.OrdersDashboardConfig'
from django.conf.urls import url
from oscar.core.application import Application
from oscar.core.loading import get_class
class RefundsDashboardApplication(Application):
name = 'refunds'
default_permissions = ['is_staff', ]
permissions_map = {
'list': (['is_staff'], ['partner.dashboard_access']),
'detail': (['is_staff'], ['partner.dashboard_access']),
}
refund_list_view = get_class('dashboard.refunds.views', 'RefundListView')
refund_detail_view = get_class('dashboard.refunds.views', 'RefundDetailView')
def get_urls(self):
urls = [
url(r'^$', self.refund_list_view.as_view(), name='list'),
url(r'^(?P<pk>[\d]+)/$', self.refund_detail_view.as_view(), name='detail'),
]
return self.post_process_urls(urls)
application = RefundsDashboardApplication()
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class RefundsDashboardConfig(AppConfig):
label = 'refunds_dashboard'
name = 'ecommerce.extensions.dashboard.refunds'
verbose_name = _('Refunds Dashboard')
from django import forms
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
Refund = get_model('refund', 'Refund')
class RefundSearchForm(forms.Form):
id = forms.IntegerField(required=False, label=_('Refund ID'))
status_choices = (('', '---------'),) + tuple([(status, status) for status in Refund.all_statuses()])
status = forms.ChoiceField(choices=status_choices, label=_("Status"), required=False)
from django.core.urlresolvers import reverse
from django.test import TestCase
from ecommerce.extensions.refund.status import REFUND
from ecommerce.extensions.refund.tests.factories import RefundFactory
from ecommerce.tests.mixins import UserMixin
class RefundListViewTests(UserMixin, TestCase):
path = reverse('dashboard:refunds:list')
def setUp(self):
super(RefundListViewTests, self).setUp()
self.user = self.create_user(is_superuser=True, is_staff=True)
def assert_successful_response(self, response, refunds=None):
self.assertEqual(response.status_code, 200)
if refunds:
self.assertListEqual(list(response.context['refunds']), refunds)
def test_staff_permissions_required(self):
""" The view should only be accessible by staff users. """
# Non-staff users cannot view the page.
non_staff_user = self.create_user(is_staff=False, is_superuser=False)
self.client.login(username=non_staff_user.username, password=self.password)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 302) # Redirect to logon page
# Staff users should be able to view the page.
staff_user = self.create_user(is_staff=True, is_superuser=False)
self.client.login(username=staff_user.username, password=self.password)
response = self.client.get(self.path)
self.assert_successful_response(response)
# Superusers should be able to view the page, regardless of the staff setting.
superuser = self.create_user(is_staff=False, is_superuser=True)
self.client.login(username=superuser.username, password=self.password)
response = self.client.get(self.path)
self.assert_successful_response(response)
def test_filtering(self):
""" The view should allow filtering by ID and status. """
refund = RefundFactory()
open_refund = RefundFactory(status=REFUND.OPEN)
complete_refund = RefundFactory(status=REFUND.COMPLETE)
self.client.login(username=self.user.username, password=self.password)
# Sanity check for an unfiltered query
response = self.client.get(self.path)
self.assert_successful_response(response, [refund, open_refund, complete_refund])
# ID filtering
response = self.client.get('{path}?id={id}'.format(path=self.path, id=open_refund.id))
self.assert_successful_response(response, [open_refund])
# Status filtering
response = self.client.get('{path}?status={status}'.format(path=self.path, status=REFUND.COMPLETE))
self.assert_successful_response(response, [complete_refund])
def test_sorting(self):
""" The view should allow sorting by ID. """
refunds = [RefundFactory(), RefundFactory(), RefundFactory()]
self.client.login(username=self.user.username, password=self.password)
response = self.client.get('{path}?sort=id&dir=asc'.format(path=self.path))
self.assert_successful_response(response, refunds)
response = self.client.get('{path}?sort=id&dir=desc'.format(path=self.path))
self.assert_successful_response(response, list(reversed(refunds)))
from django.views.generic import ListView, DetailView
from oscar.core.loading import get_class, get_model
from oscar.views import sort_queryset
Refund = get_model('refund', 'Refund')
RefundSearchForm = get_class('dashboard.refunds.forms', 'RefundSearchForm')
class RefundListView(ListView):
""" Dashboard view to list refunds. """
model = Refund
context_object_name = 'refunds'
template_name = 'dashboard/refunds/refund_list.html'
paginate_by = 25
form_class = RefundSearchForm
form = None
def get_queryset(self):
queryset = super(RefundListView, self).get_queryset()
queryset = queryset.prefetch_related('lines')
queryset = sort_queryset(queryset, self.request, ['id'], 'id')
self.form = self.form_class(self.request.GET)
if self.form.is_valid():
for field, value in self.form.cleaned_data.iteritems():
if value:
queryset = queryset.filter(**{field: value})
return queryset
def get_context_data(self, **kwargs):
context = super(RefundListView, self).get_context_data(**kwargs)
context['form'] = self.form
return context
class RefundDetailView(DetailView):
model = Refund
context_object_name = 'refund'
template_name = 'dashboard/refunds/refund_detail.html'
......@@ -40,6 +40,9 @@ class StatusMixin(object):
self.status = new_status
self.save()
def __str__(self):
return unicode(self.id)
class Refund(StatusMixin, TimeStampedModel):
"""Main refund model, used to represent the state of a refund."""
......@@ -52,6 +55,19 @@ class Refund(StatusMixin, TimeStampedModel):
history = HistoricalRecords()
pipeline_setting = 'OSCAR_REFUND_STATUS_PIPELINE'
@classmethod
def all_statuses(cls):
""" Returns all possible statuses for a refund. """
return list(getattr(settings, cls.pipeline_setting).keys())
@property
def num_items(self):
""" Returns the number of items in this refund. """
num_items = 0
for line in self.lines.all():
num_items += line.quantity
return num_items
class RefundLine(StatusMixin, TimeStampedModel):
"""A refund line, used to represent the state of a single item as part of a larger Refund."""
......
from django.test import TestCase, override_settings
from oscar.core.loading import get_model
from ecommerce.extensions.refund.exceptions import InvalidStatus
from ecommerce.extensions.refund.status import REFUND, REFUND_LINE
from ecommerce.extensions.refund.tests.factories import RefundFactory, RefundLineFactory
OSCAR_REFUND_STATUS_PIPELINE = {
REFUND.OPEN: (REFUND.DENIED, REFUND.ERROR, REFUND.COMPLETE),
REFUND.ERROR: (REFUND.COMPLETE, REFUND.ERROR),
......@@ -21,6 +21,8 @@ OSCAR_REFUND_LINE_STATUS_PIPELINE = {
REFUND_LINE.COMPLETE: ()
}
Refund = get_model('refund', 'Refund')
class StatusTestsMixin(object):
pipeline = None
......@@ -67,6 +69,19 @@ class RefundTests(StatusTestsMixin, TestCase):
def _get_instance(self, **kwargs):
return RefundFactory(**kwargs)
def test_num_items(self):
""" The method should return the total number of items being refunded. """
refund_line = RefundLineFactory(quantity=1)
refund = refund_line.refund
self.assertEqual(refund.num_items, 1)
RefundLineFactory(quantity=3, refund=refund)
self.assertEqual(refund.num_items, 4)
def test_all_statuses(self):
""" Refund.all_statuses should return all possible statuses for a refund. """
self.assertEqual(Refund.all_statuses(), OSCAR_REFUND_STATUS_PIPELINE.keys())
@override_settings(OSCAR_REFUND_LINE_STATUS_PIPELINE=OSCAR_REFUND_LINE_STATUS_PIPELINE)
class RefundLineTests(StatusTestsMixin, TestCase):
......
......@@ -4,6 +4,7 @@ from __future__ import absolute_import
from os.path import abspath, join, dirname
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from oscar.defaults import *
from oscar import get_core_apps
......@@ -25,6 +26,7 @@ OSCAR_APPS = [
'ecommerce.extensions.analytics',
'ecommerce.extensions.catalogue',
'ecommerce.extensions.checkout',
'ecommerce.extensions.dashboard',
'ecommerce.extensions.order',
'ecommerce.extensions.partner',
'ecommerce.extensions.payment',
......@@ -146,3 +148,80 @@ OSCAR_REFUND_LINE_STATUS_PIPELINE = {
REFUND_LINE.COMPLETE: ()
}
# END REFUND PROCESSING
# DASHBOARD NAVIGATION MENU
OSCAR_DASHBOARD_NAVIGATION = [
{
'label': _('Dashboard'),
'icon': 'icon-th-list',
'url_name': 'dashboard:index',
},
{
'label': _('Catalogue'),
'icon': 'icon-sitemap',
'children': [
{
'label': _('Products'),
'url_name': 'dashboard:catalogue-product-list',
},
{
'label': _('Product Types'),
'url_name': 'dashboard:catalogue-class-list',
},
{
'label': _('Categories'),
'url_name': 'dashboard:catalogue-category-list',
},
{
'label': _('Ranges'),
'url_name': 'dashboard:range-list',
},
{
'label': _('Low stock alerts'),
'url_name': 'dashboard:stock-alert-list',
},
]
},
{
'label': _('Fulfillment'),
'icon': 'icon-shopping-cart',
'children': [
{
'label': _('Orders'),
'url_name': 'dashboard:order-list',
},
{
'label': _('Statistics'),
'url_name': 'dashboard:order-stats',
},
{
'label': _('Partners'),
'url_name': 'dashboard:partner-list',
},
{
'label': _('Refunds'),
'url_name': 'dashboard:refunds:list',
},
]
},
{
'label': _('Customers'),
'icon': 'icon-group',
'children': [
{
'label': _('Customers'),
'url_name': 'dashboard:users-index',
},
{
'label': _('Stock alert requests'),
'url_name': 'dashboard:user-alert-list',
},
]
},
{
'label': _('Reports'),
'icon': 'icon-bar-chart',
'url_name': 'dashboard:reports-index',
},
]
# END DASHBOARD NAVIGATION MENU
......@@ -365,8 +365,8 @@ SOCIAL_AUTH_EDX_OIDC_URL_ROOT = None
# This value should be the same as SOCIAL_AUTH_EDX_OIDC_SECRET
SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY = SOCIAL_AUTH_EDX_OIDC_SECRET
# Redirect successfully authenticated users to the Oscar dashboard, located at the root
LOGIN_REDIRECT_URL = ''
# Redirect successfully authenticated users to the Oscar dashboard.
LOGIN_REDIRECT_URL = '/dashboard/'
EXTRA_SCOPE = ['permissions']
# END AUTHENTICATION
......
{% load i18n %}
{% load currency_filters %}
<table class="table table-striped table-bordered table-hover">
<caption>{% trans "Refunds" %}</caption>
{% if refunds %}
<tr>
<th>{% trans "Refund ID" %}</th>
<th>{% trans "Number of Items" %}</th>
<th>{% trans "Total Credit" %}</th>
<th>{% trans "Date Created" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
{% for refund in refunds %}
<tr>
<td>
<a href="{% url 'dashboard:refunds:detail' refund.id %}">{{ refund.id }}</a>
</td>
<td>{{ refund.num_items }}</td>
<td>{{ refund.total_credit_excl_tax|currency:refund.currency }}</td>
<td>{{ refund.created }}</td>
<td>{{ refund.status|default:"-" }}</td>
<td><a href="{% url 'dashboard:refunds:detail' refund.id %}"
class="btn btn-info">{% trans "View" %}</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td>{% trans "No refunds found." %}</td>
</tr>
{% endif %}
</table>
{% extends 'dashboard/layout.html' %}
{% load compress %}
{% load currency_filters %}
{% load sorting_tags %}
{% load i18n %}
{% block body_class %}{{ block.super }} refunds{% endblock %}
{% block title %}
{% trans "Refunds" %} | {{ block.super }}
{% endblock %}
{% block breadcrumbs %}
<ul class="breadcrumb">
<li>
<a href="{% url 'dashboard:index' %}">{% trans "Dashboard" %}</a>
<span class="divider">/</span>
</li>
<li class="active">{% trans "Refunds" %}</li>
</ul>
{% endblock %}
{% block header %}
<div class="page-header">
<h1>{% trans "Refunds" %}</h1>
</div>
{% endblock header %}
{% block dashboard_content %}
<div class="table-header">
<h3><i class="icon-search icon-large"></i>{% trans "Search" %}</h3>
</div>
<div class="well">
<form action="" method="get" class="form-inline" id="search_form">
{% for field in form %}
{% if field.id_for_label == 'id_id' %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<span class="control-group {% if field.errors %}error{% endif %}">
{{ field.label_tag }}
{{ field }}
{% for error in field.errors %}
<ul class="error-block">
<li>{{ error }}</li>
</ul>
{% endfor %}
</span>
{% endif %}
{% endif %}
{% endfor %}
<input type="submit" value="{% trans "Search" %}" class="btn btn-primary"/>
<a data-toggle="modal" href="#SearchModal">{% trans "Advanced Search" %}</a>
</form>
<div class="modal hide fade" id="SearchModal">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>{% trans "Advanced Search" %}</h3>
</div>
<form action="" method="get" class="form-horizontal">
<div class="modal-body">
{% include "partials/form_fields.html" with form=form %}
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">{% trans "Close" %}</a>
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
</div>
</form>
</div>
</div>
{% if refunds %}
{% block refund_list %}
<table class="table table-striped table-bordered table-hover">
<caption>
<h3 class="pull-left"><i class="icon-repeat icon-large icon-flip-horizontal"></i></h3>
</caption>
<thead>
<tr>
<th>{% anchor 'id' _("Refund ID") %}</th>
<th>{% trans "Total Credit" %}</th>
<th>{% trans "Number of Items" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Customer" %}</th>
<th>{% trans "Date Created" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for refund in refunds %}
<tr data-refund-id="{{ refund.id }}">
<td><a href="{% url 'dashboard:refunds:detail' pk=refund.id %}">{{ refund.id }}</a>
</td>
<td>{{ refund.total_credit_excl_tax|currency:refund.currency }}</td>
<td>{{ refund.num_items }}</td>
<td class="refund-status">{{ refund.status|default:"-" }}</td>
<td>
<a href="{% url 'dashboard:user-detail' pk=refund.user.id %}">
{{ refund.user.get_full_name|default:"-" }}</a>
</td>
<td>{{ refund.created }}</td>
<td>
<a class="btn btn-info"
href="{% url 'dashboard:refunds:detail' pk=refund.id %}">{% trans "View" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock refund_list %}
{% include "partials/pagination.html" %}
{% else %}
<table class="table table-striped table-bordered">
<caption><i class="icon-repeat icon-large icon-flip-horizontal"></i>{{ queryset_description }}</caption>
<tr>
<td>{% trans "No refunds found." %}</td>
</tr>
</table>
{% endif %}
{% endblock dashboard_content %}
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