diff --git a/.gitignore b/.gitignore index 00396778..fd6e8b84 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,7 @@ target/ # media media/ +private-media/ # Database/fixture dumps *.pgdump diff --git a/commercialoperator/components/approvals/api.py b/commercialoperator/components/approvals/api.py index 5da886ee..0eefc2a0 100755 --- a/commercialoperator/components/approvals/api.py +++ b/commercialoperator/components/approvals/api.py @@ -271,7 +271,7 @@ def process_document(self, request, *args, **kwargs): _file = request.FILES.get('_file') document = instance.qaofficer_documents.get_or_create(input_name=section, name=filename)[0] - path = default_storage.save('{}/proposals/{}/approvals/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) + path = private_storage.save('{}/proposals/{}/approvals/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) document._file = path document.save() diff --git a/commercialoperator/components/approvals/models.py b/commercialoperator/components/approvals/models.py index acaa129c..a60a43cf 100755 --- a/commercialoperator/components/approvals/models.py +++ b/commercialoperator/components/approvals/models.py @@ -32,6 +32,9 @@ from commercialoperator.utils import search_keys, search_multiple_keys from commercialoperator.helpers import is_customer #from commercialoperator.components.approvals.email import send_referral_email_notification +#TODO: improvable - these three lines are repeated throughout the models and ought to be set in one place +from django.core.files.storage import FileSystemStorage +private_storage = FileSystemStorage(location=settings.BASE_DIR+"/private-media/", base_url='/private-media/') def update_approval_doc_filename(instance, filename): @@ -43,7 +46,7 @@ def update_approval_comms_log_filename(instance, filename): class ApprovalDocument(Document): approval = models.ForeignKey('Approval',related_name='documents') - _file = models.FileField(upload_to=update_approval_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_approval_doc_filename, max_length=512, storage=private_storage) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted def delete(self): @@ -655,7 +658,7 @@ def save(self, **kwargs): class ApprovalLogDocument(Document): log_entry = models.ForeignKey('ApprovalLogEntry',related_name='documents', null=True,) - _file = models.FileField(upload_to=update_approval_comms_log_filename, null=True, max_length=512) + _file = models.FileField(upload_to=update_approval_comms_log_filename, null=True, max_length=512, storage=private_storage) class Meta: app_label = 'commercialoperator' diff --git a/commercialoperator/components/compliances/models.py b/commercialoperator/components/compliances/models.py index 97e263c6..6d17158f 100755 --- a/commercialoperator/components/compliances/models.py +++ b/commercialoperator/components/compliances/models.py @@ -36,6 +36,9 @@ ) from ledger.payments.invoice.models import Invoice +from django.core.files.storage import FileSystemStorage +private_storage = FileSystemStorage(location=settings.BASE_DIR+"/private-media/", base_url='/private-media/') + import logging logger = logging.getLogger(__name__) @@ -281,7 +284,7 @@ def update_proposal_complaince_filename(instance, filename): class ComplianceDocument(Document): compliance = models.ForeignKey('Compliance',related_name='documents') - _file = models.FileField(upload_to=update_proposal_complaince_filename, max_length=512) + _file = models.FileField(upload_to=update_proposal_complaince_filename, max_length=512, storage=private_storage) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted def delete(self): @@ -338,7 +341,7 @@ def update_compliance_comms_log_filename(instance, filename): class ComplianceLogDocument(Document): log_entry = models.ForeignKey('ComplianceLogEntry',related_name='documents') - _file = models.FileField(upload_to=update_compliance_comms_log_filename, max_length=512) + _file = models.FileField(upload_to=update_compliance_comms_log_filename, max_length=512, storage=private_storage) class Meta: app_label = 'commercialoperator' diff --git a/commercialoperator/components/organisations/models.py b/commercialoperator/components/organisations/models.py index e7bcb750..9455935d 100755 --- a/commercialoperator/components/organisations/models.py +++ b/commercialoperator/components/organisations/models.py @@ -27,6 +27,9 @@ send_organisation_request_link_email_notification, ) +from django.conf import settings +from django.core.files.storage import FileSystemStorage +private_storage = FileSystemStorage(location=settings.BASE_DIR+"/private-media/", base_url='/private-media/') @python_2_unicode_compatible class Organisation(models.Model): @@ -592,12 +595,12 @@ class Meta: app_label = 'commercialoperator' def update_organisation_comms_log_filename(instance, filename): - return 'organisations/{}/communications/{}/{}'.format(instance.log_entry.organisation.id,instance.id,filename) + return 'organisations/{}/communications/{}/{}'.format(instance.log_entry.organisation.id,instance.log_entry.id,filename) class OrganisationLogDocument(Document): log_entry = models.ForeignKey('OrganisationLogEntry',related_name='documents') - _file = models.FileField(upload_to=update_organisation_comms_log_filename, max_length=512) + _file = models.FileField(upload_to=update_organisation_comms_log_filename, max_length=512, storage=private_storage) class Meta: app_label = 'commercialoperator' @@ -630,7 +633,7 @@ class OrganisationRequest(models.Model): abn = models.CharField(max_length=50, null=True, blank=True, verbose_name='ABN') requester = models.ForeignKey(EmailUser) assigned_officer = models.ForeignKey(EmailUser, blank=True, null=True, related_name='org_request_assignee') - identification = models.FileField(upload_to='organisation/requests/%Y/%m/%d', max_length=512, null=True, blank=True) + identification = models.FileField(upload_to='organisation/requests/%Y/%m/%d', max_length=512, null=True, blank=True, storage=private_storage) status = models.CharField(max_length=100,choices=STATUS_CHOICES, default="with_assessor") lodgement_date = models.DateTimeField(auto_now_add=True) role = models.CharField(max_length=100,choices=ROLE_CHOICES, default="employee") @@ -785,12 +788,12 @@ class Meta: app_label = 'commercialoperator' def update_organisation_request_comms_log_filename(instance, filename): - return 'organisation_requests/{}/communications/{}/{}'.format(instance.log_entry.request.id,instance.id,filename) + return 'organisation_requests/{}/communications/{}/{}'.format(instance.log_entry.request.id,instance.log_entry.id,filename) class OrganisationRequestLogDocument(Document): log_entry = models.ForeignKey('OrganisationRequestLogEntry',related_name='documents') - _file = models.FileField(upload_to=update_organisation_request_comms_log_filename, max_length=512) + _file = models.FileField(upload_to=update_organisation_request_comms_log_filename, max_length=512, storage=private_storage) class Meta: app_label = 'commercialoperator' diff --git a/commercialoperator/components/proposals/api.py b/commercialoperator/components/proposals/api.py index 11c5eff5..a8b22804 100755 --- a/commercialoperator/components/proposals/api.py +++ b/commercialoperator/components/proposals/api.py @@ -30,6 +30,7 @@ from commercialoperator.components.proposals.models import searchKeyWords, search_reference, ProposalUserAction from commercialoperator.utils import missing_required_fields from commercialoperator.components.main.utils import check_db_connection +from commercialoperator.components.proposals import models from django.urls import reverse from django.shortcuts import render, redirect, get_object_or_404 @@ -125,7 +126,7 @@ from commercialoperator.helpers import is_customer, is_internal from django.core.files.base import ContentFile -from django.core.files.storage import default_storage +#from django.core.files.storage import default_storage from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination from rest_framework_datatables.pagination import DatatablesPageNumberPagination from rest_framework_datatables.filters import DatatablesFilterBackend @@ -137,6 +138,7 @@ import logging logger = logging.getLogger(__name__) +private_storage = models.private_storage class GetProposalType(views.APIView): renderer_classes = [JSONRenderer, ] @@ -758,7 +760,7 @@ def process_document(self, request, *args, **kwargs): _file = request.FILES.get('_file') document = instance.documents.get_or_create(input_name=section, name=filename)[0] - path = default_storage.save('{}/proposals/{}/documents/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) + path = private_storage.save('{}/proposals/{}/documents/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) document._file = path document.save() @@ -818,7 +820,7 @@ def process_onhold_document(self, request, *args, **kwargs): _file = request.FILES.get('_file') document = instance.onhold_documents.get_or_create(input_name=section, name=filename)[0] - path = default_storage.save('{}/proposals/{}/onhold/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) + path = private_storage.save('{}/proposals/{}/onhold/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) document._file = path document.save() @@ -866,7 +868,7 @@ def process_qaofficer_document(self, request, *args, **kwargs): _file = request.FILES.get('_file') document = instance.qaofficer_documents.get_or_create(input_name=section, name=filename)[0] - path = default_storage.save('{}/proposals/{}/qaofficer/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) + path = private_storage.save('{}/proposals/{}/qaofficer/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) document._file = path document.save() @@ -1237,7 +1239,7 @@ def process_required_document(self, request, *args, **kwargs): required_doc_instance=RequiredDocument.objects.get(id=required_doc_id) document = instance.required_documents.get_or_create(input_name=section, name=filename, required_doc=required_doc_instance)[0] - path = default_storage.save('{}/proposals/{}/required_documents/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) + path = private_storage.save('{}/proposals/{}/required_documents/{}'.format(settings.MEDIA_APP_DIR, proposal_id, filename), ContentFile(_file.read())) document._file = path document.save() diff --git a/commercialoperator/components/proposals/api_event.py b/commercialoperator/components/proposals/api_event.py index 0cead96d..b4b2eb48 100644 --- a/commercialoperator/components/proposals/api_event.py +++ b/commercialoperator/components/proposals/api_event.py @@ -55,7 +55,7 @@ from commercialoperator.helpers import is_customer, is_internal from django.core.files.base import ContentFile -from django.core.files.storage import default_storage +#from django.core.files.storage import default_storage from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination from rest_framework_datatables.pagination import DatatablesPageNumberPagination from rest_framework_datatables.filters import DatatablesFilterBackend diff --git a/commercialoperator/components/proposals/api_filming.py b/commercialoperator/components/proposals/api_filming.py index c372a509..a315c904 100644 --- a/commercialoperator/components/proposals/api_filming.py +++ b/commercialoperator/components/proposals/api_filming.py @@ -52,7 +52,7 @@ from commercialoperator.helpers import is_customer, is_internal from django.core.files.base import ContentFile -from django.core.files.storage import default_storage +#from django.core.files.storage import default_storage from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination from rest_framework_datatables.pagination import DatatablesPageNumberPagination from rest_framework_datatables.filters import DatatablesFilterBackend diff --git a/commercialoperator/components/proposals/email.py b/commercialoperator/components/proposals/email.py index ca28d085..60dd0cb2 100755 --- a/commercialoperator/components/proposals/email.py +++ b/commercialoperator/components/proposals/email.py @@ -4,7 +4,7 @@ from django.utils.encoding import smart_text from django.core.urlresolvers import reverse from django.conf import settings -from django.core.files.storage import default_storage +#from django.core.files.storage import default_storage from django.core.files.base import ContentFile from commercialoperator.components.emails.emails import TemplateEmailBase @@ -810,7 +810,7 @@ def _log_proposal_email(email_message, proposal, sender=None, file_bytes=None, f if file_bytes and filename: # attach the file to the comms_log also path_to_file = '{}/proposals/{}/communications/{}'.format(settings.MEDIA_APP_DIR, proposal.id, filename) - path = default_storage.save(path_to_file, ContentFile(file_bytes)) + path = private_storage.save(path_to_file, ContentFile(file_bytes)) email_entry.documents.get_or_create(_file=path_to_file, name=filename) return email_entry diff --git a/commercialoperator/components/proposals/models.py b/commercialoperator/components/proposals/models.py index adc94d52..c33f6e76 100644 --- a/commercialoperator/components/proposals/models.py +++ b/commercialoperator/components/proposals/models.py @@ -47,7 +47,8 @@ import time from multiselectfield import MultiSelectField - +from django.core.files.storage import FileSystemStorage +private_storage = FileSystemStorage(location=settings.BASE_DIR+"/private-media/", base_url='/private-media/') import logging logger = logging.getLogger(__name__) @@ -247,7 +248,7 @@ def delete(self): class ProposalDocument(Document): proposal = models.ForeignKey('Proposal',related_name='documents') - _file = models.FileField(upload_to=update_proposal_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_proposal_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted can_hide= models.BooleanField(default=False) # after initial submit, document cannot be deleted but can be hidden @@ -259,7 +260,7 @@ class Meta: class OnHoldDocument(Document): proposal = models.ForeignKey('Proposal',related_name='onhold_documents') - _file = models.FileField(upload_to=update_onhold_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_onhold_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history @@ -271,7 +272,7 @@ def delete(self): #Documents on Activities(land)and Activities(Marine) tab for T-Class related to required document questions class ProposalRequiredDocument(Document): proposal = models.ForeignKey('Proposal',related_name='required_documents') - _file = models.FileField(upload_to=update_proposal_required_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_proposal_required_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted required_doc = models.ForeignKey('RequiredDocument',related_name='proposals') @@ -288,7 +289,7 @@ class Meta: class QAOfficerDocument(Document): proposal = models.ForeignKey('Proposal',related_name='qaofficer_documents') - _file = models.FileField(upload_to=update_qaofficer_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_qaofficer_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history @@ -304,7 +305,7 @@ class Meta: class ReferralDocument(Document): referral = models.ForeignKey('Referral',related_name='referral_documents') - _file = models.FileField(upload_to=update_referral_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_referral_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted @@ -318,7 +319,7 @@ class Meta: class RequirementDocument(Document): requirement = models.ForeignKey('ProposalRequirement',related_name='requirement_documents') - _file = models.FileField(upload_to=update_requirement_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_requirement_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history @@ -2835,7 +2836,7 @@ class Meta: class ProposalLogDocument(Document): log_entry = models.ForeignKey('ProposalLogEntry',related_name='documents') - _file = models.FileField(upload_to=update_proposal_comms_log_filename, max_length=512) + _file = models.FileField(upload_to=update_proposal_comms_log_filename, max_length=512, storage=private_storage) class Meta: app_label = 'commercialoperator' @@ -4960,7 +4961,7 @@ def add_documents(self, request): class FilmingParkDocument(Document): filming_park = models.ForeignKey('ProposalFilmingParks',related_name='filming_park_documents') - _file = models.FileField(upload_to=update_filming_park_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_filming_park_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history @@ -5879,7 +5880,7 @@ class Meta: class EventsParkDocument(Document): events_park = models.ForeignKey('ProposalEventsParks',related_name='events_park_documents') - _file = models.FileField(upload_to=update_events_park_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_events_park_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history @@ -5928,7 +5929,7 @@ def add_documents(self, request): return class PreEventsParkDocument(Document): pre_event_park = models.ForeignKey('ProposalPreEventsParks',related_name='pre_event_park_documents') - _file = models.FileField(upload_to=update_pre_event_park_doc_filename, max_length=512) + _file = models.FileField(upload_to=update_pre_event_park_doc_filename, max_length=512, storage=private_storage) input_name = models.CharField(max_length=255,null=True,blank=True) can_delete = models.BooleanField(default=True) # after initial submit prevent document from being deleted visible = models.BooleanField(default=True) # to prevent deletion on file system, hidden and still be available in history diff --git a/commercialoperator/components/proposals/serializers.py b/commercialoperator/components/proposals/serializers.py index 86528659..b2d6ce37 100644 --- a/commercialoperator/components/proposals/serializers.py +++ b/commercialoperator/components/proposals/serializers.py @@ -435,7 +435,7 @@ def get_other_details(self,obj): return None def get_documents_url(self,obj): - return '/media/{}/proposals/{}/documents/'.format(settings.MEDIA_APP_DIR, obj.id) + return '/private-media/{}/proposals/{}/documents/'.format(settings.MEDIA_APP_DIR, obj.id) def get_readonly(self,obj): return False diff --git a/commercialoperator/components/proposals/serializers_filming.py b/commercialoperator/components/proposals/serializers_filming.py index bb965da8..05d853cb 100644 --- a/commercialoperator/components/proposals/serializers_filming.py +++ b/commercialoperator/components/proposals/serializers_filming.py @@ -210,7 +210,7 @@ class Meta: def get_documents_url(self,obj): - return '/media/{}/proposals/{}/documents/'.format(settings.MEDIA_APP_DIR, obj.id) + return '/private-media/{}/proposals/{}/documents/'.format(settings.MEDIA_APP_DIR, obj.id) def get_readonly(self,obj): return False diff --git a/commercialoperator/urls.py b/commercialoperator/urls.py index b616c3f4..6565cb65 100755 --- a/commercialoperator/urls.py +++ b/commercialoperator/urls.py @@ -196,13 +196,15 @@ url(r'^history/helppage/(?P\d+)/$', proposal_views.HelpPageHistoryCompareView.as_view(), name='helppage_history'), url(r'^history/organisation/(?P\d+)/$', organisation_views.OrganisationHistoryCompareView.as_view(), name='organisation_history'), + url(r'^private-media/', views.getPrivateFile, name='view_private_file'), -] + ledger_patterns + media_serv_patterns +] + ledger_patterns #+ media_serv_patterns # if settings.DEBUG: # Serve media locally in development. # urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.SHOW_DEBUG_TOOLBAR: import debug_toolbar diff --git a/commercialoperator/views.py b/commercialoperator/views.py index 30f92b95..2e492413 100755 --- a/commercialoperator/views.py +++ b/commercialoperator/views.py @@ -13,7 +13,7 @@ from datetime import datetime, timedelta -from commercialoperator.helpers import is_internal +from commercialoperator.helpers import is_internal, is_customer from commercialoperator.forms import * from commercialoperator.components.proposals.models import Referral, Proposal, HelpPage, DistrictProposal from commercialoperator.components.compliances.models import Compliance @@ -21,6 +21,10 @@ from commercialoperator.components.main.models import Park from commercialoperator.components.bookings.email import send_invoice_tclass_email_notification, send_confirmation_tclass_email_notification +import os +import mimetypes +from django.db.models import Q + from ledger.checkout.utils import create_basket_session, create_checkout_session, place_order_submission, get_cookie_basket from django.core.management import call_command import json @@ -166,4 +170,56 @@ def post(self, request): return render(request, self.template_name, data) +def is_authorised_to_access_proposal_document(request,document_id): + if is_internal(request): + return True + elif is_customer(request): + user = request.user + user_orgs = [org.id for org in user.commercialoperator_organisations.all()] + return Proposal.objects.filter(id=document_id).filter( + Q(org_applicant_id__in=user_orgs) | + Q(submitter=user)).exists() + +def get_file_path_id(check_str,file_path): + file_name_path_split = file_path.split("/") + #if the check_str is in the file path, the next value should be the id + if check_str in file_name_path_split: + id_index = file_name_path_split.index(check_str)+1 + if len(file_name_path_split) > id_index and file_name_path_split[id_index].isnumeric(): + return int(file_name_path_split[id_index]) + else: + return False + else: + return False + +def is_authorised_to_access_document(request): + if is_internal(request): + return True + elif is_customer(request): + p_document_id = get_file_path_id("proposals",request.path) + if p_document_id: + return is_authorised_to_access_proposal_document(request,p_document_id) + else: + return False + +def getPrivateFile(request): + + if is_authorised_to_access_document(request): + file_name_path = request.path + #norm path will convert any traversal or repeat / in to its normalised form + full_file_path= os.path.normpath(settings.BASE_DIR+file_name_path) + #we then ensure the normalised path is within the BASE_DIR (and the file exists) + if full_file_path.startswith(settings.BASE_DIR) and os.path.isfile(full_file_path): + extension = file_name_path.split(".")[-1] + the_file = open(full_file_path, 'rb') + the_data = the_file.read() + the_file.close() + if extension == 'msg': + return HttpResponse(the_data, content_type="application/vnd.ms-outlook") + if extension == 'eml': + return HttpResponse(the_data, content_type="application/vnd.ms-outlook") + + return HttpResponse(the_data, content_type=mimetypes.types_map['.'+str(extension)]) + + return HttpResponse() \ No newline at end of file