Blackness.

This commit is contained in:
Víðir Valberg Guðmundsson 2019-06-16 14:32:24 +02:00
parent 908d753eb1
commit bf2f0c7898
441 changed files with 9458 additions and 7456 deletions

1
src/.coverage Normal file

File diff suppressed because one or more lines are too long

122
src/.ropeproject/config.py Normal file
View file

@ -0,0 +1,122 @@
# The default ``config.py``
# flake8: noqa
def set_prefs(prefs):
"""This function is called before opening the project"""
# Specify which files and folders to ignore in the project.
# Changes to ignored resources are not added to the history and
# VCSs. Also they are not returned in `Project.get_files()`.
# Note that ``?`` and ``*`` match all characters but slashes.
# '*.pyc': matches 'test.pyc' and 'pkg/test.pyc'
# 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc'
# '.svn': matches 'pkg/.svn' and all of its children
# 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
# 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
prefs["ignored_resources"] = [
"*.pyc",
"*~",
".ropeproject",
".hg",
".svn",
"_svn",
".git",
".tox",
]
# Specifies which files should be considered python files. It is
# useful when you have scripts inside your project. Only files
# ending with ``.py`` are considered to be python files by
# default.
# prefs['python_files'] = ['*.py']
# Custom source folders: By default rope searches the project
# for finding source folders (folders that should be searched
# for finding modules). You can add paths to that list. Note
# that rope guesses project source folders correctly most of the
# time; use this if you have any problems.
# The folders should be relative to project root and use '/' for
# separating folders regardless of the platform rope is running on.
# 'src/my_source_folder' for instance.
# prefs.add('source_folders', 'src')
# You can extend python path for looking up modules
# prefs.add('python_path', '~/python/')
# Should rope save object information or not.
prefs["save_objectdb"] = True
prefs["compress_objectdb"] = False
# If `True`, rope analyzes each module when it is being saved.
prefs["automatic_soa"] = True
# The depth of calls to follow in static object analysis
prefs["soa_followed_calls"] = 0
# If `False` when running modules or unit tests "dynamic object
# analysis" is turned off. This makes them much faster.
prefs["perform_doa"] = True
# Rope can check the validity of its object DB when running.
prefs["validate_objectdb"] = True
# How many undos to hold?
prefs["max_history_items"] = 32
# Shows whether to save history across sessions.
prefs["save_history"] = True
prefs["compress_history"] = False
# Set the number spaces used for indenting. According to
# :PEP:`8`, it is best to use 4 spaces. Since most of rope's
# unit-tests use 4 spaces it is more reliable, too.
prefs["indent_size"] = 4
# Builtin and c-extension modules that are allowed to be imported
# and inspected by rope.
prefs["extension_modules"] = []
# Add all standard c-extensions to extension_modules list.
prefs["import_dynload_stdmods"] = True
# If `True` modules with syntax errors are considered to be empty.
# The default value is `False`; When `False` syntax errors raise
# `rope.base.exceptions.ModuleSyntaxError` exception.
prefs["ignore_syntax_errors"] = False
# If `True`, rope ignores unresolvable imports. Otherwise, they
# appear in the importing namespace.
prefs["ignore_bad_imports"] = False
# If `True`, rope will insert new module imports as
# `from <package> import <module>` by default.
prefs["prefer_module_from_imports"] = False
# If `True`, rope will transform a comma list of imports into
# multiple separate import statements when organizing
# imports.
prefs["split_imports"] = False
# If `True`, rope will remove all top-level import statements and
# reinsert them at the top of the module when making changes.
prefs["pull_imports_to_top"] = True
# If `True`, rope will sort imports alphabetically by module name instead of
# alphabetically by import statement, with from imports after normal
# imports.
prefs["sort_imports_alphabetically"] = False
# Location of implementation of rope.base.oi.type_hinting.interfaces.ITypeHintingFactory
# In general case, you don't have to change this value, unless you're an rope expert.
# Change this value to inject you own implementations of interfaces
# listed in module rope.base.oi.type_hinting.providers.interfaces
# For example, you can add you own providers for Django Models, or disable the search
# type-hinting in a class hierarchy, etc.
prefs[
"type_hinting_factory"
] = "rope.base.oi.type_hinting.factory.default_type_hinting_factory"
def project_opened(project):
"""This function is called after opening the project"""
# Do whatever you like here!

Binary file not shown.

BIN
src/.ropeproject/history Normal file

Binary file not shown.

BIN
src/.ropeproject/objectdb Normal file

Binary file not shown.

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class BackofficeConfig(AppConfig):
name = 'backoffice'
name = "backoffice"

View file

@ -5,6 +5,7 @@ class OrgaTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Orga Team
"""
permission_required = ("camps.backoffice_permission", "camps.orgateam_permission")
@ -12,13 +13,18 @@ class EconomyTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Economy Team
"""
permission_required = ("camps.backoffice_permission", "camps.economyteam_permission")
permission_required = (
"camps.backoffice_permission",
"camps.economyteam_permission",
)
class InfoTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Info Team/InfoDesk
"""
permission_required = ("camps.backoffice_permission", "camps.infoteam_permission")
@ -26,5 +32,8 @@ class ContentTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Content Team
"""
permission_required = ("camps.backoffice_permission", "program.contentteam_permission")
permission_required = (
"camps.backoffice_permission",
"program.contentteam_permission",
)

View file

@ -2,93 +2,156 @@ from django.urls import path, include
from .views import *
app_name = 'backoffice'
app_name = "backoffice"
urlpatterns = [
path('', BackofficeIndexView.as_view(), name='index'),
path("", BackofficeIndexView.as_view(), name="index"),
# infodesk
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
path("product_handout/", ProductHandoutView.as_view(), name="product_handout"),
path("badge_handout/", BadgeHandoutView.as_view(), name="badge_handout"),
path("ticket_checkin/", TicketCheckinView.as_view(), name="ticket_checkin"),
# public names
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
path(
"public_credit_names/", ApproveNamesView.as_view(), name="public_credit_names"
),
# merchandise orders
path('merchandise_orders/', MerchandiseOrdersView.as_view(), name='merchandise_orders'),
path('merchandise_to_order/', MerchandiseToOrderView.as_view(), name='merchandise_to_order'),
path(
"merchandise_orders/",
MerchandiseOrdersView.as_view(),
name="merchandise_orders",
),
path(
"merchandise_to_order/",
MerchandiseToOrderView.as_view(),
name="merchandise_to_order",
),
# village orders
path('village_orders/', VillageOrdersView.as_view(), name='village_orders'),
path('village_to_order/', VillageToOrderView.as_view(), name='village_to_order'),
path("village_orders/", VillageOrdersView.as_view(), name="village_orders"),
path("village_to_order/", VillageToOrderView.as_view(), name="village_to_order"),
# manage proposals
path('manage_proposals/', include([
path('', ManageProposalsView.as_view(), name='manage_proposals'),
path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'),
path('events/<uuid:pk>/', EventProposalManageView.as_view(), name='eventproposal_manage'),
])),
path(
"manage_proposals/",
include(
[
path("", ManageProposalsView.as_view(), name="manage_proposals"),
path(
"speakers/<uuid:pk>/",
SpeakerProposalManageView.as_view(),
name="speakerproposal_manage",
),
path(
"events/<uuid:pk>/",
EventProposalManageView.as_view(),
name="eventproposal_manage",
),
]
),
),
# economy
path('economy/',
include([
path(
"economy/",
include(
[
# chains & credebtors
path('chains/',
include([
path(
'',
ChainListView.as_view(),
name='chain_list'
),
path('<slug:chain_slug>/',
include([
"chains/",
include(
[
path("", ChainListView.as_view(), name="chain_list"),
path(
'',
"<slug:chain_slug>/",
include(
[
path(
"",
ChainDetailView.as_view(),
name='chain_detail'
name="chain_detail",
),
path(
'<slug:credebtor_slug>/',
"<slug:credebtor_slug>/",
CredebtorDetailView.as_view(),
name='credebtor_detail'
name="credebtor_detail",
),
]),
]
),
),
]
),
]),
),
# expenses
path('expenses/',
include([
path('', ExpenseListView.as_view(), name='expense_list'),
path('<uuid:pk>/', ExpenseDetailView.as_view(), name='expense_detail'),
]),
path(
"expenses/",
include(
[
path("", ExpenseListView.as_view(), name="expense_list"),
path(
"<uuid:pk>/",
ExpenseDetailView.as_view(),
name="expense_detail",
),
]
),
),
# revenues
path('revenues/',
include([
path('', RevenueListView.as_view(), name='revenue_list'),
path('<uuid:pk>/', RevenueDetailView.as_view(), name='revenue_detail'),
]),
path(
"revenues/",
include(
[
path("", RevenueListView.as_view(), name="revenue_list"),
path(
"<uuid:pk>/",
RevenueDetailView.as_view(),
name="revenue_detail",
),
]
),
),
# reimbursements
path('reimbursements/',
include([
path('', ReimbursementListView.as_view(), name='reimbursement_list'),
path('<uuid:pk>/',
include([
path('', ReimbursementDetailView.as_view(), name='reimbursement_detail'),
path('update/', ReimbursementUpdateView.as_view(), name='reimbursement_update'),
path('delete/', ReimbursementDeleteView.as_view(), name='reimbursement_delete'),
]),
path(
"reimbursements/",
include(
[
path(
"",
ReimbursementListView.as_view(),
name="reimbursement_list",
),
path('create/', ReimbursementCreateUserSelectView.as_view(), name='reimbursement_create_userselect'),
path('create/<int:user_id>/', ReimbursementCreateView.as_view(), name='reimbursement_create'),
]),
path(
"<uuid:pk>/",
include(
[
path(
"",
ReimbursementDetailView.as_view(),
name="reimbursement_detail",
),
path(
"update/",
ReimbursementUpdateView.as_view(),
name="reimbursement_update",
),
path(
"delete/",
ReimbursementDeleteView.as_view(),
name="reimbursement_delete",
),
]
),
),
path(
"create/",
ReimbursementCreateUserSelectView.as_view(),
name="reimbursement_create_userselect",
),
path(
"create/<int:user_id>/",
ReimbursementCreateView.as_view(),
name="reimbursement_create",
),
]
),
),
]
),
]),
),
]

View file

@ -30,7 +30,8 @@ class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateV
"""
The Backoffice index view only requires camps.backoffice_permission so we use RaisePermissionRequiredMixin directly
"""
permission_required = ("camps.backoffice_permission")
permission_required = "camps.backoffice_permission"
template_name = "index.html"
@ -42,13 +43,13 @@ class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False
).order_by('order')
order__cancelled=False,
).order_by("order")
class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
template_name = "badge_handout.html"
context_object_name = 'tickets'
context_object_name = "tickets"
def get_queryset(self, **kwargs):
shoptickets = ShopTicket.objects.filter(badge_handed_out=False)
@ -59,7 +60,7 @@ class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView):
template_name = "ticket_checkin.html"
context_object_name = 'tickets'
context_object_name = "tickets"
def get_queryset(self, **kwargs):
shoptickets = ShopTicket.objects.filter(checked_in=False)
@ -70,30 +71,31 @@ class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView):
class ApproveNamesView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
template_name = "approve_public_credit_names.html"
context_object_name = 'profiles'
context_object_name = "profiles"
def get_queryset(self, **kwargs):
return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='')
return Profile.objects.filter(public_credit_name_approved=False).exclude(
public_credit_name=""
)
class ManageProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView):
"""
This view shows a list of pending SpeakerProposal and EventProposals.
"""
template_name = "manage_proposals.html"
context_object_name = 'speakerproposals'
context_object_name = "speakerproposals"
def get_queryset(self, **kwargs):
return SpeakerProposal.objects.filter(
camp=self.camp,
proposal_status=SpeakerProposal.PROPOSAL_PENDING
camp=self.camp, proposal_status=SpeakerProposal.PROPOSAL_PENDING
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['eventproposals'] = EventProposal.objects.filter(
track__camp=self.camp,
proposal_status=EventProposal.PROPOSAL_PENDING
context["eventproposals"] = EventProposal.objects.filter(
track__camp=self.camp, proposal_status=EventProposal.PROPOSAL_PENDING
)
return context
@ -102,6 +104,7 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi
"""
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
"""
fields = []
def form_valid(self, form):
@ -109,21 +112,24 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi
We have two submit buttons in this form, Approve and Reject
"""
logger.debug(form.data)
if 'approve' in form.data:
if "approve" in form.data:
# approve button was pressed
form.instance.mark_as_approved(self.request)
elif 'reject' in form.data:
elif "reject" in form.data:
# reject button was pressed
form.instance.mark_as_rejected(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(reverse('backoffice:manage_proposals', kwargs={'camp_slug': self.camp.slug}))
return redirect(
reverse("backoffice:manage_proposals", kwargs={"camp_slug": self.camp.slug})
)
class SpeakerProposalManageView(ProposalManageBaseView):
"""
This view allows an admin to approve/reject SpeakerProposals
"""
model = SpeakerProposal
template_name = "manage_speakerproposal.html"
@ -132,6 +138,7 @@ class EventProposalManageView(ProposalManageBaseView):
"""
This view allows an admin to approve/reject EventProposals
"""
model = EventProposal
template_name = "manage_eventproposal.html"
@ -140,34 +147,34 @@ class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
template_name = "orders_merchandise.html"
def get_queryset(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
camp_prefix = "BornHack {}".format(timezone.now().year)
return OrderProductRelation.objects.filter(
return (
OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Merchandise',
).filter(
product__name__startswith=camp_prefix
).order_by('order')
product__category__name="Merchandise",
)
.filter(product__name__startswith=camp_prefix)
.order_by("order")
)
class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
template_name = "merchandise_to_order.html"
def get_context_data(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
camp_prefix = "BornHack {}".format(timezone.now().year)
order_relations = OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Merchandise',
).filter(
product__name__startswith=camp_prefix
)
product__category__name="Merchandise",
).filter(product__name__startswith=camp_prefix)
merchandise_orders = {}
for relation in order_relations:
@ -178,7 +185,7 @@ class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateVie
merchandise_orders[relation.product.name] = relation.quantity
context = super().get_context_data(**kwargs)
context['merchandise'] = merchandise_orders
context["merchandise"] = merchandise_orders
return context
@ -186,34 +193,34 @@ class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
template_name = "orders_village.html"
def get_queryset(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
camp_prefix = "BornHack {}".format(timezone.now().year)
return OrderProductRelation.objects.filter(
return (
OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Villages',
).filter(
product__name__startswith=camp_prefix
).order_by('order')
product__category__name="Villages",
)
.filter(product__name__startswith=camp_prefix)
.order_by("order")
)
class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
template_name = "village_to_order.html"
def get_context_data(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
camp_prefix = "BornHack {}".format(timezone.now().year)
order_relations = OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Villages',
).filter(
product__name__startswith=camp_prefix
)
product__category__name="Villages",
).filter(product__name__startswith=camp_prefix)
village_orders = {}
for relation in order_relations:
@ -224,7 +231,7 @@ class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
village_orders[relation.product.name] = relation.quantity
context = super().get_context_data(**kwargs)
context['village'] = village_orders
context["village"] = village_orders
return context
@ -234,27 +241,28 @@ class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Chain
template_name = 'chain_list_backoffice.html'
template_name = "chain_list_backoffice.html"
class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Chain
template_name = 'chain_detail_backoffice.html'
slug_url_kwarg = 'chain_slug'
template_name = "chain_detail_backoffice.html"
slug_url_kwarg = "chain_slug"
class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Credebtor
template_name = 'credebtor_detail_backoffice.html'
slug_url_kwarg = 'credebtor_slug'
template_name = "credebtor_detail_backoffice.html"
slug_url_kwarg = "credebtor_slug"
################################
########### EXPENSES ###########
class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Expense
template_name = 'expense_list_backoffice.html'
template_name = "expense_list_backoffice.html"
def get_queryset(self, **kwargs):
"""
@ -268,46 +276,53 @@ class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Include unapproved expenses seperately
"""
context = super().get_context_data(**kwargs)
context['unapproved_expenses'] = Expense.objects.filter(camp=self.camp, approved__isnull=True)
context["unapproved_expenses"] = Expense.objects.filter(
camp=self.camp, approved__isnull=True
)
return context
class ExpenseDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Expense
template_name = 'expense_detail_backoffice.html'
fields = ['notes']
template_name = "expense_detail_backoffice.html"
fields = ["notes"]
def form_valid(self, form):
"""
We have two submit buttons in this form, Approve and Reject
"""
expense = form.save()
if 'approve' in form.data:
if "approve" in form.data:
# approve button was pressed
expense.approve(self.request)
elif 'reject' in form.data:
elif "reject" in form.data:
# reject button was pressed
expense.reject(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(reverse('backoffice:expense_list', kwargs={'camp_slug': self.camp.slug}))
return redirect(
reverse("backoffice:expense_list", kwargs={"camp_slug": self.camp.slug})
)
######################################
########### REIMBURSEMENTS ###########
class ReimbursementListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Reimbursement
template_name = 'reimbursement_list_backoffice.html'
template_name = "reimbursement_list_backoffice.html"
class ReimbursementDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Reimbursement
template_name = 'reimbursement_detail_backoffice.html'
template_name = "reimbursement_detail_backoffice.html"
class ReimbursementCreateUserSelectView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
template_name = 'reimbursement_create_userselect.html'
class ReimbursementCreateUserSelectView(
CampViewMixin, EconomyTeamPermissionMixin, ListView
):
template_name = "reimbursement_create_userselect.html"
def get_queryset(self):
queryset = User.objects.filter(
@ -316,20 +331,22 @@ class ReimbursementCreateUserSelectView(CampViewMixin, EconomyTeamPermissionMixi
reimbursement__isnull=True,
paid_by_bornhack=False,
approved=True,
).values_list('user', flat=True).distinct()
)
.values_list("user", flat=True)
.distinct()
)
return queryset
class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateView):
model = Reimbursement
template_name = 'reimbursement_create.html'
fields = ['notes', 'paid']
template_name = "reimbursement_create.html"
fields = ["notes", "paid"]
def dispatch(self, request, *args, **kwargs):
""" Get the user from kwargs """
print("inside dispatch() with method %s" % request.method)
self.reimbursement_user = get_object_or_404(User, pk=kwargs['user_id'])
self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"])
# get response now so we have self.camp available below
response = super().dispatch(request, *args, **kwargs)
@ -339,21 +356,27 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
def get(self, request, *args, **kwargs):
# does this user have any approved and un-reimbursed expenses?
if not self.reimbursement_user.expenses.filter(reimbursement__isnull=True, approved=True, paid_by_bornhack=False):
messages.error(request, "This user has no approved and unreimbursed expenses!")
return(redirect(reverse('backoffice:index', kwargs={'camp_slug': self.camp.slug})))
if not self.reimbursement_user.expenses.filter(
reimbursement__isnull=True, approved=True, paid_by_bornhack=False
):
messages.error(
request, "This user has no approved and unreimbursed expenses!"
)
return redirect(
reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug})
)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['expenses'] = Expense.objects.filter(
context["expenses"] = Expense.objects.filter(
user=self.reimbursement_user,
approved=True,
reimbursement__isnull=True,
paid_by_bornhack=False,
)
context['total_amount'] = context['expenses'].aggregate(Sum('amount'))
context['reimbursement_user'] = self.reimbursement_user
context["total_amount"] = context["expenses"].aggregate(Sum("amount"))
context["reimbursement_user"] = self.reimbursement_user
return context
def form_valid(self, form):
@ -361,17 +384,34 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
Set user and camp for the Reimbursement before saving
"""
# get the expenses for this user
expenses = Expense.objects.filter(user=self.reimbursement_user, approved=True, reimbursement__isnull=True, paid_by_bornhack=False)
expenses = Expense.objects.filter(
user=self.reimbursement_user,
approved=True,
reimbursement__isnull=True,
paid_by_bornhack=False,
)
if not expenses:
messages.error(self.request, "No expenses found")
return redirect(reverse('backoffice:reimbursement_list', kwargs={'camp_slug': self.camp.slug}))
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
# get the Economy team for this camp
try:
economyteam = Team.objects.get(camp=self.camp, name=settings.ECONOMYTEAM_NAME)
economyteam = Team.objects.get(
camp=self.camp, name=settings.ECONOMYTEAM_NAME
)
except Team.DoesNotExist:
messages.error(self.request, "No economy team found")
return redirect(reverse('backoffice:reimbursement_list', kwargs={'camp_slug': self.camp.slug}))
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
# create reimbursement in database
reimbursement = form.save(commit=False)
@ -387,50 +427,83 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
# create expense for this reimbursement
expense = Expense()
expense.camp=self.camp
expense.user=self.request.user
expense.amount=reimbursement.amount
expense.description="Payment of reimbursement %s to %s" % (reimbursement.pk, reimbursement.reimbursement_user)
expense.paid_by_bornhack=True
expense.responsible_team=economyteam
expense.approved=True
expense.reimbursement=reimbursement
expense.invoice_date=timezone.now()
expense.creditor=Credebtor.objects.get(name='Reimbursement')
expense.invoice.save("na.jpg", File(open(os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/na.jpg"), "rb")))
expense.camp = self.camp
expense.user = self.request.user
expense.amount = reimbursement.amount
expense.description = "Payment of reimbursement %s to %s" % (
reimbursement.pk,
reimbursement.reimbursement_user,
)
expense.paid_by_bornhack = True
expense.responsible_team = economyteam
expense.approved = True
expense.reimbursement = reimbursement
expense.invoice_date = timezone.now()
expense.creditor = Credebtor.objects.get(name="Reimbursement")
expense.invoice.save(
"na.jpg",
File(
open(
os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/na.jpg"),
"rb",
)
),
)
expense.save()
messages.success(self.request, "Reimbursement %s has been created with invoice_date %s" % (reimbursement.pk, timezone.now()))
return redirect(reverse('backoffice:reimbursement_detail', kwargs={'camp_slug': self.camp.slug, 'pk': reimbursement.pk}))
messages.success(
self.request,
"Reimbursement %s has been created with invoice_date %s"
% (reimbursement.pk, timezone.now()),
)
return redirect(
reverse(
"backoffice:reimbursement_detail",
kwargs={"camp_slug": self.camp.slug, "pk": reimbursement.pk},
)
)
class ReimbursementUpdateView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Reimbursement
template_name = 'reimbursement_form.html'
fields = ['notes', 'paid']
template_name = "reimbursement_form.html"
fields = ["notes", "paid"]
def get_success_url(self):
return reverse('backoffice:reimbursement_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.get_object().pk})
return reverse(
"backoffice:reimbursement_detail",
kwargs={"camp_slug": self.camp.slug, "pk": self.get_object().pk},
)
class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteView):
model = Reimbursement
template_name = 'reimbursement_delete.html'
fields = ['notes', 'paid']
template_name = "reimbursement_delete.html"
fields = ["notes", "paid"]
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if self.get_object().paid:
messages.error(request, "This reimbursement has already been paid so it cannot be deleted")
return redirect(reverse('backoffice:reimbursement_list', kwargs={'camp_slug': self.camp.slug}))
messages.error(
request,
"This reimbursement has already been paid so it cannot be deleted",
)
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
return response
################################
########### REVENUES ###########
class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Revenue
template_name = 'revenue_list_backoffice.html'
template_name = "revenue_list_backoffice.html"
def get_queryset(self, **kwargs):
"""
@ -444,27 +517,30 @@ class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Include unapproved revenues seperately
"""
context = super().get_context_data(**kwargs)
context['unapproved_revenues'] = Revenue.objects.filter(camp=self.camp, approved__isnull=True)
context["unapproved_revenues"] = Revenue.objects.filter(
camp=self.camp, approved__isnull=True
)
return context
class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Revenue
template_name = 'revenue_detail_backoffice.html'
fields = ['notes']
template_name = "revenue_detail_backoffice.html"
fields = ["notes"]
def form_valid(self, form):
"""
We have two submit buttons in this form, Approve and Reject
"""
revenue = form.save()
if 'approve' in form.data:
if "approve" in form.data:
# approve button was pressed
revenue.approve(self.request)
elif 'reject' in form.data:
elif "reject" in form.data:
# reject button was pressed
revenue.reject(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(reverse('backoffice:revenue_list', kwargs={'camp_slug': self.camp.slug}))
return redirect(
reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug})
)

View file

@ -9,5 +9,5 @@ class ProductCategoryAdmin(admin.ModelAdmin):
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'price', 'category', 'in_stock']
list_editable = ['in_stock']
list_display = ["name", "price", "category", "in_stock"]
list_editable = ["in_stock"]

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class BarConfig(AppConfig):
name = 'bar'
name = "bar"

View file

@ -10,36 +10,55 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('camps', '0022_camp_colour'),
]
dependencies = [("camps", "0022_camp_colour")]
operations = [
migrations.CreateModel(
name='Product',
name="Product",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('price', models.IntegerField()),
('in_stock', models.BooleanField(default=True)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("price", models.IntegerField()),
("in_stock", models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='ProductCategory',
name="ProductCategory",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255)),
('camp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='camps.Camp')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=255)),
(
"camp",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="camps.Camp"
),
),
],
options={
'abstract': False,
},
options={"abstract": False},
),
migrations.AddField(
model_name='product',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bar.ProductCategory'),
model_name="product",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="bar.ProductCategory"
),
),
]

View file

@ -8,22 +8,20 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('bar', '0001_initial'),
]
dependencies = [("bar", "0001_initial")]
operations = [
migrations.AlterModelOptions(name="product", options={"ordering": ("name",)}),
migrations.AlterModelOptions(
name='product',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='productcategory',
options={'ordering': ('name',)},
name="productcategory", options={"ordering": ("name",)}
),
migrations.AlterField(
model_name='product',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='bar.ProductCategory'),
model_name="product",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="products",
to="bar.ProductCategory",
),
),
]

View file

@ -8,19 +8,23 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('bar', '0002_auto_20170916_2128'),
]
dependencies = [("bar", "0002_auto_20170916_2128")]
operations = [
migrations.AlterField(
model_name='product',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='bar.ProductCategory'),
model_name="product",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="products",
to="bar.ProductCategory",
),
),
migrations.AlterField(
model_name='productcategory',
name='camp',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='camps.Camp'),
model_name="productcategory",
name="camp",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="camps.Camp"
),
),
]

View file

@ -4,7 +4,7 @@ from utils.models import CampRelatedModel
class ProductCategory(CampRelatedModel):
name = models.CharField(max_length=255)
camp = models.ForeignKey('camps.Camp', on_delete=models.PROTECT)
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
def __str__(self):
return self.name
@ -17,9 +17,7 @@ class Product(models.Model):
name = models.CharField(max_length=255)
price = models.IntegerField()
category = models.ForeignKey(
ProductCategory,
related_name="products",
on_delete=models.PROTECT
ProductCategory, related_name="products", on_delete=models.PROTECT
)
in_stock = models.BooleanField(default=True)

View file

@ -9,4 +9,3 @@ from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bornhack.settings")
django.setup()
application = get_default_application()

View file

@ -5,10 +5,10 @@ from channels.auth import AuthMiddlewareStack
from program.consumers import ScheduleConsumer
application = ProtocolTypeRouter({
application = ProtocolTypeRouter(
{
"websocket": AuthMiddlewareStack(
URLRouter([
url(r"^schedule/", ScheduleConsumer)
])
URLRouter([url(r"^schedule/", ScheduleConsumer)])
)
})
}
)

View file

@ -11,19 +11,17 @@ from program.schema import ProgramQuery
class CampNode(DjangoObjectType):
class Meta:
model = Camp
interfaces = (relay.Node, )
filter_fields = {
'title': ['icontains', 'iexact'],
}
interfaces = (relay.Node,)
filter_fields = {"title": ["icontains", "iexact"]}
only_fields = (
'title',
'slug',
'tagline',
'shortslug',
'buildup',
'camp',
'teardown',
'colour',
"title",
"slug",
"tagline",
"shortslug",
"buildup",
"camp",
"teardown",
"colour",
)
def resolve_buildup(self, info):

View file

@ -2,6 +2,7 @@ import os
from .environment_settings import *
def local_dir(entry):
return os.path.join(os.path.dirname(os.path.dirname(__file__)), entry)

View file

@ -19,206 +19,115 @@ from villages.views import *
admin.site.login = login_required(admin.site.login)
urlpatterns = [
path('profile/', include('allauth.urls')),
path('profile/', include('allauth_2fa.urls')),
path('profile/', include('profiles.urls', namespace='profiles')),
path("profile/", include("allauth.urls")),
path("profile/", include("allauth_2fa.urls")),
path("profile/", include("profiles.urls", namespace="profiles")),
path("tickets/", include("tickets.urls", namespace="tickets")),
path("shop/", include("shop.urls", namespace="shop")),
path("news/", include("news.urls", namespace="news")),
path(
'tickets/',
include('tickets.urls', namespace='tickets')
"contact/", TemplateView.as_view(template_name="contact.html"), name="contact"
),
path("conduct/", TemplateView.as_view(template_name="coc.html"), name="conduct"),
path("login/", LoginView.as_view(), name="account_login"),
path("logout/", LogoutView.as_view(), name="account_logout"),
path(
"privacy-policy/",
TemplateView.as_view(template_name="legal/privacy_policy.html"),
name="privacy-policy",
),
path(
'shop/',
include('shop.urls', namespace='shop')
"general-terms-and-conditions/",
TemplateView.as_view(template_name="legal/general_terms_and_conditions.html"),
name="general-terms",
),
path(
'news/',
include('news.urls', namespace='news')
),
path(
'contact/',
TemplateView.as_view(template_name='contact.html'),
name='contact'
),
path(
'conduct/',
TemplateView.as_view(template_name='coc.html'),
name='conduct'
),
path(
'login/',
LoginView.as_view(),
name='account_login',
),
path(
'logout/',
LogoutView.as_view(),
name='account_logout',
),
path(
'privacy-policy/',
TemplateView.as_view(template_name='legal/privacy_policy.html'),
name='privacy-policy'
),
path(
'general-terms-and-conditions/',
TemplateView.as_view(template_name='legal/general_terms_and_conditions.html'),
name='general-terms'
),
path('admin/', admin.site.urls),
path("admin/", admin.site.urls),
# We don't need CSRF checks for the API
path('api/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
path(
'camps/',
CampListView.as_view(),
name='camp_list'
),
path(
'token/',
include('tokens.urls', namespace='tokens'),
),
path("api/", csrf_exempt(GraphQLView.as_view(graphiql=True))),
path("camps/", CampListView.as_view(), name="camp_list"),
path("token/", include("tokens.urls", namespace="tokens")),
# camp redirect views here
path(
'',
"",
CampRedirectView.as_view(),
kwargs={'page': 'camp_detail'},
name='camp_detail_redirect',
kwargs={"page": "camp_detail"},
name="camp_detail_redirect",
),
path(
'program/',
"program/",
CampRedirectView.as_view(),
kwargs={'page': 'schedule_index'},
name='schedule_index_redirect',
kwargs={"page": "schedule_index"},
name="schedule_index_redirect",
),
path(
'info/',
"info/",
CampRedirectView.as_view(),
kwargs={'page': 'info'},
name='info_redirect',
kwargs={"page": "info"},
name="info_redirect",
),
path(
'sponsors/',
"sponsors/",
CampRedirectView.as_view(),
kwargs={'page': 'sponsors'},
name='sponsors_redirect',
kwargs={"page": "sponsors"},
name="sponsors_redirect",
),
path(
'villages/',
"villages/",
CampRedirectView.as_view(),
kwargs={'page': 'village_list'},
name='village_list_redirect',
kwargs={"page": "village_list"},
name="village_list_redirect",
),
path(
'people/',
PeopleView.as_view(),
name='people',
),
path("people/", PeopleView.as_view(), name="people"),
# camp specific urls below here
path(
'<slug:camp_slug>/', include([
"<slug:camp_slug>/",
include(
[
path("", CampDetailView.as_view(), name="camp_detail"),
path("info/", CampInfoView.as_view(), name="info"),
path("program/", include("program.urls", namespace="program")),
path("sponsors/", SponsorsView.as_view(), name="sponsors"),
path("bar/menu/", MenuView.as_view(), name="menu"),
path(
'',
CampDetailView.as_view(),
name='camp_detail'
),
"villages/",
include(
[
path("", VillageListView.as_view(), name="village_list"),
path(
'info/',
CampInfoView.as_view(),
name='info'
),
path(
'program/',
include('program.urls', namespace='program'),
),
path(
'sponsors/',
SponsorsView.as_view(),
name='sponsors'
),
path(
'bar/menu/',
MenuView.as_view(),
name='menu'
),
path(
'villages/', include([
path(
'',
VillageListView.as_view(),
name='village_list'
),
path(
'create/',
"create/",
VillageCreateView.as_view(),
name='village_create'
name="village_create",
),
path(
'<slug:slug>/delete/',
"<slug:slug>/delete/",
VillageDeleteView.as_view(),
name='village_delete'
name="village_delete",
),
path(
'<slug:slug>/edit/',
"<slug:slug>/edit/",
VillageUpdateView.as_view(),
name='village_update'
name="village_update",
),
# this has to be the last url in the list
path(
'<slug:slug>/',
"<slug:slug>/",
VillageDetailView.as_view(),
name='village_detail'
name="village_detail",
),
])
]
),
path(
'teams/',
include('teams.urls', namespace='teams')
),
path(
'rideshare/',
include('rideshare.urls', namespace='rideshare')
path("teams/", include("teams.urls", namespace="teams")),
path("rideshare/", include("rideshare.urls", namespace="rideshare")),
path("backoffice/", include("backoffice.urls", namespace="backoffice")),
path("feedback/", FeedbackCreate.as_view(), name="feedback"),
path("economy/", include("economy.urls", namespace="economy")),
]
),
path(
'backoffice/',
include('backoffice.urls', namespace='backoffice')
),
path(
'feedback/',
FeedbackCreate.as_view(),
name='feedback'
),
path(
'economy/',
include('economy.urls', namespace='economy'),
),
])
)
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns

View file

@ -5,4 +5,3 @@ from . import models
@admin.register(models.Camp)
class CampModelAdmin(admin.ModelAdmin):
pass

View file

@ -7,19 +7,15 @@ def camp(request):
Return it after adding the slug to request.session along with a "camps"
queryset containing all camps (used to build the menu and such)
"""
if request.resolver_match and 'camp_slug' in request.resolver_match.kwargs:
if request.resolver_match and "camp_slug" in request.resolver_match.kwargs:
try:
camp = Camp.objects.get(slug=request.resolver_match.kwargs['camp_slug'])
request.session['campslug'] = camp.slug
camp = Camp.objects.get(slug=request.resolver_match.kwargs["camp_slug"])
request.session["campslug"] = camp.slug
except Camp.DoesNotExist:
request.session['campslug'] = None
request.session["campslug"] = None
camp = None
else:
request.session['campslug'] = None
request.session["campslug"] = None
camp = None
return {
'camps': Camp.objects.all().order_by('-camp'),
'camp': camp
}
return {"camps": Camp.objects.all().order_by("-camp"), "camp": camp}

View file

@ -6,64 +6,58 @@ import os
class Command(BaseCommand):
help = 'Creates html files needed for a camp'
help = "Creates html files needed for a camp"
def add_arguments(self, parser):
parser.add_argument('camp_slug', type=str)
parser.add_argument("camp_slug", type=str)
def output(self, message):
self.stdout.write('{}: {}'.format(
timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
message
)
self.stdout.write(
"{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message)
)
def local_dir(self, entry):
return os.path.join(
settings.DJANGO_BASE_PATH,
entry
)
return os.path.join(settings.DJANGO_BASE_PATH, entry)
def handle(self, *args, **options):
# files to create, relative to DJANGO_BASE_PATH
files = [
'camps/templates/{camp_slug}_camp_detail.html',
]
files = ["camps/templates/{camp_slug}_camp_detail.html"]
# directories to create, relative to DJANGO_BASE_PATH
dirs = [
'static_src/img/{camp_slug}/logo'
]
dirs = ["static_src/img/{camp_slug}/logo"]
camp_slug = options['camp_slug']
camp_slug = options["camp_slug"]
for _file in files:
path = self.local_dir(_file.format(camp_slug=camp_slug))
if os.path.isfile(_file):
self.output('File {} exists...'.format(path))
self.output("File {} exists...".format(path))
else:
self.output('Creating {}'.format(path))
with open(path, mode='w', encoding='utf-8') as f:
self.output("Creating {}".format(path))
with open(path, mode="w", encoding="utf-8") as f:
f.write(_file.format(camp_slug=camp_slug))
for _dir in dirs:
path = self.local_dir(_file.format(camp_slug=camp_slug))
if os.path.exists(path):
self.output('Path {} exists...'.format(path))
self.output("Path {} exists...".format(path))
else:
self.output('Creating {}'.format(path))
self.output("Creating {}".format(path))
os.mkdir(path, mode=0o644)
self.output('All there is left is to create:')
self.output("All there is left is to create:")
self.output(
self.local_dir(
'static_src/img/{camp_slug}/logo/{camp_slug}-logo-large.png'.format(camp_slug=camp_slug)
"static_src/img/{camp_slug}/logo/{camp_slug}-logo-large.png".format(
camp_slug=camp_slug
)
)
)
self.output(
self.local_dir(
'static_src/img/{camp_slug}/logo/{camp_slug}-logo-small.png'.format(camp_slug=camp_slug)
"static_src/img/{camp_slug}/logo/{camp_slug}-logo-small.png".format(
camp_slug=camp_slug
)
)
)

View file

@ -8,71 +8,196 @@ from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name='Camp',
name="Camp",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, serialize=False, editable=False, primary_key=True)),
('name', models.CharField(max_length=255, help_text='Name of the camp, ie. Bornhack.', verbose_name='Name')),
('start', models.DateTimeField(help_text='When the camp starts.', unique=True, verbose_name='Start date')),
('end', models.DateTimeField(help_text='When the camp ends.', unique=True, verbose_name='End date')),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
serialize=False,
editable=False,
primary_key=True,
),
),
(
"name",
models.CharField(
max_length=255,
help_text="Name of the camp, ie. Bornhack.",
verbose_name="Name",
),
),
(
"start",
models.DateTimeField(
help_text="When the camp starts.",
unique=True,
verbose_name="Start date",
),
),
(
"end",
models.DateTimeField(
help_text="When the camp ends.",
unique=True,
verbose_name="End date",
),
),
],
options={
'verbose_name_plural': 'Camps',
'verbose_name': 'Camp',
},
options={"verbose_name_plural": "Camps", "verbose_name": "Camp"},
),
migrations.CreateModel(
name='Day',
name="Day",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, serialize=False, editable=False, primary_key=True)),
('date', models.DateField(help_text='What date?', verbose_name='Date')),
('camp', models.ForeignKey(on_delete=models.PROTECT, to='camps.Camp', help_text='Which camp does this day belong to.', verbose_name='Camp')),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
serialize=False,
editable=False,
primary_key=True,
),
),
("date", models.DateField(help_text="What date?", verbose_name="Date")),
(
"camp",
models.ForeignKey(
on_delete=models.PROTECT,
to="camps.Camp",
help_text="Which camp does this day belong to.",
verbose_name="Camp",
),
),
],
options={
'verbose_name_plural': 'Days',
'verbose_name': 'Day',
},
options={"verbose_name_plural": "Days", "verbose_name": "Day"},
),
migrations.CreateModel(
name='Expense',
name="Expense",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, serialize=False, editable=False, primary_key=True)),
('description', models.CharField(max_length=255, help_text='What this expense covers.', verbose_name='Description')),
('amount', models.DecimalField(max_digits=7, help_text='The amount of the expense.', verbose_name='Amount', decimal_places=2)),
('currency', models.CharField(max_length=3, choices=[('btc', 'BTC'), ('dkk', 'DKK'), ('eur', 'EUR'), ('sek', 'SEK')], help_text='What currency the amount is in.', verbose_name='Currency')),
('camp', models.ForeignKey(on_delete=models.PROTECT, to='camps.Camp', help_text='The camp to which this expense relates to.', verbose_name='Camp')),
('covered_by', models.ForeignKey(on_delete=models.PROTECT, to=settings.AUTH_USER_MODEL, blank=True, help_text='Which user, if any, covered this expense.', verbose_name='Covered by', null=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
serialize=False,
editable=False,
primary_key=True,
),
),
(
"description",
models.CharField(
max_length=255,
help_text="What this expense covers.",
verbose_name="Description",
),
),
(
"amount",
models.DecimalField(
max_digits=7,
help_text="The amount of the expense.",
verbose_name="Amount",
decimal_places=2,
),
),
(
"currency",
models.CharField(
max_length=3,
choices=[
("btc", "BTC"),
("dkk", "DKK"),
("eur", "EUR"),
("sek", "SEK"),
],
options={
'verbose_name_plural': 'Expenses',
'verbose_name': 'Expense',
},
help_text="What currency the amount is in.",
verbose_name="Currency",
),
),
(
"camp",
models.ForeignKey(
on_delete=models.PROTECT,
to="camps.Camp",
help_text="The camp to which this expense relates to.",
verbose_name="Camp",
),
),
(
"covered_by",
models.ForeignKey(
on_delete=models.PROTECT,
to=settings.AUTH_USER_MODEL,
blank=True,
help_text="Which user, if any, covered this expense.",
verbose_name="Covered by",
null=True,
),
),
],
options={"verbose_name_plural": "Expenses", "verbose_name": "Expense"},
),
migrations.CreateModel(
name='Signup',
name="Signup",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, serialize=False, editable=False, primary_key=True)),
('cost', models.DecimalField(default=1500.0, decimal_places=2, help_text='What the user should/is willing to pay for this signup.', verbose_name='Cost', max_digits=7)),
('paid', models.BooleanField(help_text='Whether the user has paid.', verbose_name='Paid?', default=False)),
('camp', models.ForeignKey(on_delete=models.PROTECT, to='camps.Camp', help_text='The camp that has been signed up for.', verbose_name='Camp')),
('user', models.ForeignKey(on_delete=models.PROTECT, to=settings.AUTH_USER_MODEL, help_text='The user that has signed up.', verbose_name='User')),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
serialize=False,
editable=False,
primary_key=True,
),
),
(
"cost",
models.DecimalField(
default=1500.0,
decimal_places=2,
help_text="What the user should/is willing to pay for this signup.",
verbose_name="Cost",
max_digits=7,
),
),
(
"paid",
models.BooleanField(
help_text="Whether the user has paid.",
verbose_name="Paid?",
default=False,
),
),
(
"camp",
models.ForeignKey(
on_delete=models.PROTECT,
to="camps.Camp",
help_text="The camp that has been signed up for.",
verbose_name="Camp",
),
),
(
"user",
models.ForeignKey(
on_delete=models.PROTECT,
to=settings.AUTH_USER_MODEL,
help_text="The user that has signed up.",
verbose_name="User",
),
),
],
options={
'verbose_name_plural': 'Signups',
'verbose_name': 'Signup',
},
options={"verbose_name_plural": "Signups", "verbose_name": "Signup"},
),
]

View file

@ -8,14 +8,18 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0001_initial'),
]
dependencies = [("camps", "0001_initial")]
operations = [
migrations.AlterField(
model_name='day',
name='camp',
field=models.ForeignKey(help_text='Which camp does this day belong to.', on_delete=django.db.models.deletion.CASCADE, related_name='days', to='camps.Camp', verbose_name='Camp'),
model_name="day",
name="camp",
field=models.ForeignKey(
help_text="Which camp does this day belong to.",
on_delete=django.db.models.deletion.CASCADE,
related_name="days",
to="camps.Camp",
verbose_name="Camp",
),
)
]

View file

@ -7,20 +7,10 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0002_auto_20160117_1718'),
]
dependencies = [("camps", "0002_auto_20160117_1718")]
operations = [
migrations.RemoveField(
model_name='signup',
name='camp',
),
migrations.RemoveField(
model_name='signup',
name='user',
),
migrations.DeleteModel(
name='Signup',
),
migrations.RemoveField(model_name="signup", name="camp"),
migrations.RemoveField(model_name="signup", name="user"),
migrations.DeleteModel(name="Signup"),
]

View file

@ -7,14 +7,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0003_auto_20160422_2019'),
]
dependencies = [("camps", "0003_auto_20160422_2019")]
operations = [
migrations.AddField(
model_name='camp',
name='ticket_sale_open',
field=models.BooleanField(default=False, help_text='Whether tickets are for sale or not.', verbose_name='Ticket sale open?'),
model_name="camp",
name="ticket_sale_open",
field=models.BooleanField(
default=False,
help_text="Whether tickets are for sale or not.",
verbose_name="Ticket sale open?",
),
)
]

View file

@ -7,18 +7,17 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0004_camp_ticket_sale_open'),
]
dependencies = [("camps", "0004_camp_ticket_sale_open")]
operations = [
migrations.RemoveField(
model_name='camp',
name='ticket_sale_open',
),
migrations.RemoveField(model_name="camp", name="ticket_sale_open"),
migrations.AddField(
model_name='camp',
name='shop_open',
field=models.BooleanField(default=False, help_text='Whether the shop is open or not.', verbose_name='Shop open?'),
model_name="camp",
name="shop_open",
field=models.BooleanField(
default=False,
help_text="Whether the shop is open or not.",
verbose_name="Shop open?",
),
),
]

View file

@ -7,13 +7,15 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0005_auto_20160510_2011'),
]
dependencies = [("camps", "0005_auto_20160510_2011")]
operations = [
migrations.AlterModelOptions(
name='day',
options={'ordering': ['date'], 'verbose_name': 'Day', 'verbose_name_plural': 'Days'},
),
name="day",
options={
"ordering": ["date"],
"verbose_name": "Day",
"verbose_name_plural": "Days",
},
)
]

View file

@ -13,81 +13,101 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('camps', '0006_auto_20160804_1705'),
("camps", "0006_auto_20160804_1705"),
]
operations = [
migrations.RemoveField(
model_name='day',
name='camp',
),
migrations.RemoveField(
model_name='camp',
name='shop_open',
),
migrations.RemoveField(
model_name='expense',
name='amount',
),
migrations.RemoveField(
model_name='expense',
name='camp',
),
migrations.RemoveField(
model_name='expense',
name='covered_by',
),
migrations.RemoveField(
model_name='expense',
name='currency',
),
migrations.RemoveField(model_name="day", name="camp"),
migrations.RemoveField(model_name="camp", name="shop_open"),
migrations.RemoveField(model_name="expense", name="amount"),
migrations.RemoveField(model_name="expense", name="camp"),
migrations.RemoveField(model_name="expense", name="covered_by"),
migrations.RemoveField(model_name="expense", name="currency"),
migrations.AddField(
model_name='camp',
name='slug',
field=models.SlugField(default='', help_text=b'The url slug to use for this camp', verbose_name=b'Url Slug'),
model_name="camp",
name="slug",
field=models.SlugField(
default="",
help_text=b"The url slug to use for this camp",
verbose_name=b"Url Slug",
),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='dkk_amount',
field=models.DecimalField(decimal_places=2, default=0, help_text=b'The DKK amount of the expense.', max_digits=7, verbose_name=b'DKK Amount'),
model_name="expense",
name="dkk_amount",
field=models.DecimalField(
decimal_places=2,
default=0,
help_text=b"The DKK amount of the expense.",
max_digits=7,
verbose_name=b"DKK Amount",
),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='payment_time',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 12, 18, 3, 10, 378604, tzinfo=utc), help_text=b'The date and time this expense was paid.', verbose_name=b'Expense date/time'),
model_name="expense",
name="payment_time",
field=models.DateTimeField(
default=datetime.datetime(2016, 12, 12, 18, 3, 10, 378604, tzinfo=utc),
help_text=b"The date and time this expense was paid.",
verbose_name=b"Expense date/time",
),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='receipt',
field=models.ImageField(default='', help_text=b'Upload a scan or image of the receipt', upload_to=b'', verbose_name=b'Image of receipt'),
model_name="expense",
name="receipt",
field=models.ImageField(
default="",
help_text=b"Upload a scan or image of the receipt",
upload_to=b"",
verbose_name=b"Image of receipt",
),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='refund_paid',
field=models.BooleanField(default=False, help_text=b'Has this expense been refunded to the user?', verbose_name=b'Refund paid?'),
model_name="expense",
name="refund_paid",
field=models.BooleanField(
default=False,
help_text=b"Has this expense been refunded to the user?",
verbose_name=b"Refund paid?",
),
),
migrations.AddField(
model_name='expense',
name='refund_user',
field=models.ForeignKey(blank=True, help_text=b'Which user, if any, covered this expense and should be refunded.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name=b'Refund user'),
model_name="expense",
name="refund_user",
field=models.ForeignKey(
blank=True,
help_text=b"Which user, if any, covered this expense and should be refunded.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name=b"Refund user",
),
),
migrations.AlterField(
model_name='camp',
name='end',
field=models.DateTimeField(help_text=b'When the camp ends.', verbose_name=b'End date'),
model_name="camp",
name="end",
field=models.DateTimeField(
help_text=b"When the camp ends.", verbose_name=b"End date"
),
),
migrations.AlterField(
model_name='camp',
name='name',
field=models.CharField(help_text=b'Name of the camp, ie. Bornhack 2016.', max_length=255, verbose_name=b'Name'),
model_name="camp",
name="name",
field=models.CharField(
help_text=b"Name of the camp, ie. Bornhack 2016.",
max_length=255,
verbose_name=b"Name",
),
),
migrations.AlterField(
model_name='camp',
name='start',
field=models.DateTimeField(help_text=b'When the camp starts.', verbose_name=b'Start date'),
model_name="camp",
name="start",
field=models.DateTimeField(
help_text=b"When the camp starts.", verbose_name=b"Start date"
),
),
]

View file

@ -8,12 +8,8 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('program', '0010_auto_20161212_1809'),
('camps', '0007_auto_20161212_1803'),
("program", "0010_auto_20161212_1809"),
("camps", "0007_auto_20161212_1803"),
]
operations = [
migrations.DeleteModel(
name='Day',
),
]
operations = [migrations.DeleteModel(name="Day")]

View file

@ -9,31 +9,31 @@ from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('camps', '0008_delete_day'),
]
dependencies = [("camps", "0008_delete_day")]
operations = [
migrations.RenameField(model_name="camp", old_name="end", new_name="camp_end"),
migrations.RenameField(
model_name='camp',
old_name='end',
new_name='camp_end',
),
migrations.RenameField(
model_name='camp',
old_name='start',
new_name='camp_start',
model_name="camp", old_name="start", new_name="camp_start"
),
migrations.AddField(
model_name='camp',
name='buildup_start',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 20, 16, 45, 39, 609630, tzinfo=utc), help_text=b'When the camp buildup starts.', verbose_name=b'Buildup Start date'),
model_name="camp",
name="buildup_start",
field=models.DateTimeField(
default=datetime.datetime(2016, 12, 20, 16, 45, 39, 609630, tzinfo=utc),
help_text=b"When the camp buildup starts.",
verbose_name=b"Buildup Start date",
),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='teardown_end',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 20, 16, 45, 44, 532143, tzinfo=utc), help_text=b'When the camp teardown ends.', verbose_name=b'Start date'),
model_name="camp",
name="teardown_end",
field=models.DateTimeField(
default=datetime.datetime(2016, 12, 20, 16, 45, 44, 532143, tzinfo=utc),
help_text=b"When the camp teardown ends.",
verbose_name=b"Start date",
),
preserve_default=False,
),
]

View file

@ -7,25 +7,30 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0009_auto_20161220_1645'),
]
dependencies = [("camps", "0009_auto_20161220_1645")]
operations = [
migrations.RemoveField(
model_name='camp',
name='name',
),
migrations.RemoveField(model_name="camp", name="name"),
migrations.AddField(
model_name='camp',
name='tagline',
field=models.CharField(default='', help_text=b'Tagline of the camp, ie. "Initial Commit"', max_length=255, verbose_name=b'Tagline'),
model_name="camp",
name="tagline",
field=models.CharField(
default="",
help_text=b'Tagline of the camp, ie. "Initial Commit"',
max_length=255,
verbose_name=b"Tagline",
),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='title',
field=models.CharField(default='', help_text=b'Title of the camp, ie. Bornhack 2016.', max_length=255, verbose_name=b'Title'),
model_name="camp",
name="title",
field=models.CharField(
default="",
help_text=b"Title of the camp, ie. Bornhack 2016.",
max_length=255,
verbose_name=b"Title",
),
preserve_default=False,
),
]

View file

@ -7,21 +7,29 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0010_auto_20161220_1714'),
]
dependencies = [("camps", "0010_auto_20161220_1714")]
operations = [
migrations.AddField(
model_name='camp',
name='logo_large',
field=models.CharField(default='', help_text=b'The filename of the large logo to use on the frontpage of this camp', max_length=100, verbose_name=b'Large logo for this camp'),
model_name="camp",
name="logo_large",
field=models.CharField(
default="",
help_text=b"The filename of the large logo to use on the frontpage of this camp",
max_length=100,
verbose_name=b"Large logo for this camp",
),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='logo_small',
field=models.CharField(default='', help_text=b'The filename of the small logo to use in the top of the page for this camp', max_length=100, verbose_name=b'Small logo for this camp'),
model_name="camp",
name="logo_small",
field=models.CharField(
default="",
help_text=b"The filename of the small logo to use in the top of the page for this camp",
max_length=100,
verbose_name=b"Small logo for this camp",
),
preserve_default=False,
),
]

View file

@ -7,17 +7,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0011_auto_20161228_1750'),
]
dependencies = [("camps", "0011_auto_20161228_1750")]
operations = [
migrations.RemoveField(
model_name='camp',
name='logo_large',
),
migrations.RemoveField(
model_name='camp',
name='logo_small',
),
migrations.RemoveField(model_name="camp", name="logo_large"),
migrations.RemoveField(model_name="camp", name="logo_small"),
]

View file

@ -8,40 +8,36 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0012_auto_20161228_2312'),
]
dependencies = [("camps", "0012_auto_20161228_2312")]
operations = [
migrations.RemoveField(
model_name='camp',
name='buildup_start',
migrations.RemoveField(model_name="camp", name="buildup_start"),
migrations.RemoveField(model_name="camp", name="camp_end"),
migrations.RemoveField(model_name="camp", name="camp_start"),
migrations.RemoveField(model_name="camp", name="teardown_end"),
migrations.AddField(
model_name="camp",
name="buildup",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp buildup period.",
null=True,
verbose_name=b"Buildup Period",
),
migrations.RemoveField(
model_name='camp',
name='camp_end',
),
migrations.RemoveField(
model_name='camp',
name='camp_start',
),
migrations.RemoveField(
model_name='camp',
name='teardown_end',
),
migrations.AddField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp buildup period.', null=True, verbose_name=b'Buildup Period'),
model_name="camp",
name="camp",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp period.", null=True, verbose_name=b"Camp Period"
),
),
migrations.AddField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp period.', null=True, verbose_name=b'Camp Period'),
model_name="camp",
name="teardown",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp teardown period.",
null=True,
verbose_name=b"Teardown period",
),
migrations.AddField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp teardown period.', null=True, verbose_name=b'Teardown period'),
),
]

View file

@ -9,24 +9,34 @@ from django.utils import timezone
class Migration(migrations.Migration):
dependencies = [
('camps', '0013_auto_20161229_2201'),
]
dependencies = [("camps", "0013_auto_20161229_2201")]
operations = [
migrations.AlterField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp buildup period.', verbose_name=b'Buildup Period', default=(timezone.now(),None)),
model_name="camp",
name="buildup",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp buildup period.",
verbose_name=b"Buildup Period",
default=(timezone.now(), None),
),
),
migrations.AlterField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp period.', verbose_name=b'Camp Period', default=(timezone.now(),None)),
model_name="camp",
name="camp",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp period.",
verbose_name=b"Camp Period",
default=(timezone.now(), None),
),
),
migrations.AlterField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp teardown period.', verbose_name=b'Teardown period', default=(timezone.now(),None)),
model_name="camp",
name="teardown",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp teardown period.",
verbose_name=b"Teardown period",
default=(timezone.now(), None),
),
),
]

View file

@ -7,16 +7,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0014_auto_20161229_2202'),
]
dependencies = [("camps", "0014_auto_20161229_2202")]
operations = [
migrations.RemoveField(
model_name='expense',
name='refund_user',
),
migrations.DeleteModel(
name='Expense',
),
migrations.RemoveField(model_name="expense", name="refund_user"),
migrations.DeleteModel(name="Expense"),
]

View file

@ -7,14 +7,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0015_auto_20170116_1634'),
]
dependencies = [("camps", "0015_auto_20170116_1634")]
operations = [
migrations.AddField(
model_name='camp',
name='description',
field=models.TextField(default=b'', help_text=b'Description of the camp, shown on the camp frontpage. HTML and markdown supported.', verbose_name=b'Description'),
model_name="camp",
name="description",
field=models.TextField(
default=b"",
help_text=b"Description of the camp, shown on the camp frontpage. HTML and markdown supported.",
verbose_name=b"Description",
),
)
]

View file

@ -7,13 +7,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0016_camp_description'),
]
dependencies = [("camps", "0016_camp_description")]
operations = [
migrations.RemoveField(
model_name='camp',
name='description',
),
]
operations = [migrations.RemoveField(model_name="camp", name="description")]

View file

@ -8,24 +8,28 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0017_remove_camp_description'),
]
dependencies = [("camps", "0017_remove_camp_description")]
operations = [
migrations.AlterField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp buildup period.', verbose_name=b'Buildup Period'),
model_name="camp",
name="buildup",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp buildup period.", verbose_name=b"Buildup Period"
),
),
migrations.AlterField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp period.', verbose_name=b'Camp Period'),
model_name="camp",
name="camp",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp period.", verbose_name=b"Camp Period"
),
),
migrations.AlterField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp teardown period.', verbose_name=b'Teardown period'),
model_name="camp",
name="teardown",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text=b"The camp teardown period.", verbose_name=b"Teardown period"
),
),
]

View file

@ -8,39 +8,53 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0018_auto_20170128_1841'),
]
dependencies = [("camps", "0018_auto_20170128_1841")]
operations = [
migrations.AlterField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='The camp buildup period.', verbose_name='Buildup Period'),
model_name="camp",
name="buildup",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text="The camp buildup period.", verbose_name="Buildup Period"
),
),
migrations.AlterField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='The camp period.', verbose_name='Camp Period'),
model_name="camp",
name="camp",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text="The camp period.", verbose_name="Camp Period"
),
),
migrations.AlterField(
model_name='camp',
name='slug',
field=models.SlugField(help_text='The url slug to use for this camp', verbose_name='Url Slug'),
model_name="camp",
name="slug",
field=models.SlugField(
help_text="The url slug to use for this camp", verbose_name="Url Slug"
),
),
migrations.AlterField(
model_name='camp',
name='tagline',
field=models.CharField(help_text='Tagline of the camp, ie. "Initial Commit"', max_length=255, verbose_name='Tagline'),
model_name="camp",
name="tagline",
field=models.CharField(
help_text='Tagline of the camp, ie. "Initial Commit"',
max_length=255,
verbose_name="Tagline",
),
),
migrations.AlterField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text='The camp teardown period.', verbose_name='Teardown period'),
model_name="camp",
name="teardown",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text="The camp teardown period.", verbose_name="Teardown period"
),
),
migrations.AlterField(
model_name='camp',
name='title',
field=models.CharField(help_text='Title of the camp, ie. Bornhack 2016.', max_length=255, verbose_name='Title'),
model_name="camp",
name="title",
field=models.CharField(
help_text="Title of the camp, ie. Bornhack 2016.",
max_length=255,
verbose_name="Title",
),
),
]

View file

@ -7,14 +7,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0019_auto_20170131_1849'),
]
dependencies = [("camps", "0019_auto_20170131_1849")]
operations = [
migrations.AddField(
model_name='camp',
name='read_only',
field=models.BooleanField(default=False, help_text='Whether the camp is read only (i.e. in the past)'),
model_name="camp",
name="read_only",
field=models.BooleanField(
default=False,
help_text="Whether the camp is read only (i.e. in the past)",
),
)
]

View file

@ -7,13 +7,15 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0020_camp_read_only'),
]
dependencies = [("camps", "0020_camp_read_only")]
operations = [
migrations.AlterModelOptions(
name='camp',
options={'ordering': ['-title'], 'verbose_name': 'Camp', 'verbose_name_plural': 'Camps'},
),
name="camp",
options={
"ordering": ["-title"],
"verbose_name": "Camp",
"verbose_name_plural": "Camps",
},
)
]

View file

@ -7,15 +7,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0021_auto_20170711_2247'),
]
dependencies = [("camps", "0021_auto_20170711_2247")]
operations = [
migrations.AddField(
model_name='camp',
name='colour',
field=models.CharField(default='#000000', help_text='The primary colour for the camp in hex', max_length=7, verbose_name='Colour'),
preserve_default=False,
model_name="camp",
name="colour",
field=models.CharField(
default="#000000",
help_text="The primary colour for the camp in hex",
max_length=7,
verbose_name="Colour",
),
preserve_default=False,
)
]

View file

@ -7,14 +7,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0022_camp_colour'),
]
dependencies = [("camps", "0022_camp_colour")]
operations = [
migrations.AddField(
model_name='camp',
name='shortslug',
field=models.SlugField(blank=True, help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.', verbose_name='Short Slug'),
model_name="camp",
name="shortslug",
field=models.SlugField(
blank=True,
help_text="Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.",
verbose_name="Short Slug",
),
)
]

View file

@ -4,19 +4,16 @@ from __future__ import unicode_literals
from django.db import migrations
def populate_camp_shortslugs(apps, schema_editor):
Camp = apps.get_model('camps', 'Camp')
Camp = apps.get_model("camps", "Camp")
for camp in Camp.objects.all():
if not camp.shortslug:
camp.shortslug = camp.slug
camp.save()
class Migration(migrations.Migration):
dependencies = [
('camps', '0023_camp_shortslug'),
]
operations = [
migrations.RunPython(populate_camp_shortslugs),
]
dependencies = [("camps", "0023_camp_shortslug")]
operations = [migrations.RunPython(populate_camp_shortslugs)]

View file

@ -7,14 +7,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0024_populate_camp_shortslugs'),
]
dependencies = [("camps", "0024_populate_camp_shortslugs")]
operations = [
migrations.AlterField(
model_name='camp',
name='shortslug',
field=models.SlugField(help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.', verbose_name='Short Slug'),
model_name="camp",
name="shortslug",
field=models.SlugField(
help_text="Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.",
verbose_name="Short Slug",
),
)
]

View file

@ -5,19 +5,23 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0025_auto_20180318_1250'),
]
dependencies = [("camps", "0025_auto_20180318_1250")]
operations = [
migrations.AddField(
model_name='camp',
name='call_for_participation_open',
field=models.BooleanField(default=False, help_text='Check if the Call for Participation is open for this camp'),
model_name="camp",
name="call_for_participation_open",
field=models.BooleanField(
default=False,
help_text="Check if the Call for Participation is open for this camp",
),
),
migrations.AddField(
model_name='camp',
name='call_for_sponsors_open',
field=models.BooleanField(default=False, help_text='Check if the Call for Sponsors is open for this camp'),
model_name="camp",
name="call_for_sponsors_open",
field=models.BooleanField(
default=False,
help_text="Check if the Call for Sponsors is open for this camp",
),
),
]

View file

@ -5,19 +5,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0026_auto_20180506_1633'),
]
dependencies = [("camps", "0026_auto_20180506_1633")]
operations = [
migrations.AddField(
model_name='camp',
name='call_for_participation',
field=models.TextField(blank=True, help_text='The CFP markdown for this Camp'),
model_name="camp",
name="call_for_participation",
field=models.TextField(
blank=True, help_text="The CFP markdown for this Camp"
),
),
migrations.AddField(
model_name='camp',
name='call_for_sponsors',
field=models.TextField(blank=True, help_text='The CFS markdown for this Camp'),
model_name="camp",
name="call_for_sponsors",
field=models.TextField(
blank=True, help_text="The CFS markdown for this Camp"
),
),
]

View file

@ -5,19 +5,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0027_auto_20180525_1019'),
]
dependencies = [("camps", "0027_auto_20180525_1019")]
operations = [
migrations.AlterField(
model_name='camp',
name='call_for_participation',
field=models.TextField(blank=True, default='The Call For Participation for this Camp has not been written yet', help_text='The CFP markdown for this Camp'),
model_name="camp",
name="call_for_participation",
field=models.TextField(
blank=True,
default="The Call For Participation for this Camp has not been written yet",
help_text="The CFP markdown for this Camp",
),
),
migrations.AlterField(
model_name='camp',
name='call_for_sponsors',
field=models.TextField(blank=True, default='The Call For Sponsors for this Camp has not been written yet', help_text='The CFS markdown for this Camp'),
model_name="camp",
name="call_for_sponsors",
field=models.TextField(
blank=True,
default="The Call For Sponsors for this Camp has not been written yet",
help_text="The CFS markdown for this Camp",
),
),
]

View file

@ -5,13 +5,16 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0028_auto_20180525_1025'),
]
dependencies = [("camps", "0028_auto_20180525_1025")]
operations = [
migrations.AlterModelOptions(
name='camp',
options={'ordering': ['-title'], 'permissions': (('infodesk_permission', 'Infodesk permission'),), 'verbose_name': 'Camp', 'verbose_name_plural': 'Camps'},
),
name="camp",
options={
"ordering": ["-title"],
"permissions": (("infodesk_permission", "Infodesk permission"),),
"verbose_name": "Camp",
"verbose_name_plural": "Camps",
},
)
]

View file

@ -5,14 +5,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0029_auto_20180815_2018'),
]
dependencies = [("camps", "0029_auto_20180815_2018")]
operations = [
migrations.AddField(
model_name='camp',
name='light_text',
field=models.BooleanField(default=True, help_text='Check if this camps colour requires white text, uncheck if black text is better'),
model_name="camp",
name="light_text",
field=models.BooleanField(
default=True,
help_text="Check if this camps colour requires white text, uncheck if black text is better",
),
)
]

View file

@ -5,24 +5,41 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0030_camp_light_text'),
]
dependencies = [("camps", "0030_camp_light_text")]
operations = [
migrations.CreateModel(
name='Permission',
name="Permission",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
)
],
options={
'permissions': (('backoffice_permission', 'BackOffice access'), ('orgateam_permission', 'Orga Team permissions set'), ('infoteam_permission', 'Info Team permissions set'), ('economyteam_permission', 'Economy Team permissions set'), ('contentteam_permission', 'Content Team permissions set'), ('expense_create_permission', 'Expense Create permission')),
'default_permissions': (),
'managed': False,
"permissions": (
("backoffice_permission", "BackOffice access"),
("orgateam_permission", "Orga Team permissions set"),
("infoteam_permission", "Info Team permissions set"),
("economyteam_permission", "Economy Team permissions set"),
("contentteam_permission", "Content Team permissions set"),
("expense_create_permission", "Expense Create permission"),
),
"default_permissions": (),
"managed": False,
},
),
migrations.AlterModelOptions(
name='camp',
options={'ordering': ['-title'], 'verbose_name': 'Camp', 'verbose_name_plural': 'Camps'},
name="camp",
options={
"ordering": ["-title"],
"verbose_name": "Camp",
"verbose_name_plural": "Camps",
},
),
]

View file

@ -5,13 +5,23 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0031_auto_20180830_0014'),
]
dependencies = [("camps", "0031_auto_20180830_0014")]
operations = [
migrations.AlterModelOptions(
name='permission',
options={'default_permissions': (), 'managed': False, 'permissions': (('backoffice_permission', 'BackOffice access'), ('orgateam_permission', 'Orga Team permissions set'), ('infoteam_permission', 'Info Team permissions set'), ('economyteam_permission', 'Economy Team permissions set'), ('contentteam_permission', 'Content Team permissions set'), ('expense_create_permission', 'Expense Create permission'), ('revenue_create_permission', 'Revenue Create permission'))},
name="permission",
options={
"default_permissions": (),
"managed": False,
"permissions": (
("backoffice_permission", "BackOffice access"),
("orgateam_permission", "Orga Team permissions set"),
("infoteam_permission", "Info Team permissions set"),
("economyteam_permission", "Economy Team permissions set"),
("contentteam_permission", "Content Team permissions set"),
("expense_create_permission", "Expense Create permission"),
("revenue_create_permission", "Revenue Create permission"),
),
},
)
]

View file

@ -21,7 +21,7 @@ class CampViewMixin(object):
return queryset
# do we have a camp_filter on this model
if not hasattr(self.model, 'camp_filter'):
if not hasattr(self.model, "camp_filter"):
return queryset
# get the camp_filter from the model
@ -36,14 +36,14 @@ class CampViewMixin(object):
filter_dict = {_filter: self.camp}
# get pk from kwargs if we have it
if hasattr(self, 'pk_url_kwarg'):
if hasattr(self, "pk_url_kwarg"):
pk = self.kwargs.get(self.pk_url_kwarg)
if pk is not None:
# We should also filter for the pk of the object
filter_dict['pk'] = pk
filter_dict["pk"] = pk
# get slug from kwargs if we have it
if hasattr(self, 'slug_url_kwarg'):
if hasattr(self, "slug_url_kwarg"):
slug = self.kwargs.get(self.slug_url_kwarg)
if slug is not None and (pk is None or self.query_pk_and_slug):
# we should also filter for the slug of the object
@ -57,4 +57,3 @@ class CampViewMixin(object):
# no camp_filter returned any results, return an empty queryset
return result

View file

@ -8,6 +8,7 @@ from datetime import timedelta
from django.utils import timezone
from django.urls import reverse
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
@ -15,9 +16,10 @@ class Permission(models.Model):
"""
An unmanaged field-less model which holds our non-model permissions (such as team permission sets)
"""
class Meta:
managed = False
default_permissions=()
default_permissions = ()
permissions = (
("backoffice_permission", "BackOffice access"),
("orgateam_permission", "Orga Team permissions set"),
@ -31,102 +33,94 @@ class Permission(models.Model):
class Camp(CreatedUpdatedModel, UUIDModel):
class Meta:
verbose_name = 'Camp'
verbose_name_plural = 'Camps'
ordering = ['-title']
verbose_name = "Camp"
verbose_name_plural = "Camps"
ordering = ["-title"]
title = models.CharField(
verbose_name='Title',
help_text='Title of the camp, ie. Bornhack 2016.',
verbose_name="Title",
help_text="Title of the camp, ie. Bornhack 2016.",
max_length=255,
)
tagline = models.CharField(
verbose_name='Tagline',
verbose_name="Tagline",
help_text='Tagline of the camp, ie. "Initial Commit"',
max_length=255,
)
slug = models.SlugField(
verbose_name='Url Slug',
help_text='The url slug to use for this camp'
verbose_name="Url Slug", help_text="The url slug to use for this camp"
)
shortslug = models.SlugField(
verbose_name='Short Slug',
help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.',
verbose_name="Short Slug",
help_text="Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.",
)
buildup = DateTimeRangeField(
verbose_name='Buildup Period',
help_text='The camp buildup period.',
verbose_name="Buildup Period", help_text="The camp buildup period."
)
camp = DateTimeRangeField(
verbose_name='Camp Period',
help_text='The camp period.',
)
camp = DateTimeRangeField(verbose_name="Camp Period", help_text="The camp period.")
teardown = DateTimeRangeField(
verbose_name='Teardown period',
help_text='The camp teardown period.',
verbose_name="Teardown period", help_text="The camp teardown period."
)
read_only = models.BooleanField(
help_text='Whether the camp is read only (i.e. in the past)',
default=False
help_text="Whether the camp is read only (i.e. in the past)", default=False
)
colour = models.CharField(
verbose_name='Colour',
help_text='The primary colour for the camp in hex',
max_length=7
verbose_name="Colour",
help_text="The primary colour for the camp in hex",
max_length=7,
)
light_text = models.BooleanField(
default=True,
help_text='Check if this camps colour requires white text, uncheck if black text is better',
help_text="Check if this camps colour requires white text, uncheck if black text is better",
)
call_for_participation_open = models.BooleanField(
help_text='Check if the Call for Participation is open for this camp',
help_text="Check if the Call for Participation is open for this camp",
default=False,
)
call_for_participation = models.TextField(
blank=True,
help_text='The CFP markdown for this Camp',
default='The Call For Participation for this Camp has not been written yet',
help_text="The CFP markdown for this Camp",
default="The Call For Participation for this Camp has not been written yet",
)
call_for_sponsors_open = models.BooleanField(
help_text='Check if the Call for Sponsors is open for this camp',
default=False,
help_text="Check if the Call for Sponsors is open for this camp", default=False
)
call_for_sponsors = models.TextField(
blank=True,
help_text='The CFS markdown for this Camp',
default='The Call For Sponsors for this Camp has not been written yet',
help_text="The CFS markdown for this Camp",
default="The Call For Sponsors for this Camp has not been written yet",
)
def get_absolute_url(self):
return reverse('camp_detail', kwargs={'camp_slug': self.slug})
return reverse("camp_detail", kwargs={"camp_slug": self.slug})
def clean(self):
''' Make sure the dates make sense - meaning no overlaps and buildup before camp before teardown '''
""" Make sure the dates make sense - meaning no overlaps and buildup before camp before teardown """
errors = []
# check for overlaps buildup vs. camp
if self.buildup.upper > self.camp.lower:
msg = "End of buildup must not be after camp start"
errors.append(ValidationError({'buildup', msg}))
errors.append(ValidationError({'camp', msg}))
errors.append(ValidationError({"buildup", msg}))
errors.append(ValidationError({"camp", msg}))
# check for overlaps camp vs. teardown
if self.camp.upper > self.teardown.lower:
msg = "End of camp must not be after teardown start"
errors.append(ValidationError({'camp', msg}))
errors.append(ValidationError({'teardown', msg}))
errors.append(ValidationError({"camp", msg}))
errors.append(ValidationError({"teardown", msg}))
if errors:
raise ValidationError(errors)
@ -137,40 +131,48 @@ class Camp(CreatedUpdatedModel, UUIDModel):
@property
def event_types(self):
""" Return all event types with at least one event in this camp """
return EventType.objects.filter(event__instances__isnull=False, event__camp=self).distinct()
return EventType.objects.filter(
event__instances__isnull=False, event__camp=self
).distinct()
@property
def event_locations(self):
''' Return all event locations with at least one event in this camp'''
return EventLocation.objects.filter(eventinstances__isnull=False, camp=self).distinct()
""" Return all event locations with at least one event in this camp"""
return EventLocation.objects.filter(
eventinstances__isnull=False, camp=self
).distinct()
@property
def logo_small(self):
return 'img/%(slug)s/logo/%(slug)s-logo-s.png' % {'slug': self.slug}
return "img/%(slug)s/logo/%(slug)s-logo-s.png" % {"slug": self.slug}
@property
def logo_small_svg(self):
return 'img/%(slug)s/logo/%(slug)s-logo-small.svg' % {'slug': self.slug}
return "img/%(slug)s/logo/%(slug)s-logo-small.svg" % {"slug": self.slug}
@property
def logo_large(self):
return 'img/%(slug)s/logo/%(slug)s-logo-l.png' % {'slug': self.slug}
return "img/%(slug)s/logo/%(slug)s-logo-l.png" % {"slug": self.slug}
@property
def logo_large_svg(self):
return 'img/%(slug)s/logo/%(slug)s-logo-large.svg' % {'slug': self.slug}
return "img/%(slug)s/logo/%(slug)s-logo-large.svg" % {"slug": self.slug}
def get_days(self, camppart):
'''
"""
Returns a list of DateTimeTZRanges representing the days during the specified part of the camp.
'''
"""
if not hasattr(self, camppart):
logger.error("nonexistant field/attribute")
return False
field = getattr(self, camppart)
if not hasattr(field, '__class__') or not hasattr(field.__class__, '__name__') or not field.__class__.__name__ == 'DateTimeTZRange':
if (
not hasattr(field, "__class__")
or not hasattr(field.__class__, "__name__")
or not field.__class__.__name__ == "DateTimeTZRange"
):
logger.error("this attribute is not a datetimetzrange field: %s" % field)
return False
@ -182,45 +184,44 @@ class Camp(CreatedUpdatedModel, UUIDModel):
days.append(
DateTimeTZRange(
field.lower,
(field.lower+timedelta(days=i+1)).replace(hour=0)
(field.lower + timedelta(days=i + 1)).replace(hour=0),
)
)
elif i == daycount-1:
elif i == daycount - 1:
# on the last day use actual end time instead of midnight
days.append(
DateTimeTZRange(
(field.lower+timedelta(days=i)).replace(hour=0),
field.lower+timedelta(days=i+1)
(field.lower + timedelta(days=i)).replace(hour=0),
field.lower + timedelta(days=i + 1),
)
)
else:
# neither first nor last day, goes from midnight to midnight
days.append(
DateTimeTZRange(
(field.lower+timedelta(days=i)).replace(hour=0),
(field.lower+timedelta(days=i+1)).replace(hour=0)
(field.lower + timedelta(days=i)).replace(hour=0),
(field.lower + timedelta(days=i + 1)).replace(hour=0),
)
)
return days
@property
def buildup_days(self):
'''
"""
Returns a list of DateTimeTZRanges representing the days during the buildup.
'''
return self.get_days('buildup')
"""
return self.get_days("buildup")
@property
def camp_days(self):
'''
"""
Returns a list of DateTimeTZRanges representing the days during the camp.
'''
return self.get_days('camp')
"""
return self.get_days("camp")
@property
def teardown_days(self):
'''
"""
Returns a list of DateTimeTZRanges representing the days during the buildup.
'''
return self.get_days('teardown')
"""
return self.get_days("teardown")

View file

@ -15,8 +15,9 @@ class CampPropertyListFilter(admin.SimpleListFilter):
SimpleListFilter to filter models by camp when camp is
a property and not a real model field.
"""
title = 'Camp'
parameter_name = 'camp'
title = "Camp"
parameter_name = "camp"
def lookups(self, request, model_admin):
# get the current queryset

View file

@ -6,36 +6,37 @@ from .mixins import CampViewMixin
from django.views import View
from django.conf import settings
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
class CampRedirectView(CampViewMixin, View):
def dispatch(self, request, *args, **kwargs):
now = timezone.now()
try:
camp = Camp.objects.get(
camp__contains=now
camp = Camp.objects.get(camp__contains=now)
logger.debug(
"Redirecting to camp '%s' for page '%s' because it is now!"
% (camp.slug, kwargs["page"])
)
logger.debug("Redirecting to camp '%s' for page '%s' because it is now!" % (camp.slug, kwargs['page']))
return redirect(kwargs['page'], camp_slug=camp.slug)
return redirect(kwargs["page"], camp_slug=camp.slug)
except Camp.DoesNotExist:
pass
# no ongoing camp, find the closest camp in the past
try:
prevcamp = Camp.objects.filter(
camp__endswith__lt=now
).order_by('-camp').first()
prevcamp = (
Camp.objects.filter(camp__endswith__lt=now).order_by("-camp").first()
)
except Camp.DoesNotExist:
prevcamp = None
# find the closest upcoming camp
try:
nextcamp = Camp.objects.filter(
camp__startswith__gt=now
).order_by('camp').first()
nextcamp = (
Camp.objects.filter(camp__startswith__gt=now).order_by("camp").first()
)
except Camp.DoesNotExist:
nextcamp = None
@ -59,19 +60,18 @@ class CampRedirectView(CampViewMixin, View):
camp = prevcamp
# do the redirect
return redirect(kwargs['page'], camp_slug=camp.slug)
return redirect(kwargs["page"], camp_slug=camp.slug)
class CampDetailView(DetailView):
model = Camp
slug_url_kwarg = 'camp_slug'
slug_url_kwarg = "camp_slug"
def get_template_names(self):
return '%s_camp_detail.html' % self.get_object().slug
return "%s_camp_detail.html" % self.get_object().slug
class CampListView(ListView):
model = Camp
template_name = 'camp_list.html'
queryset = Camp.objects.all().order_by('camp')
template_name = "camp_list.html"
queryset = Camp.objects.all().order_by("camp")

View file

@ -5,71 +5,108 @@ from .models import Chain, Credebtor, Expense, Reimbursement, Revenue
### chains and credebtors
@admin.register(Chain)
class ChainAdmin(admin.ModelAdmin):
list_filter = ['name']
list_display = ['name', 'notes']
search_fields = ['name', 'notes']
list_filter = ["name"]
list_display = ["name", "notes"]
search_fields = ["name", "notes"]
@admin.register(Credebtor)
class ChainAdmin(admin.ModelAdmin):
list_filter = ['chain', 'name']
list_display = ['chain', 'name', 'notes']
search_fields = ['chain', 'name', 'notes']
list_filter = ["chain", "name"]
list_display = ["chain", "name", "notes"]
search_fields = ["chain", "name", "notes"]
### expenses
def approve_expenses(modeladmin, request, queryset):
for expense in queryset.all():
expense.approve(request)
approve_expenses.short_description = "Approve Expenses"
def reject_expenses(modeladmin, request, queryset):
for expense in queryset.all():
expense.reject(request)
reject_expenses.short_description = "Reject Expenses"
@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
list_filter = ['camp', 'creditor__chain', 'creditor', 'responsible_team', 'approved', 'user']
list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'creditor', 'responsible_team', 'approved', 'reimbursement']
search_fields = ['description', 'amount', 'uuid']
list_filter = [
"camp",
"creditor__chain",
"creditor",
"responsible_team",
"approved",
"user",
]
list_display = [
"user",
"description",
"invoice_date",
"amount",
"camp",
"creditor",
"responsible_team",
"approved",
"reimbursement",
]
search_fields = ["description", "amount", "uuid"]
actions = [approve_expenses, reject_expenses]
### revenues
def approve_revenues(modeladmin, request, queryset):
for revenue in queryset.all():
revenue.approve(request)
approve_revenues.short_description = "Approve Revenues"
def reject_revenues(modeladmin, request, queryset):
for revenue in queryset.all():
revenue.reject(request)
reject_revenues.short_description = "Reject Revenues"
@admin.register(Revenue)
class RevenueAdmin(admin.ModelAdmin):
list_filter = ['camp', 'responsible_team', 'approved', 'user']
list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'responsible_team', 'approved']
search_fields = ['description', 'amount', 'user']
list_filter = ["camp", "responsible_team", "approved", "user"]
list_display = [
"user",
"description",
"invoice_date",
"amount",
"camp",
"responsible_team",
"approved",
]
search_fields = ["description", "amount", "user"]
actions = [approve_revenues, reject_revenues]
### reimbursements
@admin.register(Reimbursement)
class ReimbursementAdmin(admin.ModelAdmin):
def get_amount(self, obj):
return obj.amount
list_filter = ['camp', 'user', 'reimbursement_user', 'paid']
list_display = ['camp', 'user', 'reimbursement_user', 'paid', 'notes', 'get_amount']
search_fields = ['user__username', 'reimbursement_user__username', 'notes']
list_filter = ["camp", "user", "reimbursement_user", "paid"]
list_display = ["camp", "user", "reimbursement_user", "paid", "notes", "get_amount"]
search_fields = ["user__username", "reimbursement_user__username", "notes"]

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class EconomyConfig(AppConfig):
name = 'economy'
name = "economy"

View file

@ -6,6 +6,7 @@ from utils.email import add_outgoing_email
# expense emails
def send_accountingsystem_expense_email(expense):
"""
Sends an email to the accountingsystem with the invoice as an attachment,
@ -44,8 +45,10 @@ def send_expense_rejected_email(expense):
to_recipients=[expense.user.emailaddress_set.get(primary=True).email],
)
# revenue emails
def send_accountingsystem_revenue_email(revenue):
"""
Sends an email to the accountingsystem with the invoice as an attachment,
@ -83,4 +86,3 @@ def send_revenue_rejected_email(revenue):
subject="Your revenue for %s has been rejected." % revenue.camp.title,
to_recipients=[revenue.user.emailaddress_set.get(primary=True).email],
)

View file

@ -9,11 +9,12 @@ class CleanInvoiceForm(forms.ModelForm):
We have to define this form explicitly because we want our ImageField to accept PDF files as well as images,
and we cannot change the clean_* methods with an autogenerated form from inside views.py
"""
invoice = forms.FileField()
def clean_invoice(self):
# get the uploaded file from cleaned_data
uploaded_file = self.cleaned_data['invoice']
uploaded_file = self.cleaned_data["invoice"]
# is this a valid image?
try:
# create an ImageField instance
@ -36,23 +37,41 @@ class CleanInvoiceForm(forms.ModelForm):
class ExpenseCreateForm(CleanInvoiceForm):
class Meta:
model = Expense
fields = ['description', 'amount', 'invoice_date', 'invoice', 'paid_by_bornhack', 'responsible_team']
fields = [
"description",
"amount",
"invoice_date",
"invoice",
"paid_by_bornhack",
"responsible_team",
]
class ExpenseUpdateForm(forms.ModelForm):
class Meta:
model = Expense
fields = ['description', 'amount', 'invoice_date', 'paid_by_bornhack', 'responsible_team']
fields = [
"description",
"amount",
"invoice_date",
"paid_by_bornhack",
"responsible_team",
]
class RevenueCreateForm(CleanInvoiceForm):
class Meta:
model = Revenue
fields = ['description', 'amount', 'invoice_date', 'invoice', 'responsible_team']
fields = [
"description",
"amount",
"invoice_date",
"invoice",
"responsible_team",
]
class RevenueUpdateForm(forms.ModelForm):
class Meta:
model = Revenue
fields = ['description', 'amount', 'invoice_date', 'responsible_team']
fields = ["description", "amount", "invoice_date", "responsible_team"]

View file

@ -12,58 +12,168 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('teams', '0049_auto_20180815_1119'),
('camps', '0031_auto_20180830_0014'),
("teams", "0049_auto_20180815_1119"),
("camps", "0031_auto_20180830_0014"),
]
operations = [
migrations.CreateModel(
name='Expense',
name="Expense",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('amount', models.DecimalField(decimal_places=2, help_text='The amount of this expense in DKK. Must match the amount on the invoice uploaded below.', max_digits=12)),
('description', models.CharField(help_text='A short description of this expense. Please keep it meningful as it helps the Economy team a lot when categorising expenses. 200 characters or fewer.', max_length=200)),
('paid_by_bornhack', models.BooleanField(default=True, help_text='Leave checked if this expense was paid by BornHack. Uncheck if you need a reimbursement for this expense.')),
('invoice', models.ImageField(help_text='The invoice for this expense. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted.', upload_to='expenses/')),
('approved', models.NullBooleanField(default=None, help_text='True if this expense has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.')),
('notes', models.TextField(blank=True, help_text='Economy Team notes for this expense. Only visible to the Economy team and the submitting user.')),
('camp', models.ForeignKey(help_text='The camp to which this expense belongs', on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='camps.Camp')),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"amount",
models.DecimalField(
decimal_places=2,
help_text="The amount of this expense in DKK. Must match the amount on the invoice uploaded below.",
max_digits=12,
),
),
(
"description",
models.CharField(
help_text="A short description of this expense. Please keep it meningful as it helps the Economy team a lot when categorising expenses. 200 characters or fewer.",
max_length=200,
),
),
(
"paid_by_bornhack",
models.BooleanField(
default=True,
help_text="Leave checked if this expense was paid by BornHack. Uncheck if you need a reimbursement for this expense.",
),
),
(
"invoice",
models.ImageField(
help_text="The invoice for this expense. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted.",
upload_to="expenses/",
),
),
(
"approved",
models.NullBooleanField(
default=None,
help_text="True if this expense has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.",
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Economy Team notes for this expense. Only visible to the Economy team and the submitting user.",
),
),
(
"camp",
models.ForeignKey(
help_text="The camp to which this expense belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to="camps.Camp",
),
),
],
options={
'abstract': False,
},
options={"abstract": False},
),
migrations.CreateModel(
name='Reimbursement',
name="Reimbursement",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('notes', models.TextField(blank=True, help_text='Economy Team notes for this reimbursement. Only visible to the Economy team and the related user.')),
('paid', models.BooleanField(default=False, help_text='Check when this reimbursement has been paid to the user')),
('camp', models.ForeignKey(help_text='The camp to which this reimbursement belongs', on_delete=django.db.models.deletion.PROTECT, related_name='reimbursements', to='camps.Camp')),
('reimbursement_user', models.ForeignKey(help_text='The user this reimbursement belongs to.', on_delete=django.db.models.deletion.PROTECT, related_name='reimbursements', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(help_text='The user who created this reimbursement.', on_delete=django.db.models.deletion.PROTECT, related_name='created_reimbursements', to=settings.AUTH_USER_MODEL)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"notes",
models.TextField(
blank=True,
help_text="Economy Team notes for this reimbursement. Only visible to the Economy team and the related user.",
),
),
(
"paid",
models.BooleanField(
default=False,
help_text="Check when this reimbursement has been paid to the user",
),
),
(
"camp",
models.ForeignKey(
help_text="The camp to which this reimbursement belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="reimbursements",
to="camps.Camp",
),
),
(
"reimbursement_user",
models.ForeignKey(
help_text="The user this reimbursement belongs to.",
on_delete=django.db.models.deletion.PROTECT,
related_name="reimbursements",
to=settings.AUTH_USER_MODEL,
),
),
(
"user",
models.ForeignKey(
help_text="The user who created this reimbursement.",
on_delete=django.db.models.deletion.PROTECT,
related_name="created_reimbursements",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'abstract': False,
},
options={"abstract": False},
),
migrations.AddField(
model_name='expense',
name='reimbursement',
field=models.ForeignKey(blank=True, help_text='The reimbursement for this expense, if any. This is a dual-purpose field. If expense.paid_by_bornhack is true then expense.reimbursement references the reimbursement which this expense is created to cover. If expense.paid_by_bornhack is false then expense.reimbursement references the reimbursement which reimbursed this expense.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='economy.Reimbursement'),
model_name="expense",
name="reimbursement",
field=models.ForeignKey(
blank=True,
help_text="The reimbursement for this expense, if any. This is a dual-purpose field. If expense.paid_by_bornhack is true then expense.reimbursement references the reimbursement which this expense is created to cover. If expense.paid_by_bornhack is false then expense.reimbursement references the reimbursement which reimbursed this expense.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to="economy.Reimbursement",
),
),
migrations.AddField(
model_name='expense',
name='responsible_team',
field=models.ForeignKey(help_text='The team to which this Expense belongs. A team responsible will need to approve the expense. When in doubt pick the Economy team.', on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='teams.Team'),
model_name="expense",
name="responsible_team",
field=models.ForeignKey(
help_text="The team to which this Expense belongs. A team responsible will need to approve the expense. When in doubt pick the Economy team.",
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to="teams.Team",
),
),
migrations.AddField(
model_name='expense',
name='user',
field=models.ForeignKey(help_text='The user to which this expense belongs', on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to=settings.AUTH_USER_MODEL),
model_name="expense",
name="user",
field=models.ForeignKey(
help_text="The user to which this expense belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -9,32 +9,101 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('shop', '0057_order_notes'),
('teams', '0049_auto_20180815_1119'),
('camps', '0031_auto_20180830_0014'),
("shop", "0057_order_notes"),
("teams", "0049_auto_20180815_1119"),
("camps", "0031_auto_20180830_0014"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('economy', '0001_initial'),
("economy", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='Revenue',
name="Revenue",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('amount', models.DecimalField(decimal_places=2, help_text='The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.', max_digits=12)),
('description', models.CharField(help_text='A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.', max_length=200)),
('invoice', models.ImageField(help_text='The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.', upload_to='revenues/')),
('approved', models.NullBooleanField(default=None, help_text='True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.')),
('notes', models.TextField(blank=True, help_text='Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.')),
('camp', models.ForeignKey(help_text='The camp to which this revenue belongs', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='camps.Camp')),
('invoice_fk', models.ForeignKey(help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='shop.Invoice')),
('responsible_team', models.ForeignKey(help_text='The team to which this revenue belongs. When in doubt pick the Economy team.', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='teams.Team')),
('user', models.ForeignKey(help_text='The user who submitted this revenue', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"amount",
models.DecimalField(
decimal_places=2,
help_text="The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.",
max_digits=12,
),
),
(
"description",
models.CharField(
help_text="A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.",
max_length=200,
),
),
(
"invoice",
models.ImageField(
help_text="The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.",
upload_to="revenues/",
),
),
(
"approved",
models.NullBooleanField(
default=None,
help_text="True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.",
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.",
),
),
(
"camp",
models.ForeignKey(
help_text="The camp to which this revenue belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="camps.Camp",
),
),
(
"invoice_fk",
models.ForeignKey(
help_text="The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.",
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="shop.Invoice",
),
),
(
"responsible_team",
models.ForeignKey(
help_text="The team to which this revenue belongs. When in doubt pick the Economy team.",
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="teams.Team",
),
),
(
"user",
models.ForeignKey(
help_text="The user who submitted this revenue",
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to=settings.AUTH_USER_MODEL,
),
),
],
options={"abstract": False},
)
]

View file

@ -6,14 +6,19 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('economy', '0002_revenue'),
]
dependencies = [("economy", "0002_revenue")]
operations = [
migrations.AlterField(
model_name='revenue',
name='invoice_fk',
field=models.ForeignKey(blank=True, help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='shop.Invoice'),
model_name="revenue",
name="invoice_fk",
field=models.ForeignKey(
blank=True,
help_text="The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="shop.Invoice",
),
)
]

View file

@ -7,14 +7,17 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('economy', '0003_auto_20180917_1933'),
]
dependencies = [("economy", "0003_auto_20180917_1933")]
operations = [
migrations.AlterField(
model_name='reimbursement',
name='user',
field=models.ForeignKey(help_text='The economy team member who created this reimbursement.', on_delete=django.db.models.deletion.PROTECT, related_name='created_reimbursements', to=settings.AUTH_USER_MODEL),
model_name="reimbursement",
name="user",
field=models.ForeignKey(
help_text="The economy team member who created this reimbursement.",
on_delete=django.db.models.deletion.PROTECT,
related_name="created_reimbursements",
to=settings.AUTH_USER_MODEL,
),
)
]

View file

@ -5,19 +5,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('economy', '0004_auto_20181120_1835'),
]
dependencies = [("economy", "0004_auto_20181120_1835")]
operations = [
migrations.AddField(
model_name='expense',
name='invoice_date',
field=models.DateTimeField(blank=True, help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below.', null=True),
model_name="expense",
name="invoice_date",
field=models.DateTimeField(
blank=True,
help_text="The invoice date for this Expense. This must match the invoice date on the documentation uploaded below.",
null=True,
),
),
migrations.AddField(
model_name='revenue',
name='invoice_date',
field=models.DateTimeField(blank=True, help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below.', null=True),
model_name="revenue",
name="invoice_date",
field=models.DateTimeField(
blank=True,
help_text="The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below.",
null=True,
),
),
]

View file

@ -5,19 +5,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('economy', '0005_auto_20190120_1532'),
]
dependencies = [("economy", "0005_auto_20190120_1532")]
operations = [
migrations.AlterField(
model_name='expense',
name='invoice_date',
field=models.DateField(blank=True, help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below.', null=True),
model_name="expense",
name="invoice_date",
field=models.DateField(
blank=True,
help_text="The invoice date for this Expense. This must match the invoice date on the documentation uploaded below.",
null=True,
),
),
migrations.AlterField(
model_name='revenue',
name='invoice_date',
field=models.DateField(blank=True, help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below.', null=True),
model_name="revenue",
name="invoice_date",
field=models.DateField(
blank=True,
help_text="The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below.",
null=True,
),
),
]

View file

@ -5,19 +5,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('economy', '0006_auto_20190120_1642'),
]
dependencies = [("economy", "0006_auto_20190120_1642")]
operations = [
migrations.AlterField(
model_name='expense',
name='invoice_date',
field=models.DateField(blank=True, help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.', null=True),
model_name="expense",
name="invoice_date",
field=models.DateField(
blank=True,
help_text="The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.",
null=True,
),
),
migrations.AlterField(
model_name='revenue',
name='invoice_date',
field=models.DateField(blank=True, help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.', null=True),
model_name="revenue",
name="invoice_date",
field=models.DateField(
blank=True,
help_text="The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.",
null=True,
),
),
]

View file

@ -7,63 +7,136 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('economy', '0007_auto_20190327_0936'),
]
dependencies = [("economy", "0007_auto_20190327_0936")]
operations = [
migrations.CreateModel(
name='Chain',
name="Chain",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.', max_length=100, unique=True)),
('slug', models.SlugField(help_text='The url slug for this Chain. Leave blank to auto generate a slug.', unique=True)),
('notes', models.TextField(blank=True, help_text='Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.')),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"name",
models.CharField(
help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.',
max_length=100,
unique=True,
),
),
(
"slug",
models.SlugField(
help_text="The url slug for this Chain. Leave blank to auto generate a slug.",
unique=True,
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.",
),
),
],
options={
'ordering': ['name'],
},
options={"ordering": ["name"]},
),
migrations.CreateModel(
name='Credebtor',
name="Credebtor",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.', max_length=100, unique=True)),
('slug', models.SlugField(help_text='The url slug for this Credebtor. Leave blank to auto generate a slug.')),
('address', models.TextField(help_text='The address of this Credebtor.')),
('notes', models.TextField(blank=True, help_text='Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.')),
('chain', models.ForeignKey(help_text='The Chain to which this Credebtor belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='credebtors', to='economy.Chain')),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"name",
models.CharField(
help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.',
max_length=100,
unique=True,
),
),
(
"slug",
models.SlugField(
help_text="The url slug for this Credebtor. Leave blank to auto generate a slug."
),
),
(
"address",
models.TextField(help_text="The address of this Credebtor."),
),
(
"notes",
models.TextField(
blank=True,
help_text="Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.",
),
),
(
"chain",
models.ForeignKey(
help_text="The Chain to which this Credebtor belongs.",
on_delete=django.db.models.deletion.PROTECT,
related_name="credebtors",
to="economy.Chain",
),
),
],
options={
'ordering': ['name'],
},
options={"ordering": ["name"]},
),
migrations.AlterField(
model_name='expense',
name='invoice_date',
field=models.DateField(help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.'),
model_name="expense",
name="invoice_date",
field=models.DateField(
help_text="The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD."
),
),
migrations.AlterField(
model_name='revenue',
name='invoice_date',
field=models.DateField(help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.'),
model_name="revenue",
name="invoice_date",
field=models.DateField(
help_text="The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD."
),
),
migrations.AddField(
model_name='expense',
name='creditor',
field=models.ForeignKey(help_text='The Creditor to which this expense belongs', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='economy.Credebtor'),
model_name="expense",
name="creditor",
field=models.ForeignKey(
help_text="The Creditor to which this expense belongs",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to="economy.Credebtor",
),
),
migrations.AddField(
model_name='revenue',
name='debtor',
field=models.ForeignKey(help_text='The Debtor to which this revenue belongs', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='economy.Credebtor'),
model_name="revenue",
name="debtor",
field=models.ForeignKey(
help_text="The Debtor to which this revenue belongs",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="economy.Credebtor",
),
),
migrations.AlterUniqueTogether(
name='credebtor',
unique_together={('chain', 'slug')},
name="credebtor", unique_together={("chain", "slug")}
),
]

View file

@ -5,19 +5,24 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('economy', '0008_auto_20190327_1721'),
]
dependencies = [("economy", "0008_auto_20190327_1721")]
operations = [
migrations.AlterField(
model_name='chain',
name='slug',
field=models.SlugField(blank=True, help_text='The url slug for this Chain. Leave blank to auto generate a slug.', unique=True),
model_name="chain",
name="slug",
field=models.SlugField(
blank=True,
help_text="The url slug for this Chain. Leave blank to auto generate a slug.",
unique=True,
),
),
migrations.AlterField(
model_name='credebtor',
name='slug',
field=models.SlugField(blank=True, help_text='The url slug for this Credebtor. Leave blank to auto generate a slug.'),
model_name="credebtor",
name="slug",
field=models.SlugField(
blank=True,
help_text="The url slug for this Credebtor. Leave blank to auto generate a slug.",
),
),
]

View file

@ -6,19 +6,27 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('economy', '0009_auto_20190328_0715'),
]
dependencies = [("economy", "0009_auto_20190328_0715")]
operations = [
migrations.AlterField(
model_name='expense',
name='creditor',
field=models.ForeignKey(help_text='The Creditor to which this expense belongs', on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='economy.Credebtor'),
model_name="expense",
name="creditor",
field=models.ForeignKey(
help_text="The Creditor to which this expense belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="expenses",
to="economy.Credebtor",
),
),
migrations.AlterField(
model_name='revenue',
name='debtor',
field=models.ForeignKey(help_text='The Debtor to which this revenue belongs', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='economy.Credebtor'),
model_name="revenue",
name="debtor",
field=models.ForeignKey(
help_text="The Debtor to which this revenue belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="revenues",
to="economy.Credebtor",
),
),
]

View file

@ -10,14 +10,16 @@ from django.utils.text import slugify
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
from .email import *
class ChainManager(models.Manager):
"""
ChainManager adds 'expenses_total' and 'revenues_total' to the Chain qs
"""
def get_queryset(self):
qs = super().get_queryset()
qs = qs.annotate(expenses_total=models.Sum('credebtors__expenses__amount'))
qs = qs.annotate(revenues_total=models.Sum('credebtors__revenues__amount'))
qs = qs.annotate(expenses_total=models.Sum("credebtors__expenses__amount"))
qs = qs.annotate(revenues_total=models.Sum("credebtors__revenues__amount"))
return qs
@ -26,25 +28,26 @@ class Chain(CreatedUpdatedModel, UUIDModel):
A chain of Credebtors. Used to group when several Creditors/Debtors
belong to the same Chain/company, like XL Byg stores or Netto stores.
"""
class Meta:
ordering = ['name']
ordering = ["name"]
objects = ChainManager()
name = models.CharField(
max_length=100,
unique=True,
help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.'
help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.',
)
slug = models.SlugField(
unique=True,
blank=True,
help_text='The url slug for this Chain. Leave blank to auto generate a slug.'
help_text="The url slug for this Chain. Leave blank to auto generate a slug.",
)
notes = models.TextField(
help_text='Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.',
help_text="Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.",
blank=True,
)
@ -69,10 +72,11 @@ class CredebtorManager(models.Manager):
"""
CredebtorManager adds 'expenses_total' and 'revenues_total' to the Credebtor qs
"""
def get_queryset(self):
qs = super().get_queryset()
qs = qs.annotate(expenses_total=models.Sum('expenses__amount'))
qs = qs.annotate(revenues_total=models.Sum('revenues__amount'))
qs = qs.annotate(expenses_total=models.Sum("expenses__amount"))
qs = qs.annotate(revenues_total=models.Sum("revenues__amount"))
return qs
@ -83,37 +87,36 @@ class Credebtor(CreatedUpdatedModel, UUIDModel):
The model is used for both creditors and debtors, since there is a
lot of overlap between them.
"""
class Meta:
ordering = ['name']
unique_together=('chain', 'slug')
ordering = ["name"]
unique_together = ("chain", "slug")
objects = CredebtorManager()
chain = models.ForeignKey(
'economy.Chain',
"economy.Chain",
on_delete=models.PROTECT,
related_name='credebtors',
help_text='The Chain to which this Credebtor belongs.',
related_name="credebtors",
help_text="The Chain to which this Credebtor belongs.",
)
name = models.CharField(
max_length=100,
unique=True,
help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.'
help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.',
)
slug = models.SlugField(
blank=True,
help_text='The url slug for this Credebtor. Leave blank to auto generate a slug.'
help_text="The url slug for this Credebtor. Leave blank to auto generate a slug.",
)
address = models.TextField(
help_text='The address of this Credebtor.',
)
address = models.TextField(help_text="The address of this Credebtor.")
notes = models.TextField(
blank=True,
help_text='Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.',
help_text="Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.",
)
def __str__(self):
@ -138,76 +141,77 @@ class Revenue(CampRelatedModel, UUIDModel):
Other Revenue objects (such as money returned from bottle deposits) will
not have a related BornHack Invoice object.
"""
camp = models.ForeignKey(
'camps.Camp',
"camps.Camp",
on_delete=models.PROTECT,
related_name='revenues',
help_text='The camp to which this revenue belongs',
related_name="revenues",
help_text="The camp to which this revenue belongs",
)
debtor = models.ForeignKey(
'economy.Credebtor',
"economy.Credebtor",
on_delete=models.PROTECT,
related_name='revenues',
help_text='The Debtor to which this revenue belongs',
related_name="revenues",
help_text="The Debtor to which this revenue belongs",
)
user = models.ForeignKey(
'auth.User',
"auth.User",
on_delete=models.PROTECT,
related_name='revenues',
help_text='The user who submitted this revenue',
related_name="revenues",
help_text="The user who submitted this revenue",
)
amount = models.DecimalField(
decimal_places=2,
max_digits=12,
help_text='The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.',
help_text="The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.",
)
description = models.CharField(
max_length=200,
help_text='A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.',
help_text="A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.",
)
invoice = models.ImageField(
help_text='The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.',
upload_to='revenues/',
help_text="The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.",
upload_to="revenues/",
)
invoice_date = models.DateField(
help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.',
help_text="The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD."
)
invoice_fk = models.ForeignKey(
'shop.Invoice',
"shop.Invoice",
on_delete=models.PROTECT,
related_name='revenues',
help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.',
related_name="revenues",
help_text="The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.",
blank=True,
null=True,
)
responsible_team = models.ForeignKey(
'teams.Team',
"teams.Team",
on_delete=models.PROTECT,
related_name='revenues',
help_text='The team to which this revenue belongs. When in doubt pick the Economy team.'
related_name="revenues",
help_text="The team to which this revenue belongs. When in doubt pick the Economy team.",
)
approved = models.NullBooleanField(
default=None,
help_text='True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.'
help_text="True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.",
)
notes = models.TextField(
blank=True,
help_text='Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.'
help_text="Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.",
)
def clean(self):
if self.amount < 0:
raise ValidationError('Amount of a Revenue object can not be negative')
raise ValidationError("Amount of a Revenue object can not be negative")
@property
def invoice_filename(self):
@ -228,7 +232,10 @@ class Revenue(CampRelatedModel, UUIDModel):
Approving a revenue triggers an email to the economy system, and another email to the user who submitted the revenue
"""
if request.user == self.user:
messages.error(request, "You cannot approve your own revenues, aka. the anti-stein-bagger defense")
messages.error(
request,
"You cannot approve your own revenues, aka. the anti-stein-bagger defense",
)
return
# mark as approved and save
@ -262,35 +269,35 @@ class Revenue(CampRelatedModel, UUIDModel):
class Expense(CampRelatedModel, UUIDModel):
camp = models.ForeignKey(
'camps.Camp',
"camps.Camp",
on_delete=models.PROTECT,
related_name='expenses',
help_text='The camp to which this expense belongs',
related_name="expenses",
help_text="The camp to which this expense belongs",
)
creditor = models.ForeignKey(
'economy.Credebtor',
"economy.Credebtor",
on_delete=models.PROTECT,
related_name='expenses',
help_text='The Creditor to which this expense belongs',
related_name="expenses",
help_text="The Creditor to which this expense belongs",
)
user = models.ForeignKey(
'auth.User',
"auth.User",
on_delete=models.PROTECT,
related_name='expenses',
help_text='The user to which this expense belongs',
related_name="expenses",
help_text="The user to which this expense belongs",
)
amount = models.DecimalField(
decimal_places=2,
max_digits=12,
help_text='The amount of this expense in DKK. Must match the amount on the invoice uploaded below.',
help_text="The amount of this expense in DKK. Must match the amount on the invoice uploaded below.",
)
description = models.CharField(
max_length=200,
help_text='A short description of this expense. Please keep it meningful as it helps the Economy team a lot when categorising expenses. 200 characters or fewer.',
help_text="A short description of this expense. Please keep it meningful as it helps the Economy team a lot when categorising expenses. 200 characters or fewer.",
)
paid_by_bornhack = models.BooleanField(
@ -299,43 +306,43 @@ class Expense(CampRelatedModel, UUIDModel):
)
invoice = models.ImageField(
help_text='The invoice for this expense. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted.',
upload_to='expenses/',
help_text="The invoice for this expense. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted.",
upload_to="expenses/",
)
invoice_date = models.DateField(
help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.',
help_text="The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD."
)
responsible_team = models.ForeignKey(
'teams.Team',
"teams.Team",
on_delete=models.PROTECT,
related_name='expenses',
help_text='The team to which this Expense belongs. A team responsible will need to approve the expense. When in doubt pick the Economy team.'
related_name="expenses",
help_text="The team to which this Expense belongs. A team responsible will need to approve the expense. When in doubt pick the Economy team.",
)
approved = models.NullBooleanField(
default=None,
help_text='True if this expense has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.'
help_text="True if this expense has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.",
)
reimbursement = models.ForeignKey(
'economy.Reimbursement',
"economy.Reimbursement",
on_delete=models.PROTECT,
related_name='expenses',
related_name="expenses",
null=True,
blank=True,
help_text='The reimbursement for this expense, if any. This is a dual-purpose field. If expense.paid_by_bornhack is true then expense.reimbursement references the reimbursement which this expense is created to cover. If expense.paid_by_bornhack is false then expense.reimbursement references the reimbursement which reimbursed this expense.'
help_text="The reimbursement for this expense, if any. This is a dual-purpose field. If expense.paid_by_bornhack is true then expense.reimbursement references the reimbursement which this expense is created to cover. If expense.paid_by_bornhack is false then expense.reimbursement references the reimbursement which reimbursed this expense.",
)
notes = models.TextField(
blank=True,
help_text='Economy Team notes for this expense. Only visible to the Economy team and the submitting user.'
help_text="Economy Team notes for this expense. Only visible to the Economy team and the submitting user.",
)
def clean(self):
if self.amount < 0:
raise ValidationError('Amount of an expense can not be negative')
raise ValidationError("Amount of an expense can not be negative")
@property
def invoice_filename(self):
@ -356,7 +363,10 @@ class Expense(CampRelatedModel, UUIDModel):
Approving an expense triggers an email to the economy system, and another email to the user who submitted the expense in the first place.
"""
if request.user == self.user:
messages.error(request, "You cannot approve your own expenses, aka. the anti-stein-bagger defense")
messages.error(
request,
"You cannot approve your own expenses, aka. the anti-stein-bagger defense",
)
return
# mark as approved and save
@ -392,30 +402,31 @@ class Reimbursement(CampRelatedModel, UUIDModel):
"""
A reimbursement covers one or more expenses.
"""
camp = models.ForeignKey(
'camps.Camp',
"camps.Camp",
on_delete=models.PROTECT,
related_name='reimbursements',
help_text='The camp to which this reimbursement belongs',
related_name="reimbursements",
help_text="The camp to which this reimbursement belongs",
)
user = models.ForeignKey(
'auth.User',
"auth.User",
on_delete=models.PROTECT,
related_name='created_reimbursements',
help_text='The economy team member who created this reimbursement.'
related_name="created_reimbursements",
help_text="The economy team member who created this reimbursement.",
)
reimbursement_user = models.ForeignKey(
'auth.User',
"auth.User",
on_delete=models.PROTECT,
related_name='reimbursements',
help_text='The user this reimbursement belongs to.'
related_name="reimbursements",
help_text="The user this reimbursement belongs to.",
)
notes = models.TextField(
blank=True,
help_text='Economy Team notes for this reimbursement. Only visible to the Economy team and the related user.'
help_text="Economy Team notes for this reimbursement. Only visible to the Economy team and the related user.",
)
paid = models.BooleanField(
@ -439,5 +450,3 @@ class Reimbursement(CampRelatedModel, UUIDModel):
for expense in self.expenses.filter(paid_by_bornhack=False):
amount += expense.amount
return amount

View file

@ -1,150 +1,132 @@
from django.urls import path, include
from .views import *
app_name = 'economy'
app_name = "economy"
urlpatterns = [
path(
'',
EconomyDashboardView.as_view(),
name='dashboard'
),
path("", EconomyDashboardView.as_view(), name="dashboard"),
# chains
path('chains/',
include([
path(
'',
ChainListView.as_view(),
name='chain_list',
"chains/",
include(
[
path("", ChainListView.as_view(), name="chain_list"),
path("add/", ChainCreateView.as_view(), name="chain_create"),
path(
"<slug:chain_slug>/",
include(
[
path(
"", CredebtorListView.as_view(), name="credebtor_list"
),
path(
'add/',
ChainCreateView.as_view(),
name='chain_create',
),
path(
'<slug:chain_slug>/',
include([
path(
'',
CredebtorListView.as_view(),
name='credebtor_list',
),
path(
'add/',
"add/",
CredebtorCreateView.as_view(),
name='credebtor_create',
name="credebtor_create",
),
path(
'<slug:credebtor_slug>/',
include([
"<slug:credebtor_slug>/",
include(
[
path(
'add_expense/',
"add_expense/",
ExpenseCreateView.as_view(),
name='expense_create',
name="expense_create",
),
path(
'add_revenue/',
"add_revenue/",
RevenueCreateView.as_view(),
name='revenue_create',
name="revenue_create",
),
]),
]
),
]),
),
]),
]
),
),
]
),
),
# expenses
path(
'expenses/',
include([
"expenses/",
include(
[
path("", ExpenseListView.as_view(), name="expense_list"),
path(
'',
ExpenseListView.as_view(),
name='expense_list',
"<uuid:pk>/",
include(
[
path(
"", ExpenseDetailView.as_view(), name="expense_detail"
),
path(
'<uuid:pk>/',
include([
path(
'',
ExpenseDetailView.as_view(),
name='expense_detail'
),
path(
'update/',
"update/",
ExpenseUpdateView.as_view(),
name='expense_update'
name="expense_update",
),
path(
'delete/',
"delete/",
ExpenseDeleteView.as_view(),
name='expense_delete'
name="expense_delete",
),
path(
'invoice/',
"invoice/",
ExpenseInvoiceView.as_view(),
name='expense_invoice'
name="expense_invoice",
),
]),
]
),
),
]
),
]),
),
# reimbursements
path(
'reimbursements/',
include([
"reimbursements/",
include(
[
path("", ReimbursementListView.as_view(), name="reimbursement_list"),
path(
'',
ReimbursementListView.as_view(),
name='reimbursement_list'
),
path(
'<uuid:pk>/',
"<uuid:pk>/",
ReimbursementDetailView.as_view(),
name='reimbursement_detail'
name="reimbursement_detail",
),
]
),
]),
),
# revenue
path(
'revenues/',
include([
"revenues/",
include(
[
path("", RevenueListView.as_view(), name="revenue_list"),
path(
'',
RevenueListView.as_view(),
name='revenue_list'
"<uuid:pk>/",
include(
[
path(
"", RevenueDetailView.as_view(), name="revenue_detail"
),
path(
'<uuid:pk>/',
include([
path(
'',
RevenueDetailView.as_view(),
name='revenue_detail'
),
path(
'update/',
"update/",
RevenueUpdateView.as_view(),
name='revenue_update'
name="revenue_update",
),
path(
'delete/',
"delete/",
RevenueDeleteView.as_view(),
name='revenue_delete'
name="revenue_delete",
),
path(
'invoice/',
"invoice/",
RevenueInvoiceView.as_view(),
name='revenue_invoice'
name="revenue_invoice",
),
]),
]
),
),
]
),
]),
),
]

View file

@ -1,12 +1,12 @@
from django.contrib import admin
from .models import Type, Routing
@admin.register(Type)
class TypeAdmin(admin.ModelAdmin):
pass
@admin.register(Routing)
class RoutingAdmin(admin.ModelAdmin):
pass

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class EventsConfig(AppConfig):
name = 'events'
name = "events"

View file

@ -2,19 +2,27 @@ from django.utils import timezone
from datetime import timedelta
from ircbot.utils import add_irc_message
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_template=None, email_formatdict=None):
def handle_team_event(
eventtype,
irc_message=None,
irc_timeout=60,
email_template=None,
email_formatdict=None,
):
"""
This method is our basic event handler.
The type of event determines which teams receive notifications.
TODO: Add some sort of priority to messages
"""
#logger.info("Inside handle_team_event, eventtype %s" % eventtype)
# logger.info("Inside handle_team_event, eventtype %s" % eventtype)
# get event type from database
from .models import Type
try:
eventtype = Type.objects.get(name=eventtype)
except Type.DoesNotExist:
@ -24,14 +32,21 @@ def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_templat
if not eventtype.teams:
# no routes found for this eventtype, do nothing
#logger.error("No routes round for eventtype %s" % eventtype)
# logger.error("No routes round for eventtype %s" % eventtype)
return
# loop over routes (teams) for this eventtype
for team in eventtype.teams:
logger.info("Handling eventtype %s for team %s" % (eventtype, team))
team_irc_notification(team=team, eventtype=eventtype, irc_message=irc_message, irc_timeout=irc_timeout)
team_email_notification(team=team, eventtype=eventtype, email_template=None, email_formatdict=None)
team_irc_notification(
team=team,
eventtype=eventtype,
irc_message=irc_message,
irc_timeout=irc_timeout,
)
team_email_notification(
team=team, eventtype=eventtype, email_template=None, email_formatdict=None
)
# handle any future notification types here..
@ -54,14 +69,14 @@ def team_irc_notification(team, eventtype, irc_message=None, irc_timeout=60):
# send an IRC message to the the channel for this team
add_irc_message(
target=team.private_irc_channel_name,
message=irc_message,
timeout=60
target=team.private_irc_channel_name, message=irc_message, timeout=60
)
logger.info("Added new IRC message for channel %s" % team.irc_channel_name)
def team_email_notification(team, eventtype, email_template=None, email_formatdict=None):
def team_email_notification(
team, eventtype, email_template=None, email_formatdict=None
):
"""
Sends email notifications for events to team mailinglists (if possible,
otherwise directly to the team responsibles)
@ -78,4 +93,3 @@ def team_email_notification(team, eventtype, email_template=None, email_formatdi
recipient_list = [resp.email for resp in team.responsible_members.all()]
# TODO: actually send the email here

View file

@ -10,42 +10,62 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('teams', '0025_auto_20180318_1318'),
]
dependencies = [("teams", "0025_auto_20180318_1318")]
operations = [
migrations.CreateModel(
name='Routing',
name="Routing",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
options={"abstract": False},
),
migrations.CreateModel(
name='Type',
name="Type",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.TextField(help_text='The type of event', unique=True)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("name", models.TextField(help_text="The type of event", unique=True)),
],
options={
'abstract': False,
},
options={"abstract": False},
),
migrations.AddField(
model_name='routing',
name='eventtype',
field=models.ForeignKey(help_text='The type of event to route', on_delete=django.db.models.deletion.PROTECT, related_name='eventroutes', to='events.Type'),
model_name="routing",
name="eventtype",
field=models.ForeignKey(
help_text="The type of event to route",
on_delete=django.db.models.deletion.PROTECT,
related_name="eventroutes",
to="events.Type",
),
),
migrations.AddField(
model_name='routing',
name='team',
field=models.ForeignKey(help_text='The team which should receive events of this type.', on_delete=django.db.models.deletion.PROTECT, related_name='eventroutes', to='teams.Team'),
model_name="routing",
name="team",
field=models.ForeignKey(
help_text="The team which should receive events of this type.",
on_delete=django.db.models.deletion.PROTECT,
related_name="eventroutes",
to="teams.Team",
),
),
]

View file

@ -4,17 +4,14 @@ from __future__ import unicode_literals
from django.db import migrations
def create_eventtypes(apps, schema_editor):
Type = apps.get_model('events', 'Type')
Type.objects.create(name='public_credit_name_changed')
Type = apps.get_model("events", "Type")
Type.objects.create(name="public_credit_name_changed")
class Migration(migrations.Migration):
dependencies = [
('events', '0001_initial'),
]
operations = [
migrations.RunPython(create_eventtypes),
]
dependencies = [("events", "0001_initial")]
operations = [migrations.RunPython(create_eventtypes)]

View file

@ -4,17 +4,14 @@ from __future__ import unicode_literals
from django.db import migrations
def create_eventtype(apps, schema_editor):
Type = apps.get_model('events', 'Type')
Type.objects.create(name='ticket_created')
Type = apps.get_model("events", "Type")
Type.objects.create(name="ticket_created")
class Migration(migrations.Migration):
dependencies = [
('events', '0002_create_eventtype'),
]
dependencies = [("events", "0002_create_eventtype")]
operations = [
migrations.RunPython(create_eventtype),
]
operations = [migrations.RunPython(create_eventtype)]

View file

@ -7,19 +7,23 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0003_create_another_eventtype'),
]
dependencies = [("events", "0003_create_another_eventtype")]
operations = [
migrations.AddField(
model_name='type',
name='email_notification',
field=models.BooleanField(default=False, help_text='Check to send email notifications for this type of event.'),
model_name="type",
name="email_notification",
field=models.BooleanField(
default=False,
help_text="Check to send email notifications for this type of event.",
),
),
migrations.AddField(
model_name='type',
name='irc_notification',
field=models.BooleanField(default=False, help_text='Check to send IRC notifications for this type of event.'),
model_name="type",
name="irc_notification",
field=models.BooleanField(
default=False,
help_text="Check to send IRC notifications for this type of event.",
),
),
]

View file

@ -2,6 +2,7 @@ from django.db import models
from utils.models import CreatedUpdatedModel
from teams.models import Team
class Type(CreatedUpdatedModel):
"""
The events.Type model contains different types of system events which can happen.
@ -10,20 +11,19 @@ class Type(CreatedUpdatedModel):
- ticket_created: Whenever a new ShopTicket is created
- public_credit_name_changed: Whenever a user changes public_credit_name in the profile
"""
name = models.TextField(
unique=True,
help_text='The type of event'
)
name = models.TextField(unique=True, help_text="The type of event")
irc_notification = models.BooleanField(
default=False,
help_text='Check to send IRC notifications for this type of event.',
help_text="Check to send IRC notifications for this type of event.",
)
email_notification = models.BooleanField(
default=False,
help_text='Check to send email notifications for this type of event.',
help_text="Check to send email notifications for this type of event.",
)
def __str__(self):
return self.name
@ -32,7 +32,7 @@ class Type(CreatedUpdatedModel):
"""
This property returns a queryset with all the teams that should receive this type of events
"""
team_ids = Routing.objects.filter(eventtype=self).values_list('team', flat=True)
team_ids = Routing.objects.filter(eventtype=self).values_list("team", flat=True)
return Team.objects.filter(pk__in=team_ids)
@ -42,20 +42,20 @@ class Routing(CreatedUpdatedModel):
Add a new entry to route events of a certain type to a team.
Several teams can receive the same type of event.
"""
eventtype = models.ForeignKey(
'events.Type',
related_name='eventroutes',
"events.Type",
related_name="eventroutes",
on_delete=models.PROTECT,
help_text='The type of event to route',
help_text="The type of event to route",
)
team = models.ForeignKey(
'teams.Team',
related_name='eventroutes',
"teams.Team",
related_name="eventroutes",
on_delete=models.PROTECT,
help_text='The team which should receive events of this type.'
help_text="The team which should receive events of this type.",
)
def __str__(self):
return "%s -> %s" % (self.eventtype, self.team)

View file

@ -5,4 +5,4 @@ from .models import Feedback
@admin.register(Feedback)
class FeedbackAdmin(admin.ModelAdmin):
list_display = ('user', 'camp', 'feedback')
list_display = ("user", "camp", "feedback")

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class FeedbackConfig(AppConfig):
name = 'feedback'
name = "feedback"

View file

@ -10,22 +10,32 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name='Feedback',
name="Feedback",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('feedback', models.TextField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("feedback", models.TextField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={"abstract": False},
)
]

View file

@ -6,16 +6,17 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0030_camp_light_text'),
('feedback', '0001_initial'),
]
dependencies = [("camps", "0030_camp_light_text"), ("feedback", "0001_initial")]
operations = [
migrations.AddField(
model_name='feedback',
name='camp',
field=models.ForeignKey(default='30fd754f-dae4-460f-8128-6638fb29ab2d', on_delete=django.db.models.deletion.PROTECT, to='camps.Camp'),
preserve_default=False,
model_name="feedback",
name="camp",
field=models.ForeignKey(
default="30fd754f-dae4-460f-8128-6638fb29ab2d",
on_delete=django.db.models.deletion.PROTECT,
to="camps.Camp",
),
preserve_default=False,
)
]

View file

@ -4,6 +4,6 @@ from utils.models import UUIDModel, CreatedUpdatedModel, CampRelatedModel
class Feedback(CampRelatedModel, UUIDModel):
camp = models.ForeignKey('camps.Camp', on_delete=models.PROTECT)
user = models.ForeignKey('auth.User', on_delete=models.PROTECT)
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
feedback = models.TextField()

View file

@ -11,7 +11,7 @@ from .models import Feedback
class FeedbackCreate(LoginRequiredMixin, CampViewMixin, CreateView):
model = Feedback
fields = ['feedback']
fields = ["feedback"]
def form_valid(self, form):
feedback = form.save(commit=False)
@ -20,14 +20,15 @@ class FeedbackCreate(LoginRequiredMixin, CampViewMixin, CreateView):
feedback.save()
thanks_message = "Thank you! Your feedback is highly appreciated!"
try:
token = Token.objects.get(
camp=self.camp,
description="Feedback thanks"
token = Token.objects.get(camp=self.camp, description="Feedback thanks")
thanks_message += " And for your efforts, here is a token: {}".format(
token.token
)
thanks_message += " And for your efforts, here is a token: {}".format(token.token)
except Token.DoesNotExist:
pass
messages.success(self.request, thanks_message)
return HttpResponseRedirect(reverse("feedback", kwargs={"camp_slug": self.camp.slug}))
return HttpResponseRedirect(
reverse("feedback", kwargs={"camp_slug": self.camp.slug})
)

View file

@ -1,26 +1,23 @@
from django.contrib import admin
from reversion.admin import VersionAdmin
from .models import (
InfoItem,
InfoCategory
)
from .models import InfoItem, InfoCategory
@admin.register(InfoItem)
class InfoItemAdmin(VersionAdmin):
list_filter = ['category', 'category__team__camp',]
list_display = ['headline',]
list_filter = ["category", "category__team__camp"]
list_display = ["headline"]
class InfoItemInlineAdmin(admin.StackedInline):
model = InfoItem
list_filter = ['category', 'category__team__camp',]
list_display = ['headline',]
list_filter = ["category", "category__team__camp"]
list_display = ["headline"]
@admin.register(InfoCategory)
class InfoCategorydmin(admin.ModelAdmin):
list_filter = ['team__camp',]
list_display = ['headline',]
search_fields = ['headline', 'body']
list_filter = ["team__camp"]
list_display = ["headline"]
search_fields = ["headline", "body"]
inlines = [InfoItemInlineAdmin]

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class InfoConfig(AppConfig):
name = 'info'
name = "info"

View file

@ -10,48 +10,107 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('camps', '0010_auto_20161220_1714'),
]
dependencies = [("camps", "0010_auto_20161220_1714")]
operations = [
migrations.CreateModel(
name='InfoCategory',
name="InfoCategory",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('headline', models.CharField(help_text='The headline of this info category', max_length=100)),
('anchor', models.SlugField(help_text='The HTML anchor to use for this info category.')),
('weight', models.PositiveIntegerField(help_text='Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically.')),
('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='infocategories', to='camps.Camp')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"headline",
models.CharField(
help_text="The headline of this info category", max_length=100
),
),
(
"anchor",
models.SlugField(
help_text="The HTML anchor to use for this info category."
),
),
(
"weight",
models.PositiveIntegerField(
help_text="Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically."
),
),
(
"camp",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="infocategories",
to="camps.Camp",
),
),
],
options={
'ordering': ['-weight', 'headline'],
},
options={"ordering": ["-weight", "headline"]},
),
migrations.CreateModel(
name='InfoItem',
name="InfoItem",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('headline', models.CharField(help_text='Headline of this info item.', max_length=100)),
('anchor', models.SlugField(help_text='The HTML anchor to use for this info item.')),
('body', models.TextField(help_text='Body of this info item. Markdown is supported.')),
('weight', models.PositiveIntegerField(help_text='Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically.')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='infoitems', to='info.InfoCategory')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"headline",
models.CharField(
help_text="Headline of this info item.", max_length=100
),
),
(
"anchor",
models.SlugField(
help_text="The HTML anchor to use for this info item."
),
),
(
"body",
models.TextField(
help_text="Body of this info item. Markdown is supported."
),
),
(
"weight",
models.PositiveIntegerField(
help_text="Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically."
),
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="infoitems",
to="info.InfoCategory",
),
),
],
options={
'ordering': ['-weight', 'headline'],
},
options={"ordering": ["-weight", "headline"]},
),
migrations.AlterUniqueTogether(
name='infoitem',
unique_together=set([('headline', 'category'), ('anchor', 'category')]),
name="infoitem",
unique_together=set([("headline", "category"), ("anchor", "category")]),
),
migrations.AlterUniqueTogether(
name='infocategory',
unique_together=set([('headline', 'camp'), ('anchor', 'camp')]),
name="infocategory",
unique_together=set([("headline", "camp"), ("anchor", "camp")]),
),
]

View file

@ -7,23 +7,30 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('info', '0001_initial'),
]
dependencies = [("info", "0001_initial")]
operations = [
migrations.AlterModelOptions(
name='infocategory',
options={'ordering': ['-weight', 'headline'], 'verbose_name_plural': 'Info Categories'},
name="infocategory",
options={
"ordering": ["-weight", "headline"],
"verbose_name_plural": "Info Categories",
},
),
migrations.AlterField(
model_name='infocategory',
name='weight',
field=models.PositiveIntegerField(default=100, help_text='Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.'),
model_name="infocategory",
name="weight",
field=models.PositiveIntegerField(
default=100,
help_text="Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.",
),
),
migrations.AlterField(
model_name='infoitem',
name='weight',
field=models.PositiveIntegerField(default=100, help_text='Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.'),
model_name="infoitem",
name="weight",
field=models.PositiveIntegerField(
default=100,
help_text="Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.",
),
),
]

View file

@ -7,17 +7,17 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('info', '0002_auto_20161228_2312'),
]
dependencies = [("info", "0002_auto_20161228_2312")]
operations = [
migrations.AlterModelOptions(
name='infocategory',
options={'ordering': ['weight', 'headline'], 'verbose_name_plural': 'Info Categories'},
name="infocategory",
options={
"ordering": ["weight", "headline"],
"verbose_name_plural": "Info Categories",
},
),
migrations.AlterModelOptions(
name='infoitem',
options={'ordering': ['weight', 'headline']},
name="infoitem", options={"ordering": ["weight", "headline"]}
),
]

View file

@ -7,14 +7,20 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teams', '0042_auto_20180413_1933'),
('info', '0003_auto_20170218_1148'),
("teams", "0042_auto_20180413_1933"),
("info", "0003_auto_20170218_1148"),
]
operations = [
migrations.AddField(
model_name='infocategory',
name='team',
field=models.ForeignKey(blank=True, help_text='The team responsible for this info category.', null=True, on_delete=django.db.models.deletion.PROTECT, to='teams.Team'),
model_name="infocategory",
name="team",
field=models.ForeignKey(
blank=True,
help_text="The team responsible for this info category.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="teams.Team",
),
)
]

View file

@ -69,10 +69,6 @@ def add_teams_to_categories(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('info', '0004_infocategory_team'),
]
dependencies = [("info", "0004_infocategory_team")]
operations = [
migrations.RunPython(add_teams_to_categories)
]
operations = [migrations.RunPython(add_teams_to_categories)]

View file

@ -6,22 +6,21 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('info', '0005_add_teams_to_categories'),
]
dependencies = [("info", "0005_add_teams_to_categories")]
operations = [
migrations.AlterField(
model_name='infocategory',
name='team',
field=models.ForeignKey(blank=True, help_text='The team responsible for this info category.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='info_categories', to='teams.Team'),
model_name="infocategory",
name="team",
field=models.ForeignKey(
blank=True,
help_text="The team responsible for this info category.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="info_categories",
to="teams.Team",
),
migrations.AlterUniqueTogether(
name='infocategory',
unique_together=set(),
),
migrations.RemoveField(
model_name='infocategory',
name='camp',
),
migrations.AlterUniqueTogether(name="infocategory", unique_together=set()),
migrations.RemoveField(model_name="infocategory", name="camp"),
]

View file

@ -6,14 +6,17 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('info', '0006_auto_20180520_1113'),
]
dependencies = [("info", "0006_auto_20180520_1113")]
operations = [
migrations.AlterField(
model_name='infocategory',
name='team',
field=models.ForeignKey(help_text='The team responsible for this info category.', on_delete=django.db.models.deletion.PROTECT, related_name='info_categories', to='teams.Team'),
model_name="infocategory",
name="team",
field=models.ForeignKey(
help_text="The team responsible for this info category.",
on_delete=django.db.models.deletion.PROTECT,
related_name="info_categories",
to="teams.Team",
),
)
]

View file

@ -7,12 +7,11 @@ import reversion
class InfoCategory(CampRelatedModel):
class Meta:
ordering = ['weight', 'headline']
ordering = ["weight", "headline"]
verbose_name_plural = "Info Categories"
headline = models.CharField(
max_length=100,
help_text="The headline of this info category"
max_length=100, help_text="The headline of this info category"
)
anchor = models.SlugField(
@ -20,62 +19,55 @@ class InfoCategory(CampRelatedModel):
)
weight = models.PositiveIntegerField(
help_text='Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.',
help_text="Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.",
default=100,
)
team = models.ForeignKey(
'teams.Team',
help_text='The team responsible for this info category.',
"teams.Team",
help_text="The team responsible for this info category.",
on_delete=models.PROTECT,
related_name='info_categories'
related_name="info_categories",
)
def clean(self):
if InfoItem.objects.filter(category__team__camp=self.camp, anchor=self.anchor).exists():
if InfoItem.objects.filter(
category__team__camp=self.camp, anchor=self.anchor
).exists():
# this anchor is already in use on an item, so it cannot be used (must be unique on the page)
raise ValidationError(
{'anchor': 'Anchor is already in use on an info item for this camp'}
{"anchor": "Anchor is already in use on an info item for this camp"}
)
@property
def camp(self):
return self.team.camp
camp_filter = 'team__camp'
camp_filter = "team__camp"
def __str__(self):
return '%s (%s)' % (self.headline, self.camp)
return "%s (%s)" % (self.headline, self.camp)
# We want to have info items under version control
@reversion.register()
class InfoItem(CampRelatedModel):
class Meta:
ordering = ['weight', 'headline']
unique_together = (('anchor', 'category'), ('headline', 'category'))
ordering = ["weight", "headline"]
unique_together = (("anchor", "category"), ("headline", "category"))
category = models.ForeignKey(
'info.InfoCategory',
related_name='infoitems',
on_delete=models.PROTECT
"info.InfoCategory", related_name="infoitems", on_delete=models.PROTECT
)
headline = models.CharField(
max_length=100,
help_text="Headline of this info item."
)
headline = models.CharField(max_length=100, help_text="Headline of this info item.")
anchor = models.SlugField(
help_text="The HTML anchor to use for this info item."
)
anchor = models.SlugField(help_text="The HTML anchor to use for this info item.")
body = models.TextField(
help_text='Body of this info item. Markdown is supported.'
)
body = models.TextField(help_text="Body of this info item. Markdown is supported.")
weight = models.PositiveIntegerField(
help_text='Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.',
help_text="Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.",
default=100,
)
@ -83,12 +75,19 @@ class InfoItem(CampRelatedModel):
def camp(self):
return self.category.camp
camp_filter = 'category__team__camp'
camp_filter = "category__team__camp"
def clean(self):
if hasattr(self, 'category') and InfoCategory.objects.filter(team__camp=self.category.team.camp, anchor=self.anchor).exists():
if (
hasattr(self, "category")
and InfoCategory.objects.filter(
team__camp=self.category.team.camp, anchor=self.anchor
).exists()
):
# this anchor is already in use on a category, so it cannot be used here (they must be unique on the entire page)
raise ValidationError({'anchor': 'Anchor is already in use on an info category for this camp'})
raise ValidationError(
{"anchor": "Anchor is already in use on an info category for this camp"}
)
def __str__(self):
return '%s (%s)' % (self.headline, self.category)
return "%s (%s)" % (self.headline, self.category)

View file

@ -2,13 +2,13 @@ from django.views.generic import ListView
from .models import *
from camps.mixins import CampViewMixin
class CampInfoView(CampViewMixin, ListView):
model = InfoCategory
template_name = 'info.html'
context_object_name = 'categories'
template_name = "info.html"
context_object_name = "categories"
def get_queryset(self):
queryset = super(CampInfoView, self).get_queryset()
# do not show categories with 0 items
return queryset.exclude(infoitems__isnull=True)

Some files were not shown because too many files have changed in this diff Show more