diff --git a/.gitignore b/.gitignore index 835c56d37f..fbbed222eb 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,9 @@ media/ *.dump *.db *.sqlite3 + +#parkstay import ignores +import/ +importo.py +build.js +build.css diff --git a/ledger/accounts/models.py b/ledger/accounts/models.py index c2a9d27071..624d6d5683 100755 --- a/ledger/accounts/models.py +++ b/ledger/accounts/models.py @@ -15,7 +15,7 @@ from reversion import revisions from django_countries.fields import CountryField -from social.apps.django_app.default.models import UserSocialAuth +from social_django.models import UserSocialAuth from datetime import datetime, date @@ -639,4 +639,4 @@ def __str__(self): class Meta: managed = False - db_table = 'accounts_emailuser_report_v' \ No newline at end of file + db_table = 'accounts_emailuser_report_v' diff --git a/ledger/accounts/pipeline.py b/ledger/accounts/pipeline.py index e6557d0c8a..bb10939752 100644 --- a/ledger/accounts/pipeline.py +++ b/ledger/accounts/pipeline.py @@ -1,5 +1,5 @@ #from django.contrib.auth.models import User -from social.exceptions import InvalidEmail +from social_core.exceptions import InvalidEmail from .models import EmailUser, EmailIdentity from django.contrib.auth import logout diff --git a/ledger/accounts/tests.py b/ledger/accounts/tests.py index ac5372d96b..7f34b4c857 100644 --- a/ledger/accounts/tests.py +++ b/ledger/accounts/tests.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from django.test import TestCase from django.test import Client -from social.apps.django_app.default.models import UserSocialAuth +from social_django.models import UserSocialAuth from ledger.accounts.models import EmailUser diff --git a/ledger/accounts/urls.py b/ledger/accounts/urls.py index e0148b992b..27ed1e262e 100644 --- a/ledger/accounts/urls.py +++ b/ledger/accounts/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include from django.contrib.auth import views as auth_views from ledger.accounts import views @@ -8,7 +8,7 @@ url(r'api/report/duplicate_identity$', UserReportView.as_view(),name='ledger-user-report'), ] -urlpatterns = patterns('accounts', +urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^done/$', views.done, name='done'), url(r'^validation-sent/$', views.validation_sent, name='validation_sent'), @@ -16,4 +16,4 @@ url(r'^logout/', views.logout, name='logout'), url(r'^firsttime/', views.first_time, name='first_time'), url(r'accounts/', include(api_patterns)), -) +] diff --git a/ledger/basket/migrations/0010_auto_20170106_1600.py b/ledger/basket/migrations/0010_auto_20170106_1600.py new file mode 100644 index 0000000000..e917ca9221 --- /dev/null +++ b/ledger/basket/migrations/0010_auto_20170106_1600.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-06 08:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('basket', '0009_auto_20160905_1208'), + ] + + operations = [ + migrations.AlterField( + model_name='line', + name='product', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='basket_lines', to='catalogue.Product', verbose_name='Product'), + ), + ] diff --git a/ledger/basket/models.py b/ledger/basket/models.py index 8aa96c558f..9108c5aa88 100644 --- a/ledger/basket/models.py +++ b/ledger/basket/models.py @@ -1,7 +1,17 @@ from django.db import models -from oscar.apps.basket.abstract_models import AbstractBasket as CoreAbstractBasket +from oscar.apps.basket.abstract_models import AbstractBasket as CoreAbstractBasket, AbstractLine as CoreAbstractLine class Basket(CoreAbstractBasket): system = models.CharField(max_length=4) +class Line(CoreAbstractLine): + product = models.ForeignKey('catalogue.Product', related_name='basket_lines', on_delete=models.SET_NULL, verbose_name='Product', blank=True, null=True) + + def __str__(self): + return _( + u"Basket #%(basket_id)d, Product #%(product_id)d, quantity" + u" %(quantity)d") % {'basket_id': self.basket.pk, + 'product_id': self.product.pk if self.product else None, + 'quantity': self.quantity} + from oscar.apps.basket.models import * # noqa diff --git a/ledger/catalogue/migrations/0013_auto_20170106_1600.py b/ledger/catalogue/migrations/0013_auto_20170106_1600.py new file mode 100644 index 0000000000..439973af81 --- /dev/null +++ b/ledger/catalogue/migrations/0013_auto_20170106_1600.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-06 08:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0012_auto_20160905_1208'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='oracle_code', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Oracle Code'), + ), + ] diff --git a/ledger/middleware.py b/ledger/middleware.py index 9dfc0a2789..ee873196ed 100644 --- a/ledger/middleware.py +++ b/ledger/middleware.py @@ -5,6 +5,7 @@ class FirstTimeNagScreenMiddleware(object): def process_request(self, request): if request.user.is_authenticated() and request.method == 'GET': + #print('DEBUG: {}: {} == {}, {} == {}, {} == {}'.format(request.user, request.user.first_name, (not request.user.first_name), request.user.last_name, (not request.user.last_name), request.user.dob, (not request.user.dob) )) if (not request.user.first_name) or (not request.user.last_name) or (not request.user.dob): path_ft = reverse('accounts:first_time') path_logout = reverse('accounts:logout') diff --git a/ledger/order/migrations/0007_auto_20170106_1600.py b/ledger/order/migrations/0007_auto_20170106_1600.py new file mode 100644 index 0000000000..c2f4467e88 --- /dev/null +++ b/ledger/order/migrations/0007_auto_20170106_1600.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-06 08:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0006_auto_20160905_1208'), + ] + + operations = [ + migrations.AlterField( + model_name='line', + name='oracle_code', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Oracle Code'), + ), + ] diff --git a/ledger/payments/bpay/dashboard/app.py b/ledger/payments/bpay/dashboard/app.py index 6e8698370a..079cd32605 100644 --- a/ledger/payments/bpay/dashboard/app.py +++ b/ledger/payments/bpay/dashboard/app.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.admin.views.decorators import staff_member_required from oscar.core.application import Application diff --git a/ledger/payments/bpay/migrations/0007_auto_20170106_1600.py b/ledger/payments/bpay/migrations/0007_auto_20170106_1600.py new file mode 100644 index 0000000000..d487fb8f97 --- /dev/null +++ b/ledger/payments/bpay/migrations/0007_auto_20170106_1600.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-06 08:00 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bpay', '0006_auto_20160912_1134'), + ] + + operations = [ + migrations.AlterField( + model_name='bpayfile', + name='created', + field=models.DateTimeField(help_text='File Creation Date Time.'), + ), + migrations.AlterField( + model_name='bpayfile', + name='file_id', + field=models.BigIntegerField(help_text='File Identification Number.'), + ), + migrations.AlterField( + model_name='bpaygrouprecord', + name='modifier', + field=models.IntegerField(choices=[(1, 'interim/previous day'), (2, 'final/previous day'), (3, 'interim/same day'), (4, 'final/same day')], help_text='As of Date modifier'), + ), + migrations.AlterField( + model_name='bpaygrouprecord', + name='settled', + field=models.DateTimeField(help_text='File Settlement Date Time'), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='car', + field=models.CharField(help_text='Customer Additional Reference.', max_length=20, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='cheque_num', + field=models.IntegerField(default=0, help_text='Number of cheques in deposit'), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='country', + field=models.CharField(help_text='Country of payment.', max_length=3, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='crn', + field=models.CharField(help_text='Customer Referencer Number', max_length=20), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='discount_method', + field=models.CharField(blank=True, help_text='Discount Method Code.', max_length=3, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='discount_ref', + field=models.CharField(blank=True, help_text='Discount Reference Code.', max_length=20, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='discretionary_data', + field=models.CharField(help_text='Reason for refund or reversal.', max_length=50, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='entry_method', + field=models.CharField(blank=True, choices=[('000', 'undefined'), ('001', 'key entry by operator'), ('002', 'touch tone entry by payer'), ('003', 'speech recognition'), ('004', 'internet/on-line banking'), ('005', 'electtronic bill presentment'), ('006', 'batch data entry'), ('007', 'mobile entry')], help_text='Manner in which the payment details are captured.', max_length=3, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='orig_ref_num', + field=models.CharField(blank=True, help_text='Contains the original/previous CRN in the case of a refund or reversal.', max_length=21, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='p_date', + field=models.DateTimeField(help_text='Date of payment.'), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='p_instruction_code', + field=models.CharField(choices=[('05', 'payment'), ('15', 'error correction'), ('25', 'reversal')], help_text='Payment instruction method.', max_length=2, validators=[django.core.validators.MinLengthValidator(2)]), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='p_method_code', + field=models.CharField(choices=[('001', 'Debit Account'), ('101', 'Visa'), ('201', 'Mastercard'), ('301', 'Bankcard')], help_text='Method of payment.', max_length=3, validators=[django.core.validators.MinLengthValidator(3)]), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='payer_name', + field=models.CharField(blank=True, help_text="Name of payer extracted from payer's account details.", max_length=40, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='ref_rev_code', + field=models.CharField(blank=True, choices=[('001', 'payer paid twice'), ('002', 'payer paid wrong account'), ('003', 'payer paid wrong biller'), ('004', 'payer paid wrong amount'), ('005', 'payer did not authorise'), ('400', 'Visa chargeback'), ('500', 'MasterCard chargeback'), ('600', 'Bankcard chargeback')], help_text='Reason code for reversal or refund.', max_length=3, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='service_code', + field=models.CharField(help_text='Unique identification for a service provider realting to a bill.', max_length=7, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='state', + field=models.CharField(blank=True, help_text='State code of payer institution.', max_length=4, null=True), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='txn_ref', + field=models.CharField(help_text='Transaction Reference Number', max_length=21, validators=[django.core.validators.MinLengthValidator(12)]), + ), + migrations.AlterField( + model_name='bpaytransaction', + name='type', + field=models.CharField(choices=[('399', 'credit'), ('699', 'debit')], help_text='Indicates whether it is a credit or debit item', max_length=3, validators=[django.core.validators.MinLengthValidator(3)]), + ), + ] diff --git a/ledger/payments/bpoint/dashboard/app.py b/ledger/payments/bpoint/dashboard/app.py index a6fe7c1e84..efe0b6a56b 100644 --- a/ledger/payments/bpoint/dashboard/app.py +++ b/ledger/payments/bpoint/dashboard/app.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.admin.views.decorators import staff_member_required from oscar.core.application import Application diff --git a/ledger/payments/invoice/dashboard/app.py b/ledger/payments/invoice/dashboard/app.py index fb5b3b4ad6..b73ef3471f 100644 --- a/ledger/payments/invoice/dashboard/app.py +++ b/ledger/payments/invoice/dashboard/app.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.admin.views.decorators import staff_member_required from oscar.core.application import Application diff --git a/ledger/settings_base.py b/ledger/settings_base.py index 36f99eede2..ec3f7cd0a6 100755 --- a/ledger/settings_base.py +++ b/ledger/settings_base.py @@ -29,7 +29,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.flatpages', - 'social.apps.django_app.default', + 'social_django', 'django_extensions', 'reversion', 'widget_tweaks', @@ -65,7 +65,8 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'dpaw_utils.middleware.SSOLoginMiddleware', 'dpaw_utils.middleware.AuditMiddleware', # Sets model creator/modifier field values. - 'ledger.middleware.FirstTimeNagScreenMiddleware', +# FIXME: find out why django 1.10 breaks +# 'ledger.middleware.FirstTimeNagScreenMiddleware', 'oscar.apps.basket.middleware.BasketMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', ] @@ -73,12 +74,12 @@ # Authentication settings LOGIN_URL = '/' AUTHENTICATION_BACKENDS = ( - 'social.backends.email.EmailAuth', + 'social_core.backends.email.EmailAuth', 'django.contrib.auth.backends.ModelBackend', ) AUTH_USER_MODEL = 'accounts.EmailUser' -SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' -SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' +SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy' +SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage' SOCIAL_AUTH_EMAIL_FORM_URL = '/ledger/' SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION = 'ledger.accounts.mail.send_validation' SOCIAL_AUTH_EMAIL_VALIDATION_URL = '/ledger/validation-sent/' @@ -87,20 +88,20 @@ SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = True SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['first_name', 'last_name', 'email'] SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_details', 'ledger.accounts.pipeline.lower_email_address', 'ledger.accounts.pipeline.logout_previous_session', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'social_core.pipeline.user.get_username', # 'social.pipeline.mail.mail_validation', 'ledger.accounts.pipeline.mail_validation', 'ledger.accounts.pipeline.user_by_email', - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' + 'social_core.pipeline.user.create_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details' ) SESSION_COOKIE_DOMAIN = env('SESSION_COOKIE_DOMAIN', None) diff --git a/ledger/urls.py b/ledger/urls.py index 3ff2056a4a..b1d24be35f 100644 --- a/ledger/urls.py +++ b/ledger/urls.py @@ -23,7 +23,7 @@ url(r'^ledger/admin/', admin.site.urls), url(r'^ledger/', include('ledger.accounts.urls', namespace='accounts')), url(r'^ledger/', include('ledger.payments.urls', namespace='payments')), - url(r'^ledger/', include('social.apps.django_app.urls', namespace='social')), + url(r'^ledger/', include('social_django.urls', namespace='social')), url(r'^ledger/checkout/', application.urls), url(r'^taxonomy/', include('ledger.taxonomy.urls')), url(r'^$', TemplateView.as_view(template_name='customers/base.html'), name='home') diff --git a/parkstay/admin.py b/parkstay/admin.py index b74e8c44ff..eda75eee61 100644 --- a/parkstay/admin.py +++ b/parkstay/admin.py @@ -1,12 +1,107 @@ +from django.contrib import messages from django.contrib import admin -from .models import Park, Campground, Campsite, CampgroundFeature, Region, CampsiteClass, CampsiteBooking, Booking - -admin.site.register(Park) -admin.site.register(Campground) -admin.site.register(Campsite) -admin.site.register(CampgroundFeature) -admin.site.register(Region) -admin.site.register(CampsiteClass) -admin.site.register(CampsiteBooking) -admin.site.register(Booking) +from parkstay import models + +@admin.register(models.CampsiteClass) +class CampsiteClassAdmin(admin.ModelAdmin): + list_display = ('name',) + ordering = ('name',) + search_fields = ('name',) + list_filter = ('name',) + +@admin.register(models.Park) +class ParkAdmin(admin.ModelAdmin): + list_display = ('name','district') + ordering = ('name',) + list_filter = ('district',) + search_fields = ('name',) + +@admin.register(models.Campground) +class CampgroundAdmin(admin.ModelAdmin): + list_display = ('name','park','promo_area','campground_type','site_type') + ordering = ('name',) + search_fields = ('name',) + list_filter = ('campground_type','site_type') + +@admin.register(models.Campsite) +class CampsiteAdmin(admin.ModelAdmin): + list_display = ('name','campground',) + ordering = ('name',) + list_filter = ('campground',) + search_fields = ('name',) + +@admin.register(models.Feature) +class FeatureAdmin(admin.ModelAdmin): + list_display = ('name','description') + ordering = ('name',) + search_fields = ('name',) + +@admin.register(models.Booking) +class BookingAdmin(admin.ModelAdmin): + list_display = ('arrival','departure','campground','legacy_id','legacy_name','cost_total') + ordering = ('-arrival',) + search_fileds = ('arrival','departure') + list_filter = ('arrival','departure') + +@admin.register(models.CampsiteBooking) +class CampsiteBookingAdmin(admin.ModelAdmin): + list_display = ('campsite','date','booking','booking_type') + ordering = ('-date',) + search_fields = ('date',) + list_filter = ('campsite','booking_type') + +@admin.register(models.CampsiteRate) +class CampsiteRateAdmin(admin.ModelAdmin): + list_display = ('campsite','rate','allow_public_holidays') + list_filter = ('campsite','rate','allow_public_holidays') + search_fields = ('campground__name',) + +@admin.register(models.Contact) +class ContactAdmin(admin.ModelAdmin): + list_display = ('name','phone_number') + search_fields = ('name','phone_number') + +class ReasonAdmin(admin.ModelAdmin): + list_display = ('code','text','editable') + search_fields = ('code','text') + readonly_fields = ('code',) + + def get_readonly_fields(self, request, obj=None): + fields = list(self.readonly_fields) + if obj and not obj.editable: + fields += ['text','editable'] + elif not obj: + fields = [] + return fields + + def has_add_permission(self, request, obj=None): + if obj and not obj.editable: + return False + return super(ReasonAdmin, self).has_delete_permission(request, obj) + + def has_delete_permission(self, request, obj=None): + if obj and not obj.editable: + return False + return super(ReasonAdmin, self).has_delete_permission(request, obj) + +@admin.register(models.MaximumStayReason) +class MaximumStayReason(ReasonAdmin): + pass + +@admin.register(models.PriceReason) +class PriceReason(ReasonAdmin): + pass + +@admin.register(models.ClosureReason) +class ClosureReason(ReasonAdmin): + pass + +@admin.register(models.OpenReason) +class OpenReason(ReasonAdmin): + pass + +admin.site.register(models.Rate) +admin.site.register(models.Region) +admin.site.register(models.District) +admin.site.register(models.PromoArea) diff --git a/parkstay/api.py b/parkstay/api.py new file mode 100644 index 0000000000..ba53a312a0 --- /dev/null +++ b/parkstay/api.py @@ -0,0 +1,1143 @@ +import traceback +import base64 +from six.moves.urllib.parse import urlparse +from django.db.models import Q +from django.core.files.base import ContentFile +from rest_framework import viewsets, serializers, status, generics, views +from rest_framework.decorators import detail_route +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser, BasePermission +from datetime import datetime, timedelta +from collections import OrderedDict + +from parkstay.models import (Campground, + CampsiteBooking, + Campsite, + CampsiteRate, + Booking, + CampgroundBookingRange, + CampsiteBookingRange, + CampsiteStayHistory, + PromoArea, + Park, + Feature, + Region, + CampsiteClass, + Booking, + CampsiteRate, + Rate, + CampgroundPriceHistory, + CampsiteClassPriceHistory, + ClosureReason, + OpenReason, + PriceReason, + MaximumStayReason + ) + +from parkstay.serialisers import ( CampsiteBookingSerialiser, + CampsiteSerialiser, + CampgroundMapSerializer, + CampgroundMapFilterSerializer, + CampgroundSerializer, + CampgroundCampsiteFilterSerializer, + PromoAreaSerializer, + ParkSerializer, + FeatureSerializer, + RegionSerializer, + CampsiteClassSerializer, + BookingSerializer, + CampgroundBookingRangeSerializer, + CampsiteBookingRangeSerializer, + CampsiteRateSerializer, + CampsiteRateReadonlySerializer, + CampsiteStayHistorySerializer, + RateSerializer, + RateDetailSerializer, + CampgroundPriceHistorySerializer, + CampsiteClassPriceHistorySerializer, + CampgroundImageSerializer, + ExistingCampgroundImageSerializer, + ClosureReasonSerializer, + OpenReasonSerializer, + PriceReasonSerializer, + MaximumStayReasonSerializer, + BulkPricingSerializer + ) +from parkstay.helpers import is_officer, is_customer + + + + +# API Views +class CampsiteBookingViewSet(viewsets.ModelViewSet): + queryset = CampsiteBooking.objects.all() + serializer_class = CampsiteBookingSerialiser + + +class CampsiteViewSet(viewsets.ModelViewSet): + queryset = Campsite.objects.all() + serializer_class = CampsiteSerialiser + + + def list(self, request, format=None): + queryset = self.get_queryset() + formatted = bool(request.GET.get("formatted", False)) + serializer = self.get_serializer(queryset, formatted=formatted, many=True, method='get') + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + formatted = bool(request.GET.get("formatted", False)) + serializer = self.get_serializer(instance, formatted=formatted, method='get') + return Response(serializer.data) + + def update(self, request, *args, **kwargs): + try: + instance = self.get_object() + serializer = self.get_serializer(instance,data=request.data,partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response(serializer.data) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + def create(self, request, *args, **kwargs): + try: + http_status = status.HTTP_200_OK + number = request.data.pop('number') + serializer = self.get_serializer(data=request.data,method='post') + serializer.is_valid(raise_exception=True) + + if number > 1: + data = dict(serializer.validated_data) + campsites = Campsite.bulk_create(number,data) + res = self.get_serializer(campsites,many=True) + else: + if number == 1 and serializer.validated_data['name'] == 'default': + latest = 0 + current_campsites = Campsite.objects.filter(campground=serializer.validated_data.get('campground')) + cs_numbers = [int(c.name) for c in current_campsites if c.name.isdigit()] + if cs_numbers: + latest = max(cs_numbers) + if len(str(latest+1)) == 1: + name = '0{}'.format(latest+1) + else: + name = str(latest+1) + serializer.validated_data['name'] = name + instance = serializer.save() + res = self.get_serializer(instance) + + return Response(res.data) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post']) + def open_close(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + # parse and validate data + mutable = request.POST._mutable + request.POST._mutable = True + request.POST['campsite'] = self.get_object().id + request.POST._mutable = mutable + serializer = CampsiteBookingRangeSerializer(data=request.data, method="post") + serializer.is_valid(raise_exception=True) + if serializer.validated_data.get('status') == 0: + self.get_object().open(dict(serializer.validated_data)) + else: + self.get_object().close(dict(serializer.validated_data)) + + # return object + ground = self.get_object() + res = CampsiteSerialiser(ground, context={'request':request}) + + return Response(res.data) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def status_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + # Check what status is required + closures = bool(request.GET.get("closures", False)) + if closures: + serializer = CampsiteBookingRangeSerializer(self.get_object().booking_ranges.filter(~Q(status=0)),many=True) + else: + serializer = CampsiteBookingRangeSerializer(self.get_object().booking_ranges,many=True) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def stay_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + serializer = CampsiteStayHistorySerializer(self.get_object().stay_history,many=True,context={'request':request},method='get') + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def price_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + price_history = self.get_object().rates.all() + serializer = CampsiteRateReadonlySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + +class CampsiteStayHistoryViewSet(viewsets.ModelViewSet): + queryset = CampsiteStayHistory.objects.all() + serializer_class = CampsiteStayHistorySerializer + + def update(self, request, *args, **kwargs): + try: + instance = self.get_object() + partial = kwargs.pop('partial', False) + serializer = self.get_serializer(instance,data=request.data,partial=partial) + serializer.is_valid(raise_exception=True) + if instance.range_end and not serializer.validated_data.get('range_end'): + instance.range_end = None + self.perform_update(serializer) + + return Response(serializer.data) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + +class CampgroundMapViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Campground.objects.all() + serializer_class = CampgroundMapSerializer + permission_classes = [] + + +class CampgroundMapFilterViewSet(viewsets.ReadOnlyModelViewSet): + # TODO: add exclude for unpublished campground objects + #queryset = Campground.objects.exclude(campground_type=1) + queryset = Campground.objects.all() + serializer_class = CampgroundMapFilterSerializer + permission_classes = [] + + + def list(self, request, *args, **kwargs): + print(request.GET) + data = { + "arrival" : request.GET.get('arrival', None), + "departure" : request.GET.get('departure', None), + "num_adult" : request.GET.get('num_adult', 0), + "num_concession" : request.GET.get('num_concession', 0), + "num_child" : request.GET.get('num_child', 0), + "num_infant" : request.GET.get('num_infant', 0), + "gear_type": request.GET.get('gear_type', 'tent') + } + + serializer = CampgroundCampsiteFilterSerializer(data=data) + serializer.is_valid(raise_exception=True) + scrubbed = serializer.validated_data + if scrubbed['arrival'] and scrubbed['departure'] and (scrubbed['arrival'] < scrubbed['departure']): + sites = Campsite.objects.exclude( + campsitebooking__date__range=( + scrubbed['arrival'], + scrubbed['departure']-timedelta(days=1) + ) + ).filter(**{scrubbed['gear_type']: True}) + ground_ids = set([s.campground.id for s in sites]) + queryset = Campground.objects.filter(id__in=ground_ids).order_by('name') + else: + ground_ids = set((x[0] for x in Campsite.objects.filter(**{scrubbed['gear_type']: True}).values_list('campground'))) + # we need to be tricky here. for the default search (tent, no timestamps), + # we want to include all of the "campgrounds" that don't have any campsites in the model! + if scrubbed['gear_type'] == 'tent': + ground_ids.update((x[0] for x in Campground.objects.filter(campsites__isnull=True).values_list('id'))) + + queryset = Campground.objects.filter(id__in=ground_ids).order_by('name') + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + +class CampgroundViewSet(viewsets.ModelViewSet): + queryset = Campground.objects.all() + serializer_class = CampgroundSerializer + + + def list(self, request, format=None): + queryset = self.get_queryset() + formatted = bool(request.GET.get("formatted", False)) + serializer = self.get_serializer(queryset, formatted=formatted, many=True, method='get') + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + formatted = bool(request.GET.get("formatted", False)) + serializer = self.get_serializer(instance, formatted=formatted, method='get') + return Response(serializer.data) + + def strip_b64_header(self, content): + if ';base64,' in content: + header, base64_data = content.split(';base64,') + return base64_data + return content + + def create(self, request, format=None): + try: + images_data = None + http_status = status.HTTP_200_OK + if "images" in request.data: + images_data = request.data.pop("images") + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance =serializer.save() + # Get and Validate campground images + initial_image_serializers = [CampgroundImageSerializer(data=image) for image in images_data] if images_data else [] + image_serializers = [] + if initial_image_serializers: + + for image_serializer in initial_image_serializers: + result = urlparse(image_serializer.initial_data['image']) + if not (result.scheme =='http' or result.scheme == 'https') and not result.netloc: + image_serializers.append(image_serializer) + + if image_serializers: + for image_serializer in image_serializers: + image_serializer.initial_data["campground"] = instance.id + image_serializer.initial_data["image"] = ContentFile(base64.b64decode(self.strip_b64_header(image_serializer.initial_data["image"]))) + image_serializer.initial_data["image"].name = 'uploaded' + + for image_serializer in image_serializers: + image_serializer.is_valid(raise_exception=True) + + for image_serializer in image_serializers: + image_serializer.save() + + return Response(serializer.data) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + def update(self, request, *args, **kwargs): + try: + images_data = None + http_status = status.HTTP_200_OK + instance = self.get_object() + if "images" in request.data: + images_data = request.data.pop("images") + serializer = self.get_serializer(instance,data=request.data,partial=True) + serializer.is_valid(raise_exception=True) + # Get and Validate campground images + initial_image_serializers = [CampgroundImageSerializer(data=image) for image in images_data] if images_data else [] + image_serializers, existing_image_serializers = [],[] + # Get campgrounds current images + current_images = instance.images.all() + if initial_image_serializers: + + for image_serializer in initial_image_serializers: + result = urlparse(image_serializer.initial_data['image']) + if not (result.scheme =='http' or result.scheme == 'https') and not result.netloc: + image_serializers.append(image_serializer) + else: + data = { + 'id':image_serializer.initial_data['id'], + 'image':image_serializer.initial_data['image'], + 'campground':instance.id + } + existing_image_serializers.append(ExistingCampgroundImageSerializer(data=data)) + + # Dealing with existing images + images_id_list = [] + for image_serializer in existing_image_serializers: + image_serializer.is_valid(raise_exception=True) + images_id_list.append(image_serializer.validated_data['id']) + + #Get current object images and check if any has been removed + for img in current_images: + if img.id not in images_id_list: + img.delete() + + # Creating new Images + if image_serializers: + for image_serializer in image_serializers: + image_serializer.initial_data["campground"] = instance.id + image_serializer.initial_data["image"] = ContentFile(base64.b64decode(self.strip_b64_header(image_serializer.initial_data["image"]))) + image_serializer.initial_data["image"].name = 'uploaded' + + for image_serializer in image_serializers: + image_serializer.is_valid(raise_exception=True) + + for image_serializer in image_serializers: + image_serializer.save() + else: + if current_images: + current_images.delete() + + self.perform_update(serializer) + return Response(serializer.data) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post']) + def open_close(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + # parse and validate data + mutable = request.POST._mutable + request.POST._mutable = True + request.POST['campground'] = self.get_object().id + request.POST._mutable = mutable + serializer = CampgroundBookingRangeSerializer(data=request.data, method="post") + serializer.is_valid(raise_exception=True) + if serializer.validated_data.get('status') == 0: + self.get_object().open(dict(serializer.validated_data)) + else: + self.get_object().close(dict(serializer.validated_data)) + + # return object + ground = self.get_object() + res = CampgroundSerializer(ground, context={'request':request}) + + return Response(res.data) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def addPrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + rate = None + serializer = RateDetailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + rate_id = serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + serializer.validated_data['rate']= rate + data = { + 'rate': rate, + 'date_start': serializer.validated_data['period_start'], + 'reason': PriceReason.objects.get(serializer.validated_data['reason']), + 'details': serializer.validated_data['details'], + 'update_level': 0 + } + self.get_object().createCampsitePriceHistory(data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def updatePrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + original_data = request.data.pop('original') + original_serializer = CampgroundPriceHistorySerializer(data=original_data,method='post') + original_serializer.is_valid(raise_exception=True) + + serializer = RateDetailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + rate_id = serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + serializer.validated_data['rate']= rate + new_data = { + 'rate': rate, + 'date_start': serializer.validated_data['period_start'], + 'reason': PriceReason.objects.get(pk=serializer.validated_data['reason']), + 'details': serializer.validated_data['details'], + 'update_level': 0 + } + self.get_object().updatePriceHistory(dict(original_serializer.validated_data),new_data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def deletePrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + serializer = CampgroundPriceHistorySerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + self.get_object().deletePriceHistory(serializer.validated_data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def status_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + # Check what status is required + closures = bool(request.GET.get("closures", False)) + if closures: + serializer = CampgroundBookingRangeSerializer(self.get_object().booking_ranges.filter(~Q(status=0)),many=True) + else: + serializer = CampgroundBookingRangeSerializer(self.get_object().booking_ranges,many=True) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def campsites(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + serializer = CampsiteSerialiser(self.get_object().campsites,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def price_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['get']) + def campsite_bookings(self, request, pk=None, format=None): + """Fetch campsite availability for a campground.""" + # convert GET parameters to objects + ground = self.get_object() + # Validate parameters + data = { + "arrival" : request.GET.get('arrival'), + "departure" : request.GET.get('departure'), + "num_adult" : request.GET.get('num_adult', 0), + "num_concession" : request.GET.get('num_concession', 0), + "num_child" : request.GET.get('num_child', 0), + "num_infant" : request.GET.get('num_infant', 0) + } + serializer = CampgroundCampsiteFilterSerializer(data=data) + serializer.is_valid(raise_exception=True) + + start_date = serializer.validated_data['arrival'] + end_date = serializer.validated_data['departure'] + num_adult = serializer.validated_data['num_adult'] + num_concession = serializer.validated_data['num_concession'] + num_child = serializer.validated_data['num_child'] + num_infant = serializer.validated_data['num_infant'] + # get a length of the stay (in days), capped if necessary to the request maximum + length = max(0, (end_date-start_date).days) + if length > settings.PS_MAX_BOOKING_LENGTH: + length = settings.PS_MAX_BOOKING_LENGTH + end_date = start_date+timedelta(days=settings.PS_MAX_BOOKING_LENGTH) + + # fetch all of the single-day CampsiteBooking objects within the date range for the campground + bookings_qs = CampsiteBooking.objects.filter( + campsite__campground=ground, + date__gte=start_date, + date__lt=end_date + ).order_by('date', 'campsite__name') + # fetch all the campsites and applicable rates for the campground + sites_qs = Campsite.objects.filter(campground=ground).order_by('name') + rates_qs = CampsiteRate.objects.filter(campsite__in=sites_qs) + + # make a map of campsite class to cost + rates_map = {r.campsite.campsite_class_id: r.get_rate(num_adult, num_concession, num_child, num_infant) for r in rates_qs} + + # from our campsite queryset, generate a digest for each site + sites_map = OrderedDict([(s.name, (s.pk, s.campsite_class, rates_map[s.campsite_class_id])) for s in sites_qs]) + bookings_map = {} + + # create our result object, which will be returned as JSON + result = { + 'arrival': start_date.strftime('%Y/%m/%d'), + 'days': length, + 'adults': 1, + 'children': 0, + 'maxAdults': 30, + 'maxChildren': 30, + 'sites': [], + 'classes': {} + } + + # make an entry under sites for each site + for k, v in sites_map.items(): + site = { + 'name': k, + 'id': v[0], + 'type': ground.campground_type, + 'class': v[1].pk, + 'price': '${}'.format(v[2]*length), + 'availability': [[True, '${}'.format(v[2]), v[2]] for i in range(length)] + } + result['sites'].append(site) + bookings_map[k] = site + if v[1].pk not in result['classes']: + result['classes'][v[1].pk] = v[1].name + + # strike out existing bookings + for b in bookings_qs: + offset = (b.date-start_date).days + bookings_map[b.campsite.name]['availability'][offset][0] = False + bookings_map[b.campsite.name]['availability'][offset][1] = 'Closed' if b.booking_type == 2 else 'Sold' + bookings_map[b.campsite.name]['price'] = False + + return Response(result) + + @detail_route(methods=['get']) + def campsite_class_bookings(self, request, pk=None, format=None): + """Fetch campsite availability for a campground, grouped by campsite class.""" + # convert GET parameters to objects + ground = self.get_object() + # Validate parameters + data = { + "arrival" : request.GET.get('arrival'), + "departure" : request.GET.get('departure'), + "num_adult" : request.GET.get('num_adult', 0), + "num_concession" : request.GET.get('num_concession', 0), + "num_child" : request.GET.get('num_child', 0), + "num_infant" : request.GET.get('num_infant', 0) + } + serializer = CampgroundCampsiteFilterSerializer(data=data) + serializer.is_valid(raise_exception=True) + + start_date = serializer.validated_data['arrival'] + end_date = serializer.validated_data['departure'] + num_adult = serializer.validated_data['num_adult'] + num_concession = serializer.validated_data['num_concession'] + num_child = serializer.validated_data['num_child'] + num_infant = serializer.validated_data['num_infant'] + + # get a length of the stay (in days), capped if necessary to the request maximum + length = max(0, (end_date-start_date).days) + if length > settings.PS_MAX_BOOKING_LENGTH: + length = settings.PS_MAX_BOOKING_LENGTH + end_date = start_date+timedelta(days=settings.PS_MAX_BOOKING_LENGTH) + + # fetch all of the single-day CampsiteBooking objects within the date range for the campground + bookings_qs = CampsiteBooking.objects.filter( + campsite__campground=ground, + date__gte=start_date, + date__lt=end_date + ).order_by('date', 'campsite__name') + # fetch all the campsites and applicable rates for the campground + sites_qs = Campsite.objects.filter(campground=ground) + rates_qs = CampsiteRate.objects.filter(campsite__in=sites_qs) + + # make a map of campsite class to cost + rates_map = {r.campsite.campsite_class_id: r.get_rate(num_adult, num_concession, num_child, num_infant) for r in rates_qs} + + # from our campsite queryset, generate a distinct list of campsite classes + classes = [x for x in sites_qs.distinct('campsite_class__name').order_by('campsite_class__name').values_list('pk', 'campsite_class', 'campsite_class__name')] + + classes_map = {} + bookings_map = {} + + # create our result object, which will be returned as JSON + result = { + 'arrival': start_date.strftime('%Y/%m/%d'), + 'days': length, + 'adults': 1, + 'children': 0, + 'maxAdults': 30, + 'maxChildren': 30, + 'sites': [], + 'classes': {} + } + + # make an entry under sites for each campsite class + for c in classes: + rate = rates_map[c[1]] + site = { + 'name': c[2], + 'id': None, + 'type': ground.campground_type, + 'price': '${}'.format(rate*length), + 'availability': [[True, '${}'.format(rate), rate, [0, 0]] for i in range(length)], + 'breakdown': OrderedDict() + } + result['sites'].append(site) + classes_map[c[1]] = site + + # make a map of class IDs to site IDs + class_sites_map = {} + for s in sites_qs: + if s.campsite_class.pk not in class_sites_map: + class_sites_map[s.campsite_class.pk] = set() + + class_sites_map[s.campsite_class.pk].add(s.pk) + rate = rates_map[s.campsite_class.pk] + classes_map[s.campsite_class.pk]['breakdown'][s.name] = [[True, '${}'.format(rate), rate] for i in range(length)] + + # store number of campsites in each class + class_sizes = {k: len(v) for k, v in class_sites_map.items()} + + + + # strike out existing bookings + for b in bookings_qs: + offset = (b.date-start_date).days + key = b.campsite.campsite_class.pk + + # clear the campsite from the class sites map + if b.campsite.pk in class_sites_map[key]: + class_sites_map[key].remove(b.campsite.pk) + + # update the per-site availability + classes_map[key]['breakdown'][b.campsite.name][offset][0] = False + classes_map[key]['breakdown'][b.campsite.name][offset][1] = 'Closed' if (b.booking_type == 2) else 'Sold' + + # update the class availability status + book_offset = 1 if (b.booking_type == 2) else 0 + classes_map[key]['availability'][offset][3][book_offset] += 1 + if classes_map[key]['availability'][offset][3][0] == class_sizes[key]: + classes_map[key]['availability'][offset][1] = 'Fully Booked' + elif classes_map[key]['availability'][offset][3][1] == class_sizes[key]: + classes_map[key]['availability'][offset][1] = 'Closed' + elif classes_map[key]['availability'][offset][3][0] >= classes_map[key]['availability'][offset][3][1]: + classes_map[key]['availability'][offset][1] = 'Partially Booked' + else: + classes_map[key]['availability'][offset][1] = 'Partially Closed' + + # tentatively flag campsite class as unavailable + classes_map[key]['availability'][offset][0] = False + classes_map[key]['price'] = False + + # convert breakdowns to a flat list + for klass in classes_map.values(): + klass['breakdown'] = [{'name': k, 'availability': v} for k, v in klass['breakdown'].items()] + + # any campsites remaining in the class sites map have zero bookings! + # check if there's any left for each class, and if so return that as the target + for k, v in class_sites_map.items(): + if v: + rate = rates_map[k] + classes_map[k].update({ + 'id': v.pop(), + 'price': '${}'.format(rate*length), + 'availability': [[True, '${}'.format(rate), rate, [0, 0]] for i in range(length)], + 'breakdown': [] + }) + + + return Response(result) + +class PromoAreaViewSet(viewsets.ModelViewSet): + queryset = PromoArea.objects.all() + serializer_class = PromoAreaSerializer + +class ParkViewSet(viewsets.ModelViewSet): + queryset = Park.objects.all() + serializer_class = ParkSerializer + +class FeatureViewSet(viewsets.ModelViewSet): + queryset = Feature.objects.all() + serializer_class = FeatureSerializer + +class RegionViewSet(viewsets.ModelViewSet): + queryset = Region.objects.all() + serializer_class = RegionSerializer + +class CampsiteClassViewSet(viewsets.ModelViewSet): + queryset = CampsiteClass.objects.all() + serializer_class = CampsiteClassSerializer + + def list(self, request, *args, **kwargs): + active_only = bool(request.GET.get('active_only',False)) + if active_only: + queryset = CampsiteClass.objects.filter(deleted=False) + else: + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True,method='get') + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance,method='get') + return Response(serializer.data) + + + @detail_route(methods=['get']) + def price_history(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + price_history = CampsiteClassPriceHistory.objects.filter(id=self.get_object().id) + # Format list + open_ranges,formatted_list,fixed_list= [],[],[] + for p in price_history: + if p.date_end == None: + open_ranges.append(p) + else: + formatted_list.append(p) + + for outer in open_ranges: + for inner in open_ranges: + if inner.date_start > outer.date_start and inner.rate_id == outer.rate_id: + open_ranges.remove(inner) + + fixed_list = formatted_list + open_ranges + fixed_list.sort(key=lambda x: x.date_start) + serializer = CampsiteClassPriceHistorySerializer(fixed_list,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def addPrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + rate = None + serializer = RateDetailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + rate_id = serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + serializer.validated_data['rate']= rate + data = { + 'rate': rate, + 'date_start': serializer.validated_data['period_start'], + 'reason': PriceReason.objects.get(pk=serializer.validated_data['reason']), + 'details': serializer.validated_data['details'], + 'update_level': 1 + } + self.get_object().createCampsitePriceHistory(data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def updatePrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + original_data = request.data.pop('original') + + original_serializer = CampgroundPriceHistorySerializer(data=original_data) + original_serializer.is_valid(raise_exception=True) + + serializer = RateDetailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + rate_id = serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + serializer.validated_data['rate']= rate + new_data = { + 'rate': rate, + 'date_start': serializer.validated_data['period_start'], + 'reason': PriceReason.objects.get(pk=serializer.validated_data['reason']), + 'details': serializer.validated_data['details'], + 'update_level': 1 + } + self.get_object().updatePriceHistory(dict(original_serializer.validated_data),new_data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + @detail_route(methods=['post'],) + def deletePrice(self, request, format='json', pk=None): + try: + http_status = status.HTTP_200_OK + serializer = CampgroundPriceHistorySerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + self.get_object().deletePriceHistory(serializer.validated_data) + price_history = CampgroundPriceHistory.objects.filter(id=self.get_object().id) + serializer = CampgroundPriceHistorySerializer(price_history,many=True,context={'request':request}) + res = serializer.data + + return Response(res,status=http_status) + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + +class BookingViewSet(viewsets.ModelViewSet): + queryset = Booking.objects.all() + serializer_class = BookingSerializer + +class CampsiteRateViewSet(viewsets.ModelViewSet): + queryset = CampsiteRate.objects.all() + serializer_class = CampsiteRateSerializer + + + def create(self, request, format=None): + try: + http_status = status.HTTP_200_OK + rate = None + print(request.data) + rate_serializer = RateDetailSerializer(data=request.data) + rate_serializer.is_valid(raise_exception=True) + rate_id = rate_serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + print(rate_serializer.validated_data) + if rate: + data = { + 'rate': rate.id, + 'date_start': rate_serializer.validated_data['period_start'], + 'campsite': rate_serializer.validated_data['campsite'], + #'reason': serializer.validated_data['reason'], + 'update_level': 2 + } + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + res = serializer.save() + + serializer = CampsiteRateReadonlySerializer(res) + return Response(serializer.data, status=http_status) + + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + + def update(self, request, *args, **kwargs): + try: + http_status = status.HTTP_200_OK + rate = None + rate_serializer = RateDetailSerializer(data=request.data) + rate_serializer.is_valid(raise_exception=True) + rate_id = rate_serializer.validated_data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + data = { + 'rate': rate.id, + 'date_start': rate_serializer.validated_data['period_start'], + 'campsite': rate_serializer.validated_data['campsite'], + #'reason': serializer.validated_data['reason'], + 'update_level': 2 + } + instance = self.get_object() + partial = kwargs.pop('partial', False) + serializer = self.get_serializer(instance,data=data,partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response(serializer.data, status=http_status) + + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + +class BookingRangeViewset(viewsets.ModelViewSet): + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + original = bool(request.GET.get("original", False)) + serializer = self.get_serializer(instance, original=original, method='get') + return Response(serializer.data) + + def update(self, request, *args, **kwargs): + try: + instance = self.get_object() + partial = kwargs.pop('partial', False) + serializer = self.get_serializer(instance,data=request.data,partial=partial) + serializer.is_valid(raise_exception=True) + if instance.range_end and not serializer.validated_data.get('range_end'): + instance.range_end = None + self.perform_update(serializer) + + return Response(serializer.data) + except serializers.ValidationError: + raise + except Exception as e: + raise serializers.ValidationError(str(e)) + +class CampgroundBookingRangeViewset(BookingRangeViewset): + queryset = CampgroundBookingRange.objects.all() + serializer_class = CampgroundBookingRangeSerializer + +class CampsiteBookingRangeViewset(BookingRangeViewset): + queryset = CampsiteBookingRange.objects.all() + serializer_class = CampsiteBookingRangeSerializer + +class RateViewset(viewsets.ModelViewSet): + queryset = Rate.objects.all() + serializer_class = RateSerializer + +# Reasons +# ========================= +class ClosureReasonViewSet(viewsets.ReadOnlyModelViewSet): + queryset = ClosureReason.objects.all() + serializer_class = ClosureReasonSerializer + +class OpenReasonViewSet(viewsets.ReadOnlyModelViewSet): + queryset = OpenReason.objects.all() + serializer_class = OpenReasonSerializer + +class PriceReasonViewSet(viewsets.ReadOnlyModelViewSet): + queryset = PriceReason.objects.all() + serializer_class = PriceReasonSerializer + +class MaximumStayReasonViewSet(viewsets.ReadOnlyModelViewSet): + queryset = MaximumStayReason.objects.all() + serializer_class = MaximumStayReasonSerializer + +# Bulk Pricing +# =========================== +class BulkPricingView(generics.CreateAPIView): + serializer_class = BulkPricingSerializer + renderer_classes = (JSONRenderer,) + + def create(self, request,*args, **kwargs): + try: + http_status = status.HTTP_200_OK + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + print(serializer.validated_data) + + rate_id = serializer.data.get('rate',None) + if rate_id: + try: + rate = Rate.objects.get(id=rate_id) + except Rate.DoesNotExist as e : + raise serializers.ValidationError('The selected rate does not exist') + else: + rate = Rate.objects.get_or_create(adult=serializer.validated_data['adult'],concession=serializer.validated_data['concession'],child=serializer.validated_data['child'])[0] + if rate: + data = { + 'rate': rate, + 'date_start': serializer.validated_data['period_start'], + 'reason': PriceReason.objects.get(pk=serializer.data['reason']), + 'details': serializer.validated_data['details'] + } + if serializer.data['type'] == 'Park': + for c in serializer.data['campgrounds']: + data['update_level'] = 0 + Campground.objects.get(pk=c).createCampsitePriceHistory(data) + elif serializer.data['type'] == 'Campsite Type': + data['update_level'] = 1 + CampsiteClass.objects.get(pk=serializer.data['campsiteType']).createCampsitePriceHistory(data) + + return Response(serializer.data, status=http_status) + + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) diff --git a/parkstay/exceptions.py b/parkstay/exceptions.py new file mode 100644 index 0000000000..831ade768e --- /dev/null +++ b/parkstay/exceptions.py @@ -0,0 +1,3 @@ + +class BookingRangeWithinException(Exception): + pass diff --git a/parkstay/fixtures/__init__.py b/parkstay/fixtures/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/parkstay/fixtures/closure_reasons.json b/parkstay/fixtures/closure_reasons.json new file mode 100644 index 0000000000..b3f6701225 --- /dev/null +++ b/parkstay/fixtures/closure_reasons.json @@ -0,0 +1,29 @@ +[ + { + "model": "parkstay.closurereason", + "pk":1, + "fields": + { + "text":"Other", + "editable": false + } + }, + { + "model": "parkstay.closurereason", + "pk":2, + "fields": + { + "text":"Closed due to natural disaster", + "editable": false + } + }, + { + "model": "parkstay.closurereason", + "pk":3, + "fields": + { + "text":"Closed for maintenance", + "editable": false + } + } +] diff --git a/parkstay/fixtures/max_stay_reasons.json b/parkstay/fixtures/max_stay_reasons.json new file mode 100644 index 0000000000..8b5f5df82d --- /dev/null +++ b/parkstay/fixtures/max_stay_reasons.json @@ -0,0 +1,11 @@ +[ + { + "model": "parkstay.maximumstayreason", + "pk":1, + "fields": + { + "text":"Other", + "editable": false + } + } +] diff --git a/parkstay/fixtures/open_reasons.json b/parkstay/fixtures/open_reasons.json new file mode 100644 index 0000000000..4d059a2be0 --- /dev/null +++ b/parkstay/fixtures/open_reasons.json @@ -0,0 +1,11 @@ +[ + { + "model": "parkstay.openreason", + "pk":1, + "fields": + { + "text":"Other", + "editable": false + } + } +] diff --git a/parkstay/fixtures/price_reasons.json b/parkstay/fixtures/price_reasons.json new file mode 100644 index 0000000000..00d80ab735 --- /dev/null +++ b/parkstay/fixtures/price_reasons.json @@ -0,0 +1,11 @@ +[ + { + "model": "parkstay.pricereason", + "pk":1, + "fields": + { + "text":"Other", + "editable": false + } + } +] diff --git a/parkstay/forms.py b/parkstay/forms.py new file mode 100644 index 0000000000..596c1c3a33 --- /dev/null +++ b/parkstay/forms.py @@ -0,0 +1,5 @@ +from django import forms + + +class LoginForm(forms.Form): + email = forms.EmailField(max_length=254) diff --git a/parkstay/frontend/README.md b/parkstay/frontend/README.md new file mode 100644 index 0000000000..975ac5611d --- /dev/null +++ b/parkstay/frontend/README.md @@ -0,0 +1,67 @@ +# vue-browserify-boilerplate + +> A full-featured Browserify + `vueify` setup with hot-reload, linting & unit testing. + +> This template is Vue 2.0 compatible. For Vue 1.x use this command: `vue init browserify#1.0 my-project` + +### Usage + +This is a project template for [vue-cli](https://github.com/vuejs/vue-cli). + +``` bash +$ npm install -g vue-cli +$ vue init browserify my-project +$ cd my-project +$ npm install +$ npm run dev +``` + +### What's Included + +- `npm run dev`: Browserify + `vueify` with proper config for source map & hot-reload. + +- `npm run build`: Production build with HTML/CSS/JS minification. + +- `npm run lint`: Lint JavaScript and `*.vue` files with ESLint. + +- `npm test`: Unit tests in PhantomJS with Karma + karma-jasmine + karma-browserify, with support for mocking and ES2015. + +For more information see the [docs for vueify](https://github.com/vuejs/vueify). + +### Customizations + +You will likely need to do some tuning to suit your own needs: + +- Install additional libraries that you need, e.g. `vue-router`, `vue-resource`, `vuex`, etc... + +- Use your preferred `.eslintrc` config. Don't forget to keep the plugin field so that ESLint can lint `*.vue` files. + +- Add your preferred CSS pre-processor, for example: + + ``` bash + npm install less --save-dev + ``` + + Then you can do: + + ``` vue + + ``` + +- The dev build is served using [http-server](https://github.com/indexzero/http-server). You can edit the NPM dev script in `package.json` to add a proxy option so that ajax requests are proxied to a separate backend API. + +- For unit testing: + + - You can run the tests in multiple real browsers by installing more [karma launchers](http://karma-runner.github.io/0.13/config/browsers.html) and adjusting the `browsers` field in `karma.conf.js`. + + - You can also swap out Jasmine for other testing frameworks, e.g. use Mocha with [karma-mocha](https://github.com/karma-runner/karma-mocha). + +### Fork It And Make Your Own + +You can fork this repo to create your own boilerplate, and use it with `vue-cli`: + +``` bash +vue init username/repo my-project +``` diff --git a/parkstay/frontend/mapdemo.json b/parkstay/frontend/mapdemo.json new file mode 100644 index 0000000000..b67e291732 --- /dev/null +++ b/parkstay/frontend/mapdemo.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [115.864, -34.3734]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

The cool, misty understorey of the towering karris houses an enchanting world of mosses and ferns complete with its own waterfall.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"'Greater Beedelup' is the unofficial name for the national park created from the incorporation of former areas of state forest into Beedelup National Park": 0}, "url": "/park/greater-beedelup", "name": "Greater Beedelup", "order": [], "img2": "\"\"", "id": 24542, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table", "50": "Entry Station", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.474, -20.4253]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

More than 58,000 hectares of ocean surrounding 265 low-lying islands and islets that are fringed by coral reefs populated with colourful tropical fish.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/montebello-islands", "name": "Montebello Islands", "order": [], "img2": "\"\"", "id": 21639, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.868, -22.1587]}, "type": "Feature", "properties": {"reg": [3, 34], "desc": "

Hike through the rocky gorges of arid, rugged Cape Range in the Ningaloo Coast World Heritage Area, and camp beachside adjacent to the vibrant, colourful Ningaloo Marine Park.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/cape-range", "name": "Cape Range", "order": [], "img2": "\"\"", "id": 18627, "fac": {"43": "Toilet", "607": "Boat Ramp", "44": "Visitor Centre", "50": "Entry Station", "610": "Lookout/Deck"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [118.339, -22.5442]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

The expansive Karijini National Park offers spectacular rugged scenery, ancient geological formations, a variety of arid-land ecosystems and a range of recreational experiences.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/karijini", "name": "Karijini", "order": [], "img2": "\"\"", "id": 18630, "fac": {"43": "Toilet", "1135": "Shade Shelter", "44": "Visitor Centre", "610": "Lookout/Deck", "45": "Info Shelter", "455": "Barbecue", "460": "Picnic Table"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [113.74, -22.8618]}, "type": "Feature", "properties": {"reg": [3, 34], "desc": "

Plunge into stunning Ningaloo Marine Park with colourful coral gardens and fish just a short snorkel from shore, or dive with gentle giants such as whale sharks and manta rays.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "DIVING": "Diving", "FISHING": "Fishing", "CANOEING": "Canoeing & kayaking", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/ningaloo", "name": "Ningaloo", "order": [], "img2": "\"\"", "id": 18626, "fac": {"43": "Toilet", "607": "Boat Ramp", "610": "Lookout/Deck"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.62, -20.5595]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

The Dampier Archipelago is a chain of 42 coastal islands, islets and rocks, where divers may explore coral reefs, while other visitors swim or relax on the beach.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "DIVING": "Diving", "SNORKEL": "Snorkelling", "FISHING": "Fishing"}, "alt": [], "url": "/park/dampier-archipelago", "name": "Dampier Archipelago", "order": [], "img2": "\"\"", "id": 18631, "fac": {"45": "Info Shelter", "607": "Boat Ramp"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.728, -35.0043]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

The untouched nature, wildlife and scenic quality of the Walpole and Nornalup inlets provide a wealth of opportunities for canoeing, boating, windsurfing, fishing and other water-based activities.

\n", "img": "\"\"", "act": {"CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/walpole-and-nornalup-inlets", "name": "Walpole and Nornalup Inlets", "order": [], "img2": "\"\"", "id": 18633, "fac": {"607": "Boat Ramp"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.507, -25.6983]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Francois Peron National Park is known for its contrasting red cliffs, white beaches and blue waters, has a fascinating pastoral history, and offers a wilderness experience to four-wheel-drivers.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "FISHING": "Fishing", "SNORKEL": "Snorkelling", "CANOEING": "Canoeing & kayaking", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/francois-peron", "name": "Francois Peron", "order": [], "img2": "\"\"", "id": 18600, "fac": {"43": "Toilet", "1135": "Shade Shelter", "44": "Visitor Centre", "610": "Lookout/Deck", "455": "Barbecue", "607": "Boat Ramp", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [122.221, -33.9519]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Within 45 minutes drive of Esperance, this grand park features sweeping heathlands, rugged coastal peaks and white sandy beaches voted the best in Australia.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/cape-le-grand", "name": "Cape Le Grand", "order": [], "img2": "\"\"", "id": 18579, "fac": {"43": "Toilet", "607": "Boat Ramp", "50": "Entry Station", "455": "Barbecue"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.798, -34.9652]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Pristine forests, rugged coastline, peaceful inlets and tannin rich rivers are just a few of this parks attraction.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "SURFING": "Surfing", "FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/walpole-nornalup", "name": "Walpole Nornalup", "order": [], "img2": "\"\"", "id": 18606, "fac": {"43": "Toilet", "44": "Visitor Centre", "610": "Lookout/Deck", "455": "Barbecue", "607": "Boat Ramp", "609": "Jetty"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.027, -34.779]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

D\u2019Entrecasteaux National Park is an important conservation area of wild, pristine beauty; blessed with white beaches, rugged coastal cliffs and towering karri forests.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "DIVING": "Diving", "FISHING": "Fishing", "CANOEING": "Canoeing & kayaking", "SURFING": "Surfing", "OFF-ROAD": "4WD & adventure motorcycling", "SNORKEL": "Snorkelling"}, "alt": {"dentrecasteaux": 0}, "url": "/park/dentrecasteaux", "name": "D'Entrecasteaux", "order": [], "img2": "\"\"", "id": 18577, "fac": {"43": "Toilet", "1135": "Shade Shelter", "610": "Lookout/Deck", "45": "Info Shelter", "455": "Barbecue", "607": "Boat Ramp", "460": "Picnic Table"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [128.534, -17.4619]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

The Bungle Bungle Range, in Purnululu National Park, is one of the most striking geological landmarks in Western Australia, offering a remote wilderness experience.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"Bungle Bungles": 0}, "url": "/park/purnululu", "name": "Purnululu", "order": [], "img2": "\"\"", "id": 18588, "fac": {"43": "Toilet", "1135": "Shade Shelter", "44": "Visitor Centre"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.044, -34.1352]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

One of Western Australia\u2019s most loved and scenic holiday spots, with rugged limestone sea cliffs and windswept granite headlands dominating the coastline, interspersed by curving beaches, sheltered bays and long, rocky shorelines.

\n", "img": "\"\"", "act": {"ABSEILING": "Abseiling", "SWIMMING": "Swimming", "CAVING": "Caving", "FISHING": "Fishing", "BUSH": "Bushwalking", "SURFING": "Surfing", "SNORKEL": "Snorkelling", "CANOEING": "Canoeing & kayaking", "DIVING": "Diving"}, "alt": [], "url": "/park/leeuwin-naturaliste", "name": "Leeuwin-Naturaliste", "order": [], "img2": "\"\"", "id": 18543, "fac": {"43": "Toilet", "610": "Lookout/Deck", "455": "Barbecue", "607": "Boat Ramp", "460": "Picnic Table", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [114.476, -27.6473]}, "type": "Feature", "properties": {"reg": [3, 33], "desc": "

One of Western Australia\u2019s best known parks, with its scenic gorges though red and white banded sandstone and its soaring coastal cliffs.

\n", "img": "\"\"", "act": {"ABSEILING": "Abseiling", "BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/kalbarri", "name": "Kalbarri", "order": [], "img2": "\"\"", "id": 18607, "fac": {"43": "Toilet", "610": "Lookout/Deck", "50": "Entry Station", "455": "Barbecue"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.156, -30.5902]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Thousands of huge limestone pillars rise from the shifting yellow sands of the Pinnacles Desert, resembling a landscape from a science fiction movie.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "FISHING": "Fishing", "SURFING": "Surfing", "SNORKEL": "Snorkelling", "BUSH": "Bushwalking"}, "alt": {"Pinnacles Desert": 0}, "url": "/park/nambung", "name": "Nambung", "order": [], "img2": "\"\"", "id": 18584, "fac": {"43": "Toilet", "455": "Barbecue", "50": "Entry Station", "44": "Visitor Centre", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [121.087, -33.8317]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Featuring one of the most picturesque estuaries along WA\u2019s southern coast, Stokes Inlet National Park is a great place for fishing, camping, bushwalking and birdwatching.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/stokes", "name": "Stokes", "order": [], "img2": "\"\"", "id": 18568, "fac": {"43": "Toilet", "607": "Boat Ramp", "455": "Barbecue", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [119.629, -33.9926]}, "type": "Feature", "properties": {"reg": [2, 36, 5, 42], "desc": "

Discover a botanical wonderland renowned for its rugged and spectacular scenery.

\n

ACCESS TO FITZGERALD RIVER NATIONAL PARK FROM HOPETOUN VIA HAMERSLEY DRIVE IS CLOSED UNTIL FURTHER NOTICE.

\n

OTHER ACCESS ROADS MAY BE CLOSED WHEN WET - GO TO PARKS, TRAILS AND ROAD CLOSURES FOR THE LATEST UPDATE.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "FISHING": "Fishing", "BUSH": "Bushwalking", "CANOEING": "Canoeing & kayaking", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/fitzgerald-river", "name": "Fitzgerald River", "order": [], "img2": "\"\"", "id": 18561, "fac": {"455": "Barbecue", "43": "Toilet", "607": "Boat Ramp", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.909, -32.7975]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Less than two hours from Perth, Dryandra Woodland is one of the prime places in the South-West for viewing native wildlife.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/dryandra-woodland", "name": "Dryandra Woodland", "order": [], "img2": "\"\"", "id": 18546, "fac": {"43": "Toilet", "44": "Visitor Centre", "455": "Barbecue"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.821, -31.9807]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Matilda Bay Reserve features grassy parkland framing the waters of the Swan River, Perth City and Kings Park. It accommodates cafes, restaurants, offices, yacht clubs and has a series of universally accessible pedestrian and cycle paths. The reserve offers the ideal place to picnic on shady river banks, as well as sail, row or simply sit and watch the sun sparkling on the waters of the bay.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/matilda-bay-reserve", "name": "Matilda Bay Reserve", "order": [], "img2": "\"\"", "id": 18602, "fac": {"43": "Toilet", "607": "Boat Ramp", "609": "Jetty", "455": "Barbecue"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.955, -35.1078]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

This wild and rugged coastal park is known for its spectacular wave-carved features including the Natural Bridge, The Gap and the Blowholes.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SURFING": "Surfing", "FISHING": "Fishing"}, "alt": [], "url": "/park/torndirrup", "name": "Torndirrup", "order": [], "img2": "\"\"", "id": 18563, "fac": {"610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.009, -32.3682]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Is best known for the waterfall that cascades over a sheer granite rock\u00a0face, abounds with the scenic beauty of ancient landforms and verdant forest.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/serpentine", "name": "Serpentine", "order": [], "img2": "\"\"", "id": 18614, "fac": {"455": "Barbecue", "460": "Picnic Table", "50": "Entry Station", "43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.062, -34.4492]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

View the karri forest from the ground or from high up in the tree canopy at Gloucester National Park.

\n", "img": "\"\"", "act": {"MOUNTAIN-BIKE": "Mountain biking", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/gloucester", "name": "Gloucester", "order": [], "img2": "\"\"", "id": 18574, "fac": {"45": "Info Shelter", "43": "Toilet", "50": "Entry Station"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.71, -25.8005]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Monkey Mia Reserve offers one of Australia's best known up-close wildlife experiences.\u00a0 Here you can stand within metres of wild bottlenose dolphins which visit the shores every morning.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/monkey-mia", "name": "Monkey Mia", "order": [], "img2": "\"\"", "id": 18598, "fac": {"607": "Boat Ramp", "47": "Wildlife Hide", "44": "Visitor Centre", "50": "Entry Station"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.562, -34.7007]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Get a birds eye view of the rugged and wild Mount Frankland National Park and the Walpole Wilderness from the summit of Mount Frankland.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/mt-frankland", "name": "Mt Frankland", "order": [], "img2": "\"\"", "id": 18593, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.523, -25.9353]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Shark Bay Marine Park is known for its large marine animals, such as the famous Monkey Mia dolphins, turtles, dugongs and sharks. The park and its vast seagrass banks form an important part of the Shark Bay World Heritage Area.

\n

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/shark-bay", "name": "Shark Bay", "order": [], "img2": "\"\"", "id": 18620, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [114.948, -33.9732]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

The Ngari Cape Marine Park features underwater landscapes of breathtaking grandeur and is a great spot for all types of water-based fun.

\n", "img": "\"\"", "act": {"DIVING": "Diving", "FISHING": "Fishing"}, "alt": [], "url": "/park/ngari-capes", "name": "Ngari Capes", "order": [], "img2": "\"\"", "id": 23587, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.833, -20.5583]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

Murujuga has the distinction of being the 100th National Park declared in Western Australia. It hosts the largest concentration of rock art in the world.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/murujuga", "name": "Murujuga", "order": [], "img2": "\"\"", "id": 23471, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.955, -33.3637]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

Perfect for relaxing beside the tranquil waters of the lake and river\u00a0or for more active pursuits on a forest trail or on the water.

\n", "img": "\"\"", "act": {"ABSEILING": "Abseiling", "BUSH": "Bushwalking", "SWIMMING": "Swimming", "ROCK": "Rock climbing", "FISHING": "Fishing", "MOUNTAIN-BIKE": "Mountain biking", "CANOEING": "Canoeing & kayaking", "OFF-ROAD": "4WD & adventure motorcycling"}, "alt": [], "url": "/park/wellington", "name": "Wellington", "order": [], "img2": "\"\"", "id": 18658, "fac": {"43": "Toilet", "44": "Visitor Centre", "610": "Lookout/Deck", "45": "Info Shelter", "455": "Barbecue", "460": "Picnic Table", "609": "Jetty"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.143, -31.9414]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

This beautiful forest location offers many popular recreation sites, birdwatching opportunities, panoramic views over Lake CY O\u2019Connor and Mundaring Weir.

\n", "img": "\"\"", "act": {"MOUNTAIN-BIKE": "Mountain biking", "ABSEILING": "Abseiling", "BUSH": "Bushwalking"}, "alt": {"Mundaring": 0}, "url": "/park/beelu", "name": "Beelu", "order": [], "img2": "\"\"", "id": 18638, "fac": {"43": "Toilet", "460": "Picnic Table", "610": "Lookout/Deck"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [125.029, -16.885]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

King Leopold Ranges Conservation Park is known for its spectacular Bell Creek and Lennard gorges, peaceful campsite at Silent Grove and the privately-operated Mount Hart Homestead.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/king-leopold-ranges", "name": "King Leopold Ranges", "order": [], "img2": "\"\"", "id": 18660, "fac": {"43": "Toilet", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.692, -32.3053]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Explore a stunning natural island and its penguin inhabitants just 700 metres offshore from Mersey Point in Rockingham.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/penguin-island", "name": "Penguin Island", "order": [], "img2": "\"\"", "id": 18646, "fac": {"43": "Toilet", "44": "Visitor Centre"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.148, -21.291]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

Millstream is an oasis in the desert nestled within the chocolate brown rocks of the Chichester Range dotted with spinifex and snappy gums.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/millstream-chichester", "name": "Millstream Chichester", "order": [], "img2": "\"\"", "id": 18629, "fac": {"43": "Toilet", "460": "Picnic Table", "1135": "Shade Shelter", "44": "Visitor Centre", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.683, -31.5442]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

With historic buildings nestled on the shores of a lake amid coastal woodland and limestone caves, Yanchep offers something for everyone.

\n", "img": "\"\"", "act": {"CAVING": "Caving", "BUSH": "Bushwalking"}, "alt": {"Yanjet": 0}, "url": "/park/yanchep", "name": "Yanchep", "order": [], "img2": "\"\"", "id": 18645, "fac": {"455": "Barbecue", "460": "Picnic Table", "50": "Entry Station", "44": "Visitor Centre", "43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.807, -31.9252]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Herdsman Lake provides a haven for humans and wildlife alike, with shared paths encircling the lake offering the visitor a scenic and peaceful break from the surrounding suburbia.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/herdsman-lake", "name": "Herdsman Lake", "order": [], "img2": "\"\"", "id": 18654, "fac": {"45": "Info Shelter", "44": "Visitor Centre"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.046, -30.0894]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

One of the most important flora conservation reseves in Western Australia, Lesueur National Park erupts into colour in late winter and spring as the park\u2019s diverse flora comes out in flower, making it a paradise for wildflower enthusiasts.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/lesueur", "name": "Lesueur", "order": [], "img2": "\"\"", "id": 18623, "fac": {"43": "Toilet", "460": "Picnic Table", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.765, -32.3735]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Prehistoric species, rare plants, seasonal lakes, underwater snorkel adventures and land yacht sailing \u2013 there is something for everyone at Rockingham Lakes Regional Park.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/rockingham-lakes", "name": "Rockingham Lakes", "order": [], "img2": "\"\"", "id": 18644, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [125.706, -18.0758]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Spectacular gorge famed for its sheer white and grey walls, abundant wildlife and awesome boat tours.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": {"Darngku": 0}, "url": "/park/geikie-gorge", "name": "Geikie Gorge", "order": [], "img2": "\"\"", "id": 18663, "fac": {"43": "Toilet", "607": "Boat Ramp", "44": "Visitor Centre", "455": "Barbecue"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [124.986, -17.4215]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Windjana Gorge National Park is one of the Kimberley\u2019s most stunning gorges, with water-streaked walls that rise majestically to heights of 100 metres.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/windjana-gorge", "name": "Windjana Gorge", "order": [], "img2": "\"\"", "id": 18665, "fac": {"43": "Toilet", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [125.144, -17.6107]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Tunnel Creek flows through a water worn tunnel in the limestone of the Napier Range, part of the 350 million-year-old Devonian Reef system.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/tunnel-creek", "name": "Tunnel Creek", "order": [], "img2": "\"\"", "id": 18666, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.899, -31.7608]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Location of Pinjar Motorcycle Area

\n", "img": "", "act": [], "alt": [], "url": "/park/gnangara", "name": "Gnangara", "order": [], "img2": "", "id": 18650, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.778, -30.3652]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "


The proposed Credo Conservation Park is a former pastoral lease that was established in 1906\u201307 by the Halford family where it first carried sheep. The family sold the lease in the mid-1980s to the Funstons who in turn sold it to the Government in 2007. Credo was purchased by the Government as it is a representative conservation area and an important water catchment area for Rowles Lagoon.\u00a0

\n

Visitors can explore the 212,126-hectare proposed reserve in a day trip or camp overnight at the campground. \u00a0The area offers nature-based recreation and tourism opportunities such as camping, sightseeing, historical mining towns, bushwalking, wildflower viewing, bird-watching and photography. The relatively remote environment has outstanding scenic landscapes that includes mature stands of Salmon Gum woodland, breakaways and Greenstone hills.

\n

Credo encompasses the Clear and Muddy Nature Reserve and Rowles Lagoon Conservation Park, the largest freshwater lake in the Coolgardie bioregion. The lagoon is a significant area for local Indigenous people who worked on the station and whose ancestors camped at Rowles. Visitors are welcome to go yabbying in one of the many dams on Credo.\u00a0\u00a0

\n

There are several active mining and exploration operations on the station, so care should be taken around unsecured shafts and dangerous excavations. Mining lease rights still apply to many of these sites on the reserve, so nothing should be disturbed and access permission should be obtained from the lease holders.\u00a0

\n

Credo is also host to an exciting science partnership between Parks and Wildlife and CSIRO with a new multi-purpose field study centre for educators and scientists working on environmental research programs and the Terrestrial Ecosystems Research Network (TERN) supersite.

\n

Getting there

\n

The homestead is 75km north-west of Coolgardie along Coolgardie North Road. Most of the tracks on the former pastoral property are for four-wheel-drive vehicles only, however access to Rowles Lagoon is open to all vehicles during dry soil conditions.

\n

\u00a0

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/credo", "name": "Credo", "order": [], "img2": "\"\"", "id": 18634, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.554, -28.9628]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Treasured for its annual display of everlastings, Coalseam Conservation Park is carpeted in pink, white and yellow blooms after winter rains.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/coalseam", "name": "Coalseam", "order": [], "img2": "\"\"", "id": 18610, "fac": {"45": "Info Shelter", "455": "Barbecue", "43": "Toilet", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.007, -34.3914]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Big Brook is a young forest, regenerated after logging in the 1920\u2019s and forms one of the most picturesque places in karri country with views across the dam to the forest.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/big-brook", "name": "Big Brook", "order": [], "img2": "\"\"", "id": 18576, "fac": {"43": "Toilet", "1135": "Shade Shelter", "47": "Wildlife Hide", "607": "Boat Ramp", "460": "Picnic Table", "50": "Entry Station"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [118.155, -34.9654]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

Two Peoples Bay Nature Reserve boasts unspoilt coastal scenery and offers a wildly beautiful haven for some of the State\u2019s most threatened animals.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "DIVING": "Diving", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/two-peoples-bay", "name": "Two Peoples Bay", "order": [], "img2": "\"\"", "id": 18565, "fac": {"43": "Toilet", "44": "Visitor Centre", "610": "Lookout/Deck", "45": "Info Shelter", "455": "Barbecue", "607": "Boat Ramp"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.899, -34.3916]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

The brooding beauty of the mountain landscape, its stunning and diverse wildflowers and the challenge of climbing Bluff Knoll have long drawn bushwalkers and climbers to the Stirling Range National Park.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/stirling-range", "name": "Stirling Range", "order": [], "img2": "\"\"", "id": 18560, "fac": {"455": "Barbecue", "43": "Toilet", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.935, -34.5039]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Warren National Park showcases the best of the region\u2019s old-growth karri forest.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/warren", "name": "Warren", "order": [], "img2": "\"\"", "id": 18572, "fac": {"43": "Toilet", "460": "Picnic Table", "50": "Entry Station", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.142, -32.8718]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Forest-cloaked valleys and meandering waterways make Lane Poole an enchanting place to visit and its close proximity to Perth ensures its popularity.

\n", "img": "\"\"", "act": {"HORSE": "Horse riding", "SWIMMING": "Swimming", "FISHING": "Fishing", "BUSH": "Bushwalking", "MOUNTAIN-BIKE": "Mountain biking", "CANOEING": "Canoeing & kayaking", "OFF-ROAD": "4WD & adventure motorcycling"}, "alt": {"Nanga, Baden Powell, Dwellingup, Murray Valley, Murray River": 0}, "url": "/park/lane-poole-reserve", "name": "Lane Poole Reserve", "order": [], "img2": "\"\"", "id": 18619, "fac": {"608": "Bridge", "455": "Barbecue", "460": "Picnic Table", "43": "Toilet"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.374, -34.7108]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

This once thriving mill town is your base for discovering the riches of Shannon National Park in the Walpole Wilderness.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/shannon", "name": "Shannon", "order": [], "img2": "\"\"", "id": 18573, "fac": {"45": "Info Shelter", "455": "Barbecue", "47": "Wildlife Hide", "43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [117.891, -34.6843]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

The massive ancient granite domes of Porongurup National Park rise 670 metres, giving exhilarating views of the landscape, especially from the Granite Skywalk suspended from Castle Rock.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/porongurup", "name": "Porongurup", "order": [], "img2": "\"\"", "id": 18562, "fac": {"455": "Barbecue", "43": "Toilet", "50": "Entry Station", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.252, -35.0153]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

William Bay National Park is characterised by turquoise green waters, white sandy beaches and towering granite rocks.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/william-bay", "name": "William Bay", "order": [], "img2": "\"\"", "id": 18592, "fac": {"43": "Toilet", "607": "Boat Ramp", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [125.705, -14.8305]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Mitchell River National Park lies in a remote part of the Kimberley and contains majestic waterfalls, Aboriginal rock art and sites of cultural significance to the Wunambal people.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/mitchell-river", "name": "Mitchell River", "order": [], "img2": "\"\"", "id": 18590, "fac": {"43": "Toilet", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [123.369, -33.519]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Bushfire information: go to alerts for the latest infromation on bushfires affecting Cape Arid National Park and Nuytsland Nature Reserve.

\n

Cape Arid National Park is a large and exceptionally scenic park best known for its stunningly beautiful beaches, clear blue seas and rocky headlands.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "BUSH": "Bushwalking", "SNORKEL": "Snorkelling", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing"}, "alt": [], "url": "/park/cape-arid", "name": "Cape Arid", "order": [], "img2": "\"\"", "id": 18580, "fac": {"43": "Toilet", "460": "Picnic Table", "50": "Entry Station"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.001, -30.2897]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Picturesque submerged reefs and extensive shallow lagoons host a huge variety of marine plants and animals and are a diver\u2019s delight.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "SNORKEL": "Snorkelling", "FISHING": "Fishing", "DIVING": "Diving"}, "alt": [], "url": "/park/jurien-bay", "name": "Jurien Bay", "order": [], "img2": "\"\"", "id": 18583, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.082, -31.8823]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

John Forrest has long been favoured as a site for a day-trip from Perth, with its variety of plant communities and wildlife and being a starting point for many walk trails. There are outstanding views from the lookout point on the scenic drive and a wide variety of attractions and facilities that make it a popular venue for families and groups.

\n", "img": "\"\"", "act": {"HORSE": "Horse riding", "BUSH": "Bushwalking"}, "alt": {"John Forrest National Park, Swan View Railway Tunnel": 0}, "url": "/park/john-forrest", "name": "John Forrest", "order": [], "img2": "\"\"", "id": 18613, "fac": {"43": "Toilet", "460": "Picnic Table", "50": "Entry Station", "455": "Barbecue"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.797, -31.7793]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Spend the day relaxing in Yellagonga Regional Park, discovering the fascinating stories of the area\u2019s early settlers or just enjoying the tranquillity of this piece of nature in the suburbs of Perth.\u00a0\u00a0

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/yellagonga", "name": "Yellagonga", "order": [], "img2": "\"\"", "id": 18603, "fac": {"610": "Lookout/Deck"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.065, -31.7205]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Blessed with rich Indigenous mythology, tree-filled valleys and the cooling waters of the Swan River, Walyunga National Park is the perfect day trip from Perth.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/walyunga", "name": "Walyunga", "order": [], "img2": "\"\"", "id": 18611, "fac": {"43": "Toilet", "460": "Picnic Table", "455": "Barbecue"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.712, -32.3126]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Only a short drive from Perth, Shoalwater Islands Marine Park is a place where penguins, sea lions, dolphins, rocky reefs, seagrass and shipwrecks converge.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "DIVING": "Diving", "FISHING": "Fishing", "CANOEING": "Canoeing & kayaking", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/shoalwater-islands", "name": "Shoalwater Islands", "order": [], "img2": "\"\"", "id": 18605, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [121.503, -29.9818]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Goongarrie National Park lies on the edge of the vast mulga woodlands of the Murchison region and offers a remote outback experience to those with an adventurous spirit.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/goongarrie", "name": "Goongarrie", "order": [], "img2": "\"\"", "id": 18553, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [128.772, -15.7745]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Mirima National Park has steep, broken walls of colourful layered rocks that come alive as they reflect the tones of changing light.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"Hidden Valley": 0}, "url": "/park/mirima", "name": "Mirima", "order": [], "img2": "\"\"", "id": 18587, "fac": {"43": "Toilet", "50": "Entry Station"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.754, -26.2018]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Countless tiny white shells have formed the amazing Shell Beach, which stretches for 60 kilometres. Some deposits are as much as ten metres deep.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming"}, "alt": [], "url": "/park/shell-beach", "name": "Shell Beach", "order": [], "img2": "\"\"", "id": 18597, "fac": {"43": "Toilet"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [121.456, -26.1337]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Lorna Glen homestead is an ideal camping location

\n", "img": "", "act": [], "alt": [], "url": "/park/lorna-glen", "name": "Lorna Glen", "order": [], "img2": "", "id": 18555, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.84, -31.9904]}, "type": "Feature", "properties": {"reg": [1], "desc": "

The Swan Canning Riverpark and the iconic rivers at its heart are the centrepiece of Perth. Blessed with diverse and resilient ecosystems, the Swan and Canning rivers are a recreational playground and a source of vibrant commercial and tourism activity.

\n

The Swan and Canning rivers are truly the heart of Perth, ensuring it is one of the world\u2019s most beautiful cities.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "DIVING": "Diving", "FISHING": "Fishing"}, "alt": [], "url": "/park/swan-canning-riverpark", "name": "Swan Canning Riverpark", "order": [], "img2": "\"\"", "id": 24928, "fac": {"43": "Toilet", "607": "Boat Ramp", "460": "Picnic Table", "609": "Jetty", "455": "Barbecue"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.055, -31.9027]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

The Goat Farm is a totally designated mountain-bike park catering for all kinds of disciplines and skill levels.

\n", "img": "", "act": {"MOUNTAIN-BIKE": "Mountain biking"}, "alt": [], "url": "/park/goat-farm", "name": "The Goat Farm", "order": [], "img2": "", "id": 24708, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.995, -33.8014]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Western Australia's largest arboretum showcases more than 60 hectares of indigenous and exotic trees in a spectacular setting.

\n", "img": "", "act": [], "alt": [], "url": "/park/golden-valley-tree-park", "name": "Golden Valley Tree Park", "order": [], "img2": "", "id": 24475, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.089, -33.0023]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

Home to the old Hoffman Mill

\n", "img": "", "act": [], "alt": [], "url": "/park/hoffman", "name": "Hoffman", "order": [], "img2": "", "id": 24473, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.122, -34.3192]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

The huge Diamond Tree was one of the network of fire lookouts of the southern forests

\n", "img": "", "act": [], "alt": [], "url": "/park/diamond", "name": "Diamond", "order": [], "img2": "", "id": 24469, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.951, -34.5648]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Gateway to the Donnelly River

\n", "img": "", "act": {"BUSH": "Bushwalking"}, "alt": {"Cleave": 0}, "url": "/park/greater-hawke", "name": "Greater Hawke", "order": [], "img2": "", "id": 24467, "fac": {"43": "Toilet", "1135": "Shade Shelter"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.913, -33.8582]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/powlalup", "name": "Powlalup", "order": [], "img2": "", "id": 24466, "fac": [], "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.942, -34.2152]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

This park features stunning karri forest, the Donnelly River and the historic One Tree Bridge.

\n", "img": "", "act": {"FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": {"Reserve 37237": 0}, "url": "/park/one-tree-bridge", "name": "One Tree Bridge", "order": [], "img2": "", "id": 24464, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.094, -30.486]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Information on this group of reserves coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/northern-yilgarn-conservation-reserves", "name": "Northern Yilgarn Conservation Reserves", "order": [], "img2": "", "id": 24462, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.64, -34.1409]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Home of Perup - Nature's Guesthouse

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/tone-perup", "name": "Tone-Perup", "order": [], "img2": "\"\"", "id": 24461, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.514, -31.4212]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/yorkrakine-rock", "name": "Yorkrakine Rock", "order": [], "img2": "", "id": 24460, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [118.215, -31.5729]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

More information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/totadgin", "name": "Totadgin", "order": [], "img2": "", "id": 24459, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [119.751, -33.2301]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/pallarup", "name": "Pallarup", "order": [], "img2": "", "id": 24458, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.133, -34.099]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

More information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/forest-grove", "name": "Forest Grove", "order": [], "img2": "", "id": 24457, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.615, -33.342]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/dumbleyung-lake", "name": "Dumbleyung Lake", "order": [], "img2": "", "id": 24456, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.984, -33.9847]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information about this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/dalgarup", "name": "Dalgarup ", "order": [], "img2": "", "id": 24455, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.092, -30.2339]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/drovers-cave", "name": "Drovers Cave", "order": [], "img2": "", "id": 24454, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.475, -34.0135]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/wiltshire-butler", "name": "Wiltshire-Butler", "order": [], "img2": "", "id": 24453, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.482, -33.7841]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/whicher", "name": "Whicher", "order": [], "img2": "", "id": 24452, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.696, -34.2315]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/hilliger", "name": "Hilliger", "order": [], "img2": "", "id": 24451, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.295, -33.8984]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Information on this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/rapids", "name": "Rapids", "order": [], "img2": "", "id": 24450, "fac": [], "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.122, -33.5873]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

Part of the old-growth forests south of Collie, Preston National Park is split into two areas of about 6 000 hectares each.\u00a0

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/preston", "name": "Preston", "order": [], "img2": "\"\"", "id": 23918, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.008, -34.7074]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

A wild and remote wilderness

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/mt-roe", "name": "Mt Roe", "order": [], "img2": "\"\"", "id": 23599, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.357, -34.0711]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

The Blackwood is the largest river in Australia's South West

\n", "img": "", "act": [], "alt": [], "url": "/park/blackwood-river", "name": "Blackwood River", "order": [], "img2": "", "id": 23472, "fac": [], "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [124.744, -32.8392]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Bushfire information: go to\u00a0alerts\u00a0for the latest infromation on bushfires affecting\u00a0Nuytsland Nature Reserve\u00a0and\u00a0Cape Arid National Park.

\n

Nuytsland Nature Reserve contains the 190km long and 80m high Baxter Cliffs - one of Australia's great scenic features and possibly the longest unbroken cliffs in the world.

\n", "img": "", "act": {"FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/nuytsland", "name": "Nuytsland", "order": [], "img2": "", "id": 21644, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.827, -31.9889]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Three biologically important areas of Perth\u2019s beautiful Swan River collectively make up the Swan Estuary Marine Park.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "DIVING": "Diving", "SNORKEL": "Snorkelling", "FISHING": "Fishing"}, "alt": [], "url": "/park/swan-estuary", "name": "Swan Estuary", "order": [], "img2": "\"\"", "id": 21640, "fac": {"607": "Boat Ramp"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.612, -34.8536]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Bushwalks, canoeing, waterfalls, picnic areas and forest art \u2013 Mount Frankland South National Park truly has something for everyone.

\n", "img": "\"\"", "act": {"CANOEING": "Canoeing & kayaking", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/mt-frankland-south", "name": "Mt Frankland South", "order": [], "img2": "\"\"", "id": 18641, "fac": {"45": "Info Shelter", "43": "Toilet", "455": "Barbecue"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.762, -32.1257]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Woodman Point marks the northern most part of Cockburn Sound with a unique coastline and the most extensive stands of Rottnest cypress found anywhere on the mainland.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "FISHING": "Fishing", "SURFING": "Surfing", "SNORKEL": "Snorkelling", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/woodman-point", "name": "Woodman Point", "order": [], "img2": "\"\"", "id": 18651, "fac": {"43": "Toilet", "455": "Barbecue"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.96, -33.0042]}, "type": "Feature", "properties": {"reg": [], "desc": "

A great lakeside location in the forest just a short drive from Perth and other major centres in the south-west.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "MOUNTAIN-BIKE": "Mountain biking", "FISHING": "Fishing"}, "alt": {"Logue Brook Dam": 0, "Lake Brockman": 0}, "url": "/park/logue-brook", "name": "Logue Brook", "order": [], "img2": "\"\"", "id": 18657, "fac": {"455": "Barbecue", "607": "Boat Ramp", "460": "Picnic Table", "1135": "Shade Shelter", "43": "Toilet"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.689, -33.2184]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

Leschenault Peninsula Conservation Park\u00a0is located on a thin peninsula, bounded on one side by the Indian Ocean and the Leschenault Estuary on the other.

\n", "img": "\"\"", "act": {"OFF-ROAD": "4WD & adventure motorcycling", "FISHING": "Fishing", "SNORKEL": "Snorkelling", "SWIMMING": "Swimming", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/leschenault-peninsula", "name": "Leschenault Peninsula", "order": [], "img2": "\"\"", "id": 18656, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table", "455": "Barbecue"}, "dog": 1, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.674, -32.8159]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Lake Hayward and the Heathlands Walk Trail are\u00a0closed\u00a0until further notice due to bushfire damage.

\n

Yalgorup National Park is known for its elongated lakes, beautiful tuart and peppermint woodlands and, above all, for the microscopic communities that reside in Lake Clifton and form thrombolites.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/yalgorup", "name": "Yalgorup", "order": [], "img2": "\"\"", "id": 18652, "fac": {"43": "Toilet", "460": "Picnic Table", "47": "Wildlife Hide", "610": "Lookout/Deck"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.922, -32.1399]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

For those seeking a unique nature experience Jandakot Regional Park offers a range of intriguing landscapes including banksia woodland, wetlands and rural remnants.

\n", "img": "\"\"", "act": {"HORSE": "Horse riding", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/jandakot", "name": "Jandakot", "order": [], "img2": "\"\"", "id": 18655, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [118.933, -17.5782]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

The Rowley Shoals Marine Park and nearby Mermaid Reef Commonwealth Marine Reserve are effectively \u2018aquariums\u2019 in the middle of the ocean with some of the best diving in Australia.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "DIVING": "Diving", "SNORKEL": "Snorkelling", "FISHING": "Fishing"}, "alt": [], "url": "/park/rowley-shoals", "name": "Rowley Shoals", "order": [], "img2": "\"\"", "id": 18664, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [113.052, -25.826]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Dirk Hartog Island National Park, in the Shark Bay World Heritage Area, has immense historical significance and offers great fishing, steep cliffs and secluded beaches.

\n", "img": "\"\"", "act": {"DIVING": "Diving", "SNORKEL": "Snorkelling", "FISHING": "Fishing"}, "alt": [], "url": "/park/dirk-hartog-island", "name": "Dirk Hartog Island", "order": [], "img2": "\"\"", "id": 18667, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [117.195, -34.8202]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

The summit of Mount Lindesay offers dramatic views of Denmark\u2019s coastline, farmland and sweeping vistas of the Walpole Wilderness.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/mt-lindesay", "name": "Mt Lindesay", "order": [], "img2": "\"\"", "id": 18642, "fac": {"45": "Info Shelter"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.559, -31.9229]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Situated between Great Southern Highway and Brookton Highway this 44,000-hectare national park features areas of wandoo forest and granite outcrops.

\n", "img": "", "act": [], "alt": [], "url": "/park/wandoo", "name": "Wandoo", "order": [], "img2": "", "id": 18622, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.928, -32.0217]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Experience the tranquillity of the Canning River Regional Park, an oasis of calm in the middle of urban Perth.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/canning-river", "name": "Canning River", "order": [], "img2": "\"\"", "id": 18601, "fac": {"455": "Barbecue", "43": "Toilet", "460": "Picnic Table", "610": "Lookout/Deck"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.612, -35.1056]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

West Cape Howe National Park has a coastline dominated by rocky headlands, sheer cliffs and sandy beaches and is a popular fishing destination.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking", "HANG": "Hang Gliding/Paragliding", "FISHING": "Fishing"}, "alt": [], "url": "/park/west-cape-howe", "name": "West Cape Howe", "order": [], "img2": "\"\"", "id": 18558, "fac": {"455": "Barbecue", "43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [122.016, -33.9625]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Get nearer to nature on this extraordinary island where dramatic rocky headlands, sheltered bays and tall eucalypt woodlands provide a haven for an abundance of wildlife.

\n

\n", "img": "", "act": {"BUSH": "Bushwalking", "SWIMMING": "Swimming", "DIVING": "Diving", "FISHING": "Fishing", "CANOEING": "Canoeing & kayaking", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/woody-island", "name": "Woody Island", "order": [], "img2": "", "id": 18582, "fac": {"607": "Boat Ramp", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.221, -31.6103]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

From summer to winter, from north to south, and from high outcrops to deep river and stream valleys, the forests of Avon Valley National Park are constantly changing.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/avon-valley", "name": "Avon Valley", "order": [], "img2": "\"\"", "id": 18612, "fac": {"43": "Toilet", "460": "Picnic Table"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [128.287, -15.6706]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

A wealth of tropical birds and lurking crocodiles can be viewed from a bird hide and boardwalk over the waterlily studded Marglu Billabong.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/parry-lagoons", "name": "Parry Lagoons", "order": [], "img2": "\"\"", "id": 18589, "fac": {"47": "Wildlife Hide"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.687, -33.9431]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

St John Brook Conservation Park is best known for the gently flowing St John Brook and the area\u2019s fascinating timber milling history.

\n", "img": "", "act": {"SWIMMING": "Swimming", "MOUNTAIN-BIKE": "Mountain biking", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/st-john-brook", "name": "St John Brook", "order": [], "img2": "", "id": 18545, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table", "609": "Jetty"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [127.796, -19.1723]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

The Wolfe Creek meteorite crater is the second largest crater in the world from which fragments of a meteorite have been collected. The crater is 880 metres across and almost circular.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/wolfe-creek-crater", "name": "Wolfe Creek Crater", "order": [], "img2": "\"\"", "id": 18586, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.733, -31.8272]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

The clear shallow lagoons, reefs and small islands of Marmion Marine Park are a diver\u2019s paradise, forming ledges, caves and swimthroughs inhabited by a wonderful array of fish and invertebrate species.

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "DIVING": "Diving", "SNORKEL": "Snorkelling", "FISHING": "Fishing"}, "alt": [], "url": "/park/marmion", "name": "Marmion", "order": [], "img2": "\"\"", "id": 18604, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.85, -24.3348]}, "type": "Feature", "properties": {"reg": [2, 37], "desc": "

This huge massif is a beacon to outback travellers and bushwalkers within an extensive surrounding sandplain of desert shrubland plants and animals

\n

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "BUSH": "Bushwalking"}, "alt": {"Burringurrah": 0}, "url": "/park/mt-augustus", "name": "Mt Augustus", "order": [], "img2": "\"\"", "id": 18609, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.458, -31.1422]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Goldfields Woodlands National Park has attractive low-key campgrounds, spring wildflower displays, is traversed by the four-wheel-drive Holland Track and is a stop on the Golden Pipeline Heritage Trail.

\n", "img": "\"\"", "act": {"OFF-ROAD": "4WD & adventure motorcycling", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/goldfields-woodlands", "name": "Goldfields Woodlands", "order": [], "img2": "\"\"", "id": 18552, "fac": {"43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [121.108, -32.9086]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Peak Charles, an ancient granite peak, and its companion, Peak Eleanora, give sweeping views over the dry sandplain heaths and salt lake systems of the surrounding countryside.

\n

\n", "img": "\"\"", "act": {"ABSEILING": "Abseiling", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/peak-charles", "name": "Peak Charles", "order": [], "img2": "\"\"", "id": 18581, "fac": {"43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.078, -31.9594]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

An easy day trip from Perth,\u00a0Kalamunda National Park\u00a0is one of the less frequented large parks of the Darling Scarp. The 375-hectare national park is notable for its diversity of native plants.

\n", "img": "\"\"", "act": {"HORSE": "Horse riding", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/kalamunda", "name": "Kalamunda", "order": [], "img2": "\"\"", "id": 18618, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.679, -34.4935]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Lake Muir is part of the Muir-Byenup wetland system which is recognised as a Wetland of International Importance.

\n", "img": "", "act": [], "alt": [], "url": "/park/lake-muir", "name": "Lake Muir", "order": [], "img2": "", "id": 18578, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table", "610": "Lookout/Deck"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [121.449, -30.7441]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Kalgoorlie Arboretum boasts a wide variety of native wildflowers, walk trails for all ages, picnic tables under the shade of river gums and a small dam that attracts waterbirds.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/kalgoorlie-arboretum", "name": "Kalgoorlie Arboretum", "order": [], "img2": "\"\"", "id": 18550, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [114.102, -26.2141]}, "type": "Feature", "properties": {"reg": [3, 35], "desc": "

Hamelin Pool boasts the most diverse and abundant examples of living marine stromatolites, or \u2018living fossils\u2019, in the world, monuments to life on Earth over 3500 million years ago.

\n

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/hamelin-pool", "name": "Hamelin Pool", "order": [], "img2": "\"\"", "id": 18599, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.686, -34.6645]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Mount Frankland - North National Park adjoins Mount Frankland National Park to the south, Mount Roe National Park to the east and Lake Muir National Park to the north.

\n", "img": "", "act": [], "alt": [], "url": "/park/mt-frankland-north", "name": "Mt Frankland North", "order": [], "img2": "", "id": 23600, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.694, -32.5659]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "
We recognise and acknowledge Bunuba people as the traditional custodians of Windjana Gorge National Park. - See more at: http://decpvs.dhmedia.com.au/park/windjana-gorge#sthash.jaRrxmwn.dpuf
\n", "img": "", "act": [], "alt": [], "url": "/park/len-howard", "name": "Len Howard", "order": [], "img2": "", "id": 21638, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.26, -34.6901]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Wildflowers, waterfalls and history - what more could you want?\u00a0

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"Boorara Gardner National Park (unofficial name)": 0}, "url": "/park/boorara-gardner", "name": "Boorara - Gardner", "order": [], "img2": "\"\"", "id": 21637, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.84, -32.2173]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

Beeliar Regional Park is an internationally recognised range of coastal wetlands containing an array of nature appreciation possibilities within the suburban confines of Perth.

\n", "img": "\"\"", "act": {"FISHING": "Fishing", "BUSH": "Bushwalking"}, "alt": [], "url": "/park/beeliar", "name": "Beeliar", "order": [], "img2": "\"\"", "id": 18653, "fac": {"45": "Info Shelter", "43": "Toilet", "460": "Picnic Table", "1135": "Shade Shelter", "610": "Lookout/Deck"}, "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.663, -32.1218]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

This picturesque limestone island, which is only a short boat ride from Fremantle, is a popular destination for commercial boat tours, which bring visitors to view the local sea lions.

\n", "img": "\"\"", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking", "SNORKEL": "Snorkelling", "DIVING": "Diving"}, "alt": [], "url": "/park/carnac-island", "name": "Carnac Island", "order": [], "img2": "\"\"", "id": 18648, "fac": {"607": "Boat Ramp"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.034, -31.9961]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Mundy Regional park stretches from the coastal plain to the top of the Darling Scarp to Lesmurdie Falls. Sections of the park provide a virtually unbroken belt of scarp woodland to explore

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/mundy", "name": "Mundy", "order": [], "img2": "\"\"", "id": 18636, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [122.345, -17.9752]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

From September through to April, the Broome Bird Observatory in Roebuck Bay is one of the best places in the world to view vast numbers of migratory shorebirds.

\n

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/yawuru", "name": "Yawuru", "order": [], "img2": "\"\"", "id": 18662, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.131, -31.8832]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

One of the Regional Parks of the Darling Range

\n", "img": "", "act": [], "alt": [], "url": "/park/wooroloo", "name": "Wooroloo", "order": [], "img2": "", "id": 18635, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [122.737, -22.3325]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

WA\u2019s largest and most remote national park is located in the Pilbara amid lands between the Great Sandy Desert and the Little Sandy Desert.

\n", "img": "", "act": [], "alt": {"Rudall River": 0}, "url": "/park/karlamilyi", "name": "Karlamilyi", "order": [], "img2": "", "id": 18628, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.052, -32.0222]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Located in the picturesque Perth hills, Korung National Park is a bushwalkers\u2019 paradise.

\n", "img": "", "act": [], "alt": {"formerly known as Pickering Brook National Park": 0}, "url": "/park/korung", "name": "Korung", "order": [], "img2": "", "id": 18639, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.15, -32.1291]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Impotrant water catchment for the Canning Reservoir

\n", "img": "", "act": [], "alt": {"formerly known as Canning National Park": 0}, "url": "/park/midgegooroo", "name": "Midgegooroo", "order": [], "img2": "", "id": 18640, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.066, -32.1496]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

One of the Regional Parks of the Darling Range

\n", "img": "", "act": [], "alt": [], "url": "/park/wungong", "name": "Wungong", "order": [], "img2": "", "id": 18643, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.447, -33.6214]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

The narrow strip of tuart (Eucalyptus gomphocephala) forest that links Capel and Busselton is one of the special places of the South-West.

\n", "img": "", "act": [], "alt": [], "url": "/park/tuart-forest", "name": "Tuart Forest", "order": [], "img2": "", "id": 18544, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.063, -31.9107]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Located on the slopes of Greenmount Hill overlooking Perth, this park offers great opportunities for bushwalking, wildlife observation, sightseeing and photography.

\n", "img": "\"\"", "act": {"ABSEILING": "Abseiling", "ROCK": "Rock climbing", "BUSH": "Bushwalking"}, "alt": {"Goat Farm Mountain Bike Park": 0}, "url": "/park/greenmount", "name": "Greenmount", "order": [], "img2": "\"\"", "id": 18615, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.054, -24.5779]}, "type": "Feature", "properties": {"reg": [2, 37], "desc": "

The extensive, elevated plateau of the Kennedy Range in WA\u2019s Midwest supports a diversity of arid wildlife communities and attracts bushwalkers and campers.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/kennedy-range", "name": "Kennedy Range", "order": [], "img2": "\"\"", "id": 18608, "fac": {"455": "Barbecue", "460": "Picnic Table", "43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [118.036, -35.0034]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

South coast beauty spot with sweeping views, plus great swimming, fishing and bushwalking.

\n", "img": "\"\"", "act": {"HORSE": "Horse riding", "SWIMMING": "Swimming", "FISHING": "Fishing", "BUSH": "Bushwalking", "DIVING": "Diving", "SURFING": "Surfing", "SNORKEL": "Snorkelling"}, "alt": [], "url": "/park/gull-rock", "name": "Gull Rock", "order": [], "img2": "\"\"", "id": 18564, "fac": [], "dog": 1, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [119.521, -27.5874]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Lake Mason, a remote former pastoral station that is now a proposed conservation park, is now used for four-wheel driving, camping, fossicking and birdwatching.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/lake-mason", "name": "Lake Mason", "order": [], "img2": "\"\"", "id": 18551, "fac": {"455": "Barbecue"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.995, -34.5146]}, "type": "Feature", "properties": {"reg": [], "desc": "

Drive through this small park on your way between Pemberton and Northcliffe.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/brockman", "name": "Brockman", "order": [], "img2": "\"\"", "id": 18569, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.047, -31.9411]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Magnificent views over the Swan Coastal Plain.

\n", "img": "", "act": [], "alt": [], "url": "/park/gooseberry-hill", "name": "Gooseberry Hill", "order": [], "img2": "", "id": 18616, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.115, -33.9595]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Located just 400 metres from Margaret River Township, Bramley National Park is a gateway to breathtaking forest experiences and world class walking trails.

\n", "img": "", "act": [], "alt": [], "url": "/park/bramley", "name": "Bramley", "order": [], "img2": "", "id": 18541, "fac": {"43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [120.856, -30.4339]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Rowles Lagoon is a scenic camping spot and haven for waterbirds in the arid Goldfields Region.

\n", "img": "", "act": {"SWIMMING": "Swimming", "CANOEING": "Canoeing & kayaking"}, "alt": [], "url": "/park/rowles-lagoon", "name": "Rowles Lagoon", "order": [], "img2": "", "id": 18554, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [116.302, -32.1099]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Includes the popular recreation site of Mount Dale.

\n", "img": "", "act": [], "alt": [], "url": "/park/helena", "name": "Helena", "order": [], "img2": "", "id": 18596, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [126.721, -15.0469]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Drysdale River National Park features open woodlands, the broad waters of the Drysdale River, pools, creeks, rugged cliffs and gorges and major waterfalls at Morgan Falls and Solea Falls.

\n

\n", "img": "", "act": [], "alt": [], "url": "/park/drysdale-river", "name": "Drysdale River", "order": [], "img2": "", "id": 18591, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.165, -31.2333]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

Visitors passing through Boorabbin National Park en route between Perth and Kalgoorlie can marvel at the large and diverse eucalypt woodlands that defy the arid climate in which they thrive.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/boorabbin", "name": "Boorabbin", "order": [], "img2": "\"\"", "id": 18547, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [121.197, -31.3886]}, "type": "Feature", "properties": {"reg": [2, 38], "desc": "

A popular day trip from Kalgoorlie or Coolgardie, this great picnic spot has an impressive backdrop of a large granite outcrop surrounded by regrowth woodland.

\n", "img": "", "act": [], "alt": [], "url": "/park/burra", "name": "Burra", "order": [], "img2": "", "id": 18548, "fac": {"43": "Toilet"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [118.38, -34.8943]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

The protected inlet of the Waychinicup River is popular for fishing, canoeing and swimming, and is extremely picturesque, with polished granite rocks tumbled along both sides.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/waychinicup", "name": "Waychinicup", "order": [], "img2": "\"\"", "id": 18556, "fac": [], "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [117.706, -31.8881]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

The third largest monolith in Australia\u00a0is recognized as an interesting and unspoilt location for flora and fauna study.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/kokerbin", "name": "Kokerbin", "order": [], "img2": "\"\"", "id": 23632, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.883, -19.6426]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Eighty Mile Beach extends for 220 kilometres. Endless stretches of white sand scattered with tropical seashells contrast with the rocky shores, seagrass meadows, tidal creeks and mangrove-lined muddy bays.

\n", "img": "\"\"", "act": [], "alt": {"80 Mile Beach": 80}, "url": "/park/eighty-mile-beach", "name": "Eighty Mile Beach", "order": [], "img2": "\"\"", "id": 23631, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.574, -34.5187]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Nestled between Shannon and Lake Muir national parks on the upper reaches of the Deep River.

\n", "img": "", "act": [], "alt": [], "url": "/park/boyndaminup", "name": "Boyndaminup", "order": [], "img2": "", "id": 23598, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.632, -30.8102]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Home to numerous plant species, including five rare and numerous species of priority flora.

\n", "img": "", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/wongan-hills", "name": "Wongan Hills", "order": [], "img2": "", "id": 23597, "fac": {"45": "Info Shelter"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.609, -32.9174]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Home to more species of waterbird than any other wetland in south western Australia.

\n", "img": "", "act": [], "alt": [], "url": "/park/toolibin", "name": "Toolibin", "order": [], "img2": "", "id": 23596, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.528, -29.7758]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Low heath on rolling sandplains renowned for their incredible diversity of endemic wildflowers.

\n", "img": "", "act": [], "alt": [], "url": "/park/tathra", "name": "Tathra", "order": [], "img2": "", "id": 23595, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.099, -29.9306]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

The Park has a several subterranean caverns which drain a small stream flowing westward into them and is mainly limestone outcrops with intermittent low gullies.

\n", "img": "", "act": [], "alt": [], "url": "/park/stockyard-gully", "name": "Stockyard Gully", "order": [], "img2": "", "id": 23594, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [118.75, -31.2307]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

A complex mosaic of exposed granite rock, with surrounding shrublands and woodlands.

\n", "img": "", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/sandford-rocks", "name": "Sandford Rocks", "order": [], "img2": "", "id": 23593, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [114.398, -21.6606]}, "type": "Feature", "properties": {"reg": [3, 34], "desc": "

\n\n \n The Muiron Islands Marine Management Area protects one of the region\u2019s most beautiful underwater wilderness areas and is one of the most popular areas for dive charters from Exmouth.\n


\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/muiron-islands", "name": "Muiron Islands", "order": [], "img2": "\"\"", "id": 23592, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.456, -31.1765]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

Popular picnicking place for local people especially during spring when it abounds with wildflowers.

\n", "img": "", "act": [], "alt": [], "url": "/park/korrelocking", "name": "Korrelocking", "order": [], "img2": "", "id": 23591, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [124.168, -15.7123]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Lalang-garram / Camden Sound Marine Park is the most important humpback whale nursery in the southern hemisphere. It features spectacular coastal scenery and Montgomery Reef provides an incredible spectacle at low tide.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/lalang-garramcamden-sound", "name": "Lalang-garram/Camden Sound", "order": [], "img2": "\"\"", "id": 23590, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.88, -32.4716]}, "type": "Feature", "properties": {"reg": [2, 39], "desc": "

An important remnant of natural bushland on the western edge of the Central Wheatbelt.

\n", "img": "", "act": [], "alt": [], "url": "/park/boyagin", "name": "Boyagin", "order": [], "img2": "", "id": 23589, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.323, -20.7588]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

Barrow Island Marine Park is a significant breeding and nesting area for threatened sea turtles and its waters support important coral reefs and a diversity of tropical marine animals.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/barrow-island", "name": "Barrow Island", "order": [], "img2": "\"\"", "id": 23588, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.09, -33.7342]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Native forest containing a particularly diverse range of vegetation types and a high concentration of delcared rare and priority flora species.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/yelverton", "name": "Yelverton", "order": [], "img2": "\"\"", "id": 23586, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.58, -30.0509]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Alexander Morrison National Park\u00a0is \u2018breakaway' country\u00a0renowned for its incredible diversity of endemic wildflowers.

\n", "img": "", "act": [], "alt": [], "url": "/park/alexander-morrison", "name": "Alexander Morrison", "order": [], "img2": "", "id": 23584, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [125.404, -15.4793]}, "type": "Feature", "properties": {"reg": [1, 30], "desc": "

Prince Regent, declared WA\u2019s 99th\u00a0national park under the State Government\u2019s Kimberley Science and Conservation Strategy, protects many areas of scenic grandeur.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/prince-regent", "name": "Prince Regent", "order": [], "img2": "\"\"", "id": 21702, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.35, -34.9627]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Denmark marks the spot where forests of towering trees give way to white sandy beaches.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/munda-biddi-trail-denmark-albany", "name": "Munda Biddi Trail Denmark Albany", "order": [], "img2": "\"\"", "id": 21689, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.73, -34.9759]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

Walpole is surrounded by the Walpole-Nornalup National Park and the Walpole-Nornalup Inlet System.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/munda-biddi-trail-walpole-denmark", "name": "Munda Biddi Trail Walpole Denmark", "order": [], "img2": "\"\"", "id": 21685, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.124, -34.6331]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Northcliffe has an easy going rural lifestyle, pristine natural environment, good winter rainfall and an abundance of flora and fauna found nowhere else in the world.

\n", "img": "", "act": [], "alt": [], "url": "/park/munda-biddi-trail-northcliffe-walpole", "name": "Munda Biddi Trail Northcliffe Walpole", "order": [], "img2": "", "id": 21682, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.147, -34.2416]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Manjimup is just 307 km from Perth, in the beautiful south west corner of Western Australia - a land of tall timbers, abundant fresh water, rich soils and undulating scenery.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/munda-biddi-trail-manjimup-northcliffe", "name": "Munda Biddi Trail Manjimup Northcliffe", "order": [], "img2": "\"\"", "id": 21679, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.666, -33.7956]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

Nestled in the heart of the Blackwood River valley, Nannup is a beautiful little town with a sense of history and a strong link to the surrounding countryside.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/munda-biddi-trail-jarrahwood-manjimup", "name": "Munda Biddi Trail Jarrahwood Manjimup", "order": [], "img2": "\"\"", "id": 21676, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.157, -33.3577]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

The town of Collie embraces its mining and forest history proudly as well as making the most of its scenic location to entertain visitors.

\n", "img": "\"\"", "act": [], "alt": [], "url": "/park/munda-biddi-trail-collie-jarrahwood", "name": "Munda Biddi Trail Collie Jarrahwood", "order": [], "img2": "\"\"", "id": 21673, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.093, -32.8055]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Dwellingup is a little forest town, now with a big reputation as a centre for recreation and outdoor education activities.

\n", "img": "", "act": [], "alt": [], "url": "/park/munda-biddi-trail-nanga-collie", "name": "Munda Biddi Trail Nanga Collie", "order": [], "img2": "", "id": 21670, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.165, -31.9042]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Jarrahdale is a picturesque, historic town in the Darling scarp, surrounded by some of WA\u2019s best Jarrah forests.

\n", "img": "", "act": [], "alt": [], "url": "/park/munda-biddi-trail-jarrahdale-nanga", "name": "Munda Biddi Trail Jarrahdale Nanga", "order": [], "img2": "", "id": 21666, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.165, -31.9042]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

The northern terminus of the Trail is in Mundaring. From there it winds its way through the Perth Hills to the picturesque, historic town of Jarrahdale.

\n", "img": "", "act": [], "alt": [], "url": "/park/munda-biddi-trail-mundaring-jarrahdale", "name": "Munda Biddi Trail Mundaring Jarrahdale", "order": [], "img2": "", "id": 21584, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [117.366, -34.9694]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

Denmark to Albany is an easier coastal section to walk than Peaceful Bay to Denmark. Walkers will need to plan ahead before crossing of the Wilson and Torbay Inlets.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-denmark-albany", "name": "Bibbulmun Track Denmark Albany", "order": [], "img2": "\"\"", "id": 21548, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.728, -34.9755]}, "type": "Feature", "properties": {"reg": [5, 46], "desc": "

This section gives walkers the best mix of forest and coast as well as, after Peaceful Bay, some of the most challenging days on the entire Track.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-walpole", "name": "Bibbulmun Track Walpole", "order": [], "img2": "\"\"", "id": 21545, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.124, -34.6332]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Between Northcliffe and Walpole, walkers heading south from Kalamunda will once again reach a few milestones; the diverse ecosystems of the Pingerup Plains, the last of the campsites with campfires allowed and most significantly the first encounter with the wild southern ocean.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-northcliffe", "name": "Bibbulmun Track Northcliffe", "order": [], "img2": "\"\"", "id": 21542, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.033, -34.4461]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

The walk from Pemberton to Northcliffe is well known for pleasant walking through the karri forest.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-pemberton", "name": "Bibbulmun Track Pemberton", "order": [], "img2": "\"\"", "id": 21539, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.978, -34.1025]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

The Donnelly River remains a companion for much of this walk.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-donnelly-river", "name": "Bibbulmun Track Donnelly River", "order": [], "img2": "\"\"", "id": 21536, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.984, -33.7869]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Another short section between towns, this section of Track is known as a section of transition.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-balingup", "name": "Bibbulmun Track Balingup", "order": [], "img2": "\"\"", "id": 21533, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.15, -33.3591]}, "type": "Feature", "properties": {"reg": [5, 43], "desc": "

This section to Balingup is a shorter section allowing walkers to go from town-to-town over three nights.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/bibbulmun-track-collie", "name": "Bibbulmun Track Collie", "order": [], "img2": "\"\"", "id": 21527, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.063, -32.7118]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Dwellingup is situated in a timber and fruit-growing district east of Pinjarra.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"Bibb Track": 0}, "url": "/park/bibbulmun-track-dwellingup", "name": "Bibbulmun Track Dwellingup", "order": [], "img2": "\"\"", "id": 21524, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.061, -31.9731]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

Located in the Perth Hills, 24km east of the centre of Perth, the Bibbulmun Track Northern Terminus in Kalamunda is the starting point for walkers heading south.

\n", "img": "\"\"", "act": {"BUSH": "Bushwalking"}, "alt": {"Kalamunda": 0, "Bib Track": 0}, "url": "/park/bibbulmun-track-darling-range", "name": "Bibbulmun Track Darling Range", "order": [], "img2": "\"\"", "id": 21518, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.746, -31.6867]}, "type": "Feature", "properties": {"reg": [24, 40], "desc": "

A haven of tranquility and isolation close to Perth.

\n", "img": "", "act": [], "alt": [], "url": "/park/neerabup", "name": "Neerabup", "order": [], "img2": "", "id": 18647, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [119.072, -24.6519]}, "type": "Feature", "properties": {"reg": [1, 31], "desc": "

Information about this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/collier-range", "name": "Collier Range", "order": [], "img2": "", "id": 18632, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.025, -32.0854]}, "type": "Feature", "properties": {"reg": [24, 41], "desc": "

One of the regional parks of the Darling Range

\n", "img": "", "act": [], "alt": [], "url": "/park/banyowla", "name": "Banyowla", "order": [], "img2": "", "id": 18637, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.694, -31.0949]}, "type": "Feature", "properties": {"reg": [], "desc": "

Information about this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/moore-river", "name": "Moore River", "order": [], "img2": "", "id": 18649, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.461, -30.3989]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

High breakaway country overlooking low undulating sandplains. The park is renowned for its incredible diversity of endemic wildflowers.

\n", "img": "", "act": [], "alt": [], "url": "/park/badgingarra", "name": "Badgingarra", "order": [], "img2": "", "id": 18625, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.639, -34.1597]}, "type": "Feature", "properties": {"reg": [], "desc": "

Information about this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/milyeannup", "name": "Milyeannup", "order": [], "img2": "", "id": 18540, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.246, -32.3453]}, "type": "Feature", "properties": {"reg": [], "desc": "

Hike the three peaks of Mount Cuthbert, Mount Vincent and Mount Cooke on the Bibbulmun Track

\n", "img": "", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/monadnocks", "name": "Monadnocks", "order": [], "img2": "", "id": 18595, "fac": {"45": "Info Shelter", "460": "Picnic Table"}, "dog": 0, "camp": 1}}, {"geometry": {"type": "Point", "coordinates": [115.226, -34.2563]}, "type": "Feature", "properties": {"reg": [5, 44], "desc": "

Scott National Park

\n", "img": "", "act": {"CANOEING": "Canoeing & kayaking"}, "alt": [], "url": "/park/scott", "name": "Scott", "order": [], "img2": "", "id": 18542, "fac": {"43": "Toilet"}, "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [128.967, -31.679]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

The Eucla National Park covers approximately 3,340 hectares in the south east corner of Western Australia, the southern section, butting up against the famous \"Great Australian Bight\".

\n", "img": "", "act": [], "alt": [], "url": "/park/eucla", "name": "Eucla", "order": [], "img2": "", "id": 18566, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [120.316, -32.8772]}, "type": "Feature", "properties": {"reg": [2, 36], "desc": "

Located in the amazing Great Western Woodlands, the Frank Hann National Park offers a spectacular range of flora.

\n", "img": "", "act": [], "alt": [], "url": "/park/frank-hann", "name": "Frank Hann", "order": [], "img2": "", "id": 18567, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.776, -34.1477]}, "type": "Feature", "properties": {"reg": [], "desc": "

Karri, jarrah and marri forest around Easter and Tom Hill brooks.

\n", "img": "", "act": [], "alt": [], "url": "/park/easter", "name": "Easter", "order": [], "img2": "", "id": 18571, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [116.257, -34.6263]}, "type": "Feature", "properties": {"reg": [5, 45], "desc": "

Take a walk through the karri forest and learn about the history of this area.

\n", "img": "", "act": [], "alt": [], "url": "/park/jane", "name": "Jane", "order": [], "img2": "", "id": 18575, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [118.329, -34.6765]}, "type": "Feature", "properties": {"reg": [5, 42], "desc": "

Information about this park coming soon

\n", "img": "", "act": [], "alt": [], "url": "/park/hassell", "name": "Hassell", "order": [], "img2": "", "id": 18559, "fac": [], "dog": 0, "camp": 0}}, {"geometry": {"type": "Point", "coordinates": [115.839, -30.2124]}, "type": "Feature", "properties": {"reg": [3, 32], "desc": "

Watheroo National Park covers a total span of 44,324 hectares and is home to the Jingemia Cave.

\n", "img": "", "act": {"BUSH": "Bushwalking"}, "alt": [], "url": "/park/watheroo", "name": "Watheroo", "order": [], "img2": "", "id": 18585, "fac": [], "dog": 0, "camp": 0}}]} \ No newline at end of file diff --git a/parkstay/frontend/meta.json b/parkstay/frontend/meta.json new file mode 100644 index 0000000000..5c941417a0 --- /dev/null +++ b/parkstay/frontend/meta.json @@ -0,0 +1,49 @@ +{ + "prompts": { + "name": { + "type": "string", + "required": true, + "label": "Project name" + }, + "description": { + "type": "string", + "required": true, + "label": "Project description", + "default": "A Vue.js project" + }, + "author": { + "type": "string", + "label": "Author" + }, + "build": { + "type": "list", + "message": "Vue build", + "choices": [ + { + "name": "Runtime + Compiler: recommended for most users", + "value": "standalone", + "short": "standalone" + }, + { + "name": "Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere", + "value": "runtime", + "short": "runtime" + } + ] + }, + "lint": { + "type": "confirm", + "message": "Use ESLint to lint your code?" + }, + "unit": { + "type": "confirm", + "message": "Setup unit tests with Karma + Mocha?" + } + }, + "filters": { + ".eslintrc.js": "lint", + "test/unit/**/*": "unit", + "test/e2e/**/*": "e2e" + }, + "completeMessage": "To get started:\n\n cd {{destDirName}}\n npm install\n npm run dev" +} diff --git a/parkstay/frontend/parkstay/.babelrc b/parkstay/frontend/parkstay/.babelrc new file mode 100644 index 0000000000..d44bd63cc6 --- /dev/null +++ b/parkstay/frontend/parkstay/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-2"], + "plugins": ["transform-runtime"] +} diff --git a/parkstay/frontend/parkstay/.eslintrc b/parkstay/frontend/parkstay/.eslintrc new file mode 100644 index 0000000000..89f65dc505 --- /dev/null +++ b/parkstay/frontend/parkstay/.eslintrc @@ -0,0 +1,9 @@ +{ + "extends": "standard", + "plugins": [ + "html" + ], + "env": { + "jasmine": true + } +} diff --git a/parkstay/frontend/parkstay/.gitignore b/parkstay/frontend/parkstay/.gitignore new file mode 100644 index 0000000000..6163809189 --- /dev/null +++ b/parkstay/frontend/parkstay/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +node_modules +dist/build.js +dist/build.css diff --git a/parkstay/frontend/parkstay/README.md b/parkstay/frontend/parkstay/README.md new file mode 100644 index 0000000000..9b3b321444 --- /dev/null +++ b/parkstay/frontend/parkstay/README.md @@ -0,0 +1,24 @@ +# parkstay + +> A Vue.js project + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# lint all *.js and *.vue files +npm run lint + +# run unit tests +npm test +``` + +For more information see the [docs for vueify](https://github.com/vuejs/vueify). diff --git a/parkstay/frontend/parkstay/karma.conf.js b/parkstay/frontend/parkstay/karma.conf.js new file mode 100644 index 0000000000..afeaee60ef --- /dev/null +++ b/parkstay/frontend/parkstay/karma.conf.js @@ -0,0 +1,20 @@ +// https://github.com/Nikku/karma-browserify +module.exports = function (config) { + config.set({ + browsers: ['PhantomJS'], + frameworks: ['browserify', 'jasmine'], + files: ['test/unit/**/*.js'], + reporters: ['spec'], + preprocessors: { + 'test/unit/**/*.js': ['browserify'] + }, + browserify: { + debug: true, + // needed to enable mocks + plugin: [require('proxyquireify').plugin] + }, + // if you want to continuously re-run tests on file-save, + // replace the following line with `autoWatch: true` + singleRun: true + }) +} diff --git a/parkstay/frontend/parkstay/package.json b/parkstay/frontend/parkstay/package.json new file mode 100644 index 0000000000..d9079044bc --- /dev/null +++ b/parkstay/frontend/parkstay/package.json @@ -0,0 +1,131 @@ +{ + "name": "parkstay", + "description": "A Vue.js project", + "author": "tawanda ", + "private": true, + "scripts": { + "watchify": "watchify -vd -p [browserify-hmr -h 0.0.0.0 -u http://$(my-local-ip):3123] -g browserify-css src/apps/main.js -e src/apps/main.js -o ../../static/ps/js/build.js", + "serve": "http-server -o -c 1 -a localhost", + "dev": "npm-run-all --parallel watchify", + "lint": "eslint --ext .js,.vue src test/unit", + "test": "karma start karma.conf.js", + "build": "cross-env NODE_ENV=production browserify -g envify -p [ vueify/plugins/extract-css -o ../../static/ps/css/build.css ] -g browserify-css src/apps/main.js -e src/apps/main.js | uglifyjs -c warnings=false -m > ../../static/ps/js/build.js", + "build_dev": "cross-env NODE_ENV=production browserify -d -g envify -p [ vueify/plugins/extract-css -o ../../static/ps/css/build.css ] -g browserify-css src/apps/main.js -e src/apps/main.js | exorcist ../../static/ps/js/build.js.map > ../../static/ps/js/build.js" + }, + "browserify": { + "transform": [ + [ + "vueify", + { + "babel": { + "presets": [ + "es2015" + ], + "plugins": [] + } + } + ], + [ + "babelify", + { + "presets": [ + "es2015" + ], + "plugins": [], + "only": "src" + } + ], + "browserify-shim" + ] + }, + "browser": { + "jquery": "./node_modules/jquery/dist/jquery.js", + "moment": "./node_modules/moment/moment.js", + "datetimepicker": "./node_modules/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js", + "jquery-validation": "./node_modules/jquery-validation/dist/jquery.validate.js", + "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.min.js", + "vue": "vue/dist/vue", + "select2": "./node_modules/select2/dist/js/select2.full.min.js" + }, + "browserify-shim": { + "jquery": "$", + "bootstrap": { + "depends": [ + "jquery: jQuery" + ] + }, + "datetimepicker": { + "depends": [ + "jquery: jQuery", + "moment: moment", + "bootstrap: bootstrap" + ], + "exports": "$.fn.datetimepicker" + }, + "jquery-validation": { + "depends": [ + "jquery: jQuery" + ], + "exports": "$.fn.validate" + }, + "select2": { + "depends": [ + "jquery: jQuery" + ], + "exports": "$.fn.select2" + } + }, + "dependencies": { + "bootstrap": "^3.3.7", + "bootstrap-daterangepicker": "^2.1.24", + "datatables.net": "^1.10.12", + "datatables.net-bs": "^1.10.12", + "datatables.net-responsive": "^2.1.0", + "datatables.net-responsive-bs": "^2.1.0", + "eonasdan-bootstrap-datetimepicker": "^4.17.43", + "jquery": "^3.1.1", + "jquery-validation": "^1.15.1", + "moment": "^2.15.2", + "quill": "^1.1.5", + "quill-render": "^1.0.5", + "select2": "^4.0.3", + "select2-bootstrap-theme": "0.1.0-beta.9", + "slick-carousel-browserify": "^1.6.12", + "vue": "^2.0.1", + "vue-router": "^2.0.1" + }, + "devDependencies": { + "babel-core": "^6.0.0", + "babel-plugin-transform-runtime": "^6.0.0", + "babel-preset-es2015": "^6.0.0", + "babel-preset-stage-2": "^6.0.0", + "babel-runtime": "^6.0.0", + "babelify": "^7.2.0", + "browserify": "^13.1.0", + "browserify-css": "^0.9.2", + "browserify-hmr": "^0.3.1", + "browserify-shim": "~3.8.12", + "cross-env": "^2.0.0", + "envify": "^3.4.1", + "eslint": "^3.3.0", + "eslint-config-standard": "^5.3.5", + "eslint-plugin-html": "^1.5.2", + "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-standard": "^2.0.0", + "exorcist": "^0.4.0", + "http-server": "^0.9.0", + "jasmine-core": "^2.4.1", + "karma": "^1.2.0", + "karma-browserify": "^5.1.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.0", + "karma-spec-reporter": "0.0.26", + "my-local-ip": "^1.0.0", + "npm-run-all": "^2.3.0", + "phantomjs-prebuilt": "^2.1.3", + "proxyquireify": "^3.0.1", + "uglify-js": "^2.5.0", + "vueify": "^9.0.0", + "watchify": "^3.4.0" + } +} diff --git a/parkstay/frontend/parkstay/src/apps/App.vue b/parkstay/frontend/parkstay/src/apps/App.vue new file mode 100644 index 0000000000..5306e7e351 --- /dev/null +++ b/parkstay/frontend/parkstay/src/apps/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/parkstay/frontend/parkstay/src/apps/api.js b/parkstay/frontend/parkstay/src/apps/api.js new file mode 100644 index 0000000000..46818d7520 --- /dev/null +++ b/parkstay/frontend/parkstay/src/apps/api.js @@ -0,0 +1,104 @@ + +module.exports = { + status_history:function(id){ + return "/api/campgrounds/" + id + "/status_history.json?closures=True" + }, + regions:"/api/regions.json", + parks:"/api/parks.json", + // Campgrounds + campgrounds:"/api/campgrounds.json", + campground:function (id) { + return "/api/campgrounds/"+id+".json"; + }, + campground_price_history: function(id){ + return "/api/campgrounds/"+ id +"/price_history.json"; + }, + addPrice: function(id){ + return "/api/campgrounds/"+ id +"/addPrice.json"; + }, + editPrice: function(id){ + return "/api/campgrounds/"+ id +"/updatePrice.json"; + }, + campgroundCampsites: function(id){ + return "/api/campgrounds/" + id + "/campsites.json" + }, + opencloseCG: function(id){ + return "/api/campgrounds/" + id + "/open_close.json" + }, + deleteBookingRange: function (id) { + return "/api/campground_booking_ranges/" + id + ".json" + }, + campground_status_history_detail: function(id){ + return "/api/campground_booking_ranges/"+ id +".json?original=true"; + }, + delete_campground_price: function(id){ + return "/api/campgrounds/" + id + "/deletePrice.json"; + }, + // Campsites + campsites:"/api/campsites.json", + campsites_stay_history: "/api/campsites_stay_history.json", + campsites_stay_history_detail: function(id){ + return "/api/campsites_stay_history/"+ id +".json"; + }, + campsites_price_history: function(id){ + return "/api/campsites/"+ id +"/price_history.json"; + }, + campsites_status_history:function(id){ + return "/api/campsites/" + id + "/status_history.json?closures=True" + }, + campsite:function (id) { + return "/api/campsites/"+id+".json"; + }, + campsiteStayHistory: function(id){ + return "/api/campsites/" + id + "/stay_history.json" + }, + opencloseCS: function(id){ + return "/api/campsites/" + id + "/open_close.json" + }, + deleteCampsiteBookingRange: function (id) { + return "/api/campsite_booking_ranges/" + id + ".json" + }, + campsite_status_history_detail: function(id){ + return "/api/campsite_booking_ranges/"+ id +".json?original=true"; + }, + features:"/api/features.json", + campsite_rate: "/api/campsite_rate.json", + campsiterate_detail:function (id) { + return "/api/campsite_rate/"+id+".json" + }, + rates:"/api/rates.json", + + // campsite types + campsite_classes:"/api/campsite_classes.json", + campsite_classes_active:"/api/campsite_classes.json?active_only=true", + campsite_class:function (id) { + return "/api/campsite_classes/"+id+".json" + }, + addCampsiteClassPrice: function(id){ + return "/api/campsite_classes/"+id+"/addPrice.json" + }, + editCampsiteClassPrice(id) { + return "/api/campsite_classes/"+id+"/updatePrice.json" + }, + deleteCampsiteClassPrice(id) { + return "/api/campsite_classes/"+id+"/deletePrice.json" + }, + campsiteclass_price_history: function(id){ + return "/api/campsite_classes/"+ id +"/price_history.json"; + }, + closureReasons:function () { + return "/api/closureReasons.json"; + }, + openReasons:function () { + return "/api/openReasons.json"; + }, + priceReasons:function () { + return "/api/priceReasons.json"; + }, + maxStayReasons:function () { + return "/api/maxStayReasons.json"; + }, + bulkPricing: function(){ + return "/api/bulkPricing"; + } +}; diff --git a/parkstay/frontend/parkstay/src/apps/main.js b/parkstay/frontend/parkstay/src/apps/main.js new file mode 100644 index 0000000000..ace6279158 --- /dev/null +++ b/parkstay/frontend/parkstay/src/apps/main.js @@ -0,0 +1,153 @@ +// The following line loads the standalone build of Vue instead of the runtime-only build, +// so you don't have to do: import Vue from 'vue/dist/vue' +// This is done with the browser options. For the config, see package.json +import Vue from 'vue' +import Campgrounds from '../components/campgrounds/campgrounds.vue' +import Campground from '../components/campgrounds/campground.vue' +import AddCampground from '../components/campgrounds/addCampground.vue' +import Campsite from '../components/campsites/campsite.vue' +import firstLevelSearch from '../components/booking/first-level-search.vue' +import page_404 from '../components/utils/404.vue' +import Router from 'vue-router' +import Campsite_type_dash from '../components/campsites-types/campsite-types-dash.vue' +import Campsite_type from '../components/campsites-types/campsite-type.vue' +import Bulkpricing from '../components/bulkpricing/bulkpricing.vue' +import $ from '../hooks' +var css = require('../hooks-css.js'); +Vue.use(Router); + +// Define global variables +global.debounce = function (func, wait, immediate) { + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + 'use strict'; + var timeout; + return function () { + var context = this; + var args = arguments; + var later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + } +}; + +global.$ = $ + +const routes = [ + { + path: '/', + component: { + render (c) { return c('router-view') } + }, + children: [ + { + path:'dashboard', + component: { + render (c) { return c('router-view') } + }, + children: [ + { + path:'campsite-types', + name:'campsite-types', + component: Campsite_type_dash + }, + { + path:'bulkpricing', + name:'bulkpricing', + component: Bulkpricing + }, + { + path:'campsite-type', + component: { + render (c) { return c('router-view') } + }, + children: [ + { + path: '/', + name: 'campsite-type', + component: Campsite_type + }, + { + path:':campsite_type_id', + name:'campsite-type-detail', + component: Campsite_type, + } + ] + }, + { + path:'campgrounds/addCampground', + name:'cg_add', + component: AddCampground + }, + { + path:'campgrounds', + component: { + render (c) { return c('router-view') } + }, + children:[ + { + path: '/', + name: 'cg_main', + component: Campgrounds, + }, + { + path:':id', + name:'cg_detail', + component: Campground, + }, + { + path:':id/campsites/add', + name:'add_campsite', + component:Campsite + }, + { + path:':id/campsites/:campsite_id', + name:'view_campsite', + component:Campsite + }, + ] + }, + { + path:'bulkpricing', + name:'bulkpricing', + component:Bulkpricing + }, + ] + }, + { + path:'booking', + component:{ + render (c) { return c('router-view') } + }, + children:[ + { + path:'/', + name:'fl-search', + component: firstLevelSearch + } + ] + } + ] + }, + { + path: '/404', + name: '404', + component: page_404 + } +]; + +const router = new Router({ + 'routes' : routes, + 'mode': 'history' +}); + +new Vue({ + 'router':router +}).$mount('#app'); diff --git a/parkstay/frontend/parkstay/src/components/booking/first-level-search.vue b/parkstay/frontend/parkstay/src/components/booking/first-level-search.vue new file mode 100644 index 0000000000..2109dc1f91 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/booking/first-level-search.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/bulkpricing/bulkpricing.vue b/parkstay/frontend/parkstay/src/components/bulkpricing/bulkpricing.vue new file mode 100644 index 0000000000..62f204ea3a --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/bulkpricing/bulkpricing.vue @@ -0,0 +1,529 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/addCampground.vue b/parkstay/frontend/parkstay/src/components/campgrounds/addCampground.vue new file mode 100644 index 0000000000..67fae8d843 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/addCampground.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/campground-attr.vue b/parkstay/frontend/parkstay/src/components/campgrounds/campground-attr.vue new file mode 100644 index 0000000000..589410ec1e --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/campground-attr.vue @@ -0,0 +1,527 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/campground.vue b/parkstay/frontend/parkstay/src/components/campgrounds/campground.vue new file mode 100644 index 0000000000..51f1acf248 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/campground.vue @@ -0,0 +1,399 @@ +openCampsite + + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/campgrounds.vue b/parkstay/frontend/parkstay/src/components/campgrounds/campgrounds.vue new file mode 100644 index 0000000000..54fab0c612 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/campgrounds.vue @@ -0,0 +1,230 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/closeCampground.vue b/parkstay/frontend/parkstay/src/components/campgrounds/closeCampground.vue new file mode 100644 index 0000000000..1b66993c47 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/closeCampground.vue @@ -0,0 +1,205 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campgrounds/openCampground.vue b/parkstay/frontend/parkstay/src/components/campgrounds/openCampground.vue new file mode 100644 index 0000000000..2608ffdd38 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campgrounds/openCampground.vue @@ -0,0 +1,185 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites-types/campsite-type.vue b/parkstay/frontend/parkstay/src/components/campsites-types/campsite-type.vue new file mode 100644 index 0000000000..e5c289c4b6 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites-types/campsite-type.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites-types/campsite-types-dash.vue b/parkstay/frontend/parkstay/src/components/campsites-types/campsite-types-dash.vue new file mode 100644 index 0000000000..f2bf9feb54 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites-types/campsite-types-dash.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites/campsite.vue b/parkstay/frontend/parkstay/src/components/campsites/campsite.vue new file mode 100644 index 0000000000..fbf0df1fd3 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites/campsite.vue @@ -0,0 +1,499 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites/closureHistory/closeCampsite.vue b/parkstay/frontend/parkstay/src/components/campsites/closureHistory/closeCampsite.vue new file mode 100644 index 0000000000..ba2c292c17 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites/closureHistory/closeCampsite.vue @@ -0,0 +1,189 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites/closureHistory/openCampsite.vue b/parkstay/frontend/parkstay/src/components/campsites/closureHistory/openCampsite.vue new file mode 100644 index 0000000000..8d2b8e2f63 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites/closureHistory/openCampsite.vue @@ -0,0 +1,163 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites/stayHistory/addMaximumStayPeriod.vue b/parkstay/frontend/parkstay/src/components/campsites/stayHistory/addMaximumStayPeriod.vue new file mode 100644 index 0000000000..0ed174e264 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites/stayHistory/addMaximumStayPeriod.vue @@ -0,0 +1,204 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/campsites/stayHistory/stayHistory.vue b/parkstay/frontend/parkstay/src/components/campsites/stayHistory/stayHistory.vue new file mode 100644 index 0000000000..bd497d5336 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/campsites/stayHistory/stayHistory.vue @@ -0,0 +1,211 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/404.vue b/parkstay/frontend/parkstay/src/components/utils/404.vue new file mode 100644 index 0000000000..4af1582dc6 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/404.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/alert.vue b/parkstay/frontend/parkstay/src/components/utils/alert.vue new file mode 100644 index 0000000000..d2ba472beb --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/alert.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/bookingpicker.vue b/parkstay/frontend/parkstay/src/components/utils/bookingpicker.vue new file mode 100644 index 0000000000..cb23c0cbe3 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/bookingpicker.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/bootstrap-modal.vue b/parkstay/frontend/parkstay/src/components/utils/bootstrap-modal.vue new file mode 100644 index 0000000000..5e382bf191 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/bootstrap-modal.vue @@ -0,0 +1,181 @@ + + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/closureHistory.vue b/parkstay/frontend/parkstay/src/components/utils/closureHistory.vue new file mode 100644 index 0000000000..c6f494aac3 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/closureHistory.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/closureHistory/close.vue b/parkstay/frontend/parkstay/src/components/utils/closureHistory/close.vue new file mode 100644 index 0000000000..716bc261fe --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/closureHistory/close.vue @@ -0,0 +1,198 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/closureHistory/open.vue b/parkstay/frontend/parkstay/src/components/utils/closureHistory/open.vue new file mode 100644 index 0000000000..0c4bb6cb8d --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/closureHistory/open.vue @@ -0,0 +1,190 @@ + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/confirmbox.vue b/parkstay/frontend/parkstay/src/components/utils/confirmbox.vue new file mode 100644 index 0000000000..562ad5a646 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/confirmbox.vue @@ -0,0 +1,131 @@ +/** +* confirmBox components +* author: Tawanda Nyakudjga +* date: 9/10/2016 +* alertOptions:{ + icon:"", + message:"Are you sure you want to Delete!!!" , + buttons:[ + { + text:"Delete", + event: "delete", + bsColor:"btn-danger", + handler:function(e) { + + vm.showAlert(); + }, + autoclose:true + } + ] + } +**/ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/datatable.vue b/parkstay/frontend/parkstay/src/components/utils/datatable.vue new file mode 100644 index 0000000000..859b69a5fe --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/datatable.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/editor.vue b/parkstay/frontend/parkstay/src/components/utils/editor.vue new file mode 100644 index 0000000000..a52b2e70b6 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/editor.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/eventBus.js b/parkstay/frontend/parkstay/src/components/utils/eventBus.js new file mode 100644 index 0000000000..afb103634b --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/eventBus.js @@ -0,0 +1,5 @@ +var Vue = require('vue'); +var bus = new Vue(); +export { + bus +} diff --git a/parkstay/frontend/parkstay/src/components/utils/helpers.js b/parkstay/frontend/parkstay/src/components/utils/helpers.js new file mode 100644 index 0000000000..a06c4275f0 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/helpers.js @@ -0,0 +1,43 @@ +module.exports = { + apiError: function(resp){ + var error_str = ''; + if (resp.status === 400) { + try { + obj = JSON.parse(resp.responseText); + error_str = obj.non_field_errors[0].replace(/[\[\]"]/g,''); + } catch(e) { + error_str = resp.responseText.replace(/[\[\]"]/g,''); + } + } + else if ( resp.status === 404) { + error_str = 'The resource you are looking for does not exist.'; + } + return error_str; + }, + goBack:function(vm){ + vm.$router.go(window.history.back()); + }, + getCookie: function(name) { + var value = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1).trim() === (name + '=')) { + value = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return value; + }, + namePopover:function ($,vmDataTable) { + vmDataTable.on('mouseover','.name_popover',function (e) { + $(this).popover('show'); + $(this).on('mouseout',function () { + $(this).popover('hide'); + }) + + }); + } +}; diff --git a/parkstay/frontend/parkstay/src/components/utils/images/imagePicker.vue b/parkstay/frontend/parkstay/src/components/utils/images/imagePicker.vue new file mode 100644 index 0000000000..5619809795 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/images/imagePicker.vue @@ -0,0 +1,445 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/loader.vue b/parkstay/frontend/parkstay/src/components/utils/loader.vue new file mode 100644 index 0000000000..e9718a13c6 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/loader.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/mixins.js b/parkstay/frontend/parkstay/src/components/utils/mixins.js new file mode 100644 index 0000000000..b8343a4e41 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/mixins.js @@ -0,0 +1,5 @@ +var mix = module.exports ={ + methods:{ + + } +} diff --git a/parkstay/frontend/parkstay/src/components/utils/modal.vue b/parkstay/frontend/parkstay/src/components/utils/modal.vue new file mode 100644 index 0000000000..6155a1245a --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/modal.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistory.vue b/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistory.vue new file mode 100644 index 0000000000..2807c6d4d4 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistory.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistoryDetail.vue b/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistoryDetail.vue new file mode 100644 index 0000000000..1370494e41 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/priceHistory/priceHistoryDetail.vue @@ -0,0 +1,248 @@ + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/reasons.vue b/parkstay/frontend/parkstay/src/components/utils/reasons.vue new file mode 100644 index 0000000000..54df2f27b0 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/reasons.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/components/utils/select-panel.vue b/parkstay/frontend/parkstay/src/components/utils/select-panel.vue new file mode 100644 index 0000000000..93f4283d82 --- /dev/null +++ b/parkstay/frontend/parkstay/src/components/utils/select-panel.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/parkstay/frontend/parkstay/src/hooks-css.js b/parkstay/frontend/parkstay/src/hooks-css.js new file mode 100644 index 0000000000..cfa8c59407 --- /dev/null +++ b/parkstay/frontend/parkstay/src/hooks-css.js @@ -0,0 +1,7 @@ +require("eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css"); +require("quill/dist/quill.snow.css"); +require("datatables.net-bs/css/dataTables.bootstrap.css"); +require("slick-carousel-browserify/slick/slick.css"); +require("select2/dist/css/select2.min.css"); +require("select2-bootstrap-theme/dist/select2-bootstrap.min.css"); +require('bootstrap-daterangepicker/daterangepicker.css'); diff --git a/parkstay/frontend/parkstay/src/hooks.js b/parkstay/frontend/parkstay/src/hooks.js new file mode 100644 index 0000000000..1ae36a9e72 --- /dev/null +++ b/parkstay/frontend/parkstay/src/hooks.js @@ -0,0 +1,30 @@ +// module for all third party dependencies + +import $ from 'jquery' +var DataTable = require( 'datatables.net' )(); +var DataTableBs = require( 'datatables.net-bs' )(); +var DataTableRes = require( 'datatables.net-responsive-bs' )(); +var Moment = require('moment'); +var datetimepicker = require('datetimepicker'); +var validate = require('jquery-validation'); +var slick = require('slick-carousel-browserify'); +var select2 = require('select2'); +var daterangepicker = require('bootstrap-daterangepicker') +import api_endpoints from './apps/api.js'; +import helpers from './components/utils/helpers.js' +import {bus} from './components/utils/eventBus.js' +export { + $, + DataTable, + DataTableBs, + DataTableRes, + Moment, + datetimepicker, + api_endpoints, + helpers, + validate, + bus, + slick, + select2, + daterangepicker +} diff --git a/parkstay/frontend/parkstay/test/unit/Hello.spec.js b/parkstay/frontend/parkstay/test/unit/Hello.spec.js new file mode 100644 index 0000000000..f5808b3686 --- /dev/null +++ b/parkstay/frontend/parkstay/test/unit/Hello.spec.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Hello from '../../src/components/Hello.vue' + +describe('Hello.vue', () => { + it('should render correct contents', () => { + const vm = new Vue({ + el: document.createElement('div'), + render: (h) => h(Hello) + }) + expect(vm.$el.querySelector('h1').textContent).toBe('Welcome to Your Vue.js App') + }) +}) + +// also see example testing a component with mocks at +// https://github.com/vuejs/vueify-example/blob/master/test/unit/a.spec.js#L22-L43 diff --git a/parkstay/frontend/template/.babelrc b/parkstay/frontend/template/.babelrc new file mode 100644 index 0000000000..c13c5f627f --- /dev/null +++ b/parkstay/frontend/template/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/parkstay/frontend/template/.eslintrc b/parkstay/frontend/template/.eslintrc new file mode 100644 index 0000000000..89f65dc505 --- /dev/null +++ b/parkstay/frontend/template/.eslintrc @@ -0,0 +1,9 @@ +{ + "extends": "standard", + "plugins": [ + "html" + ], + "env": { + "jasmine": true + } +} diff --git a/parkstay/frontend/template/.gitignore b/parkstay/frontend/template/.gitignore new file mode 100644 index 0000000000..6163809189 --- /dev/null +++ b/parkstay/frontend/template/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +node_modules +dist/build.js +dist/build.css diff --git a/parkstay/frontend/template/README.md b/parkstay/frontend/template/README.md new file mode 100644 index 0000000000..20aef7dc78 --- /dev/null +++ b/parkstay/frontend/template/README.md @@ -0,0 +1,24 @@ +# {{ name }} + +> {{ description }} + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# lint all *.js and *.vue files +npm run lint + +# run unit tests +npm test +``` + +For more information see the [docs for vueify](https://github.com/vuejs/vueify). diff --git a/parkstay/frontend/template/index.html b/parkstay/frontend/template/index.html new file mode 100644 index 0000000000..49c86b4728 --- /dev/null +++ b/parkstay/frontend/template/index.html @@ -0,0 +1,12 @@ + + + + + {{ name }} + + + +
+ + + diff --git a/parkstay/frontend/template/karma.conf.js b/parkstay/frontend/template/karma.conf.js new file mode 100644 index 0000000000..afeaee60ef --- /dev/null +++ b/parkstay/frontend/template/karma.conf.js @@ -0,0 +1,20 @@ +// https://github.com/Nikku/karma-browserify +module.exports = function (config) { + config.set({ + browsers: ['PhantomJS'], + frameworks: ['browserify', 'jasmine'], + files: ['test/unit/**/*.js'], + reporters: ['spec'], + preprocessors: { + 'test/unit/**/*.js': ['browserify'] + }, + browserify: { + debug: true, + // needed to enable mocks + plugin: [require('proxyquireify').plugin] + }, + // if you want to continuously re-run tests on file-save, + // replace the following line with `autoWatch: true` + singleRun: true + }) +} diff --git a/parkstay/frontend/template/package.json b/parkstay/frontend/template/package.json new file mode 100644 index 0000000000..281cdf76b9 --- /dev/null +++ b/parkstay/frontend/template/package.json @@ -0,0 +1,68 @@ +{ + "name": "{{ name }}", + "description": "{{ description }}", + "author": "{{ author }}", + "private": true, + "scripts": { + "watchify": "watchify -vd -p browserify-hmr -e src/main.js -o dist/build.js", + "serve": "http-server -o -c 1 -a localhost", + "dev": "npm-run-all --parallel watchify serve", + {{#lint}} + "lint": "eslint --ext .js,.vue src{{#unit}} test/unit{{/unit}}", + {{/lint}} + {{#unit}} + "test": "karma start karma.conf.js", + {{/unit}} + "build": "cross-env NODE_ENV=production browserify -g envify -p [ vueify/plugins/extract-css -o dist/build.css ] -e src/main.js | uglifyjs -c warnings=false -m > dist/build.js" + }, + "browserify": { + "transform": [ + "babelify", + "vueify" + ] + }, + {{#if_eq build "standalone"}} + "browser": { + "vue": "vue/dist/vue" + }, + {{/if_eq}} + "dependencies": { + "vue": "^2.0.1" + }, + "devDependencies": { + "babel-core": "^6.0.0", + "babel-plugin-transform-runtime": "^6.0.0", + "babel-preset-es2015": "^6.0.0", + "babel-preset-stage-2": "^6.0.0", + "babel-runtime": "^6.0.0", + "babelify": "^7.2.0", + "browserify": "^13.1.0", + "browserify-hmr": "^0.3.1", + "cross-env": "^2.0.0", + "envify": "^3.4.1", + {{#lint}} + "eslint": "^3.3.0", + "eslint-config-standard": "^5.3.5", + "eslint-plugin-html": "^1.5.2", + "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-standard": "^2.0.0", + {{/lint}} + "http-server": "^0.9.0", + {{#unit}} + "jasmine-core": "^2.4.1", + "karma": "^1.2.0", + "karma-browserify": "^5.1.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.0", + "karma-spec-reporter": "0.0.26", + {{/unit}} + "npm-run-all": "^2.3.0", + {{#unit}} + "phantomjs-prebuilt": "^2.1.3", + {{/unit}} + "proxyquireify": "^3.0.1", + "uglify-js": "^2.5.0", + "vueify": "^9.0.0", + "watchify": "^3.4.0" + } +} diff --git a/parkstay/frontend/template/src/App.vue b/parkstay/frontend/template/src/App.vue new file mode 100644 index 0000000000..1332ea122d --- /dev/null +++ b/parkstay/frontend/template/src/App.vue @@ -0,0 +1,16 @@ + + + diff --git a/parkstay/frontend/template/src/components/Hello.vue b/parkstay/frontend/template/src/components/Hello.vue new file mode 100644 index 0000000000..de990e0bcd --- /dev/null +++ b/parkstay/frontend/template/src/components/Hello.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/parkstay/frontend/template/src/main.js b/parkstay/frontend/template/src/main.js new file mode 100644 index 0000000000..96b7a7ca8c --- /dev/null +++ b/parkstay/frontend/template/src/main.js @@ -0,0 +1,12 @@ +{{#if_eq build "standalone"}} +// The following line loads the standalone build of Vue instead of the runtime-only build, +// so you don't have to do: import Vue from 'vue/dist/vue' +// This is done with the browser options. For the config, see package.json +{{/if_eq}} +import Vue from 'vue' +import App from './App.vue' + +new Vue({ // eslint-disable-line no-new + el: '#app', + render: (h) => h(App) +}) diff --git a/parkstay/frontend/template/test/unit/Hello.spec.js b/parkstay/frontend/template/test/unit/Hello.spec.js new file mode 100644 index 0000000000..f5808b3686 --- /dev/null +++ b/parkstay/frontend/template/test/unit/Hello.spec.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Hello from '../../src/components/Hello.vue' + +describe('Hello.vue', () => { + it('should render correct contents', () => { + const vm = new Vue({ + el: document.createElement('div'), + render: (h) => h(Hello) + }) + expect(vm.$el.querySelector('h1').textContent).toBe('Welcome to Your Vue.js App') + }) +}) + +// also see example testing a component with mocks at +// https://github.com/vuejs/vueify-example/blob/master/test/unit/a.spec.js#L22-L43 diff --git a/parkstay/helpers.py b/parkstay/helpers.py new file mode 100644 index 0000000000..2788cf6c59 --- /dev/null +++ b/parkstay/helpers.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +from ledger.accounts.models import EmailUser + + +def belongs_to(user, group_name): + """ + Check if the user belongs to the given group. + :param user: + :param group_name: + :return: + """ + return user.groups.filter(name=group_name).exists() + + +def is_officer(user): + return user.is_authenticated() and (belongs_to(user, 'Parkstay Officers') or user.is_superuser) + + +def is_customer(user): + """ + Test if the user is a customer + Rules: + Not an officer + :param user: + :return: + """ + return user.is_authenticated() and not is_officer(user) + + +def get_all_officers(): + return EmailUser.objects.filter(groups__name='Parkstay Officers') diff --git a/parkstay/migrations/0001_initial.py b/parkstay/migrations/0001_initial.py index 857ff1d65a..89383f96ea 100644 --- a/parkstay/migrations/0001_initial.py +++ b/parkstay/migrations/0001_initial.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-09-13 07:09 +# Generated by Django 1.9.10 on 2016-10-25 08:07 from __future__ import unicode_literals +import datetime import django.contrib.gis.db.models.fields import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion +import taggit.managers class Migration(migrations.Migration): @@ -13,6 +15,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('taggit', '0002_auto_20150616_2121'), ] operations = [ @@ -28,39 +31,42 @@ class Migration(migrations.Migration): ('cost_total', models.DecimalField(decimal_places=2, default='0.00', max_digits=8)), ], ), + migrations.CreateModel( + name='BookingRange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('min_days', models.SmallIntegerField(default=1)), + ('max_days', models.SmallIntegerField(default=28)), + ('min_sites', models.SmallIntegerField(default=1)), + ('max_sites', models.SmallIntegerField(default=12)), + ('min_dba', models.SmallIntegerField(default=0)), + ('max_dba', models.SmallIntegerField(default=180)), + ('status', models.SmallIntegerField(choices=[(0, 'Open'), (1, 'Closed due to natural disaster'), (2, 'Closed for maintenance')], default=0)), + ('details', models.TextField()), + ('range_start', models.DateTimeField(blank=True, null=True)), + ('range_end', models.DateTimeField(blank=True, null=True)), + ], + ), migrations.CreateModel( name='Campground', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, null=True)), - ('campground_type', models.SmallIntegerField(choices=[(0, 'Campground: no bookings'), (1, 'Campground: book online'), (2, 'Campground: book by phone'), (3, 'Other accomodation')], default=0)), + ('ratis_id', models.IntegerField(default=-1)), + ('campground_type', models.SmallIntegerField(choices=[(0, 'Campground: no bookings'), (1, 'Campground: book online'), (2, 'Campground: book by phone'), (3, 'Other accomodation'), (4, 'Not Published')], default=0)), ('site_type', models.SmallIntegerField(choices=[(0, 'Unnumbered Site'), (1, 'Numbered site')], default=0)), ('address', django.contrib.postgres.fields.jsonb.JSONField(null=True)), - ('mappinglink', models.TextField(blank=True, null=True)), ('description', models.TextField(blank=True, null=True)), - ('checkin_times', models.TextField(blank=True, null=True)), ('area_activities', models.TextField(blank=True, null=True)), ('driving_directions', models.TextField(blank=True, null=True)), - ('airports', models.TextField(blank=True, null=True)), + ('fees', models.TextField(blank=True, null=True)), ('othertransport', models.TextField(blank=True, null=True)), - ('policies_disclaimers', models.TextField(blank=True, null=True)), ('key', models.CharField(blank=True, max_length=255, null=True)), - ('is_published', models.BooleanField(default=False)), - ('apikey', models.CharField(blank=True, max_length=255, null=True)), ('wkb_geometry', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), - ('metatitle', models.CharField(blank=True, max_length=150, null=True)), - ('metadescription', models.CharField(blank=True, max_length=150, null=True)), - ('metakeywords', models.TextField(blank=True, null=True)), - ('timestamp', models.DateTimeField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='CampgroundFeature', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ('description', models.TextField(null=True)), - ('image', models.ImageField(null=True, upload_to=b'')), + ('bookable_per_site', models.BooleanField(default=False)), + ('dog_permitted', models.BooleanField(default=False)), + ('check_in', models.TimeField(default=datetime.time(14, 0))), + ('check_out', models.TimeField(default=datetime.time(10, 0))), ], ), migrations.CreateModel( @@ -68,7 +74,6 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255)), - ('max_people', models.SmallIntegerField(default=6)), ('wkb_geometry', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), ('campground', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Campground')), ], @@ -88,27 +93,63 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, unique=True)), + ('tents', models.SmallIntegerField(default=0)), + ('parking_spaces', models.SmallIntegerField(choices=[(0, 'Parking within site.'), (1, 'Parking for exclusive use of site occupiers next to site, but separated from tent space.'), (2, 'Parking for exclusive use of occupiers, short walk from tent space.'), (3, 'Shared parking (not allocated), short walk from tent space.')], default=0)), + ('number_vehicles', models.SmallIntegerField(choices=[(0, 'One vehicle'), (1, 'Two vehicles'), (2, 'One vehicle + small trailer'), (3, 'One vehicle + small trailer/large vehicle')], default=0)), + ('min_people', models.SmallIntegerField(default=1)), + ('max_people', models.SmallIntegerField(default=12)), + ('dimensions', models.CharField(default='6x4', max_length=12)), + ('camp_unit_suitability', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), ], ), migrations.CreateModel( - name='CampsiteFeature', + name='CampsiteRate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('allow_public_holidays', models.BooleanField(default=True)), + ('date_start', models.DateField(default=datetime.date.today)), + ('date_end', models.DateField(blank=True, null=True)), + ('rate_type', models.SmallIntegerField(choices=[(0, 'Standard'), (1, 'Discounted')], default=0)), + ('price_model', models.SmallIntegerField(choices=[(0, 'Price per Person'), (1, 'Fixed Price')], default=0)), + ('campsite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Campsite')), + ], + ), + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('phone_number', models.CharField(max_length=12)), + ], + ), + migrations.CreateModel( + name='CustomerContact', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, unique=True)), + ('phone_number', models.CharField(blank=True, max_length=50, null=True)), + ('email', models.EmailField(max_length=255)), + ('description', models.TextField()), + ('opening_hours', models.TextField()), + ('other_services', models.TextField()), ], ), migrations.CreateModel( - name='CampsiteRate', + name='District', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('min_days', models.SmallIntegerField(default=1)), - ('max_days', models.SmallIntegerField(default=28)), - ('min_people', models.SmallIntegerField(default=1)), - ('max_people', models.SmallIntegerField(default=12)), - ('allow_public_holidays', models.BooleanField(default=True)), - ('cost_per_day', models.DecimalField(decimal_places=2, default='10.00', max_digits=8)), - ('campground', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Campground')), - ('campsite_class', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.CampsiteClass')), + ('name', models.CharField(max_length=255, unique=True)), + ('abbreviation', models.CharField(max_length=16, null=True, unique=True)), + ('ratis_id', models.IntegerField(default=-1)), + ], + ), + migrations.CreateModel( + name='Feature', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('description', models.TextField(null=True)), + ('image', models.ImageField(null=True, upload_to='')), ], ), migrations.CreateModel( @@ -116,6 +157,9 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255)), + ('ratis_id', models.IntegerField(default=-1)), + ('entry_fee_required', models.BooleanField(default=True)), + ('district', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.District')), ], ), migrations.CreateModel( @@ -125,18 +169,39 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=255, unique=True)), ], ), + migrations.CreateModel( + name='Rate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('adult', models.DecimalField(decimal_places=2, default='10.00', max_digits=8)), + ('concession', models.DecimalField(decimal_places=2, default='6.60', max_digits=8)), + ('child', models.DecimalField(decimal_places=2, default='2.20', max_digits=8)), + ('infant', models.DecimalField(decimal_places=2, default='0', max_digits=8)), + ], + ), migrations.CreateModel( name='Region', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, unique=True)), + ('abbreviation', models.CharField(max_length=16, null=True, unique=True)), + ('ratis_id', models.IntegerField(default=-1)), ], ), + migrations.AlterUniqueTogether( + name='rate', + unique_together=set([('adult', 'concession', 'child', 'infant')]), + ), migrations.AddField( - model_name='park', + model_name='district', name='region', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Region'), ), + migrations.AddField( + model_name='campsiterate', + name='rate', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Rate'), + ), migrations.AddField( model_name='campsite', name='campsite_class', @@ -145,12 +210,22 @@ class Migration(migrations.Migration): migrations.AddField( model_name='campsite', name='features', - field=models.ManyToManyField(to='parkstay.CampsiteFeature'), + field=models.ManyToManyField(to='parkstay.Feature'), + ), + migrations.AddField( + model_name='campground', + name='contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.Contact'), + ), + migrations.AddField( + model_name='campground', + name='customer_contact', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.CustomerContact'), ), migrations.AddField( model_name='campground', name='features', - field=models.ManyToManyField(to='parkstay.CampgroundFeature'), + field=models.ManyToManyField(to='parkstay.Feature'), ), migrations.AddField( model_name='campground', @@ -162,6 +237,16 @@ class Migration(migrations.Migration): name='promo_area', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.PromoArea'), ), + migrations.AddField( + model_name='campground', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='bookingrange', + name='campground', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parkstay.Campground'), + ), migrations.AddField( model_name='booking', name='campground', @@ -169,11 +254,11 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='park', - unique_together=set([('name', 'region')]), + unique_together=set([('name',)]), ), migrations.AlterUniqueTogether( name='campsiterate', - unique_together=set([('campground', 'campsite_class')]), + unique_together=set([('campsite', 'rate')]), ), migrations.AlterUniqueTogether( name='campsitebooking', diff --git a/parkstay/migrations/0002_auto_20160920_1603.py b/parkstay/migrations/0002_auto_20160920_1603.py deleted file mode 100644 index 49c88c6cb6..0000000000 --- a/parkstay/migrations/0002_auto_20160920_1603.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-20 08:03 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='campsiterate', - name='rate_child', - field=models.DecimalField(decimal_places=2, default='2.20', max_digits=8), - ), - migrations.AddField( - model_name='campsiterate', - name='rate_concession', - field=models.DecimalField(decimal_places=2, default='6.60', max_digits=8), - ), - migrations.AddField( - model_name='campsiterate', - name='rate_infant', - field=models.DecimalField(decimal_places=2, default='0', max_digits=8), - ), - migrations.AlterField( - model_name='campgroundfeature', - name='image', - field=models.ImageField(null=True, upload_to=''), - ), - ] diff --git a/parkstay/migrations/0002_auto_20161104_1327.py b/parkstay/migrations/0002_auto_20161104_1327.py new file mode 100644 index 0000000000..25395e476a --- /dev/null +++ b/parkstay/migrations/0002_auto_20161104_1327.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-04 05:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='bookingrange', + name='campground', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='booking_ranges', to='parkstay.Campground'), + ), + migrations.AlterField( + model_name='bookingrange', + name='details', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='campsite', + name='campground', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='campsites', to='parkstay.Campground'), + ), + ] diff --git a/parkstay/migrations/0003_auto_20160920_1605.py b/parkstay/migrations/0003_auto_20160920_1605.py deleted file mode 100644 index 61d5f5a0df..0000000000 --- a/parkstay/migrations/0003_auto_20160920_1605.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-20 08:05 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0002_auto_20160920_1603'), - ] - - operations = [ - migrations.RenameField( - model_name='campsiterate', - old_name='cost_per_day', - new_name='rate_adult', - ), - ] diff --git a/parkstay/migrations/0003_auto_20161108_0919.py b/parkstay/migrations/0003_auto_20161108_0919.py new file mode 100644 index 0000000000..8cb6c26ff7 --- /dev/null +++ b/parkstay/migrations/0003_auto_20161108_0919.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-08 01:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0002_auto_20161104_1327'), + ] + + operations = [ + migrations.AlterField( + model_name='bookingrange', + name='range_end', + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name='bookingrange', + name='range_start', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/parkstay/migrations/0004_auto_20160922_1208.py b/parkstay/migrations/0004_auto_20160922_1208.py deleted file mode 100644 index eeb3a93de5..0000000000 --- a/parkstay/migrations/0004_auto_20160922_1208.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-22 04:08 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0003_auto_20160920_1605'), - ] - - operations = [ - migrations.RemoveField( - model_name='campsite', - name='features', - ), - migrations.AddField( - model_name='campsiteclass', - name='allow_campervan', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='campsiteclass', - name='allow_generator', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='campsiteclass', - name='allow_trailer', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='campsiteclass', - name='parking_spaces', - field=models.SmallIntegerField(default=0), - ), - migrations.AddField( - model_name='campsiteclass', - name='tents', - field=models.SmallIntegerField(default=0), - ), - migrations.DeleteModel( - name='CampsiteFeature', - ), - ] diff --git a/parkstay/migrations/0004_bookingrange_created.py b/parkstay/migrations/0004_bookingrange_created.py new file mode 100644 index 0000000000..64d71a0a09 --- /dev/null +++ b/parkstay/migrations/0004_bookingrange_created.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-10 00:32 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0003_auto_20161108_0919'), + ] + + operations = [ + migrations.AddField( + model_name='bookingrange', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 11, 10, 0, 32, 47, 223622, tzinfo=utc)), + preserve_default=False, + ), + ] diff --git a/parkstay/migrations/0005_auto_20161111_1302.py b/parkstay/migrations/0005_auto_20161111_1302.py new file mode 100644 index 0000000000..cb48834580 --- /dev/null +++ b/parkstay/migrations/0005_auto_20161111_1302.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-11 05:02 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0004_bookingrange_created'), + ] + + operations = [ + migrations.AddField( + model_name='bookingrange', + name='updated_on', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 11, 11, 5, 2, 50, 443881, tzinfo=utc), help_text='Used to check if the start and end dated were changed'), + preserve_default=False, + ), + migrations.AlterField( + model_name='bookingrange', + name='status', + field=models.SmallIntegerField(choices=[(0, 'Open'), (1, 'Closed due to natural disaster'), (2, 'Closed for maintenance'), (3, 'Other')], default=0), + ), + ] diff --git a/parkstay/migrations/0005_remove_campground_policies_disclaimers.py b/parkstay/migrations/0005_remove_campground_policies_disclaimers.py deleted file mode 100644 index 2461a716ee..0000000000 --- a/parkstay/migrations/0005_remove_campground_policies_disclaimers.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-22 07:45 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0004_auto_20160922_1208'), - ] - - operations = [ - migrations.RemoveField( - model_name='campground', - name='policies_disclaimers', - ), - ] diff --git a/parkstay/migrations/0006_auto_20160922_1639.py b/parkstay/migrations/0006_auto_20160922_1639.py deleted file mode 100644 index b7627752c4..0000000000 --- a/parkstay/migrations/0006_auto_20160922_1639.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-22 08:39 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0005_remove_campground_policies_disclaimers'), - ] - - operations = [ - migrations.RenameField( - model_name='campground', - old_name='checkin_times', - new_name='rules', - ), - migrations.RemoveField( - model_name='campground', - name='apikey', - ), - migrations.RemoveField( - model_name='campground', - name='mappinglink', - ), - migrations.RemoveField( - model_name='campground', - name='metadescription', - ), - migrations.RemoveField( - model_name='campground', - name='metatitle', - ), - migrations.RemoveField( - model_name='campground', - name='timestamp', - ), - ] diff --git a/parkstay/migrations/0006_auto_20161114_0840.py b/parkstay/migrations/0006_auto_20161114_0840.py new file mode 100644 index 0000000000..6867960ef1 --- /dev/null +++ b/parkstay/migrations/0006_auto_20161114_0840.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-14 00:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0005_auto_20161111_1302'), + ] + + operations = [ + migrations.RenameModel( + old_name='BookingRange', + new_name='CampgroundBookingRange', + ), + ] diff --git a/parkstay/migrations/0007_auto_20160922_1640.py b/parkstay/migrations/0007_auto_20160922_1640.py deleted file mode 100644 index e20e2498ab..0000000000 --- a/parkstay/migrations/0007_auto_20160922_1640.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-22 08:40 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('parkstay', '0006_auto_20160922_1639'), - ] - - operations = [ - migrations.RenameField( - model_name='campground', - old_name='airports', - new_name='fees', - ), - ] diff --git a/parkstay/migrations/0007_campsitebookingrange.py b/parkstay/migrations/0007_campsitebookingrange.py new file mode 100644 index 0000000000..2296dc8ad0 --- /dev/null +++ b/parkstay/migrations/0007_campsitebookingrange.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-14 00:46 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0006_auto_20161114_0840'), + ] + + operations = [ + migrations.CreateModel( + name='CampsiteBookingRange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated_on', models.DateTimeField(auto_now_add=True, help_text='Used to check if the start and end dated were changed')), + ('min_days', models.SmallIntegerField(default=1)), + ('max_days', models.SmallIntegerField(default=28)), + ('min_dba', models.SmallIntegerField(default=0)), + ('max_dba', models.SmallIntegerField(default=180)), + ('status', models.SmallIntegerField(choices=[(0, 'Open'), (1, 'Closed due to natural disaster'), (2, 'Closed for maintenance'), (3, 'Other')], default=0)), + ('details', models.TextField(blank=True, null=True)), + ('range_start', models.DateField(blank=True, null=True)), + ('range_end', models.DateField(blank=True, null=True)), + ('campsite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='booking_ranges', to='parkstay.Campsite')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/parkstay/migrations/0008_auto_20161114_1238.py b/parkstay/migrations/0008_auto_20161114_1238.py new file mode 100644 index 0000000000..2b3f9a2465 --- /dev/null +++ b/parkstay/migrations/0008_auto_20161114_1238.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-14 04:38 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0007_campsitebookingrange'), + ] + + operations = [ + migrations.RemoveField( + model_name='campground', + name='dog_permitted', + ), + migrations.AddField( + model_name='campground', + name='price_level', + field=models.SmallIntegerField(choices=[(0, 'Campground level'), (1, 'Campsite Class level'), (2, 'Campsite level')], default=0), + ), + ] diff --git a/parkstay/migrations/0009_auto_20161115_1204.py b/parkstay/migrations/0009_auto_20161115_1204.py new file mode 100644 index 0000000000..3891e229ea --- /dev/null +++ b/parkstay/migrations/0009_auto_20161115_1204.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-15 04:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0008_auto_20161114_1238'), + ] + + operations = [ + migrations.AlterField( + model_name='campgroundbookingrange', + name='campground', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='booking_ranges', to='parkstay.Campground'), + ), + ] diff --git a/parkstay/migrations/0010_auto_20161115_1444.py b/parkstay/migrations/0010_auto_20161115_1444.py new file mode 100644 index 0000000000..5086caa9d3 --- /dev/null +++ b/parkstay/migrations/0010_auto_20161115_1444.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-15 06:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0009_auto_20161115_1204'), + ] + + operations = [ + migrations.AlterField( + model_name='campground', + name='customer_contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.CustomerContact'), + ), + migrations.AlterField( + model_name='campground', + name='promo_area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.PromoArea'), + ), + migrations.AlterField( + model_name='campground', + name='tags', + field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/parkstay/migrations/0011_auto_20161116_1051.py b/parkstay/migrations/0011_auto_20161116_1051.py new file mode 100644 index 0000000000..cdd0ecfa92 --- /dev/null +++ b/parkstay/migrations/0011_auto_20161116_1051.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-16 02:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0010_auto_20161115_1444'), + ] + + operations = [ + migrations.CreateModel( + name='CampsiteStayHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('min_days', models.SmallIntegerField(default=1)), + ('max_days', models.SmallIntegerField(default=28)), + ('min_dba', models.SmallIntegerField(default=0)), + ('max_dba', models.SmallIntegerField(default=180)), + ('details', models.TextField(blank=True, null=True)), + ('range_start', models.DateField(blank=True, null=True)), + ('range_end', models.DateField(blank=True, null=True)), + ('campsite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stay_history', to='parkstay.Campsite')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='campgroundbookingrange', + name='max_days', + ), + migrations.RemoveField( + model_name='campgroundbookingrange', + name='max_dba', + ), + migrations.RemoveField( + model_name='campgroundbookingrange', + name='min_days', + ), + migrations.RemoveField( + model_name='campgroundbookingrange', + name='min_dba', + ), + migrations.RemoveField( + model_name='campsitebookingrange', + name='max_days', + ), + migrations.RemoveField( + model_name='campsitebookingrange', + name='max_dba', + ), + migrations.RemoveField( + model_name='campsitebookingrange', + name='min_days', + ), + migrations.RemoveField( + model_name='campsitebookingrange', + name='min_dba', + ), + ] diff --git a/parkstay/migrations/0012_campsiterate_update_level.py b/parkstay/migrations/0012_campsiterate_update_level.py new file mode 100644 index 0000000000..d6513b3d7e --- /dev/null +++ b/parkstay/migrations/0012_campsiterate_update_level.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-21 04:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0011_auto_20161116_1051'), + ] + + operations = [ + migrations.AddField( + model_name='campsiterate', + name='update_level', + field=models.SmallIntegerField(choices=[(0, 'Campground level'), (1, 'Campsite Class level'), (2, 'Campsite level')], default=0), + ), + ] diff --git a/parkstay/migrations/0013_campgroundpricehistory.py b/parkstay/migrations/0013_campgroundpricehistory.py new file mode 100644 index 0000000000..3bb26a960f --- /dev/null +++ b/parkstay/migrations/0013_campgroundpricehistory.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-22 05:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0012_campsiterate_update_level'), + ] + + operations = [ + migrations.CreateModel( + name='CampgroundPriceHistory', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('date_start', models.DateField()), + ('date_end', models.DateField()), + ('rate_id', models.IntegerField()), + ('adult', models.DecimalField(decimal_places=2, max_digits=8)), + ('concession', models.DecimalField(decimal_places=2, max_digits=8)), + ('child', models.DecimalField(decimal_places=2, max_digits=8)), + ], + options={ + 'db_table': 'parkstay_campground_pricehistory_v', + 'managed': False, + }, + ), + ] diff --git a/parkstay/migrations/0014_auto_20161122_1512.py b/parkstay/migrations/0014_auto_20161122_1512.py new file mode 100644 index 0000000000..313db6708c --- /dev/null +++ b/parkstay/migrations/0014_auto_20161122_1512.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-22 07:12 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0013_campgroundpricehistory'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='campsiterate', + unique_together=set([('campsite', 'rate', 'date_start', 'date_end')]), + ), + ] diff --git a/parkstay/migrations/0015_auto_20161125_1309.py b/parkstay/migrations/0015_auto_20161125_1309.py new file mode 100644 index 0000000000..0f00948410 --- /dev/null +++ b/parkstay/migrations/0015_auto_20161125_1309.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-25 05:09 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0014_auto_20161122_1512'), + ] + + operations = [ + migrations.AlterModelOptions( + name='campgroundpricehistory', + options={'managed': False, 'ordering': ['-date_start']}, + ), + migrations.AddField( + model_name='campsiteclass', + name='features', + field=models.ManyToManyField(to='parkstay.Feature'), + ), + migrations.AlterField( + model_name='campsiterate', + name='campsite', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='rates', to='parkstay.Campsite'), + ), + ] diff --git a/parkstay/migrations/0016_campsiteclass_deleted.py b/parkstay/migrations/0016_campsiteclass_deleted.py new file mode 100644 index 0000000000..b4d7ccbc5b --- /dev/null +++ b/parkstay/migrations/0016_campsiteclass_deleted.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-28 06:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0015_auto_20161125_1309'), + ] + + operations = [ + migrations.AddField( + model_name='campsiteclass', + name='deleted', + field=models.BooleanField(default=False), + ), + ] diff --git a/parkstay/migrations/0017_auto_20161130_1212.py b/parkstay/migrations/0017_auto_20161130_1212.py new file mode 100644 index 0000000000..090a91db8d --- /dev/null +++ b/parkstay/migrations/0017_auto_20161130_1212.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-11-30 04:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0016_campsiteclass_deleted'), + ] + + operations = [ + migrations.AddField( + model_name='campsite', + name='cs_dimensions', + field=models.CharField(default='6x4', max_length=12), + ), + migrations.AddField( + model_name='campsite', + name='cs_max_people', + field=models.SmallIntegerField(default=12), + ), + migrations.AddField( + model_name='campsite', + name='cs_min_people', + field=models.SmallIntegerField(default=1), + ), + migrations.AddField( + model_name='campsite', + name='cs_number_vehicles', + field=models.SmallIntegerField(choices=[(0, 'One vehicle'), (1, 'Two vehicles'), (2, 'One vehicle + small trailer'), (3, 'One vehicle + small trailer/large vehicle')], default=0), + ), + migrations.AddField( + model_name='campsite', + name='cs_parking_spaces', + field=models.SmallIntegerField(choices=[(0, 'Parking within site.'), (1, 'Parking for exclusive use of site occupiers next to site, but separated from tent space.'), (2, 'Parking for exclusive use of occupiers, short walk from tent space.'), (3, 'Shared parking (not allocated), short walk from tent space.')], default=0), + ), + migrations.AddField( + model_name='campsite', + name='cs_tents', + field=models.SmallIntegerField(default=0), + ), + migrations.AlterField( + model_name='campsite', + name='campsite_class', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='parkstay.CampsiteClass'), + ), + ] diff --git a/parkstay/migrations/0018_auto_20161201_1107.py b/parkstay/migrations/0018_auto_20161201_1107.py new file mode 100644 index 0000000000..d8902da679 --- /dev/null +++ b/parkstay/migrations/0018_auto_20161201_1107.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-01 03:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0017_auto_20161130_1212'), + ] + + operations = [ + migrations.AlterField( + model_name='campsite', + name='campsite_class', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='campsites', to='parkstay.CampsiteClass'), + ), + ] diff --git a/parkstay/migrations/0019_auto_20161202_0848.py b/parkstay/migrations/0019_auto_20161202_0848.py new file mode 100644 index 0000000000..4b091d3c4a --- /dev/null +++ b/parkstay/migrations/0019_auto_20161202_0848.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-02 00:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0018_auto_20161201_1107'), + ] + + operations = [ + migrations.CreateModel( + name='CampsiteClassPriceHistory', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('date_start', models.DateField()), + ('date_end', models.DateField()), + ('rate_id', models.IntegerField()), + ('adult', models.DecimalField(decimal_places=2, max_digits=8)), + ('concession', models.DecimalField(decimal_places=2, max_digits=8)), + ('child', models.DecimalField(decimal_places=2, max_digits=8)), + ], + options={ + 'ordering': ['-date_start'], + 'db_table': 'parkstay_campsiteclass_pricehistory_v', + 'managed': False, + }, + ), + migrations.AddField( + model_name='campground', + name='bookable_online', + field=models.BooleanField(default=False), + ), + ] diff --git a/parkstay/migrations/0020_create_views.py b/parkstay/migrations/0020_create_views.py new file mode 100644 index 0000000000..553cfc3d49 --- /dev/null +++ b/parkstay/migrations/0020_create_views.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-12-02 07:15 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0019_auto_20161202_0848'), + ] + + operations = [ + migrations.RunSQL( + """CREATE OR REPLACE VIEW parkstay_campsiteclass_pricehistory_v AS + SELECT DISTINCT classes.campsite_class_id AS id, + classes.date_start, + classes.date_end, + r.id AS rate_id, + r.adult, + r.concession, + r.child + FROM parkstay_rate r + INNER JOIN ( + SELECT distinct cc.id AS campsite_class_id, + cr.rate_id AS campsite_rate_id, + cr.date_start AS date_start, + cr.date_end AS date_end + FROM parkstay_campsite cs, + parkstay_campsiteclass cc, + parkstay_campsiterate cr + WHERE cs.campsite_class_id = cc.id AND + cr.campsite_id = cs.id AND + cr.update_level = 1 + ) classes ON r.id = classes.campsite_rate_id""" + ), + migrations.RunSQL( + """CREATE OR REPLACE VIEW parkstay_campground_pricehistory_v AS + SELECT DISTINCT camps.campground_id AS id, + cr.date_start, + cr.date_end, + r.id AS rate_id, + r.adult, + r.concession, + r.child + FROM parkstay_campsiterate cr + INNER JOIN parkstay_rate r ON r.id = cr.rate_id + INNER JOIN ( + SELECT cg.id AS campground_id, + cs.name AS name, + cs.id AS campsite_id + FROM parkstay_campsite cs, + parkstay_campground cg + WHERE cs.campground_id = cg.id AND + cg.id = cs.campground_id AND + cg.price_level = 0 + ) camps ON cr.campsite_id = camps.campsite_id""" + ) + ] diff --git a/parkstay/migrations/0021_campgroundimage.py b/parkstay/migrations/0021_campgroundimage.py new file mode 100644 index 0000000000..e1ec0b519c --- /dev/null +++ b/parkstay/migrations/0021_campgroundimage.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-06 02:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0020_create_views'), + ] + + operations = [ + migrations.CreateModel( + name='CampgroundImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='/home/corporateict.domain/briank/apps/work/ledger-parkstay/media/Parkstay/CampgroudImages')), + ('campground', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='parkstay.Campground')), + ], + ), + ] diff --git a/parkstay/migrations/0022_auto_20161206_1428.py b/parkstay/migrations/0022_auto_20161206_1428.py new file mode 100644 index 0000000000..df4f3e98a0 --- /dev/null +++ b/parkstay/migrations/0022_auto_20161206_1428.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-06 06:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0021_campgroundimage'), + ] + + operations = [ + migrations.AlterField( + model_name='campgroundimage', + name='image', + field=models.ImageField(max_length=255, upload_to='/home/corporateict.domain/briank/apps/work/ledger-parkstay/media/Parkstay/CampgroudImages'), + ), + ] diff --git a/parkstay/migrations/0023_auto_20161207_1015.py b/parkstay/migrations/0023_auto_20161207_1015.py new file mode 100644 index 0000000000..da2417d10c --- /dev/null +++ b/parkstay/migrations/0023_auto_20161207_1015.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-07 02:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0022_auto_20161206_1428'), + ] + + operations = [ + migrations.AlterModelOptions( + name='campgroundimage', + options={'ordering': ('id',)}, + ), + migrations.AddField( + model_name='campgroundimage', + name='checksum', + field=models.CharField(blank=True, editable=False, max_length=255), + ), + ] diff --git a/parkstay/migrations/0024_closurereason_maximumstayreason_pricereason.py b/parkstay/migrations/0024_closurereason_maximumstayreason_pricereason.py new file mode 100644 index 0000000000..862139f3de --- /dev/null +++ b/parkstay/migrations/0024_closurereason_maximumstayreason_pricereason.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-08 02:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0023_auto_20161207_1015'), + ] + + operations = [ + migrations.CreateModel( + name='ClosureReason', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ], + options={ + 'ordering': ('id',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='MaximumStayReason', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ], + options={ + 'ordering': ('id',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PriceReason', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ], + options={ + 'ordering': ('id',), + 'abstract': False, + }, + ), + ] diff --git a/parkstay/migrations/0025_auto_20161208_1056.py b/parkstay/migrations/0025_auto_20161208_1056.py new file mode 100644 index 0000000000..34ed5968ae --- /dev/null +++ b/parkstay/migrations/0025_auto_20161208_1056.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-08 02:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0024_closurereason_maximumstayreason_pricereason'), + ] + + operations = [ + migrations.AlterModelOptions( + name='closurereason', + options={'ordering': ('id',), 'verbose_name': 'Availability Reason', 'verbose_name_plural': 'Availability Reasons'}, + ), + migrations.AddField( + model_name='closurereason', + name='deletable', + field=models.BooleanField(default=True, editable=False), + ), + migrations.AddField( + model_name='maximumstayreason', + name='deletable', + field=models.BooleanField(default=True, editable=False), + ), + migrations.AddField( + model_name='pricereason', + name='deletable', + field=models.BooleanField(default=True, editable=False), + ), + ] diff --git a/parkstay/migrations/0026_auto_20161208_1103.py b/parkstay/migrations/0026_auto_20161208_1103.py new file mode 100644 index 0000000000..46c19a9d61 --- /dev/null +++ b/parkstay/migrations/0026_auto_20161208_1103.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-08 03:03 +from __future__ import unicode_literals +import os +from django.db import migrations + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0025_auto_20161208_1056'), + ] + + operations = [ + migrations.RenameField( + model_name='closurereason', + old_name='deletable', + new_name='editable', + ), + migrations.RenameField( + model_name='maximumstayreason', + old_name='deletable', + new_name='editable', + ), + migrations.RenameField( + model_name='pricereason', + old_name='deletable', + new_name='editable', + ) + ] diff --git a/parkstay/migrations/0027_auto_20161208_1233.py b/parkstay/migrations/0027_auto_20161208_1233.py new file mode 100644 index 0000000000..ef1c8e8b05 --- /dev/null +++ b/parkstay/migrations/0027_auto_20161208_1233.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-08 04:33 +from __future__ import unicode_literals +import os + +from django.db import migrations, models +import django.db.models.deletion +from django.core.management import call_command + +fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) + +def load_fixture(apps, schema_editor): + call_command('loaddata', os.path.join(fixture_dir, 'closure_reasons.json')) + call_command('loaddata', os.path.join(fixture_dir, 'open_reasons.json')) + call_command('loaddata', os.path.join(fixture_dir, 'price_reasons.json')) + call_command('loaddata', os.path.join(fixture_dir, 'max_stay_reasons.json')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0026_auto_20161208_1103'), + ] + + operations = [ + migrations.CreateModel( + name='OpenReason', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('editable', models.BooleanField(default=True, editable=False)), + ], + options={ + 'ordering': ('id',), + 'abstract': False, + }, + ), + migrations.AlterModelOptions( + name='closurereason', + options={'ordering': ('id',)}, + ), + migrations.AddField( + model_name='campgroundbookingrange', + name='closure_reason', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='parkstay.ClosureReason'), + ), + migrations.AddField( + model_name='campsitebookingrange', + name='closure_reason', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='parkstay.ClosureReason'), + ), + migrations.AlterField( + model_name='campgroundbookingrange', + name='status', + field=models.SmallIntegerField(choices=[(0, 'Open'), (1, 'Closed')], default=0), + ), + migrations.AlterField( + model_name='campsitebookingrange', + name='status', + field=models.SmallIntegerField(choices=[(0, 'Open'), (1, 'Closed')], default=0), + ), + migrations.AddField( + model_name='campgroundbookingrange', + name='open_reason', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='parkstay.OpenReason'), + ), + migrations.AddField( + model_name='campsitebookingrange', + name='open_reason', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='parkstay.OpenReason'), + ), + migrations.RunPython(load_fixture) + ] diff --git a/parkstay/migrations/0028_auto_20161208_1544.py b/parkstay/migrations/0028_auto_20161208_1544.py new file mode 100644 index 0000000000..30934391fa --- /dev/null +++ b/parkstay/migrations/0028_auto_20161208_1544.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-08 07:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0027_auto_20161208_1233'), + ] + + operations = [ + migrations.AddField( + model_name='campsiterate', + name='details', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='campsiterate', + name='reason', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='parkstay.PriceReason'), + preserve_default=False, + ), + migrations.AddField( + model_name='campsitestayhistory', + name='reason', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='parkstay.MaximumStayReason'), + preserve_default=False, + ), + ] diff --git a/parkstay/migrations/0029_auto_20161212_1153.py b/parkstay/migrations/0029_auto_20161212_1153.py new file mode 100644 index 0000000000..783aff4aff --- /dev/null +++ b/parkstay/migrations/0029_auto_20161212_1153.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-12 03:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0028_auto_20161208_1544'), + ] + + operations = [ + migrations.RemoveField( + model_name='campground', + name='bookable_online', + ), + migrations.RemoveField( + model_name='campground', + name='bookable_per_site', + ), + migrations.AlterField( + model_name='campground', + name='campground_type', + field=models.SmallIntegerField(choices=[(0, 'Bookable Online'), (1, 'Not Bookable Online')], default=0), + ), + migrations.AlterField( + model_name='campground', + name='site_type', + field=models.SmallIntegerField(choices=[(0, 'Bookable Per Site'), (1, 'Bookable Per Site Type')], default=0), + ), + ] diff --git a/parkstay/migrations/0030_feature_type.py b/parkstay/migrations/0030_feature_type.py new file mode 100644 index 0000000000..0323e5bd41 --- /dev/null +++ b/parkstay/migrations/0030_feature_type.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-12 04:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0029_auto_20161212_1153'), + ] + + operations = [ + migrations.AddField( + model_name='feature', + name='type', + field=models.SmallIntegerField(choices=[(0, 'Campground'), (1, 'Campsite'), (2, 'Not Linked')], default=2, help_text='Set the model where the feature is located.'), + ), + ] diff --git a/parkstay/migrations/0031_auto_20161212_1309.py b/parkstay/migrations/0031_auto_20161212_1309.py new file mode 100644 index 0000000000..2ceeee19d4 --- /dev/null +++ b/parkstay/migrations/0031_auto_20161212_1309.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-12-12 05:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0030_feature_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='campsite', + name='cs_dimensions', + ), + migrations.RemoveField( + model_name='campsite', + name='cs_number_vehicles', + ), + migrations.RemoveField( + model_name='campsite', + name='cs_parking_spaces', + ), + migrations.RemoveField( + model_name='campsite', + name='cs_tents', + ), + migrations.RemoveField( + model_name='campsiteclass', + name='dimensions', + ), + migrations.RemoveField( + model_name='campsiteclass', + name='number_vehicles', + ), + migrations.RemoveField( + model_name='campsiteclass', + name='parking_spaces', + ), + migrations.RemoveField( + model_name='campsiteclass', + name='tents', + ), + migrations.AddField( + model_name='campsite', + name='cs_campervan', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='campsite', + name='cs_caravan', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='campsite', + name='cs_description', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='campsite', + name='cs_tent', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='campsiteclass', + name='campervan', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='campsiteclass', + name='caravan', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='campsiteclass', + name='description', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='campsiteclass', + name='tent', + field=models.BooleanField(default=False), + ), + ] diff --git a/parkstay/migrations/0032_updateviews.py b/parkstay/migrations/0032_updateviews.py new file mode 100644 index 0000000000..c4bc821d7f --- /dev/null +++ b/parkstay/migrations/0032_updateviews.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-12-02 07:15 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0031_auto_20161212_1309'), + ] + + operations = [ + migrations.RunSQL( + """CREATE OR REPLACE VIEW parkstay_campsiteclass_pricehistory_v AS + SELECT DISTINCT classes.campsite_class_id AS id, + classes.date_start, + classes.date_end, + r.id AS rate_id, + r.adult, + r.concession, + r.child, + classes.details, + classes.reason_id + FROM parkstay_rate r + INNER JOIN ( + SELECT distinct cc.id AS campsite_class_id, + cr.rate_id AS campsite_rate_id, + cr.date_start AS date_start, + cr.date_end AS date_end, + cr.details AS details, + cr.reason_id AS reason_id + FROM parkstay_campsite cs, + parkstay_campsiteclass cc, + parkstay_campsiterate cr + WHERE cs.campsite_class_id = cc.id AND + cr.campsite_id = cs.id AND + cr.update_level = 1 + ) classes ON r.id = classes.campsite_rate_id""" + ), + migrations.RunSQL( + """CREATE OR REPLACE VIEW parkstay_campground_pricehistory_v AS + SELECT DISTINCT camps.campground_id AS id, + cr.date_start, + cr.date_end, + r.id AS rate_id, + r.adult, + r.concession, + r.child, + cr.details, + cr.reason_id + FROM parkstay_campsiterate cr + INNER JOIN parkstay_rate r ON r.id = cr.rate_id + INNER JOIN ( + SELECT cg.id AS campground_id, + cs.name AS name, + cs.id AS campsite_id + FROM parkstay_campsite cs, + parkstay_campground cg + WHERE cs.campground_id = cg.id AND + cg.id = cs.campground_id AND + cg.price_level = 0 + ) camps ON cr.campsite_id = camps.campsite_id""" + ) + ] diff --git a/parkstay/migrations/0033_auto_20161219_1125.py b/parkstay/migrations/0033_auto_20161219_1125.py new file mode 100644 index 0000000000..c93aa8f292 --- /dev/null +++ b/parkstay/migrations/0033_auto_20161219_1125.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-12-19 03:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import parkstay.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0032_updateviews'), + ] + + operations = [ + migrations.AlterField( + model_name='campground', + name='campground_type', + field=models.SmallIntegerField(choices=[(0, 'Bookable Online'), (1, 'Not Bookable Online'), (2, 'Other Accomodation')], default=0), + ), + migrations.AlterField( + model_name='campground', + name='park', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='campgrounds', to='parkstay.Park'), + ), + migrations.AlterField( + model_name='campgroundimage', + name='image', + field=models.ImageField(max_length=255, upload_to=parkstay.models.campground_image_path), + ), + ] diff --git a/parkstay/migrations/0034_auto_20161222_1642.py b/parkstay/migrations/0034_auto_20161222_1642.py new file mode 100644 index 0000000000..5582918b83 --- /dev/null +++ b/parkstay/migrations/0034_auto_20161222_1642.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-12-22 08:42 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0033_auto_20161219_1125'), + ] + + operations = [ + migrations.RenameField( + model_name='campsite', + old_name='cs_campervan', + new_name='campervan', + ), + migrations.RenameField( + model_name='campsite', + old_name='cs_caravan', + new_name='caravan', + ), + migrations.RenameField( + model_name='campsite', + old_name='cs_description', + new_name='description', + ), + migrations.RenameField( + model_name='campsite', + old_name='cs_max_people', + new_name='max_people', + ), + migrations.RenameField( + model_name='campsite', + old_name='cs_min_people', + new_name='min_people', + ), + migrations.RenameField( + model_name='campsite', + old_name='cs_tent', + new_name='tent', + ), + ] diff --git a/parkstay/migrations/0035_auto_20161228_1735.py b/parkstay/migrations/0035_auto_20161228_1735.py new file mode 100644 index 0000000000..fea160f1b8 --- /dev/null +++ b/parkstay/migrations/0035_auto_20161228_1735.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-12-28 09:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkstay', '0034_auto_20161222_1642'), + ] + + operations = [ + migrations.AlterField( + model_name='campsite', + name='tent', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='campsiteclass', + name='tent', + field=models.BooleanField(default=True), + ), + ] diff --git a/parkstay/models.py b/parkstay/models.py index 9bb2d6df31..7f409e1743 100644 --- a/parkstay/models.py +++ b/parkstay/models.py @@ -1,19 +1,60 @@ from __future__ import unicode_literals +import os +import uuid +import base64 +import binascii +import hashlib +from django.core.files.base import ContentFile +from django.core.exceptions import ValidationError +from django.db.models import Q from django.contrib.gis.db import models from django.contrib.postgres.fields import JSONField +from django.db import IntegrityError, transaction, connection +from django.utils import timezone +from datetime import date, time, datetime, timedelta +from django.conf import settings +from taggit.managers import TaggableManager +from django.dispatch import receiver +from django.db.models.signals import post_delete, pre_save, post_save +from parkstay.exceptions import BookingRangeWithinException # Create your models here. +PARKING_SPACE_CHOICES = ( + (0, 'Parking within site.'), + (1, 'Parking for exclusive use of site occupiers next to site, but separated from tent space.'), + (2, 'Parking for exclusive use of occupiers, short walk from tent space.'), + (3, 'Shared parking (not allocated), short walk from tent space.') +) + +NUMBER_VEHICLE_CHOICES = ( + (0, 'One vehicle'), + (1, 'Two vehicles'), + (2, 'One vehicle + small trailer'), + (3, 'One vehicle + small trailer/large vehicle') +) + +class CustomerContact(models.Model): + name = models.CharField(max_length=255, unique=True) + phone_number = models.CharField(max_length=50, null=True, blank=True) + email = models.EmailField(max_length=255) + description = models.TextField() + opening_hours = models.TextField() + other_services = models.TextField() + + class Park(models.Model): name = models.CharField(max_length=255) - region = models.ForeignKey('Region', on_delete=models.PROTECT) - + district = models.ForeignKey('District', null=True, on_delete=models.PROTECT) + ratis_id = models.IntegerField(default=-1) + entry_fee_required = models.BooleanField(default=True) + def __str__(self): - return '{} - {}'.format(self.name, self.region) + return '{} - {}'.format(self.name, self.district) class Meta: - unique_together = (('name', 'region'),) + unique_together = (('name',),) class PromoArea(models.Model): @@ -22,51 +63,387 @@ class PromoArea(models.Model): def __str__(self): return self.name +class Contact(models.Model): + name = models.CharField(max_length=255) + phone_number = models.CharField(max_length=12) + + def __str__(self): + return "{}: {}".format(self.name, self.phone_number) class Campground(models.Model): CAMPGROUND_TYPE_CHOICES = ( - (0, 'Campground: no bookings'), - (1, 'Campground: book online'), - (2, 'Campground: book by phone'), - (3, 'Other accomodation') + (0, 'Bookable Online'), + (1, 'Not Bookable Online'), + (2, 'Other Accomodation'), + ) + CAMPGROUND_PRICE_LEVEL_CHOICES = ( + (0, 'Campground level'), + (1, 'Campsite Class level'), + (2, 'Campsite level'), ) - SITE_TYPE_CHOICES = ( - (0, 'Unnumbered Site'), - (1, 'Numbered site') + (0, 'Bookable Per Site'), + (1, 'Bookable Per Site Type') ) name = models.CharField(max_length=255, null=True) - park = models.ForeignKey('Park', on_delete=models.PROTECT) + park = models.ForeignKey('Park', on_delete=models.PROTECT, related_name='campgrounds') + ratis_id = models.IntegerField(default=-1) + contact = models.ForeignKey('Contact', on_delete=models.PROTECT, blank=True, null=True) campground_type = models.SmallIntegerField(choices=CAMPGROUND_TYPE_CHOICES, default=0) - promo_area = models.ForeignKey('PromoArea', on_delete=models.PROTECT, null=True) + promo_area = models.ForeignKey('PromoArea', on_delete=models.PROTECT,blank=True, null=True) site_type = models.SmallIntegerField(choices=SITE_TYPE_CHOICES, default=0) address = JSONField(null=True) - features = models.ManyToManyField('CampgroundFeature') + features = models.ManyToManyField('Feature') description = models.TextField(blank=True, null=True) - rules = models.TextField(blank=True, null=True) area_activities = models.TextField(blank=True, null=True) + # Tags for communications methods available and access type + tags = TaggableManager(blank=True) driving_directions = models.TextField(blank=True, null=True) fees = models.TextField(blank=True, null=True) othertransport = models.TextField(blank=True, null=True) key = models.CharField(max_length=255, blank=True, null=True) - is_published = models.BooleanField(default=False) + price_level = models.SmallIntegerField(choices=CAMPGROUND_PRICE_LEVEL_CHOICES, default=0) + customer_contact = models.ForeignKey('CustomerContact', blank=True, null=True, on_delete=models.PROTECT) + wkb_geometry = models.PointField(srid=4326, blank=True, null=True) - metakeywords = models.TextField(blank=True, null=True) + dog_permitted = models.BooleanField(default=False) + check_in = models.TimeField(default=time(14)) + check_out = models.TimeField(default=time(10)) def __str__(self): return self.name class Meta: - unique_together = (('name','park'),) + unique_together = (('name', 'park'),) + + # Properties + # ======================================= + @property + def region(self): + return self.park.district.region.name + + @property + def active(self): + return self._is_open(datetime.now().date()) + + @property + def current_closure(self): + closure = self._get_current_closure() + if closure: + return 'Start: {} End: {}'.format(closure.range_start, closure.range_end) + + @property + def dog_permitted(self): + try: + self.features.get(name='NO DOGS') + return False + except Feature.DoesNotExist: + return True + + @property + def campfires_allowed(self): + try: + self.features.get(name='NO CAMPFIRES') + return False + except Feature.DoesNotExist: + return True + + # Methods + # ======================================= + def _is_open(self,period): + '''Check if the campground is open on a specified datetime + ''' + open_ranges, closed_ranges = None, None + # Get all booking ranges + try: + open_ranges = self.booking_ranges.filter(Q(status=0),Q(range_start__lte=period), Q(range_end__gte=period) | Q(range_end__isnull=True) ).latest('updated_on') + except CampgroundBookingRange.DoesNotExist: + pass + try: + closed_ranges = self.booking_ranges.filter(Q(range_start__lte=period),~Q(status=0),Q(range_end__gte=period) | Q(range_end__isnull=True) ).latest('updated_on') + except CampgroundBookingRange.DoesNotExist: + return True if open_ranges else False + + if not open_ranges: + return False + if open_ranges.updated_on > closed_ranges.updated_on: + return True + return False + + def _get_current_closure(self): + closure_period = None + period = datetime.now().date() + if not self.active: + closure = self.booking_ranges.get(Q(range_start__lte=period),~Q(status=0),Q(range_end__isnull=True) |Q(range_end__gte=period)) + closure_period = closure + return closure_period + + def open(self, data): + if self.active: + raise ValidationError('This campground is already open.') + b = CampgroundBookingRange(**data) + try: + within = CampgroundBookingRange.objects.filter(Q(campground=b.campground),Q(status=0),Q(range_start__lte=b.range_start), Q(range_end__gte=b.range_start) | Q(range_end__isnull=True) ).latest('updated_on') + if within: + within.updated_on = timezone.now() + within.save(skip_validation=True) + except CampgroundBookingRange.DoesNotExist: + #if (self.__get_current_closure().range_start <= b.range_start and not self.__get_current_closure().range_end) or (self.__get_current_closure().range_start <= b.range_start <= self.__get_current_closure().range_end): + # self.__get_current_closure().delete() + b.save() + + def close(self, data): + if not self.active: + raise ValidationError('This campground is already closed.') + b = CampgroundBookingRange(**data) + try: + within = CampgroundBookingRange.objects.filter(Q(campground=b.campground),~Q(status=0),Q(range_start__lte=b.range_start), Q(range_end__gte=b.range_start) | Q(range_end__isnull=True) ).latest('updated_on') + if within: + within.updated_on = timezone.now() + within.save(skip_validation=True) + except CampgroundBookingRange.DoesNotExist: + b.save() + + def createCampsitePriceHistory(self,data): + '''Create Multiple campsite rates + ''' + try: + with transaction.atomic(): + for c in self.campsites.all(): + cr = CampsiteRate(**data) + cr.campsite = c + cr.save() + except Exception as e: + raise + + def updatePriceHistory(self,original,_new): + '''Update Multiple campsite rates + ''' + try: + rates = CampsiteRate.objects.filter(**original) + campsites = self.campsites.all() + with transaction.atomic(): + for r in rates: + if r.campsite in campsites and r.update_level == 0: + r.update(_new) + except Exception as e: + raise + + def deletePriceHistory(self,data): + '''Delete Multiple campsite rates + ''' + try: + rates = CampsiteRate.objects.filter(**data) + campsites = self.campsites.all() + with transaction.atomic(): + for r in rates: + if r.campsite in campsites and r.update_level == 0: + r.delete() + except Exception as e: + raise + +def campground_image_path(instance, filename): + return '/'.join(['parkstay', 'campground_images', filename]) + +class CampgroundImage(models.Model): + image = models.ImageField(max_length=255, upload_to=campground_image_path) + campground = models.ForeignKey(Campground, related_name='images') + checksum = models.CharField(blank=True, max_length=255, editable=False) + + class Meta: + ordering = ('id',) + def get_file_extension(self, file_name, decoded_file): + import imghdr + + extension = imghdr.what(file_name, decoded_file) + extension = "jpg" if extension == "jpeg" else extension + return extension + + def strip_b64_header(self, content): + if ';base64,' in content: + header, base64_data = content.split(';base64,') + return base64_data + return content + + def _calculate_checksum(self, content): + checksum = hashlib.md5() + checksum.update(content.read()) + return base64.b64encode(checksum.digest()) + + def createImage(self, content): + base64_data = self.strip_b64_header(content) + try: + decoded_file = base64.b64decode(base64_data) + except (TypeError, binascii.Error): + raise ValidationError(self.INVALID_FILE_MESSAGE) + file_name = str(uuid.uuid4())[:12] + file_extension = self.get_file_extension(file_name,decoded_file) + complete_file_name = "{}.{}".format(file_name, file_extension) + uploaded_image = ContentFile(decoded_file, name=complete_file_name) + return uploaded_image + + def save(self, *args, **kwargs): + self.checksum = self._calculate_checksum(self.image) + self.image.seek(0) + if not self.pk: + self.image = self.createImage(base64.b64encode(self.image.read())) + else: + orig = CampgroundImage.objects.get(pk=self.pk) + if orig.image: + if orig.checksum != self.checksum: + if os.path.isfile(orig.image.path): + os.remove(orig.image) + self.image = self.createImage(base64.b64encode(self.image.read())) + else: + pass + + super(CampgroundImage,self).save(*args,**kwargs) + + def delete(self, *args, **kwargs): + try: + os.remove(self.image) + except: + pass + super(CampgroundImage,self).delete(*args,**kwargs) + +class BookingRange(models.Model): + BOOKING_RANGE_CHOICES = ( + (0, 'Open'), + (1, 'Closed'), + ) + created = models.DateTimeField(auto_now_add=True) + updated_on = models.DateTimeField(auto_now_add=True,help_text='Used to check if the start and end dated were changed') + + status = models.SmallIntegerField(choices=BOOKING_RANGE_CHOICES, default=0) + closure_reason = models.ForeignKey('ClosureReason',null=True,blank=True) + open_reason = models.ForeignKey('OpenReason',null=True,blank=True) + details = models.TextField(blank=True,null=True) + range_start = models.DateField(blank=True, null=True) + range_end = models.DateField(blank=True, null=True) + + class Meta: + abstract = True + + # Properties + # ==================================== + @property + def editable(self): + today = datetime.now().date() + if self.status != 0 and((self.range_start <= today and not self.range_end) or (self.range_start > today and not self.range_end) or ( self.range_start > datetime.now().date() <= self.range_end)): + return True + elif self.status == 0 and ((self.range_start <= today and not self.range_end) or self.range_start > today): + return True + return False + + @property + def reason(self): + if self.status == 0: + return self.open_reason.text + return self.closure_reason.text + + # Methods + # ===================================== + def _is_same(self,other): + if not isinstance(other, BookingRange) and self.id != other.id: + return False + if self.range_start == other.range_start and self.range_end == other.range_end: + return True + return False + + def save(self, *args, **kwargs): + skip_validation = bool(kwargs.pop('skip_validation',False)) + if not skip_validation: + self.full_clean() + if self.status == 1 and not self.closure_reason: + self.closure_reason = ClosureReason.objects.get(pk=1) + elif self.status == 0 and not self.open_reason: + self.open_reason = OpenReason.objects.get(pk=1) + + super(BookingRange, self).save(*args, **kwargs) + +class StayHistory(models.Model): + created = models.DateTimeField(auto_now_add=True) + # minimum/maximum consecutive days allowed for a booking + min_days = models.SmallIntegerField(default=1) + max_days = models.SmallIntegerField(default=28) + # Minimum and Maximum days that a booking can be made before arrival + min_dba = models.SmallIntegerField(default=0) + max_dba = models.SmallIntegerField(default=180) + + reason = models.ForeignKey('MaximumStayReason') + details = models.TextField(blank=True,null=True) + range_start = models.DateField(blank=True, null=True) + range_end = models.DateField(blank=True, null=True) + + class Meta: + abstract = True + + # Properties + # ==================================== + @property + def editable(self): + now = datetime.now().date() + if (self.range_start <= now and not self.range_end) or ( self.range_start <= now <= self.range_end): + return True + elif (self.range_start >= now and not self.range_end) or ( self.range_start >= now <= self.range_end): + return True + return False + + # Methods + # ===================================== + def clean(self, *args, **kwargs): + if self.min_days < 1: + raise ValidationError('The minimum days should be greater than 0.') + if self.max_days > 28: + raise ValidationError('The maximum days should not be grater than 28.') + +class CampgroundBookingRange(BookingRange): + campground = models.ForeignKey('Campground', on_delete=models.CASCADE,related_name='booking_ranges') + # minimum/maximum number of campsites allowed for a booking + min_sites = models.SmallIntegerField(default=1) + max_sites = models.SmallIntegerField(default=12) + + # Properties + # ==================================== + + # Methods + # ===================================== + def _is_same(self,other): + if not isinstance(other, CampgroundBookingRange) and self.id != other.id: + return False + if self.range_start == other.range_start and self.range_end == other.range_end: + return True + return False + + def clean(self, *args, **kwargs): + original = None + + # Preventing ranges within other ranges + within = CampgroundBookingRange.objects.filter(Q(campground=self.campground),~Q(pk=self.pk),Q(status=self.status),Q(range_start__lte=self.range_start), Q(range_end__gte=self.range_start) | Q(range_end__isnull=True) ) + if within: + raise BookingRangeWithinException('This Booking Range is within the range of another one') + if self.pk: + original = CampgroundBookingRange.objects.get(pk=self.pk) + if not original.editable: + raise ValidationError('This Booking Range is not editable') + if self.range_start < datetime.now().date() and original.range_start != self.range_start: + raise ValidationError('The start date can\'t be in the past') + class Campsite(models.Model): - campground = models.ForeignKey('Campground', db_index=True, on_delete=models.PROTECT) + campground = models.ForeignKey('Campground', db_index=True, on_delete=models.PROTECT, related_name='campsites') name = models.CharField(max_length=255) - campsite_class = models.ForeignKey('CampsiteClass', on_delete=models.PROTECT) - max_people = models.SmallIntegerField(default=6) + campsite_class = models.ForeignKey('CampsiteClass', on_delete=models.PROTECT, null=True,blank=True, related_name='campsites') wkb_geometry = models.PointField(srid=4326, blank=True, null=True) + features = models.ManyToManyField('Feature') + tent = models.BooleanField(default=True) + campervan = models.BooleanField(default=False) + caravan = models.BooleanField(default=False) + min_people = models.SmallIntegerField(default=1) + max_people = models.SmallIntegerField(default=12) + description = models.TextField(null=True) def __str__(self): return '{} - {}'.format(self.campground, self.name) @@ -74,11 +451,170 @@ def __str__(self): class Meta: unique_together = (('campground', 'name'),) + # Properties + # ============================== + @property + def type(self): + return self.campsite_class.name + + @property + def price(self): + return 'Set at {}'.format(self.campground.get_price_level_display()) + + @property + def can_add_rate(self): + return self.campground.price_level == 2 + + @property + def active(self): + return self._is_open(datetime.now().date()) + + @property + def campground_open(self): + return self.__is_campground_open() + + @property + def current_closure(self): + closure = self.__get_current_closure() + if closure: + return 'Start: {} End: {}'.format(closure.range_start, closure.range_end) + # Methods + # ======================================= + def __is_campground_open(self): + return self.campground.active + + def _is_open(self,period): + '''Check if the campsite is open on a specified datetime + ''' + if self.__is_campground_open(): + open_ranges, closed_ranges = None, None + # Get all booking ranges + try: + open_ranges = self.booking_ranges.filter(Q(status=0),Q(range_start__lte=period), Q(range_end__gte=period) | Q(range_end__isnull=True) ).latest('updated_on') + except CampsiteBookingRange.DoesNotExist: + pass + try: + closed_ranges = self.booking_ranges.filter(Q(range_start__lte=period),~Q(status=0),Q(range_end__gte=period) | Q(range_end__isnull=True) ).latest('updated_on') + except CampsiteBookingRange.DoesNotExist: + return True if open_ranges else False + + if not open_ranges: + return False + if open_ranges.updated_on > closed_ranges.updated_on: + return True + return False + + def __get_current_closure(self): + if self.__is_campground_open(): + closure_period = None + period = datetime.now().date() + if not self.active: + closure = self.booking_ranges.get(Q(range_start__lte=period),~Q(status=0),Q(range_end__isnull=True) |Q(range_end__gte=period)) + closure_period = closure + return closure_period + else: + return self.campground._get_current_closure() + + def open(self, data): + if not self.campground_open: + raise ValidationError('You can\'t open this campsite until the campground is open') + if self.active: + raise ValidationError('This campsite is already open.') + b = CampsiteBookingRange(**data) + try: + within = CampsiteBookingRange.objects.filter(Q(campsite=b.campsite),Q(status=0),Q(range_start__lte=b.range_start), Q(range_end__gte=b.range_start) | Q(range_end__isnull=True) ).latest('updated_on') + if within: + within.updated_on = timezone.now() + within.save(skip_validation=True) + except CampsiteBookingRange.DoesNotExist: + b.save() + + def close(self, data): + if not self.active: + raise ValidationError('This campsite is already closed.') + b = CampsiteBookingRange(**data) + try: + within = CampsiteBookingRange.objects.filter(Q(campsite=b.campsite),~Q(status=0),Q(range_start__lte=b.range_start), Q(range_end__gte=b.range_start) | Q(range_end__isnull=True) ).latest('updated_on') + if within: + within.updated_on = timezone.now() + within.save(skip_validation=True) + except CampsiteBookingRange.DoesNotExist: + b.save() + + @staticmethod + def bulk_create(number,data): + try: + created_campsites = [] + with transaction.atomic(): + campsites = [] + latest = 0 + current_campsites = Campsite.objects.filter(campground=data['campground']) + cs_numbers = [int(c.name) for c in current_campsites if c.name.isdigit()] + if cs_numbers: + latest = max(cs_numbers) + for i in range(number): + latest += 1 + c = Campsite(**data) + name = str(latest) + if len(name) == 1: + name = '0{}'.format(name) + c.name = name + c.save() + if c.campsite_class: + for attr in ['tent', 'campervan', 'caravan', 'min_people', 'max_people', 'description']: + if attr not in data: + setattr(c, attr, getattr(c.campsite_class, attr)) + c.features = c.campsite_class.features.all() + c.save() + created_campsites.append(c) + return created_campsites + except Exception: + raise + +class CampsiteBookingRange(BookingRange): + campsite = models.ForeignKey('Campsite', on_delete=models.PROTECT,related_name='booking_ranges') + + # Properties + # ==================================== + + # Methods + # ===================================== + def _is_same(self,other): + if not isinstance(other, CampsiteBookingRange) and self.id != other.id: + return False + if self.range_start == other.range_start and self.range_end == other.range_end: + return True + return False + + def clean(self, *args, **kwargs): + original = None + + # Preventing ranges within other ranges + within = CampsiteBookingRange.objects.filter(Q(campsite=self.campsite),~Q(pk=self.pk),Q(status=self.status),Q(range_start__lte=self.range_start), Q(range_end__gte=self.range_start) | Q(range_end__isnull=True) ) + if within: + raise BookingRangeWithinException('This Booking Range is within the range of another one') + if self.pk: + original = CampsiteBookingRange.objects.get(pk=self.pk) + if not original.editable: + raise ValidationError('This Booking Range is not editable') + if self.range_start < datetime.now().date() and original.range_start != self.range_start: + raise ValidationError('The start date can\'t be in the past') -class CampgroundFeature(models.Model): + +class CampsiteStayHistory(StayHistory): + campsite = models.ForeignKey('Campsite', on_delete=models.PROTECT,related_name='stay_history') + + +class Feature(models.Model): + TYPE_CHOICES = ( + (0, 'Campground'), + (1, 'Campsite'), + (2, 'Not Linked') + ) name = models.CharField(max_length=255, unique=True) description = models.TextField(null=True) image = models.ImageField(null=True) + type = models.SmallIntegerField(choices=TYPE_CHOICES,default=2,help_text="Set the model where the feature is located.") def __str__(self): return self.name @@ -86,23 +622,96 @@ def __str__(self): class Region(models.Model): name = models.CharField(max_length=255, unique=True) + abbreviation = models.CharField(max_length=16, null=True, unique=True) + ratis_id = models.IntegerField(default=-1) + + def __str__(self): + return self.name + + +class District(models.Model): + name = models.CharField(max_length=255, unique=True) + abbreviation = models.CharField(max_length=16, null=True, unique=True) + region = models.ForeignKey('Region', on_delete=models.PROTECT) + ratis_id = models.IntegerField(default=-1) def __str__(self): return self.name class CampsiteClass(models.Model): + name = models.CharField(max_length=255, unique=True) - tents = models.SmallIntegerField(default=0) - parking_spaces = models.SmallIntegerField(default=0) - allow_campervan = models.BooleanField(default=False) - allow_trailer = models.BooleanField(default=False) - allow_generator = models.BooleanField(default=False) + camp_unit_suitability = TaggableManager() + tent = models.BooleanField(default=True) + campervan = models.BooleanField(default=False) + caravan = models.BooleanField(default=False) + min_people = models.SmallIntegerField(default=1) + max_people = models.SmallIntegerField(default=12) + features = models.ManyToManyField('Feature') + deleted = models.BooleanField(default=False) + description = models.TextField(null=True) def __str__(self): return self.name + def delete(self, permanently=False,using=None): + if not permanently: + self.deleted = True + self.save() + else: + super(CampsiteClass, self).delete(using) + + # Property + # =========================== + def can_add_rate(self): + can_add = False + campsites = self.campsites.all() + for c in campsites: + if c.campground.price_level == 1: + can_add = True + break + return can_add + # Methods + # =========================== + def createCampsitePriceHistory(self,data): + '''Create Multiple campsite rates + ''' + try: + with transaction.atomic(): + for c in self.campsites.all(): + cr = CampsiteRate(**data) + cr.campsite = c + cr.save() + except Exception as e: + raise + + def updatePriceHistory(self,original,_new): + '''Update Multiple campsite rates + ''' + try: + rates = CampsiteRate.objects.filter(**original) + campsites = self.campsites.all() + with transaction.atomic(): + for r in rates: + if r.campsite in campsites and r.update_level == 1: + r.update(_new) + except Exception as e: + raise + + def deletePriceHistory(self,data): + '''Delete Multiple campsite rates + ''' + try: + rates = CampsiteRate.objects.filter(**data) + campsites = self.campsites.all() + with transaction.atomic(): + for r in rates: + if r.campsite in campsites and r.update_level == 1: + r.delete() + except Exception as e: + raise class CampsiteBooking(models.Model): BOOKING_TYPE_CHOICES = ( (0, 'Reception booking'), @@ -122,6 +731,83 @@ class Meta: unique_together = (('campsite', 'date'),) +class Rate(models.Model): + adult = models.DecimalField(max_digits=8, decimal_places=2, default='10.00') + concession = models.DecimalField(max_digits=8, decimal_places=2, default='6.60') + child = models.DecimalField(max_digits=8, decimal_places=2, default='2.20') + infant = models.DecimalField(max_digits=8, decimal_places=2, default='0') + + def __str__(self): + return 'adult: ${}, concession: ${}, child: ${}, infant: ${}'.format(self.adult, self.concession, self.child, self.infant) + + class Meta: + unique_together = (('adult', 'concession', 'child', 'infant'),) + + # Properties + # ================================= + @property + def name(self): + return 'adult: ${}, concession: ${}, child: ${}, infant: ${}'.format(self.adult, self.concession, self.child, self.infant) + +class CampsiteRate(models.Model): + RATE_TYPE_CHOICES = ( + (0, 'Standard'), + (1, 'Discounted'), + ) + + UPDATE_LEVEL_CHOICES = ( + (0, 'Campground level'), + (1, 'Campsite Class level'), + (2, 'Campsite level'), + ) + PRICE_MODEL_CHOICES = ( + (0, 'Price per Person'), + (1, 'Fixed Price'), + ) + campsite = models.ForeignKey('Campsite', on_delete=models.PROTECT, related_name='rates') + rate = models.ForeignKey('Rate', on_delete=models.PROTECT) + allow_public_holidays = models.BooleanField(default=True) + date_start = models.DateField(default=date.today) + date_end = models.DateField(null=True, blank=True) + rate_type = models.SmallIntegerField(choices=RATE_TYPE_CHOICES, default=0) + price_model = models.SmallIntegerField(choices=PRICE_MODEL_CHOICES, default=0) + reason = models.ForeignKey('PriceReason') + details = models.TextField(null=True,blank=True) + update_level = models.SmallIntegerField(choices=UPDATE_LEVEL_CHOICES, default=0) + + def get_rate(self, num_adult=0, num_concession=0, num_child=0, num_infant=0): + return self.rate.adult*num_adult + self.rate.concession*num_concession + \ + self.rate.child*num_child + self.rate.infant*num_infant + + def __str__(self): + return '{} - ({})'.format(self.campsite, self.rate) + + class Meta: + unique_together = (('campsite', 'rate', 'date_start','date_end'),) + + # Properties + # ================================= + @property + def deletable(self): + today = datetime.now().date() + if self.date_start >= today: + return True + return False + + @property + def editable(self): + today = datetime.now().date() + if (self.date_start > today and not self.date_end) or ( self.date_start > today <= self.date_end): + return True + return False + + # Methods + # ================================= + def update(self,data): + for attr, value in data.items(): + setattr(self, attr, value) + self.save() + class Booking(models.Model): legacy_id = models.IntegerField(unique=True) legacy_name = models.CharField(max_length=255, blank=True) @@ -131,28 +817,345 @@ class Booking(models.Model): cost_total = models.DecimalField(max_digits=8, decimal_places=2, default='0.00') campground = models.ForeignKey('Campground', null=True) +# REASON MODELS +# ===================================== +class Reason(models.Model): + text = models.TextField() + editable = models.BooleanField(default=True,editable=False) -class CampsiteRate(models.Model): - campground = models.ForeignKey('Campground', on_delete=models.PROTECT) - campsite_class = models.ForeignKey('CampsiteClass', on_delete=models.PROTECT) - min_days = models.SmallIntegerField(default=1) - max_days = models.SmallIntegerField(default=28) - min_people = models.SmallIntegerField(default=1) - max_people = models.SmallIntegerField(default=12) - allow_public_holidays = models.BooleanField(default=True) - rate_adult = models.DecimalField(max_digits=8, decimal_places=2, default='10.00') - rate_concession = models.DecimalField(max_digits=8, decimal_places=2, default='6.60') - rate_child = models.DecimalField(max_digits=8, decimal_places=2, default='2.20') - rate_infant = models.DecimalField(max_digits=8, decimal_places=2, default='0') - - def rate(self, num_adult=0, num_concession=0, num_child=0, num_infant=0): - return self.rate_adult*num_adult + self.rate_concession*num_concession + \ - self.rate_child*num_child + self.rate_infant*num_infant + class Meta: + ordering = ('id',) + abstract = True - def __str__(self): - return '{} - {} (adult: ${}, concession: ${}, child: ${}, infant: ${})'.format(self.campground, self.campsite_class, self.rate_adult, self.rate_concession, self.rate_child, self.rate_infant) + # Properties + # ============================== + def code(self): + return self.__get_code() + + # Methods + # ============================== + def __get_code(self): + length = len(str(self.id)) + val = '0' + return '{}{}'.format((val*(4-length)),self.id) + +class MaximumStayReason(Reason): + pass + +class ClosureReason(Reason): + pass + +class OpenReason(Reason): + pass +class PriceReason(Reason): + pass +# VIEWS +# ===================================== +class ViewPriceHistory(models.Model): + id = models.IntegerField(primary_key=True) + date_start = models.DateField() + date_end = models.DateField() + rate_id = models.IntegerField() + adult = models.DecimalField(max_digits=8, decimal_places=2) + concession = models.DecimalField(max_digits=8, decimal_places=2) + child = models.DecimalField(max_digits=8, decimal_places=2) + details = models.TextField() + reason_id = models.IntegerField() + + class Meta: + abstract =True + + # Properties + # ==================================== + @property + def deletable(self): + today = datetime.now().date() + if self.date_start >= today: + return True + return False + + @property + def editable(self): + today = datetime.now().date() + if (self.date_start > today and not self.date_end) or ( self.date_start > today <= self.date_end): + return True + return False + + @property + def reason(self): + reason = '' + if self.reason_id: + reason = self.reason_id + return reason + +class CampgroundPriceHistory(ViewPriceHistory): + class Meta: + managed = False + db_table = 'parkstay_campground_pricehistory_v' + ordering = ['-date_start',] + +class CampsiteClassPriceHistory(ViewPriceHistory): class Meta: - unique_together = (('campground', 'campsite_class')) + managed = False + db_table = 'parkstay_campsiteclass_pricehistory_v' + ordering = ['-date_start',] + +# LISTENERS +# ====================================== +class CampgroundBookingRangeListener(object): + """ + Event listener for CampgroundBookingRange + """ + + @staticmethod + @receiver(pre_save, sender=CampgroundBookingRange) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = CampgroundBookingRange.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + + if not instance._is_same(original_instance): + instance.updated_on = timezone.now() + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + else: + try: + within = CampgroundBookingRange.objects.get(~Q(id=instance.id),Q(campground=instance.campground),Q(range_start__lte=instance.range_start), Q(range_end__gte=instance.range_start) | Q(range_end__isnull=True) ) + within.range_end = instance.range_start + within.save(skip_validation=True) + except CampgroundBookingRange.DoesNotExist: + pass + if instance.status == 0 and not instance.range_end: + try: + another_open = CampgroundBookingRange.objects.filter(campground=instance.campground,range_start=instance.range_start+timedelta(days=1),status=0).latest('updated_on') + instance.range_end = instance.range_start + except CampgroundBookingRange.DoesNotExist: + pass + + @staticmethod + @receiver(post_delete, sender=CampgroundBookingRange) + def _post_delete(sender, instance, **kwargs): + today = datetime.now().date() + if instance.status != 0 and instance.range_end: + try: + linked_open = CampgroundBookingRange.objects.get(range_start=instance.range_end + timedelta(days=1), status=0) + if instance.range_start >= today: + linked_open.range_start = instance.range_start + else: + linked_open.range_start = today + linked_open.save(skip_validation=True) + except CampgroundBookingRange.DoesNotExist: + pass + elif instance.status != 0 and not instance.range_end: + try: + if instance.range_start >= today: + CampgroundBookingRange.objects.create(campground=instance.campground,range_start=instance.range_start,status=0) + else: + CampgroundBookingRange.objects.create(campsite=instance.campground,range_start=today,status=0) + except: + pass + + @staticmethod + @receiver(post_save, sender=CampgroundBookingRange) + def _post_save(sender, instance, **kwargs): + original_instance = getattr(instance, "_original_instance") if hasattr(instance, "_original_instance") else None + if not original_instance: + pass + + # Check if its a closure and has an end date to create new opening range + if instance.status != 0 and instance.range_end: + another_open = CampgroundBookingRange.objects.filter(campground=instance.campground,range_start=datetime.now().date()+timedelta(days=1),status=0) + if not another_open: + try: + CampgroundBookingRange.objects.create(campground=instance.campground,range_start=instance.range_end+timedelta(days=1),status=0) + except BookingRangeWithinException as e: + pass + +class CampgroundListener(object): + """ + Event listener for Campgrounds + """ + + @staticmethod + @receiver(pre_save, sender=Campground) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = Campground.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + + @staticmethod + @receiver(post_save, sender=Campground) + def _post_save(sender, instance, **kwargs): + original_instance = getattr(instance, "_original_instance") if hasattr(instance, "_original_instance") else None + if not original_instance: + # Create an opening booking range on creation of Campground + CampgroundBookingRange.objects.create(campground=instance,range_start=datetime.now().date(),status=0) + else: + if original_instance.price_level != instance.price_level: + # Get all campsites + today = datetime.now().date() + campsites = instance.campsites.all() + campsite_list = campsites.values_list('id', flat=True) + rates = CampsiteRate.objects.filter(campsite__in=campsite_list,update_level=original_instance.price_level) + current_rates = rates.filter(Q(date_end__isnull=True),Q(date_start__lte = today)).update(date_end=today) + future_rates = rates.filter(date_start__gt = today).delete() + if instance.price_level == 1: + #Check if there are any existant campsite class rates + for c in campsites: + try: + ch = CampsiteClassPriceHistory.objects.get(Q(date_end__isnull=True),id=c.campsite_class_id,date_start__lte = today) + cr = CampsiteRate(campsite=c,rate_id=ch.rate_id,date_start=today + timedelta(days=1)) + cr.save() + except CampsiteClassPriceHistory.DoesNotExist: + pass + except Exception: + pass + +class CampsiteBookingRangeListener(object): + """ + Event listener for CampsiteBookingRange + """ + + @staticmethod + @receiver(pre_save, sender=CampsiteBookingRange) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = CampsiteBookingRange.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + + if not instance._is_same(original_instance): + instance.updated_on = timezone.now() + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + else: + try: + within = CampsiteBookingRange.objects.get(Q(campsite=instance.campsite),Q(range_start__lte=instance.range_start), Q(range_end__gte=instance.range_start) | Q(range_end__isnull=True) ) + within.range_end = instance.range_start + within.save(skip_validation=True) + except CampsiteBookingRange.DoesNotExist: + pass + if instance.status == 0 and not instance.range_end: + try: + another_open = CampsiteBookingRange.objects.filter(campsite=instance.campsite,range_start=instance.range_start+timedelta(days=1),status=0).latest('updated_on') + instance.range_end = instance.range_start + except CampsiteBookingRange.DoesNotExist: + pass + + @staticmethod + @receiver(post_delete, sender=CampsiteBookingRange) + def _post_delete(sender, instance, **kwargs): + today = datetime.now().date() + if instance.status != 0 and instance.range_end: + try: + linked_open = CampsiteBookingRange.objects.get(range_start=instance.range_end + timedelta(days=1), status=0) + if instance.range_start >= today: + linked_open.range_start = instance.range_start + else: + linked_open.range_start = today + linked_open.save(skip_validation=True) + except CampsiteBookingRange.DoesNotExist: + pass + elif instance.status != 0 and not instance.range_end: + try: + if instance.range_start >= today: + CampsiteBookingRange.objects.create(campsite=instance.campsite,range_start=instance.range_start,status=0) + else: + CampsiteBookingRange.objects.create(campsite=instance.campsite,range_start=today,status=0) + except: + pass + + @staticmethod + @receiver(post_save, sender=CampsiteBookingRange) + def _post_save(sender, instance, **kwargs): + original_instance = getattr(instance, "_original_instance") if hasattr(instance, "_original_instance") else None + if not original_instance: + pass + + # Check if its a closure and has an end date to create new opening range + if instance.status != 0 and instance.range_end: + another_open = CampsiteBookingRange.objects.filter(campsite=instance.campsite,range_start=datetime.now().date()+timedelta(days=1),status=0) + if not another_open: + try: + CampsiteBookingRange.objects.create(campsite=instance.campsite,range_start=instance.range_end+timedelta(days=1),status=0) + except BookingRangeWithinException as e: + pass + +class CampsiteListener(object): + """ + Event listener for Campsites + """ + + @staticmethod + @receiver(pre_save, sender=Campsite) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = Campsite.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + + @staticmethod + @receiver(post_save, sender=Campsite) + def _post_save(sender, instance, **kwargs): + original_instance = getattr(instance, "_original_instance") if hasattr(instance, "_original_instance") else None + if not original_instance: + # Create an opening booking range on creation of Campground + CampsiteBookingRange.objects.create(campsite=instance,range_start=datetime.now().date(),status=0) + +class CampsiteRateListener(object): + """ + Event listener for Campsite Rate + """ + + @staticmethod + @receiver(pre_save, sender=CampsiteRate) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = CampsiteRate.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + else: + try: + within = CampsiteRate.objects.get(Q(campsite=instance.campsite),Q(date_start__lte=instance.date_start), Q(date_end__gte=instance.date_start) | Q(date_end__isnull=True) ) + within.date_end = instance.date_start + within.save() + instance.date_start = instance.date_start + timedelta(days=1) + except CampsiteRate.DoesNotExist: + pass + + @staticmethod + @receiver(post_delete, sender=CampsiteRate) + def _post_delete(sender, instance, **kwargs): + if not instance.date_end: + CampsiteRate.objects.filter(date_end=instance.date_start- timedelta(days=2),campsite=instance.campsite).update(date_end=None) + +class CampsiteStayHistoryListener(object): + """ + Event listener for Campsite Stay History + """ + @staticmethod + @receiver(pre_save, sender=CampsiteStayHistory) + def _pre_save(sender, instance, **kwargs): + if instance.pk: + original_instance = CampsiteStayHistory.objects.get(pk=instance.pk) + setattr(instance, "_original_instance", original_instance) + elif hasattr(instance, "_original_instance"): + delattr(instance, "_original_instance") + else: + try: + within = CampsiteStayHistory.objects.get(Q(campsite=instance.campsite),Q(range_start__lte=instance.range_start), Q(range_end__gte=instance.range_start) | Q(range_end__isnull=True) ) + within.range_end = instance.range_start - timedelta(days=1) + within.save() + except CampsiteStayHistory.DoesNotExist: + pass + @staticmethod + @receiver(post_delete, sender=CampsiteStayHistory) + def _post_delete(sender, instance, **kwargs): + if not instance.range_end: + CampsiteStayHistory.objects.filter(range_end=instance.range_start- timedelta(days=1),campsite=instance.campsite).update(range_end=None) diff --git a/parkstay/perms.py b/parkstay/perms.py new file mode 100644 index 0000000000..e02d13fdbe --- /dev/null +++ b/parkstay/perms.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission +from parkstay.helpers import is_officer, is_customer + + +# REST permissions +class OfficerPermission(BasePermission): + def has_permission(self, request, view): + return is_officer(request.user) diff --git a/parkstay/serialisers.py b/parkstay/serialisers.py index d8711885b4..07ab1bbb18 100644 --- a/parkstay/serialisers.py +++ b/parkstay/serialisers.py @@ -1,18 +1,498 @@ -from parkstay.models import CampsiteBooking, Campsite, Campground, Park +from django.conf import settings +from parkstay.models import ( CampgroundPriceHistory, + CampsiteClassPriceHistory, + Rate, + CampsiteStayHistory, + District, + CampsiteBooking, + BookingRange, + CampsiteBookingRange, + CampgroundBookingRange, + Campsite, + Campground, + Park, + PromoArea, + Feature, + Region, + CampsiteClass, + Booking, + CampsiteRate, + Contact, + CampgroundImage, + ClosureReason, + OpenReason, + PriceReason, + MaximumStayReason + ) from rest_framework import serializers +import rest_framework_gis.serializers as gis_serializers + +class DistrictSerializer(serializers.ModelSerializer): + class Meta: + model = District + +class PromoAreaSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = PromoArea + +class CampgroundCampsiteFilterSerializer(serializers.Serializer): + arrival = serializers.DateField(input_formats=['%Y/%m/%d'], allow_null=True) + departure = serializers.DateField(input_formats=['%Y/%m/%d'], allow_null=True) + num_adult = serializers.IntegerField(default=0) + num_concession = serializers.IntegerField(default=0) + num_child = serializers.IntegerField(default=0) + num_infant = serializers.IntegerField(default=0) + gear_type = serializers.ChoiceField(choices=('tent', 'caravan', 'campervan')) + +class BookingRangeSerializer(serializers.ModelSerializer): + + details = serializers.CharField(required=False) + range_start = serializers.DateField(input_formats=['%d/%m/%Y']) + range_end = serializers.DateField(input_formats=['%d/%m/%Y'],required=False) + + def get_status(self, obj): + return dict(BookingRange.BOOKING_RANGE_CHOICES).get(obj.status) + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop("method") + except: + method = 'get' + try: + original = kwargs.pop("original") + except: + original = False; + super(BookingRangeSerializer, self).__init__(*args, **kwargs) + if method == 'post': + self.fields['status'] = serializers.ChoiceField(choices=BookingRange.BOOKING_RANGE_CHOICES) + elif method == 'get': + if not original: + self.fields['status'] = serializers.SerializerMethodField() + else: + self.fields['range_start'] = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y']) + self.fields['range_end'] = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y'],required=False) + +class CampgroundBookingRangeSerializer(BookingRangeSerializer): + + class Meta: + model = CampgroundBookingRange + fields = ( + 'id', + 'status', + 'closure_reason', + 'open_reason', + 'range_start', + 'range_end', + 'reason', + 'details', + 'editable', + 'campground' + ) + read_only_fields = ('reason',) + write_only_fields = ( + 'campground' + ) + +class CampsiteBookingRangeSerializer(BookingRangeSerializer): + + class Meta: + model = CampsiteBookingRange + fields = ( + 'id', + 'status', + 'closure_reason', + 'open_reason', + 'range_start', + 'range_end', + 'reason', + 'details', + 'editable', + 'campsite' + ) + read_only_fields = ('reason',) + write_only_fields = ( + 'campsite' + ) + +class ContactSerializer(serializers.ModelSerializer): + class Meta: + model = Contact + fields = ('name','phone_number') + +class FeatureSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Feature + fields = ('url','id','name','description','image') + +class CampgroundMapFeatureSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Feature + fields = ('id', 'name', 'description', 'image') + +class CampgroundMapRegionSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Region + fields = ('id', 'name', 'abbreviation') + +class CampgroundMapDistrictSerializer(serializers.HyperlinkedModelSerializer): + region = CampgroundMapRegionSerializer(read_only=True) + class Meta: + model = District + fields = ('id', 'name', 'abbreviation', 'region') + +class CampgroundMapParkSerializer(serializers.HyperlinkedModelSerializer): + district = CampgroundMapDistrictSerializer(read_only=True) + class Meta: + model = Park + fields = ('id','name', 'entry_fee_required', 'district') + +class CampgroundMapFilterSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Campground + fields = ('id',) + +class CampgroundMapSerializer(gis_serializers.GeoFeatureModelSerializer): + features = CampgroundMapFeatureSerializer(read_only=True, many=True) + park = CampgroundMapParkSerializer(read_only=True) + + class Meta: + model = Campground + geo_field = 'wkb_geometry' + fields = ( + 'id', + 'name', + 'description', + 'features', + 'campground_type', + 'park', + ) + +class CampgroundImageSerializer(serializers.ModelSerializer): + image = serializers.ImageField(max_length=17) + + def get_image(self, obj): + return self.context['request'].build_absolute_uri('/media{}'.format(obj.image.url.split(settings.MEDIA_ROOT)[1])) + + class Meta: + model = CampgroundImage + fields = ('id','image','campground') + read_only_fields = ('id',) + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop('method') + except: + method = 'post' + super(CampgroundImageSerializer, self).__init__(*args, **kwargs) + if method == 'get': + self.fields['image'] = serializers.SerializerMethodField() + +class ExistingCampgroundImageSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() + image = serializers.URLField() + class Meta: + model = CampgroundImage + fields = ('id','image','campground') + + +class CampgroundSerializer(serializers.HyperlinkedModelSerializer): + address = serializers.JSONField() + contact = ContactSerializer(required=False) + images = CampgroundImageSerializer(many=True,required=False) + class Meta: + model = Campground + fields = ( + 'url', + 'id', + 'site_type', + 'campground_type', + 'name', + 'address', + 'contact', + 'park', + 'region', + 'wkb_geometry', + 'price_level', + 'description', + 'promo_area', + 'ratis_id', + 'area_activities', + 'features', + 'driving_directions', + 'active', + 'current_closure', + 'campfires_allowed', + 'dog_permitted', + 'check_in', + 'check_out', + 'images', + ) + + def get_site_type(self, obj): + return dict(Campground.SITE_TYPE_CHOICES).get(obj.site_type) + + def get_address(self, obj): + if not obj.address: + return {} + return obj.address + + def get_price_level(self, obj): + return dict(Campground.CAMPGROUND_PRICE_LEVEL_CHOICES).get(obj.price_level) + + def get_campground_type(self, obj): + return dict(Campground.CAMPGROUND_TYPE_CHOICES).get(obj.campground_type) + + def __init__(self, *args, **kwargs): + try: + formatted = bool(kwargs.pop('formatted')) + except: + formatted = False + try: + method = kwargs.pop('method') + except: + method = 'post' + super(CampgroundSerializer, self).__init__(*args, **kwargs) + if formatted: + self.fields['site_type'] = serializers.SerializerMethodField() + self.fields['campground_type'] = serializers.SerializerMethodField() + self.fields['price_level'] = serializers.SerializerMethodField() + if method == 'get': + self.fields['features'] = FeatureSerializer(many=True) + self.fields['address'] = serializers.SerializerMethodField() + self.fields['images'] = CampgroundImageSerializer(many=True,required=False,method='get') + +class ParkSerializer(serializers.HyperlinkedModelSerializer): + district = DistrictSerializer() + campgrounds = CampgroundSerializer(many=True) + class Meta: + model = Park + fields = ('id','district', 'url', 'name', 'entry_fee_required', 'campgrounds') + +class CampsiteStayHistorySerializer(serializers.ModelSerializer): + details = serializers.CharField(required=False) + range_start = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y']) + range_end = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y'],required=False) + class Meta: + model = CampsiteStayHistory + fields = ('id','created','range_start','range_end','min_days','max_days','min_dba','max_dba','reason','details','campsite','editable') + read_only_fields =('editable',) + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop('method') + except: + method = 'post' + super(CampsiteStayHistorySerializer, self).__init__(*args, **kwargs) + if method == 'get': + self.fields['reason'] = serializers.CharField(source='reason.text') + +class CampsiteSerialiser(serializers.HyperlinkedModelSerializer): + name = serializers.CharField(default='default',required=False) + class Meta: + model = Campsite + fields = ('id','campground', 'name', 'type','campsite_class','price','features','wkb_geometry','campground_open','active','current_closure','can_add_rate','tent','campervan','caravan','min_people','max_people','description',) + + def __init__(self, *args, **kwargs): + try: + formatted = bool(kwargs.pop('formatted')) + except: + formatted = False + try: + method = kwargs.pop('method') + except: + method = 'put' + super(CampsiteSerialiser, self).__init__(*args, **kwargs) + if method == 'get': + self.fields['features'] = FeatureSerializer(many=True) + elif method == 'post': + self.fields['features'] = serializers.HyperlinkedRelatedField(many=True,read_only=True,required=False,view_name='features-detail') + elif method == 'put': + self.fields['features'] = serializers.HyperlinkedRelatedField(many=True,allow_empty=True, queryset=Feature.objects.all(),view_name='feature-detail') + +class RegionSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Region + +class CampsiteClassSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = CampsiteClass + fields = ('url','id','name','tent','campervan','min_people','max_people','caravan','description','features','deleted','can_add_rate','campsites') + read_only_fields = ('campsites','can_add_rate',) + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop('method') + except: + method = 'post' + print(method) + super(CampsiteClassSerializer, self).__init__(*args, **kwargs) + if method == 'get': + self.fields['features'] = FeatureSerializer(many=True) + elif method == 'post': + self.fields['features'] = serializers.HyperlinkedRelatedField(required=False,many=True,allow_empty=True, queryset=Feature.objects.all(),view_name='feature-detail') + elif method == 'put': + self.fields['features'] = serializers.HyperlinkedRelatedField(required=False,many=True,allow_empty=True, queryset=Feature.objects.all(),view_name='feature-detail') class CampsiteBookingSerialiser(serializers.HyperlinkedModelSerializer): + booking_type = serializers.SerializerMethodField() class Meta: model = CampsiteBooking fields = ('campsite', 'date', 'booking_type') -class CampsiteSerialiser(serializers.HyperlinkedModelSerializer): + def get_booking_type(self, obj): + return dict(CampsiteBooking.BOOKING_TYPE_CHOICES).get(obj.booking_type) + +class BookingSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = Campsite - fields = ('campground', 'name', 'campsite_class', 'features', 'max_people') -""" -class CampgroundSerialiser((Serializer.HyperlinkedModelSerializer) + model = Booking + +class RateSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = Campground - fields = ('', '', '', '', '') -""" + model = Rate + fields = ('url','id','adult','concession','child','infant','name') + +class CampsiteRateSerializer(serializers.ModelSerializer): + date_start = serializers.DateField(format='%d/%m/%Y') + details = serializers.CharField(required=False) + class Meta: + model = CampsiteRate + read_only_fields = ('date_end',) + +class CampsiteRateReadonlySerializer(serializers.ModelSerializer): + adult = serializers.DecimalField(max_digits=5, decimal_places=2,source='rate.adult') + concession = serializers.DecimalField(max_digits=5, decimal_places=2,source='rate.concession') + child = serializers.DecimalField(max_digits=5, decimal_places=2,source='rate.child') + class Meta: + model = CampsiteRate + fields = ('id','adult','concession','child','date_start','date_end','rate','editable','deletable','update_level') + +class RateDetailSerializer(serializers.Serializer): + '''Used to validate rates from the frontend + ''' + rate = serializers.IntegerField(required=False) + adult = serializers.DecimalField(max_digits=5, decimal_places=2) + concession = serializers.DecimalField(max_digits=5, decimal_places=2) + child = serializers.DecimalField(max_digits=5, decimal_places=2) + period_start = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y']) + reason = serializers.IntegerField() + details = serializers.CharField(required=False) + campsite = serializers.IntegerField(required=False) + + + def validate_rate(self, value): + if value: + try: + Rate.objects.get(id=value) + except Rate.DoesNotExist: + raise serializers.ValidationError('This rate does not exist') + return value + +class CampgroundPriceHistorySerializer(serializers.ModelSerializer): + date_end = serializers.DateField(required=False) + details = serializers.CharField(required=False) + class Meta: + model = CampgroundPriceHistory + fields = ('id','date_start','date_end','rate_id','adult','concession','child','editable','deletable','reason','details') + read_only_fields = ('id','editable','deletable','adult','concession','child') + + def validate(self,obj): + if obj.get('reason') == 1 and not obj.get('details'): + raise serializers.ValidationError('Details is rtequired if the reason is other.') + return obj + + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop('method') + except: + method = 'get' + super(CampgroundPriceHistorySerializer, self).__init__(*args, **kwargs) + if method == 'post': + self.fields['reason'] = serializers.IntegerField(write_only=True) + +class CampsiteClassPriceHistorySerializer(serializers.ModelSerializer): + date_end = serializers.DateField(required=False) + details = serializers.CharField(required=False) + class Meta: + model = CampsiteClassPriceHistory + fields = ('id','date_start','date_end','rate_id','adult','concession','child','editable','deletable','reason','details') + read_only_fields = ('id','editable','deletable','adult','concession','child') + + def validate(self,obj): + if obj.get('reason') == 1 and not obj.get('details'): + raise serializers.ValidationError('Details is rtequired if the reason is other.') + return obj + + def __init__(self, *args, **kwargs): + try: + method = kwargs.pop('method') + except: + method = 'get' + super(CampsiteClassPriceHistorySerializer, self).__init__(*args, **kwargs) + if method == 'post': + self.fields['reason'] = serializers.IntegerField() + +# Reasons +# ============================ +class ClosureReasonSerializer(serializers.ModelSerializer): + class Meta: + model = ClosureReason + fields = ('id','text') + +class OpenReasonSerializer(serializers.ModelSerializer): + class Meta: + model = OpenReason + fields = ('id','text') + +class PriceReasonSerializer(serializers.ModelSerializer): + class Meta: + model = PriceReason + fields = ('id','text') + +class MaximumStayReasonSerializer(serializers.ModelSerializer): + class Meta: + model = MaximumStayReason + fields = ('id','text') + +# Bulk Pricing +# ========================== +class BulkPricingSerializer(serializers.Serializer): + TYPE_CHOICES = ( + ('Park','Park'), + ('Campsite Type','Campsite Type') + ) + park = serializers.IntegerField(required=False) + campgrounds = serializers.ListField( + child=serializers.IntegerField() + ) + campsiteType = serializers.IntegerField(required=False) + adult = serializers.DecimalField(max_digits=8, decimal_places=2) + concession = serializers.DecimalField(max_digits=8, decimal_places=2) + child = serializers.DecimalField(max_digits=8, decimal_places=2) + period_start = serializers.DateField(format='%d/%m/%Y',input_formats=['%d/%m/%Y']) + reason = serializers.IntegerField() + details =serializers.CharField() + type = serializers.ChoiceField(choices=TYPE_CHOICES) + + def validate_park(self, val): + try: + park = Park.objects.get(pk=int(val)) + except Park.DoesNotExist: + raise + return val + + def validate_campgrounds(self,val): + for v in val: + try: + Campground.objects.get(pk=v) + except Campground.DoesNotExist: + raise + return val + + def validate_reason(self, val): + reason = None + try: + reason = PriceReason.objects.get(pk=int(val)) + except PriceReason.DoesNotExist: + raise + return val diff --git a/parkstay/settings.py b/parkstay/settings.py index c7d6309950..45aa6454a2 100755 --- a/parkstay/settings.py +++ b/parkstay/settings.py @@ -4,7 +4,11 @@ SITE_ID = 1 INSTALLED_APPS += [ - 'parkstay' + 'bootstrap3', + 'parkstay', + 'taggit', + 'rest_framework', + 'rest_framework_gis' ] # maximum number of days allowed for a booking @@ -12,7 +16,25 @@ WSGI_APPLICATION = 'parkstay.wsgi.application' -TEMPLATES[0]['DIRS'].append(os.path.join(BASE_DIR, 'parkstay', 'templates')) +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'parkstay.perms.OfficerPermission', + ) +} +TEMPLATES[0]['DIRS'].append(os.path.join(BASE_DIR, 'parkstay', 'templates')) +'''BOOTSTRAP3 = { + 'jquery_url': '//static.dpaw.wa.gov.au/static/libs/jquery/2.2.1/jquery.min.js', + 'base_url': '//static.dpaw.wa.gov.au/static/libs/twitter-bootstrap/3.3.6/', + 'css_url': None, + 'theme_url': None, + 'javascript_url': None, + 'javascript_in_head': False, + 'include_jquery': False, + 'required_css_class': 'required-form-field', + 'set_placeholder': False, +}''' STATICFILES_DIRS.append(os.path.join(os.path.join(BASE_DIR, 'parkstay', 'static'))) +STATICFILES_DIRS.append(os.path.join(os.path.join(BASE_DIR, 'parkstay', 'frontend', 'parkstay', 'dist'))) + diff --git a/parkstay/static/ps/.gitignore b/parkstay/static/ps/.gitignore new file mode 100644 index 0000000000..fece4b835f --- /dev/null +++ b/parkstay/static/ps/.gitignore @@ -0,0 +1,2 @@ +js/build.js +css/build.css diff --git a/parkstay/static/ps/css/base.css b/parkstay/static/ps/css/base.css new file mode 100755 index 0000000000..3fa409790e --- /dev/null +++ b/parkstay/static/ps/css/base.css @@ -0,0 +1,74 @@ +.topmast { + height: 115px; + padding-top: 0px; + background: #3580ca url(https://parkstay.dpaw.wa.gov.au/templates/dpaw01/images/top-bg-grasses.gif) repeat-x center bottom; +} + +@media (max-width: 992px) { + .ps-banner h1 { + display: none; + } +} + +.ps-banner h1 { + margin-top: 15px; + font-size: 50px; + color: #fcb13f; +} + +.top-buffer { + margin-top: 20px; +} + +.bottom-buffer { + margin-bottom: 20px; +} + +.left-buffer { + margin-left: 20px; +} + +.right-buffer { + margin-right: 20px; +} + +.ps-breadcrumbs { + background-color: white; + margin-top: -20px; + margin-bottom: 0px; + padding-left: 0px; +} + +.inline { + display: inline; +} + +.center { + text-align: center; +} + +.no-margin { + margin: 0px; +} + +.horizontal-scroll { + overflow-x: scroll; +} + +.modal-header { + background-color: #529b6b; + color: #fcb13f; +} + +.modal-body { + background-color: #529b6b; + color: #fff; +} + +.modal-footer { + background-color: #529b6b; +} + +.popover { + max-width: 100%; +} diff --git a/parkstay/static/ps/css/campicon.woff b/parkstay/static/ps/css/campicon.woff new file mode 100644 index 0000000000..442f3d6a53 Binary files /dev/null and b/parkstay/static/ps/css/campicon.woff differ diff --git a/parkstay/static/ps/css/dashboard.css b/parkstay/static/ps/css/dashboard.css new file mode 100644 index 0000000000..86feef5bda --- /dev/null +++ b/parkstay/static/ps/css/dashboard.css @@ -0,0 +1,12 @@ +.pad-bottom-md { + margin-bottom: 20px; +} + +.pad-top-s { + margin-top: 10px; +} + +.treeview .node-dashboard-tree { + color: inherit; + font-size: 1.2em; +} \ No newline at end of file diff --git a/parkstay/static/ps/img/pin.svg b/parkstay/static/ps/img/pin.svg new file mode 100644 index 0000000000..d4f01e0071 --- /dev/null +++ b/parkstay/static/ps/img/pin.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/parkstay/static/ps/img/pin_alt.svg b/parkstay/static/ps/img/pin_alt.svg new file mode 100644 index 0000000000..63f39acde6 --- /dev/null +++ b/parkstay/static/ps/img/pin_alt.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/parkstay/static/ps/img/pin_offline.svg b/parkstay/static/ps/img/pin_offline.svg new file mode 100644 index 0000000000..ee1bc7652e --- /dev/null +++ b/parkstay/static/ps/img/pin_offline.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/parkstay/templates/ps/base.html b/parkstay/templates/ps/base.html new file mode 100755 index 0000000000..836e15ddc4 --- /dev/null +++ b/parkstay/templates/ps/base.html @@ -0,0 +1,117 @@ +{% load bootstrap3 %} + +{% load static %} + +{% load users %} + + + + + + + {% bootstrap_css %} + + {% block extra_css %} + {% endblock %} + {% block extra_js %} + {% endblock %} + + {% block title %} + Parkstay - Department of Parks and Wildlife + {% endblock %} + + + + + + +
+ {% block header %} + {% include 'ps/header.html' %} + {% endblock %} +
+ {% block menu %} +
+
+ +
+
+ {% endblock %} + {% block breadcrumbs %} + {% endblock %} + {% block messages %} +
+
+
+ {% for message in messages %} +
+ + {{ message|safe }} +
+ {% endfor %} +
+
+
+ {% endblock %} + {% block content %} + {% endblock %} + {% block modals %} + {% endblock %} + {% block custom_js %} + {% endblock %} + + diff --git a/parkstay/templates/ps/booking/base.html b/parkstay/templates/ps/booking/base.html new file mode 100644 index 0000000000..cc29c5f5b2 --- /dev/null +++ b/parkstay/templates/ps/booking/base.html @@ -0,0 +1,121 @@ +{% load bootstrap3 %} + +{% load static %} + +{% load users %} + + + + + + + {% bootstrap_css %} + + {% block extra_css %} + {% endblock %} + {% block extra_js %} + {% endblock %} + + {% block title %} + Parkstay - Department of Parks and Wildlife + {% endblock %} + + + + + + +
+ {% block header %} + {% include 'ps/header.html' %} + {% endblock %} +
+ {% block menu %} +
+
+ +
+
+ {% endblock %} + {% block breadcrumbs %} + {% endblock %} + {% block messages %} +
+
+
+ {% for message in messages %} +
+ + {{ message|safe }} +
+ {% endfor %} +
+
+
+ {% endblock %} + {% block content %} +
+ {% block vue-component %} + + {% endblock %} + +
+ {% endblock %} + {% block modals %} + {% endblock %} + {% block custom_js %} + {% endblock %} + + diff --git a/parkstay/templates/ps/booking/booking.html b/parkstay/templates/ps/booking/booking.html new file mode 100644 index 0000000000..6b024c8d0c --- /dev/null +++ b/parkstay/templates/ps/booking/booking.html @@ -0,0 +1,16 @@ +{% extends 'ps/booking/base.html' %} +{% load static %} +{% block extra_css %} + {{ block.super }} + + +{% endblock %} +{% block vue-component %} +
+ +
+{% endblock %} + +{% block custom_js %} + +{% endblock %} diff --git a/parkstay/templates/ps/campsite_booking_selector.html b/parkstay/templates/ps/campsite_booking_selector.html index 93175b929f..a82dafc754 100644 --- a/parkstay/templates/ps/campsite_booking_selector.html +++ b/parkstay/templates/ps/campsite_booking_selector.html @@ -255,8 +255,7 @@ update: function() { var vm = this; debounce(function() { - var url = '{% url 'get_campsite_class_bookings' %}?'+$.param({ - ground_id: '{{ ground_id }}', + var url = '{% url 'campground-campsite-class-bookings' pk=ground_id %}?'+$.param({ arrival: moment(vm.arrivalDate).format('YYYY/MM/DD'), departure: moment(vm.departureDate).format('YYYY/MM/DD'), num_adult: vm.numAdults, @@ -281,10 +280,6 @@ } }, ready: function () { - //arrivalData.update('04/04/2016'); - //departureData.update('09/04/2016'); - //this.arrivalDate = moment(arrivalData.date); - //this.departureDate = moment(departureData.date); arrivalData.date = this.arrivalDate.toDate(); arrivalData.setValue(); arrivalData.fill(); diff --git a/parkstay/templates/ps/dash/dash_tables.html b/parkstay/templates/ps/dash/dash_tables.html new file mode 100644 index 0000000000..84d264c429 --- /dev/null +++ b/parkstay/templates/ps/dash/dash_tables.html @@ -0,0 +1,24 @@ +{% extends 'ps/base.html' %} +{% load static %} +{% load users %} + +{% block extra_css %} + + + + + +{% endblock %} +{% block extra_js %} +{% endblock %} +{% block requirements %} +{% endblock %} + +{% block content %} +
+ {% block vue-component %} + + {% endblock %} + +
+{% endblock %} diff --git a/parkstay/templates/ps/dash/dash_tables_campgrounds.html b/parkstay/templates/ps/dash/dash_tables_campgrounds.html new file mode 100644 index 0000000000..88d9876b10 --- /dev/null +++ b/parkstay/templates/ps/dash/dash_tables_campgrounds.html @@ -0,0 +1,15 @@ +{% extends 'ps/dash/dash_tables.html' %} +{% load static %} +{% block extra_css %} + {{ block.super }} + +{% endblock %} +{% block vue-component %} +
+ +
+{% endblock %} + +{% block custom_js %} + +{% endblock %} diff --git a/parkstay/templates/ps/header.html b/parkstay/templates/ps/header.html new file mode 100644 index 0000000000..284b26a563 --- /dev/null +++ b/parkstay/templates/ps/header.html @@ -0,0 +1,17 @@ +{% load bootstrap3 %} + +{% load static %} +
+
+
+
+
+
+

+ Department of Parks and Wildlife - Park Stay

+
+
+
+
+
+
diff --git a/parkstay/templates/ps/index.html b/parkstay/templates/ps/index.html new file mode 100644 index 0000000000..da398c8401 --- /dev/null +++ b/parkstay/templates/ps/index.html @@ -0,0 +1,56 @@ +{% extends 'ps/base.html' %} + +{% load bootstrap3 %} + +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block left_menu_items %} +
  • Contact Us
  • +
  • Further Information
  • +{% endblock %} + +{% block messages %} +{% endblock %} + +{% block content %} +
    +
    +
    +

    + Welcome to Parkstay +

    +
    +
    + {% if not request.user.is_authenticated %} +
    +

    + Access Parkstay +

    + {% bootstrap_messages %} +
    + {% csrf_token %} + {% bootstrap_form form %} +
    + {% bootstrap_button 'Submit' button_class='btn-primary' %} +
    +
    +

    Submit your email to login or start the new-user registration process.

    +
    +

    + Parkstay Password-less Logins +

    +

    + At the Department of Parks and Wildlife, we employ a password-less authentication system, meaning you never need to remember + a password. When you need to login to a site, such as Wildlife Licensing, simply enter your email and an + authentication link will be sent to your registered email address. From there, simply follow the link to complete the login process. +

    +
    + {% endif %} +
    +
    +
    +{% endblock %} diff --git a/parkstay/templates/ps/map.html b/parkstay/templates/ps/map.html new file mode 100644 index 0000000000..290e2d30af --- /dev/null +++ b/parkstay/templates/ps/map.html @@ -0,0 +1,826 @@ + + + + + + Park Finder + + + + + + + + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +{% verbatim %} + +{% endverbatim %} +
    +
    +
    +
    +
    + +
    +
    +

    Pic goes here

    +

    Description goes here

    + + +
    +
    +
    +
    +{% verbatim %} + +
    +
    + {{ f.name }} +
    +
    +

    Pic goes here

    +
    +
    +

    Description goes here

    + + +
    +
    +
    +
    + +
    +{% endverbatim %} +
    + + + + + + + + + + + + diff --git a/parkstay/templates/ps/my_bookings.html b/parkstay/templates/ps/my_bookings.html new file mode 100644 index 0000000000..0efc5e1613 --- /dev/null +++ b/parkstay/templates/ps/my_bookings.html @@ -0,0 +1,32 @@ +{% extends 'ps/base.html' %} + +{% load bootstrap3 %} + +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block left_menu_items %} +
  • Contact Us
  • +
  • Further Information
  • +{% endblock %} + +{% block messages %} +{% endblock %} + +{% block content %} +
    +
    +
    +

    + My Bookings +

    +
    +
    + [insert content here] +
    +
    +
    +{% endblock %} diff --git a/parkstay/templatetags/__init__.py b/parkstay/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/parkstay/templatetags/users.py b/parkstay/templatetags/users.py new file mode 100644 index 0000000000..ff9c3e7777 --- /dev/null +++ b/parkstay/templatetags/users.py @@ -0,0 +1,14 @@ +from django.template import Library +from wildlifelicensing.apps.main import helpers + +register = Library() + + +@register.filter(name='is_customer') +def is_customer(user): + return helpers.is_customer(user) + + +@register.filter(name='is_officer') +def is_officer(user): + return helpers.is_officer(user) diff --git a/parkstay/urls.py b/parkstay/urls.py index 1d2bee71ee..ad9f7403ee 100644 --- a/parkstay/urls.py +++ b/parkstay/urls.py @@ -1,19 +1,56 @@ +from django.conf import settings from django.conf.urls import url, include +from django.conf.urls.static import static from rest_framework import routers -from parkstay import views +from parkstay import views, api from parkstay.admin import admin from ledger.urls import urlpatterns as ledger_patterns +# API patterns router = routers.DefaultRouter() +router.register(r'campground_map', api.CampgroundMapViewSet) +router.register(r'campground_map_filter', api.CampgroundMapFilterViewSet) +router.register(r'campgrounds', api.CampgroundViewSet) +router.register(r'campsites', api.CampsiteViewSet) +router.register(r'campsite_bookings', api.CampsiteBookingViewSet) +router.register(r'promo_areas',api.PromoAreaViewSet) +router.register(r'parks',api.ParkViewSet) +router.register(r'features',api.FeatureViewSet) +router.register(r'regions',api.RegionViewSet) +router.register(r'campsite_classes',api.CampsiteClassViewSet) +router.register(r'booking',api.BookingViewSet) +router.register(r'campground_booking_ranges',api.CampgroundBookingRangeViewset) +router.register(r'campsite_booking_ranges',api.CampsiteBookingRangeViewset) +router.register(r'campsite_rate',api.CampsiteRateViewSet) +router.register(r'campsites_stay_history',api.CampsiteStayHistoryViewSet) +router.register(r'rates',api.RateViewset) +router.register(r'closureReasons',api.ClosureReasonViewSet) +router.register(r'openReasons',api.OpenReasonViewSet) +router.register(r'priceReasons',api.PriceReasonViewSet) +router.register(r'maxStayReasons',api.MaximumStayReasonViewSet) +api_patterns = [ + url(r'api/bulkPricing', api.BulkPricingView.as_view(),name='bulkpricing-api'), + url(r'api/',include(router.urls)) +] + +# URL Patterns urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'', include(api_patterns)), + url(r'^$', views.ParkstayRoutingView.as_view(), name='ps_home'), + url(r'^my_bookings/$', views.MyBookingsView.as_view(), name='my-bookings'), url(r'^campsites/(?P[0-9]+)/$', views.CampsiteBookingSelector.as_view(), name='campsite_booking_selector'), url(r'^campsite_classes/(?P[0-9]+)/$', views.CampsiteBookingSelector.as_view(), name='campsite_booking_selector'), url(r'^ical/campground/(?P[0-9]+)/$', views.CampgroundFeed(), name='campground_calendar'), - url(r'^api/campsites/$', views.get_campsite_bookings, name='get_campsite_bookings'), - url(r'^api/campsite_classes/$', views.get_campsite_class_bookings, name='get_campsite_class_bookings') - + url(r'^dashboard/campgrounds$', views.DashboardView.as_view(), name='dash-campgrounds'), + url(r'^dashboard/campsite-types$', views.DashboardView.as_view(), name='dash-campsite-types'), + url(r'^dashboard/bulkpricing$', views.DashboardView.as_view(), name='dash-bulkpricing'), + url(r'^dashboard/', views.DashboardView.as_view(), name='dash'), + url(r'^booking/', views.MyBookingsView.as_view(), name='dash'), + url(r'^map/', views.MapView.as_view(), name='map'), ] + ledger_patterns +if settings.DEBUG: # Serve media locally in development. + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/parkstay/views.py b/parkstay/views.py index 75b1cca041..c9e210e635 100644 --- a/parkstay/views.py +++ b/parkstay/views.py @@ -1,228 +1,35 @@ from django.http import Http404, HttpResponse, JsonResponse -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, get_object_or_404, redirect from django.views.generic.base import View, TemplateView from django.conf import settings - -from parkstay.serialisers import CampsiteBookingSerialiser, CampsiteSerialiser -from parkstay.models import Campground, CampsiteBooking, Campsite, CampsiteRate, Booking - +from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin + +from parkstay.models import (Campground, + CampsiteBooking, + Campsite, + CampsiteRate, + Booking, + PromoArea, + Park, + Feature, + Region, + CampsiteClass, + Booking, + CampsiteRate + ) from django_ical.views import ICalFeed -from rest_framework import viewsets from datetime import datetime, timedelta -from collections import OrderedDict + +from parkstay.helpers import is_officer +from parkstay.forms import LoginForm class CampsiteBookingSelector(TemplateView): template_name = 'ps/campsite_booking_selector.html' def get(self, request, *args, **kwargs): - print(request.__dict__) return super(CampsiteBookingSelector, self).get(request, *args, **kwargs) - -def get_campsite_bookings(request): - """Fetch campsite availability for a campground.""" - # FIXME: port this junk to rest_framework - - # convert GET parameters to objects - ground = Campground.objects.get(pk=request.GET['ground_id']) - start_date = datetime.strptime(request.GET['arrival'], '%Y/%m/%d').date() - end_date = datetime.strptime(request.GET['departure'], '%Y/%m/%d').date() - num_adult = int(request.GET.get('num_adult', 0)) - num_concession = int(request.GET.get('num_concession', 0)) - num_child = int(request.GET.get('num_child', 0)) - num_infant = int(request.GET.get('num_infant', 0)) - - - # get a length of the stay (in days), capped if necessary to the request maximum - length = max(0, (end_date-start_date).days) - if length > settings.PS_MAX_BOOKING_LENGTH: - length = settings.PS_MAX_BOOKING_LENGTH - end_date = start_date+timedelta(days=settings.PS_MAX_BOOKING_LENGTH) - - # fetch all of the single-day CampsiteBooking objects within the date range for the campground - bookings_qs = CampsiteBooking.objects.filter( - campsite__campground=ground, - date__gte=start_date, - date__lt=end_date - ).order_by('date', 'campsite__name') - # fetch all the campsites and applicable rates for the campground - sites_qs = Campsite.objects.filter(campground=ground).order_by('name') - rates_qs = CampsiteRate.objects.filter(campground=ground) - - # make a map of campsite class to cost - rates_map = {r.campsite_class_id: r.rate(num_adult, num_concession, num_child, num_infant) for r in rates_qs} - - # from our campsite queryset, generate a digest for each site - sites_map = OrderedDict([(s.name, (s.pk, s.campsite_class, rates_map[s.campsite_class_id])) for s in sites_qs]) - bookings_map = {} - - # create our result object, which will be returned as JSON - result = { - 'arrival': start_date.strftime('%Y/%m/%d'), - 'days': length, - 'adults': 1, - 'children': 0, - 'maxAdults': 30, - 'maxChildren': 30, - 'sites': [], - 'classes': {} - } - - # make an entry under sites for each site - for k, v in sites_map.items(): - site = { - 'name': k, - 'id': v[0], - 'type': ground.campground_type, - 'class': v[1].pk, - 'price': '${}'.format(v[2]*length), - 'availability': [[True, '${}'.format(v[2]), v[2]] for i in range(length)] - } - result['sites'].append(site) - bookings_map[k] = site - if v[1].pk not in result['classes']: - result['classes'][v[1].pk] = v[1].name - - # strike out existing bookings - for b in bookings_qs: - offset = (b.date-start_date).days - bookings_map[b.campsite.name]['availability'][offset][0] = False - bookings_map[b.campsite.name]['availability'][offset][1] = 'Closed' if b.booking_type == 2 else 'Sold' - bookings_map[b.campsite.name]['price'] = False - - return JsonResponse(result) - - -def get_campsite_class_bookings(request): - """Fetch campsite availability for a campground, grouped by campsite class.""" - # FIXME: port this junk to rest_framework maybe? - - # convert GET parameters to objects - ground = Campground.objects.get(pk=request.GET['ground_id']) - start_date = datetime.strptime(request.GET['arrival'], '%Y/%m/%d').date() - end_date = datetime.strptime(request.GET['departure'], '%Y/%m/%d').date() - num_adult = int(request.GET.get('num_adult', 0)) - num_concession = int(request.GET.get('num_concession', 0)) - num_child = int(request.GET.get('num_child', 0)) - num_infant = int(request.GET.get('num_infant', 0)) - - # get a length of the stay (in days), capped if necessary to the request maximum - length = max(0, (end_date-start_date).days) - if length > settings.PS_MAX_BOOKING_LENGTH: - length = settings.PS_MAX_BOOKING_LENGTH - end_date = start_date+timedelta(days=settings.PS_MAX_BOOKING_LENGTH) - - # fetch all of the single-day CampsiteBooking objects within the date range for the campground - bookings_qs = CampsiteBooking.objects.filter( - campsite__campground=ground, - date__gte=start_date, - date__lt=end_date - ).order_by('date', 'campsite__name') - # fetch all the campsites and applicable rates for the campground - sites_qs = Campsite.objects.filter(campground=ground) - rates_qs = CampsiteRate.objects.filter(campground=ground) - - # make a map of campsite class to cost - rates_map = {r.campsite_class_id: r.rate(num_adult, num_concession, num_child, num_infant) for r in rates_qs} - - # from our campsite queryset, generate a distinct list of campsite classes - classes = [x for x in sites_qs.distinct('campsite_class__name').order_by('campsite_class__name').values_list('pk', 'campsite_class', 'campsite_class__name')] - - classes_map = {} - bookings_map = {} - - # create our result object, which will be returned as JSON - result = { - 'arrival': start_date.strftime('%Y/%m/%d'), - 'days': length, - 'adults': 1, - 'children': 0, - 'maxAdults': 30, - 'maxChildren': 30, - 'sites': [], - 'classes': {} - } - - # make an entry under sites for each campsite class - for c in classes: - rate = rates_map[c[1]] - site = { - 'name': c[2], - 'id': None, - 'type': ground.campground_type, - 'price': '${}'.format(rate*length), - 'availability': [[True, '${}'.format(rate), rate, [0, 0]] for i in range(length)], - 'breakdown': OrderedDict() - } - result['sites'].append(site) - classes_map[c[1]] = site - - # make a map of class IDs to site IDs - class_sites_map = {} - for s in sites_qs: - if s.campsite_class.pk not in class_sites_map: - class_sites_map[s.campsite_class.pk] = set() - - class_sites_map[s.campsite_class.pk].add(s.pk) - rate = rates_map[s.campsite_class.pk] - classes_map[s.campsite_class.pk]['breakdown'][s.name] = [[True, '${}'.format(rate), rate] for i in range(length)] - - # store number of campsites in each class - class_sizes = {k: len(v) for k, v in class_sites_map.items()} - - - - # strike out existing bookings - for b in bookings_qs: - offset = (b.date-start_date).days - key = b.campsite.campsite_class.pk - - # clear the campsite from the class sites map - if b.campsite.pk in class_sites_map[key]: - class_sites_map[key].remove(b.campsite.pk) - - # update the per-site availability - classes_map[key]['breakdown'][b.campsite.name][offset][0] = False - classes_map[key]['breakdown'][b.campsite.name][offset][1] = 'Closed' if (b.booking_type == 2) else 'Sold' - - # update the class availability status - book_offset = 1 if (b.booking_type == 2) else 0 - classes_map[key]['availability'][offset][3][book_offset] += 1 - if classes_map[key]['availability'][offset][3][0] == class_sizes[key]: - classes_map[key]['availability'][offset][1] = 'Fully Booked' - elif classes_map[key]['availability'][offset][3][1] == class_sizes[key]: - classes_map[key]['availability'][offset][1] = 'Closed' - elif classes_map[key]['availability'][offset][3][0] >= classes_map[key]['availability'][offset][3][1]: - classes_map[key]['availability'][offset][1] = 'Partially Booked' - else: - classes_map[key]['availability'][offset][1] = 'Partially Closed' - - # tentatively flag campsite class as unavailable - classes_map[key]['availability'][offset][0] = False - classes_map[key]['price'] = False - - # convert breakdowns to a flat list - for klass in classes_map.values(): - klass['breakdown'] = [{'name': k, 'availability': v} for k, v in klass['breakdown'].items()] - - # any campsites remaining in the class sites map have zero bookings! - # check if there's any left for each class, and if so return that as the target - for k, v in class_sites_map.items(): - if v: - rate = rates_map[k] - classes_map[k].update({ - 'id': v.pop(), - 'price': '${}'.format(rate*length), - 'availability': [[True, '${}'.format(rate), rate, [0, 0]] for i in range(length)], - 'breakdown': [] - }) - - - return JsonResponse(result) - - - class CampgroundFeed(ICalFeed): timezone = 'UTC+8' @@ -234,7 +41,6 @@ def title(self, obj): return 'Bookings for {}'.format(obj.name) def items(self, obj): -# return CampsiteBooking.objects.filter(campsite__campground__name='Yardie Creek').order_by('-date','campsite__name') now = datetime.utcnow() low_bound = now - timedelta(days=60) up_bound = now + timedelta(days=90) @@ -255,14 +61,30 @@ def item_end_datetime(self, item): def item_location(self, item): return '{} - {}'.format(item.campground.name, ', '.join([ x[0] for x in item.campsitebooking_set.values_list('campsite__name').distinct() - ] )) + ] )) + +class DashboardView(UserPassesTestMixin, TemplateView): + template_name = 'ps/dash/dash_tables_campgrounds.html' + + def test_func(self): + return is_officer(self.request.user) + + +class MyBookingsView(LoginRequiredMixin, TemplateView): + template_name = 'ps/booking/booking.html' + + +class ParkstayRoutingView(TemplateView): + template_name = 'ps/index.html' + def get(self, *args, **kwargs): + if self.request.user.is_authenticated(): + if is_officer(self.request.user): + return redirect('dash-campgrounds') + return redirect('my-bookings') + kwargs['form'] = LoginForm + return super(ParkstayRoutingView, self).get(*args, **kwargs) -# Create your views here. -class CampsiteBookingViewSet(viewsets.ModelViewSet): - queryset = CampsiteBooking.objects.all() - serializer_class = CampsiteBookingSerialiser -class CampsiteViewSet(viewsets.ModelViewSet): - queryset = Campsite.objects.all() - serializer_class = CampsiteSerialiser +class MapView(TemplateView): + template_name = 'ps/map.html' diff --git a/requirements.txt b/requirements.txt index 5831993bec..f10d38cc4b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -Django<1.10.0 --e git+https://github.com/parksandwildlife/dpaw-utils@0.3a13#egg=dpaw-utils -django-oscar>=1.3,<1.4 -python-social-auth>=0.2.21 +Django<1.11.0 +git+https://github.com/parksandwildlife/dpaw-utils.git@0.3a13#egg=dpaw-utils +git+https://github.com/django-oscar/django-oscar.git@feature/django-110#egg=django-oscar +social-auth-app-django>=0.1.0 coverage>=4.0.3 coveralls>=1.1 reportlab==3.3.0 @@ -9,7 +9,7 @@ django_bootstrap3>=6.2.2 django-braces>=1.8.1 django-datatables-view==1.13.0 django-reversion==1.10.1 -django-preserialize==1.1.0 +git+https://github.com/scottp-dpaw/django-preserialize.git#egg=django-preserialize django-countries==3.4.1 django-cron==0.4.6 django-dynamic-fixture>=1.9.0 @@ -19,6 +19,8 @@ jsontableschema==0.6.5 python-dateutil==2.5.3 py4j==0.10.2.1 djangorestframework==3.4.0 +djangorestframework-gis pycountry==1.20 six>=1.10.0 django-ical>=1.4 +django-taggit diff --git a/wildlifelicensing/apps/applications/views/entry.py b/wildlifelicensing/apps/applications/views/entry.py index c0563b77d3..a6ebf7e71e 100755 --- a/wildlifelicensing/apps/applications/views/entry.py +++ b/wildlifelicensing/apps/applications/views/entry.py @@ -37,7 +37,7 @@ def get_context_data(self, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') kwargs['licence_type'] = application.licence_type @@ -159,7 +159,7 @@ def post(self, request, *args, **kwargs): try: application = utils.get_session_application(request.session) except Exception as e: - messages.error(request, e.message) + messages.error(request, str(e)) return redirect('wl_applications:new_application') if 'select' in request.POST: @@ -191,7 +191,7 @@ def get(self, request, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') application.licence_type = WildlifeLicenceType.objects.get(id=self.args[0]) @@ -277,7 +277,7 @@ def get(self, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if application.licence_type.identification_required and application.applicant.identification is None: @@ -289,7 +289,7 @@ def get_context_data(self, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') kwargs['application'] = application @@ -302,7 +302,7 @@ def form_valid(self, form): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if application.applicant.identification is not None: @@ -329,7 +329,7 @@ def get(self, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if application.licence_type.senior_applicable \ @@ -347,7 +347,7 @@ def form_valid(self, form): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if application.applicant.senior_card is not None: @@ -366,7 +366,7 @@ def get_context_data(self, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') kwargs['application'] = application @@ -395,7 +395,7 @@ def post(self, request, *args, **kwargs): try: application = utils.get_session_application(request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if 'select' in request.POST: @@ -440,7 +440,7 @@ def get_context_data(self, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') if application.review_status == 'awaiting_amendments': @@ -458,7 +458,7 @@ def post(self, request, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') application.data = utils.create_data_from_form(application.licence_type.application_schema, @@ -511,7 +511,7 @@ def get_context_data(self, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') kwargs['is_payment_required'] = not is_licence_free(generate_product_title(application)) and \ @@ -535,7 +535,7 @@ def post(self, request, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') application.correctness_disclaimer = request.POST.get('correctnessDisclaimer', '') == 'on' @@ -578,7 +578,7 @@ def get(self, request, *args, **kwargs): try: application = utils.get_session_application(self.request.session) except Exception as e: - messages.error(self.request, e.message) + messages.error(self.request, str(e)) return redirect('wl_applications:new_application') # update invoice reference if received, else keep the same diff --git a/wildlifelicensing/apps/applications/views/process.py b/wildlifelicensing/apps/applications/views/process.py index 8b75615dc6..84247ad801 100755 --- a/wildlifelicensing/apps/applications/views/process.py +++ b/wildlifelicensing/apps/applications/views/process.py @@ -1,6 +1,6 @@ from datetime import date -from django.core.context_processors import csrf +from django.template.context_processors import csrf from django.contrib import messages from django.http import JsonResponse from django.views.generic import TemplateView, View diff --git a/wildlifelicensing/apps/main/tests/helpers.py b/wildlifelicensing/apps/main/tests/helpers.py index 0defc577db..743c2c357b 100644 --- a/wildlifelicensing/apps/main/tests/helpers.py +++ b/wildlifelicensing/apps/main/tests/helpers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os +import datetime import re from django.contrib.auth.models import Group @@ -22,7 +23,7 @@ class TestData(object): 'email': 'customer@test.com', 'first_name': 'Homer', 'last_name': 'Cust', - 'dob': '1989-08-12', + 'dob': datetime.date(1989, 8, 12), } DEFAULT_PROFILE = { 'email': 'customer@test.com', @@ -36,13 +37,13 @@ class TestData(object): 'email': 'officer@test.com', 'first_name': 'Offy', 'last_name': 'Sir', - 'dob': '1979-12-13', + 'dob': datetime.date(1979, 12, 13), } DEFAULT_ASSESSOR = { 'email': 'assessor@test.com', 'first_name': 'Assess', 'last_name': 'Ore', - 'dob': '1979-10-05', + 'dob': datetime.date(1979, 10, 5), } DEFAULT_ASSESSOR_GROUP = { 'name': 'ass group', @@ -96,8 +97,8 @@ def add_to_group(user, group_name): return user -def get_or_create_user(email, defaults): - user, created = EmailUser.objects.get_or_create(defaults=defaults, email=email) +def get_or_create_user(params): + user, created = EmailUser.objects.get_or_create(**params) return user, created @@ -110,7 +111,7 @@ def create_random_customer(): def get_or_create_default_customer(include_default_profile=False): - user, created = get_or_create_user(TestData.DEFAULT_CUSTOMER['email'], TestData.DEFAULT_CUSTOMER) + user, created = get_or_create_user(TestData.DEFAULT_CUSTOMER) if include_default_profile: address = Address.objects.create(user=user, **TestData.DEFAULT_ADDRESS) @@ -121,14 +122,14 @@ def get_or_create_default_customer(include_default_profile=False): def get_or_create_default_officer(): - user, created = get_or_create_user(TestData.DEFAULT_OFFICER['email'], TestData.DEFAULT_OFFICER) + user, created = get_or_create_user(TestData.DEFAULT_OFFICER) if created: add_to_group(user, 'Officers') return user def get_or_create_api_user(): - user, created = get_or_create_user(TestData.DEFAULT_API_USER['email'], TestData.DEFAULT_API_USER) + user, created = get_or_create_user(TestData.DEFAULT_API_USER) if created: add_to_group(user, 'API') return user @@ -145,7 +146,7 @@ def create_licence(holder, issuer, product_title='regulation-17'): def get_or_create_default_assessor(): - user, created = get_or_create_user(TestData.DEFAULT_ASSESSOR['email'], TestData.DEFAULT_ASSESSOR) + user, created = get_or_create_user(TestData.DEFAULT_ASSESSOR) if created: add_to_group(user, 'Assessors') return user