diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000000000000000000000000000000000000..33b71828dc2a1173e6f1fa3da0ec514b73f74c99 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,37 @@ +## Result at the history endpoint + +I've taken the liberty to produce the results in the history endpoint +as such: + +```json +[{ +"day": "YYYY-MM-DD", +"links": 0, +"files": 1 +}, +... +] +``` + +Because keying them together by day is going to make the thing +a lot harder for the frontend programmer to parse. + + +## Documentation + +I couldn't get the DRF documentation to cooperate with me, and frankly +while with a bigger project I'd probably stick with drf-yasg, +the inline pydocs that I've written here will suffice. + +`/swagger-ui/` is your go-to URL when it comes to listing endpoints. +Submissions are to follow `/api/add`, because DRF happened to generate +nice documentation there and not for Swagger. + +## Authorization + +You can authorize your API requests either via Django cookies +or through a Web-Basic authentication. I care little which you choose. +Getting the session token for your API requests is also on you. + +Since it was not specifically requested for history endpoint to be +available for admin only, it was made available for any logged in user. diff --git a/README.md b/README.md index b0cde87495d9b6d1dbb29367a285b1259267ad4e..68d6da000cc1c9a23276a357ea6b56f72164b2de 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Netguru This contains a solution of the Netguru's recruiment task. +Please read the [notes](NOTES.md). + # Local development To install the application, you check it out from the repo and do: diff --git a/agent/models.py b/agent/models.py index 6174cc90f419054d219a38d31eaa95ba7f49d96f..ab2d6777549f8c47eb318840bd152a7ce3869468 100644 --- a/agent/models.py +++ b/agent/models.py @@ -8,4 +8,3 @@ class UserAgentStorage(models.Model): def __str__(self): return f'{self.user} - {self.ua}' - diff --git a/agent/tests.py b/agent/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/agent/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/counting/admin.py b/counting/admin.py index de486224cbd2e4d948990a3431489d1b2547d05a..e7b4efa90bfbc6d11769c3283f44ec80655ff503 100644 --- a/counting/admin.py +++ b/counting/admin.py @@ -5,8 +5,7 @@ from .models import StoryOfADay @admin.register(StoryOfADay) class StoryOfADayAdmin(admin.ModelAdmin): - readonly_fields = 'day', 'links_visited', 'files_visited' + readonly_fields = 'day', 'links', 'files' def has_add_permission(self, request): return False - diff --git a/counting/cron.py b/counting/cron.py index 8597d58e2496cb2fce4a0b8bf0a2c1f07c58e9d8..a457c138dbed2d5d5a4cadeaff9bb13c568fbbfd 100644 --- a/counting/cron.py +++ b/counting/cron.py @@ -1,7 +1,7 @@ import enum import logging -from datetime import datetime, timedelta +import datetime from django_cron import CronJobBase, Schedule from satella.instrumentation.metrics import getMetric @@ -32,12 +32,13 @@ class ReaperJob(CronJobBase): Let's talk a moment about it's logic - days can be divided into one of 3 categories displayed above in DayType """ - RUN_EVERY_MINS = 24*60 # once each day + RUN_EVERY_MINS = 24 * 60 # once each day schedule = Schedule(run_every_mins=RUN_EVERY_MINS) code = 'reaper-job' # a unique code - def get_day_type(self, date: datetime.date) -> DayType: + @staticmethod + def get_day_type(date: datetime.date) -> DayType: if date >= datetime.date.today() - datetime.timedelta(days=2): return DayType.DAY_LIVE @@ -57,18 +58,18 @@ class ReaperJob(CronJobBase): return # no links to process while self.get_day_type(cur_day) != DayType.LIVE: - links_visited, files_visited = 0, 0 + links, files = 0, 0 for share in Share.objects.get_for_day(cur_day): if share.times_used: if share.share_type == SHARE_FILE: - files_visited += 1 + files += 1 else: - links_visited += 1 - sod = StoryOfADay(day=cur_day, links_visited=links_visited, - files_visited=files_visited) + links += 1 + sod = StoryOfADay(day=cur_day, links=links, + files=files) logger.info('Historic info for %s compiled, %s files visited, %s links visited', - cur_day, files_visited, links_visited) + cur_day, files, links) entries_compiled.runtime(+1) sod.save() share.delete() - cur_day = cur_day + timedelta(days=1) + cur_day = cur_day + datetime.timedelta(days=1) diff --git a/counting/migrations/0001_initial.py b/counting/migrations/0001_initial.py index 10dbe62635a0961adc34a358b60b0afd4555f7f4..ed3c63c84da9b1c317da94bc8b89a4380a48005e 100644 --- a/counting/migrations/0001_initial.py +++ b/counting/migrations/0001_initial.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,8 +14,8 @@ class Migration(migrations.Migration): name='StoryOfADay', fields=[ ('day', models.DateField(primary_key=True, serialize=False, verbose_name='Date')), - ('links_visited', models.IntegerField(verbose_name='Links visited')), - ('files_visited', models.IntegerField(verbose_name='Files visited')), + ('links', models.IntegerField(verbose_name='Links visited')), + ('files', models.IntegerField(verbose_name='Files visited')), ], ), ] diff --git a/counting/models.py b/counting/models.py index 5266213c8c640d9dc6fa446108bf41dff98cac7e..3671a88871fc3047c0c76e1f03adeba43e8f657b 100644 --- a/counting/models.py +++ b/counting/models.py @@ -3,5 +3,13 @@ from django.db import models class StoryOfADay(models.Model): day = models.DateField(verbose_name='Date', primary_key=True) - links_visited = models.IntegerField(verbose_name='Links visited') - files_visited = models.IntegerField(verbose_name='Files visited') + links = models.IntegerField(verbose_name='Links visited') + files = models.IntegerField(verbose_name='Files visited') + + def __hash__(self): + return hash(self.day) + + def __eq__(self, other): + if isinstance(other, StoryOfADay): + other = other.day + return self.day == other diff --git a/counting/tests.py b/counting/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/counting/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/counting/views.py b/counting/views.py index 91ea44a218fbd2f408430959283f0419c921093e..00eb177cd84823d843bd6b2b1cde8b0114108d85 100644 --- a/counting/views.py +++ b/counting/views.py @@ -1,3 +1,57 @@ -from django.shortcuts import render +import datetime # Create your views here. +from rest_framework import serializers, status +from rest_framework.response import Response +from rest_framework.views import APIView + +from counting.cron import ReaperJob, DayType +from counting.models import StoryOfADay +from shares.models import Share, SHARE_FILE + + +class StoryOfADaySerializer(serializers.ModelSerializer): + class Meta: + model = StoryOfADay + fields = ['day', 'links', 'files'] + + +def get_history_for(date: datetime.date) -> StoryOfADay: + dt = ReaperJob.get_day_type(date) + if dt == DayType.DAY_WITH_HISTORY: + return StoryOfADay.objects.get(date=date) + else: + # Count them ad hoc + links, files = 0, 0 + for share in Share.objects.get_for_day(date): + if share.times_used: + if share.share_type == SHARE_FILE: + files += 1 + else: + links += 1 + return StoryOfADay(day=date, links=links, files=files) + + +class GetHistory(APIView): + """ + Loads the history. + + You must be authorized to access this endpoint. + + The result will be a list of elements like + { + "day": "YYYY-MM-DD", + "links": 0, + "files": 1 + } + """ + + def get(self, request): + # if not request.user.is_authenticated: + # return Response({}, status=status.HTTP_401_UNAUTHORIZED) + items = set(StoryOfADay.objects.all()) + today = datetime.date.today() + items.add(get_history_for(today)) + items.add(get_history_for(today - datetime.timedelta(days=1))) + items = list(items) + return Response(StoryOfADaySerializer(items, many=True).data) diff --git a/netguru/context.py b/netguru/context.py index d8089f77a2ea7a255263fec234b7cec99d747fc2..1bba96744dcf1b594dbea3c279234d43b6ce53a1 100644 --- a/netguru/context.py +++ b/netguru/context.py @@ -1,5 +1,3 @@ -def add_user(request): - a = {} - if hasattr(request, 'user'): - a['user'] = request.user +def add_request(request): + a = {'request': request} return a diff --git a/netguru/settings.py b/netguru/settings.py index b2ab496a7af2f8508be5af30435e0693b760e0c0..d12d8e478e96913b462425569cd4a3ea3c42cbf9 100644 --- a/netguru/settings.py +++ b/netguru/settings.py @@ -37,9 +37,20 @@ CRON_CLASSES = [ ] REST_FRAMEWORK = { - 'DEFAULT_PARSER_CLASSES': [ + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', - ] + ), + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication' + ], + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ), + 'UNICODE_JSON': True, } MIDDLEWARE = [ @@ -68,7 +79,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'netguru.context.add_user', + 'netguru.context.add_request', ], }, }, @@ -130,6 +141,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'media') FILE_SAVE_LOCATION = '/data' +# Configure DRF and Swagger +DEFAULT_SCHEMA_CLASS = 'rest_framework.schemas.openapi.AutoSchema' + # Configure tracing # ================= OPENTRACING_TRACE_ALL = True diff --git a/netguru/urls.py b/netguru/urls.py index ca971356fcb2e49695a894eccaa62c12771772bc..99f10a658f65d9236dc820b6c581629b100e4e0a 100644 --- a/netguru/urls.py +++ b/netguru/urls.py @@ -1,12 +1,32 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include, re_path +from django.views.generic import TemplateView +from rest_framework.schemas import get_schema_view -from shares import views +from netguru import settings +from shares import views as shares_views, api as shares_api +from counting import views as counting_views urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), - path('accounts/profile', views.add_share), - re_path(r'shares/(?P<share_id>[0-9]+)$', views.view_share), + path('accounts/profile', shares_views.add_share), + path('api/add', shares_api.AddShare.as_view()), + path('api/history', counting_views.GetHistory.as_view()), + path('api/get/<int:share_id>', shares_api.GetShare.as_view()), + re_path(r'shares/(?P<share_id>[0-9]+)$', shares_views.view_share), path('admin/', admin.site.urls), + path('', shares_views.add_share), ] + static('/static/', document_root='/app/media/') + +if settings.DEBUG: + urlpatterns += [ + path('swagger-ui/', TemplateView.as_view( + template_name='swagger-ui.html', + extra_context={'schema_url': 'openapi-schema'} + ), name='swagger-ui'), + path('openapi', get_schema_view( + title="Netguru recruitment task", + version="1.0" + ), name='openapi-schema'), + ] diff --git a/requirements.txt b/requirements.txt index e01c6e4c1e723b2d0e5b5514517a770e211915fe..f588852fb862e635077aa0515dcb1d4966f78746 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,6 @@ satella django-satella-metrics django_opentracing djangorestframework +pyyaml +uritemplate +coreapi diff --git a/shares/admin.py b/shares/admin.py index 9d38c9917212d35f6ee1047abe31b9a0ff5f85d9..adb7bc3e8b0c25cc1488ce300e749ea6834bcdf6 100644 --- a/shares/admin.py +++ b/shares/admin.py @@ -6,7 +6,6 @@ from shares.views import hash_password class ShareForm(forms.ModelForm): - class Meta: model = Share exclude = ['pwd_hash'] @@ -28,9 +27,13 @@ class ShareAdmin(admin.ModelAdmin): list_select_related = 'creator', def has_add_permission(self, request): + """Block user from adding new shares - you can use the /accounts/panel link to do that :D""" return False def delete_queryset(self, request, queryset): - """We need to unlink files""" + """ + Since Django does not call each instance's delete on mass delete in admin panel + we need to do that ourselves. + """ for share in queryset: share.delete() diff --git a/shares/api.py b/shares/api.py new file mode 100644 index 0000000000000000000000000000000000000000..8eb2d1fec12a23858536665424358c663db9b919 --- /dev/null +++ b/shares/api.py @@ -0,0 +1,88 @@ +from django.shortcuts import get_object_or_404, redirect +from django.http import StreamingHttpResponse as SHTTPResponse +from rest_framework import serializers, status +from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView +from satella.instrumentation.metrics import getMetric + +from shares.models import Share, hash_password, SHARE_FILE, add_file, add_url + +files = getMetric('visited.api.files', 'counter') +links = getMetric('visited.api.links', 'counter') + + +class AddShareSerializer(serializers.Serializer): + """this is for documentation autogeneration purpose only""" + file = serializers.FileField(required=False) + url = serializers.CharField(required=False) + + +class ShareSerializer(serializers.Serializer): + url = serializers.CharField(required=True) + password = serializers.CharField(required=True) + + +class AddShare(APIView): + """ + Adds a share + + You have to provide either a field called url or upload a file. If you're using + multipart, name it file. + The result will be a JSON object containing two properties: url and password. + The result will be passed using HTTP 201. + + You must be authorized to access this endpoint. + """ + parser_classes = FileUploadParser, MultiPartParser, JSONParser + + def get_serializer(self): + return AddShareSerializer() + + def post(self, request, format=None): + if not request.user.is_authenticated: + return Response({}, status=status.HTTP_401_UNAUTHORIZED) + if 'file' in request.data: + dct = add_file(request.data['file'], request, escape_url=False) + elif 'url' in request.data: + dct = add_url(request.data['url'], request, escape_url=False) + else: + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + return Response(dct, status=status.HTTP_201_CREATED) + + +class GetShareSerializer(serializers.Serializer): + password = serializers.CharField(required=True, label='Password') + + +class GetShare(APIView): + """ + Loads the history + + One JSON argument is expected - password, to contain the password. + You will receive either a 302 Redirect or a 200 OK with the file contents + """ + + def get_serializer(self, *args): + return GetShareSerializer(*args) + + def post(self, request, share_id: str, format=None): + serializer = self.get_serializer(request.data) + if not serializer.is_valid(): + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + share = get_object_or_404(Share, id=share_id) + if hash_password(serializer.data['password']) != share.pwd_hash: + return Response({}, status=status.HTTP_401_UNAUTHORIZED) + + try: + if share.share_type == SHARE_FILE: + files.runtime(+1) + return SHTTPResponse(**share.get_kwargs_for_s_http_response()) + else: + links.runtime(+1) + return redirect(share.resource) + finally: + share.times_used += 1 + share.save() diff --git a/shares/models.py b/shares/models.py index 2748ede00773a5228df18cbe1d26db9cfe3af072..694c2245148c7e30f50b58341124e6cc944c0f4a 100644 --- a/shares/models.py +++ b/shares/models.py @@ -1,17 +1,24 @@ +import hashlib import typing as tp import mimetypes import os import datetime +import uuid +from django.core.files.uploadedfile import UploadedFile from django.db import models +from django.utils.safestring import mark_safe +from django_common.auth_backends import User from satella.coding import silence_excs from satella.instrumentation.metrics import getMetric -from netguru.settings import FILE_SAVE_LOCATION +from netguru.settings import FILE_SAVE_LOCATION, SECRET_KEY files_deleted = getMetric('deleted.files', 'counter') links_deleted = getMetric('deleted.links', 'counter') +files_created = getMetric('created.files', 'counter') +links_created = getMetric('created.links', 'counter') SHARE_URL = 0 SHARE_FILE = 1 @@ -24,8 +31,8 @@ SHARE_TYPES = [ class ShareManager(models.Manager): def get_for_day(self, date: datetime.date): - return super().get_queryset().filter(created_on__ge=date, - created_on__lt=date+datetime.timedelta(days=1)) + return super().get_queryset().filter(created_on__gte=date, + created_on__lt=date + datetime.timedelta(days=1)) class Share(models.Model): @@ -129,3 +136,70 @@ class Share(models.Model): links_deleted.runtime(+1) super().delete(*args, **kwargs) + +def hash_password(password: str) -> str: + return hashlib.sha256(password.encode('utf-8') + SECRET_KEY.encode('utf-8')).hexdigest() + + +def generate_correct_uuid() -> str: + f = uuid.uuid4().hex + # I know that UUID collisions practically don't happen, but since it's so cheap for us + # to go that extra mile + # and since we're at risk at overwriting somebody's files, I guess i should check it + # I realize that it's susceptible to race conditions, but then again what is the chance? + # The chance that UUID will repeat during the 24 hours a link is active is much higher + # than the chance that it will repeat during the same 2 second window + while os.path.exists(os.path.join(FILE_SAVE_LOCATION, f)): + f = uuid.uuid4().hex + return f + + +def add_file(file: UploadedFile, request, escape_url: bool = True) -> dict: + """ + Adds a new file + + :param file: file to add + :param request: request to use + :return: a dictionary with following keys (password, url, type) + """ + data_added = {'password': User.objects.make_random_password(), 'type': 'file'} + pwd_hash = hash_password(data_added['password']) + file_name = generate_correct_uuid() + resource_name = f'{file_name}.{file.name}' + with open(os.path.join(FILE_SAVE_LOCATION, file_name), 'wb') as f_out: + for chunk in file.chunks(): + f_out.write(chunk) + share = Share(creator=request.user, + resource=resource_name, + pwd_hash=pwd_hash, + share_type=SHARE_FILE) + files_created.runtime(+1) + share.save() + url = f'https://{request.get_host()}/shares/{share.id}' + if escape_url: + url = mark_safe(url) + data_added['url'] = url + return data_added + + +def add_url(url: str, request, escape_url: bool = True) -> dict: + """ + Adds a new URL + + :param file: file to add + :param request: request to use + :return: a dictionary with following keys (password, url, type) + """ + data_added = {'password': User.objects.make_random_password(), 'type': 'file'} + pwd_hash = hash_password(data_added['password']) + share = Share(creator=request.user, + resource=url, + pwd_hash=pwd_hash, + share_type=SHARE_URL) + links_created.runtime(+1) + share.save() + url = f'https://{request.get_host()}/shares/{share.id}' + if escape_url: + url = mark_safe(url) + data_added['url'] = url + return data_added diff --git a/shares/templates/share/add.html b/shares/templates/share/add.html index d9e6d8df474ff832e2371b5a88c51abd49d5eb9b..d813766dd4d4190dc24c4661084c213cfafe034a 100644 --- a/shares/templates/share/add.html +++ b/shares/templates/share/add.html @@ -6,7 +6,9 @@ {% endblock %} {% block body %} {% if added %} - <div><em>Resource successfully added:</em><br/> + <div> + <em>{% if added.type == 'file' %}File{% else %}Link{% endif %} successfully + added:</em><br> Your password is <em id="link_password">{{ added.password }}</em> <button onclick="copyTextToClipboard('{{ added.password }}')">Copy the password to clipboard @@ -25,5 +27,6 @@ {{ form.as_p }} <p> <input type="submit" value="Submit"> - </form> + </form><br> + <a href="/accounts/logout">Log out</a> {% endblock %} diff --git a/shares/templates/share/view.html b/shares/templates/share/view.html index 24aabc16eabe1d043903201965453b0362c6e60c..40191cb768ab2ec5107a3e676efe913c390a9d6c 100644 --- a/shares/templates/share/view.html +++ b/shares/templates/share/view.html @@ -18,6 +18,9 @@ <p> <input type="submit" value="Submit"> </p> - </form> + </form><br> + {% endif %} + {% if request.user.is_authenticated %} + <a href="/accounts/logout">Log out</a> {% endif %} {% endblock %} diff --git a/shares/views.py b/shares/views.py index a5ffd8f1f5a67c1e7c26d467d82dda3c6d71e6d8..2c28bcdddf629ab847330e5033cbe07ba527c0d9 100644 --- a/shares/views.py +++ b/shares/views.py @@ -6,6 +6,7 @@ import logging import string import uuid from django import forms +from django.contrib.auth.models import User from django.core.validators import URLValidator from django.forms import fields from django.core.exceptions import ValidationError @@ -15,14 +16,12 @@ from django.contrib.auth.decorators import login_required from django.utils.safestring import mark_safe from satella.instrumentation.metrics import getMetric -from shares.models import Share, SHARE_URL, SHARE_FILE +from shares.models import Share, SHARE_URL, SHARE_FILE, hash_password, add_file, add_url import hashlib from netguru.settings import SECRET_KEY, FILE_SAVE_LOCATION -files_created = getMetric('created.files', 'counter') -links_created = getMetric('created.links', 'counter') -files_visited = getMetric('visited.files', 'counter') -links_visited = getMetric('visited.links', 'counter') +files = getMetric('visited.form.files', 'counter') +links = getMetric('visited.form.links', 'counter') logger = logging.getLogger(__name__) @@ -31,33 +30,6 @@ logger = logging.getLogger(__name__) # ============== -def hash_password(password: str) -> str: - return hashlib.sha256(password.encode('utf-8') + SECRET_KEY.encode('utf-8')).hexdigest() - - -def generate_correct_uuid() -> str: - f = uuid.uuid4().hex - # I know that UUID collisions practically don't happen, but since it's so cheap for us - # to go that extra mile - # and since we're at risk at overwriting somebody's files, I guess i should check it - # I realize that it's susceptible to race conditions, but then again what is the chance? - # The chance that UUID will repeat during the 24 hours a link is active is much higher - # than the chance that it will repeat during the same 2 second window - while os.path.exists(os.path.join(FILE_SAVE_LOCATION, f)): - f = uuid.uuid4().hex - return f - - -# Cut off O, I and S so that the user doesn't confuse them with 0, 1 and 5 respectively -CHARACTERS_TO_USE = string.ascii_uppercase.replace('O', '') \ - .replace('I', '').replace('S', '') + string.digits - - -def random_password() -> str: - """Generate a eight character reasonably safe password""" - return ''.join(random.choice(CHARACTERS_TO_USE) for i in range(6)) - - class AddShareURLForm(forms.Form): url = fields.CharField(label='URL', required=False, validators=[URLValidator()]) @@ -77,31 +49,16 @@ def add_share(request): if request.method == 'POST': form = AddShareURLForm(request.POST, request.FILES) if form.is_valid(): - data_added = {'password': random_password()} + pwd_hash = hash_password(data_added['password']) data = form.cleaned_data if data.get('url'): # Create a URL resource - share = Share(creator=request.user, - resource=data['url'], - pwd_hash=pwd_hash, - share_type=SHARE_URL) - links_created.runtime(+1) - logger.info('Created a link') + data_added = add_url(data['url'], request) + logger.info('Created a link via form') else: - file_name = generate_correct_uuid() - file = request.FILES['file'] - resource_name = f'{file_name}.{file.name}' - with open(os.path.join(FILE_SAVE_LOCATION, file_name), 'wb') as f_out: - for chunk in file.chunks(): - f_out.write(chunk) - share = Share(creator=request.user, - resource=resource_name, - pwd_hash=pwd_hash, - share_type=SHARE_FILE) - files_created.runtime(+1) - share.save() - data_added['url'] = mark_safe(f'https://{request.get_host()}/shares/{share.id}') + data_added = add_file(request.FILES['file'], request) + logger.info('Created a file via form') else: form = AddShareURLForm() @@ -121,7 +78,6 @@ class InvalidPassword(Exception): def view_share(request, share_id: str): - def render_me(arg, **kwargs): return render(request, 'share/view.html', arg, **kwargs) @@ -140,14 +96,14 @@ def view_share(request, share_id: str): raise InvalidPassword() try: if share.share_type == SHARE_URL: - links_visited.runtime(+1) + links.runtime(+1) return redirect(share.resource) else: if not os.path.exists(share.path): raise Http404() # Django will have the UTF-8 mumbo jumbo for us - files_visited.runtime(+1) + files.runtime(+1) return SHTTPResponse(**share.get_kwargs_for_s_http_response()) finally: share.times_used += 1 diff --git a/templates/swagger-ui.html b/templates/swagger-ui.html new file mode 100644 index 0000000000000000000000000000000000000000..efb94b5bf296147c1f93a441d960a440809aa463 --- /dev/null +++ b/templates/swagger-ui.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <title>Swagger</title> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css"/> +</head> +<body> +<div id="swagger-ui"></div> +<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script> +<script> + const ui = SwaggerUIBundle({ + url: "{% url schema_url %}", + dom_id: '#swagger-ui', + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset + ], + layout: "BaseLayout", + requestInterceptor: (request) => { + request.headers['X-CSRFToken'] = "{{ csrf_token }}" + return request; + } + }) +</script> +</body> +</html>