Compare commits

..

21 Commits

Author SHA1 Message Date
3328c6af3b sve radi 2024-01-24 12:31:43 +01:00
279c7df84a prosli testovi nakon dodavanja user-a 2024-01-22 15:32:17 +01:00
bc9afd396b dodan user 2024-01-22 09:12:29 +01:00
44a9b4570b uredjen serializer 2024-01-18 16:02:54 +01:00
f46bc8337e PROSLI TESTOVI 2024-01-18 14:29:48 +01:00
298219ab30 dodan user 2024-01-18 12:03:37 +01:00
75c00b0e91 testovi ne rade 2024-01-17 08:32:22 +01:00
92d98a299c prije uspostave API documentation 2024-01-16 08:49:37 +01:00
ade8ea32e2 dodani testovi 2024-01-15 11:49:58 +01:00
9908fcad02 prije restarta 2024-01-12 10:11:50 +01:00
6e89ea4000 provedena paginacija 2024-01-11 15:08:33 +01:00
3931fc57bf implementiran REST API dio 2024-01-11 12:38:56 +01:00
6614ae2e44 dodani svi 2024-01-10 13:53:34 +01:00
eaac31978f dodan folder resources 2024-01-10 12:44:55 +01:00
c5fb12ffb8 uspješna skripta, pokušaj rendera 2024-01-10 12:29:03 +01:00
6994f43490 uspješno učitana skripta 2024-01-10 10:26:23 +01:00
3709991263 dodana skripta2 2024-01-09 13:04:44 +01:00
7c144293a3 dodana skripta 2024-01-09 12:14:03 +01:00
7d2df70d53 obrisani lat i lon 2024-01-09 09:42:20 +01:00
1b80bda3d5 izbrisana lokacija 2024-01-09 08:46:33 +01:00
b89fde9c92 prije dodavanja aplikacije 2024-01-08 15:41:02 +01:00
83 changed files with 1913 additions and 38 deletions

34
Dockerfile.dev Normal file
View File

@@ -0,0 +1,34 @@
# Use an official Python runtime as a parent image
FROM python:3.8-slim
# Set environment variables for Django
ENV DJANGO_SETTINGS_MODULE=plovidba_projekt.settings
ENV PYTHONUNBUFFERED 1
# Install PostgreSQL development headers, build tools, and other dependencies
RUN apt-get update && \
apt-get install -y libpq-dev gcc && \
apt-get clean
# Install GDAL
RUN apt-get update \
&& apt-get install -y binutils libproj-dev gdal-bin
# Install the PostgreSQL client
RUN apt-get update && apt-get install -y postgresql-client
# Create and set the working directory in the container
WORKDIR /app/plovidba_projekt
# Copy the requirements file into the container and install dependencies
COPY requirements.txt /app/plovidba_projekt/
RUN pip install -r requirements.txt
# Copy the rest of the application code into the container
COPY . /app/plovidba_projekt/
# Expose the port the application will run on (if necessary)
# EXPOSE 8000
# Define the default command to run when starting the container
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

40
Dockerfile.prod Normal file
View File

@@ -0,0 +1,40 @@
# Use an official Python runtime as a parent image
FROM python:3.8-slim
# Set environment variables for Django
ENV DJANGO_SETTINGS_MODULE=plovidba_projekt.settings
ENV PYTHONUNBUFFERED 1
# Install PostgreSQL development headers, build tools, and other dependencies
RUN apt-get update && \
apt-get install -y libpq-dev gcc && \
apt-get clean
# Install GDAL
RUN apt-get update \
&& apt-get install -y binutils libproj-dev gdal-bin
# Install PostgreSQL client and PostGIS extension
RUN apt-get update && \
apt-get install -y postgresql-client postgis && \
apt-get clean
# .Create and set the working directory in the container
RUN mkdir -p /app/plovidba_projekt
WORKDIR /app/plovidba_projekt
# .Copy the requirements file into the container and install dependencies
COPY requirements.txt /app/plovidba_projekt/
RUN pip install -r requirements.txt
# .Copy the rest of the application code into the container
COPY . /app/plovidba_projekt/
VOLUME /app/media
VOLUME /app/static
# Expose the port the application will run on (if necessary)
EXPOSE 8000
# Run the Django application using Gunicorn in production mode
CMD ["gunicorn", "plovidba_projekt.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]

33
docker-compose.yaml Normal file
View File

@@ -0,0 +1,33 @@
version: "3"
volumes:
media:
static:
services:
# Development Environment
api_dev:
build:
context: .
dockerfile: Dockerfile.dev
container_name: plovidba_dev_container10
restart: on-failure
ports:
- "8000:8000" # Map host port 8000 to container port 8000
volumes:
- .:/app # Mount your application code into the container
env_file:
- .env # Specify the location of your .env files
# Production Environment
api_prod:
build:
context: .
dockerfile: Dockerfile.prod
container_name: plovidba_prod_container
restart: on-failure
ports:
- "8000:8000" # Map host port 8000 to container port 8000
env_file:
- C:\Users\Student1\Desktop\plovidba\myenv\plovidba_projekt\plovidba_projekt # Specify the location of your .env file
volumes:
- static:/app/static
- media:/app/media

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,6 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. # Register your models here.
from .models import ObjektSigurnosti
admin.site.register(ObjektSigurnosti)

View File

@@ -0,0 +1,69 @@
import json
import os
import sys
from django.conf import settings
from django.core.management.base import BaseCommand
from django.contrib.gis.geos import Point
from plovidba_aplikacija.models import ObjektSigurnosti
from django.db import transaction
# dodani podaci
class Command(BaseCommand):
help = """
Programatically create entries for plovidba_aplikacija model ObjektSigurnosti
"""
def handle(self, *args, **options):
json_rpath = os.path.join(settings.RESOURCES_DIR, 'testnipodaci.json')
created_entries = 0
existing_entries = 0
with open(json_rpath, encoding='utf-8') as r:
data = json.load(r)
with transaction.atomic():
for feature in data.get('features', []):
properties = feature.get('properties', {})
geometry = feature.get('geometry', {})
latitude = geometry.get('coordinates', [])[1]
longitude = geometry.get('coordinates', [])[0]
naziv_objekta = properties.get('naziv_objekta', '')
ps_br = properties.get('ps_br', None)
e_br = properties.get('e_br', None)
tip_objekta = properties.get('tip_objekta', None)
lucka_kapetanija = properties.get('lucka_kapetanija', None)
fotografija = properties.get('fotografija', '')
id_ais = properties.get('id_ais', None)
simbol_oznaka = properties.get('simbol_oznaka', '')
if not (isinstance(latitude, (float, int)) and isinstance(longitude, (float, int))):
continue
if not (naziv_objekta and latitude and longitude):
continue
print(f"Latitude: {latitude}, Longitude: {longitude}, Naziv Objekta: {naziv_objekta}")
obj, created = ObjektSigurnosti.objects.get_or_create(
naziv=naziv_objekta,
lokacija=Point(float(longitude), float(latitude)),
ps_br = ps_br,
e_br = e_br,
tip_objekta = tip_objekta,
lucka_kapetanija = lucka_kapetanija,
fotografija = fotografija,
id_ais = id_ais,
simbol_oznaka = simbol_oznaka,
)
if created:
created_entries +=1
print("Kreiran OS {} - {}".format(obj.naziv, obj.lokacija))
else:
existing_entries += 1
print("Existing OS {} - {}".format(obj.naziv, obj.lokacija))
print("Created: {}".format(created_entries))
print("Existing:".format(existing_entries))

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,6 @@ class Migration(migrations.Migration):
name='ObjektSigurnosti', name='ObjektSigurnosti',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lokacija', django.contrib.gis.db.models.fields.PointField(srid=4326)),
('naziv', models.CharField(max_length=255)), ('naziv', models.CharField(max_length=255)),
], ],
), ),

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.9 on 2024-01-08 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objektsigurnosti',
name='lat',
field=models.FloatField(default=0.0),
),
migrations.AddField(
model_name='objektsigurnosti',
name='lon',
field=models.FloatField(default=0.0),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-01-09 08:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0002_remove_objektsigurnosti_lokacija_and_more'),
]
operations = [
migrations.AddField(
model_name='objektsigurnosti',
name='nazi1v',
field=models.CharField(default='test', max_length=255),
preserve_default=False,
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.9 on 2024-01-09 08:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0003_objektsigurnosti_nazi1v'),
]
operations = [
migrations.RemoveField(
model_name='objektsigurnosti',
name='nazi1v',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-01-09 08:16
import django.contrib.gis.db.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0004_remove_objektsigurnosti_nazi1v'),
]
operations = [
migrations.AddField(
model_name='objektsigurnosti',
name='lokacija',
field=django.contrib.gis.db.models.fields.PointField(null=True, srid=4326),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-01-09 08:16
import django.contrib.gis.db.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0005_objektsigurnosti_lokacija'),
]
operations = [
migrations.AlterField(
model_name='objektsigurnosti',
name='lokacija',
field=django.contrib.gis.db.models.fields.PointField(null=True, srid=3765),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-01-09 08:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0006_alter_objektsigurnosti_lokacija'),
]
operations = [
migrations.RemoveField(
model_name='objektsigurnosti',
name='lat',
),
migrations.RemoveField(
model_name='objektsigurnosti',
name='lon',
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 4.2.9 on 2024-01-10 12:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plovidba_aplikacija', '0007_remove_objektsigurnosti_lat_and_more'),
]
operations = [
migrations.AddField(
model_name='objektsigurnosti',
name='e_br',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='fotografija',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='id_ais',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='lucka_kapetanija',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='ps_br',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='simbol_oznaka',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='objektsigurnosti',
name='tip_objekta',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.9 on 2024-01-22 07:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('plovidba_aplikacija', '0008_objektsigurnosti_e_br_objektsigurnosti_fotografija_and_more'),
]
operations = [
migrations.AddField(
model_name='objektsigurnosti',
name='operater',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-01-22 12:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('plovidba_aplikacija', '0009_objektsigurnosti_operater'),
]
operations = [
migrations.AlterField(
model_name='objektsigurnosti',
name='operater',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='imenovana_mjesta', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-01-22 14:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('plovidba_aplikacija', '0010_alter_objektsigurnosti_operater'),
]
operations = [
migrations.AlterField(
model_name='objektsigurnosti',
name='operater',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,11 +1,20 @@
from django.db import models from django.db import models
# Create your models here.
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.auth.models import User
from django.conf import settings
class ObjektSigurnosti(models.Model): class ObjektSigurnosti(models.Model):
lokacija = models.PointField()
naziv = models.CharField(max_length=255) naziv = models.CharField(max_length=255)
lokacija = models.PointField(null=True, srid=3765)
ps_br = models.CharField(max_length=255, null=True, blank=True)
e_br = models.CharField(max_length=255, null=True, blank=True)
tip_objekta = models.IntegerField(null=True, blank=True)
lucka_kapetanija = models.CharField(max_length=255, null=True, blank=True)
fotografija = models.CharField(max_length=255, null=True, blank=True)
id_ais = models.CharField(max_length=255, null=True, blank=True)
simbol_oznaka = models.CharField(max_length=255, null=True, blank=True)
operater = models.ForeignKey("user.User", on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self):
return self.naziv

View File

@@ -1,9 +1,63 @@
# serializers.py
from rest_framework import serializers from rest_framework import serializers
from django.contrib.gis.geos import Point
from .models import ObjektSigurnosti from .models import ObjektSigurnosti
import json
from rest_framework.exceptions import ValidationError
class ObjektSigurnostiSerializer(serializers.ModelSerializer): class ObjektSigurnostiSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ObjektSigurnosti model = ObjektSigurnosti
fields = '__all__' fields = '__all__'
def create(self, validated_data):
lokacija_data = validated_data.pop('lokacija', None)
try:
lokacija_data = json.loads(lokacija_data)
except (json.JSONDecodeError, TypeError):
raise ValidationError("Nevaljan format za 'lokacija'. Mora biti JSON.")
if isinstance(lokacija_data, dict):
lon = lokacija_data.get('lon')
lat = lokacija_data.get('lat')
if lon is not None and lat is not None:
lokacija = Point(lon, lat, srid=3765)
objekt = ObjektSigurnosti.objects.create(lokacija=lokacija, **validated_data)
return objekt
raise ValidationError("Nevaljani 'lon' i 'lat' vrijednosti za 'lokacija'.")
def to_representation(self, instance): #u sklopu ovog definiran i update
representation = super().to_representation(instance) #da se prikažu sva polja
representation['lon'] = instance.lokacija.x
representation['lat'] = instance.lokacija.y
return representation
def update(self, instance, validated_data):
lokacija_data = validated_data.get('lokacija', None)
if lokacija_data:
try:
lokacija_data = json.loads(lokacija_data)
except (json.JSONDecodeError, TypeError):
raise ValidationError("Nevaljan format za 'lokacija'. Mora biti JSON.")
if isinstance(lokacija_data, dict):
lon = lokacija_data.get('lon')
lat = lokacija_data.get('lat')
if lon is not None and lat is not None:
lokacija = Point(lon, lat, srid=3765)
instance.lokacija = lokacija
instance.save()
return instance
raise ValidationError("Nevaljani 'lon' i 'lat' vrijednosti za 'lokacija'.")
return super().update(instance, validated_data)

View File

@@ -1,3 +1,52 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.gis.geos import Point
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from plovidba_aplikacija.models import ObjektSigurnosti
# Create your tests here.
# Testiranje listanja objekata
class ObjektSigurnostiListTest(APITestCase):
def test_list_objekti_sigurnosti(self):
url = reverse('objektisigurnosti-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Testiranje stvaranja objekata
class ObjektSigurnostiCreateTest(APITestCase):
def setUp(self):
self.url = reverse('objektisigurnosti-list')
def test_create_objekt_sigurnosti(self):
data = {
'lokacija': '{"lat": 45.123, "lon": 18.456}',
'naziv': 'test-naziv',
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(ObjektSigurnosti.objects.filter(naziv='test-naziv').exists())
# Testiranje dohvaćanja pojedinog objekata
class ObjektSigurnostiDetailTest(APITestCase):
def setUp(self):
self.objekt = ObjektSigurnosti.objects.create(
lokacija=Point(18.456, 45.123),
naziv='test-naziv',
)
self.url = reverse('objektisigurnosti-detail', args=[self.objekt.pk])
def test_get_objekt_sigurnosti(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_update_objekt_sigurnosti(self):
data = {
'lokacija': '{"lat": 45.123, "lon": 18.456}',
'naziv': 'test-naziv',
}
response = self.client.patch(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -1,10 +1,9 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ObjektSigurnostiViewSet
router = DefaultRouter() from django.urls import path, include
router.register(r'objekti', ObjektSigurnostiViewSet, basename='objekt-sigurnosti') from .views import ObjektSigurnostiList, ObjektSigurnostiDetail
from plovidba_aplikacija import views
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('objekti/', ObjektSigurnostiList.as_view(), name='objektisigurnosti-list'),
path('objekti/<int:pk>/', ObjektSigurnostiDetail.as_view(), name='objektisigurnosti-detail' ),
] ]

View File

@@ -1,12 +1,30 @@
from django.shortcuts import render from rest_framework.views import APIView
from rest_framework.generics import ListAPIView
# Create your views here. from rest_framework.response import Response
from rest_framework import status
from rest_framework import viewsets from rest_framework import generics
from .models import ObjektSigurnosti from .models import ObjektSigurnosti
from .serializers import ObjektSigurnostiSerializer from .serializers import ObjektSigurnostiSerializer
from django.shortcuts import get_object_or_404
from rest_framework.pagination import LimitOffsetPagination
class ObjektSigurnostiViewSet(viewsets.ModelViewSet):
class CustomObjektSigurnostiPagination(LimitOffsetPagination):
default_limit = 20
class ObjektSigurnostiList(generics.ListCreateAPIView):
queryset = ObjektSigurnosti.objects.all().order_by("naziv")
serializer_class = ObjektSigurnostiSerializer
pagination_class = CustomObjektSigurnostiPagination
permission_classes = []
def get_serializer_class(self):
if self.request.method == "GET":
return ObjektSigurnostiSerializer
return self.serializer_class
class ObjektSigurnostiDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = ObjektSigurnosti.objects.all() queryset = ObjektSigurnosti.objects.all()
serializer_class = ObjektSigurnostiSerializer serializer_class = ObjektSigurnostiSerializer
permission_classes = []

Binary file not shown.

Binary file not shown.

101
plovidba_projekt/env.py Normal file
View File

@@ -0,0 +1,101 @@
import os
__all__ = [
'BASE_DIR', 'ABS_PATH', 'ENV_BOOL', 'ENV_NUM', 'ENV_STR', 'ENV_LIST', 'PARDIR', 'ENV_TUPLE'
]
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
PARDIR = os.pardir
APPLICATION_NAME = "plovidba_aplikacija "
ENV_PATH = os.path.join('/etc/secrets/', APPLICATION_NAME)
def ABS_PATH(*args):
return os.path.join(BASE_DIR, *args)
def ENV_BOOL(name, default=False):
"""
Get a boolean value from environment variable.
If the environment variable is not set or value is not one or "true" or
"false", the default value is returned instead.
"""
if name not in os.environ:
return default
if os.environ[name].lower() in ['true', 'yes', '1']:
return True
elif os.environ[name].lower() in ['false', 'no', '0']:
return False
else:
return default
def ENV_NUM(name, default=None):
"""
Get a integer value from environment variable.
If the environment variable is not set, the default value is returned
instead.
"""
return int(os.environ.get(name, default))
def ENV_STR(name, default=None):
"""
Get a string value from environment variable.
If the environment variable is not set, the default value is returned
instead.
"""
return os.environ.get(name, default)
def ENV_LIST(name, separator, default=None):
"""
Get a list of string values from environment variable.
If the environment variable is not set, the default value is returned
instead.
"""
if default is None:
default = []
if name not in os.environ:
return default
return os.environ[name].split(separator)
def ENV_TUPLE(name, separator, default=None):
"""
Get a tuple of string values from environment variable.
If the environment variable is not set, the default value is returned
instead.
"""
if default is None:
default = ()
if name not in os.environ:
return default
return tuple(os.environ[name].split(separator))
def _load_env_file():
if os.path.exists(os.path.join(BASE_DIR, ".env")):
envfile = os.path.join(BASE_DIR, ".env")
else:
envfile = os.path.join(ENV_PATH, ".env")
if os.path.isfile(envfile):
for line in open(envfile):
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
os.environ[k] = v
_load_env_file()

View File

@@ -17,18 +17,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# dodano # dodano
import os import os
import GDAL from osgeo import gdal
from datetime import timedelta
from .env import BASE_DIR, ENV_BOOL, ENV_LIST, ENV_NUM, ENV_STR, PARDIR # noqa
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# use this if setting up on Windows 10 with GDAL installed from OSGeo4W using defaults
if os.name == 'nt':
VIRTUAL_ENV_BASE = os.environ['VIRTUAL_ENV']
os.environ['PATH'] = os.path.join(VIRTUAL_ENV_BASE, r'.\Lib\site-packages\osgeo') + ';' + os.environ['PATH']
os.environ['PROJ_LIB'] = os.path.join(VIRTUAL_ENV_BASE, r'.\Lib\site-packages\osgeo\data\proj') + ';' + os.environ['PATH']
GDAL_LIBRARY_PATH = r'C:\Users\Student1\Desktop\plovidba\myenv\djangoenv\Lib\site-packages\osgeo\gdal304.dll'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
@@ -37,10 +32,25 @@ GDAL_LIBRARY_PATH = r'C:\Users\Student1\Desktop\plovidba\myenv\djangoenv\Lib\sit
SECRET_KEY = 'django-insecure-6iovioyxw5hqwp)0=zu&yu&p!ql34g+x(p4xgk79vs57zpinio' SECRET_KEY = 'django-insecure-6iovioyxw5hqwp)0=zu&yu&p!ql34g+x(p4xgk79vs57zpinio'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
# DEBUG = ENV_BOOL("DEBUG")
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# CSRF_TRUSTED_ORIGINS = ENV_LIST("CSRF_TRUSTED_ORIGINS", ",", []),
# CORS settings
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ENV_LIST("CORS_ALLOWED_ORIGINS", ",", [])
# HTTP -> HTTPS
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Frontend
FRONTEND_URL = ENV_STR("FRONTEND_URL")
# Application definition # Application definition
@@ -51,8 +61,14 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.gis',
'django_extensions',
# 3rd party
'rest_framework', 'rest_framework',
'drf_yasg',
# Custom apps:
'plovidba_aplikacija', 'plovidba_aplikacija',
'user',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -70,7 +86,7 @@ ROOT_URLCONF = 'plovidba_projekt.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -100,6 +116,17 @@ DATABASES = {
} }
} }
DATABASES["default"]["TEST"] = {
"NAME": ENV_STR("DATABASE_TEST_NAME", "test_plovidba_dev_db")
}
AUTH_USER_MODEL = "user.User"
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
"django.contrib.auth.backends.ModelBackend",
]
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
@@ -119,25 +146,122 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Substituting a custom User model
AUTH_USER_MODEL = "user.User"
# Define DRF settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 100,
}
# # # AUTH SETTINGS
# Force email based auth
# Custom auth variables
AUTH_AUTHENTICATION_METHOD = "email" # Options: username | email | username_email
AUTH_EMAIL_REQUIRED = True
AUTH_USERNAME_REQUIRED = False # Require username in registration
AUTH_EMAIL_VERIFICATION = "mandatory" # <mandatory|optional|None>
AUTH_ACCESS_TOKEN_NAME = "access" # set access token name
AUTH_REFRESH_TOKEN_NAME = "refresh" # set access token name
# Simple JWT config
SIMPLE_JWT = {
"REFRESH_TOKEN_LIFETIME": timedelta(
days=ENV_NUM("JWT_REFRESH_TOKEN_LIFETIME_DAYS", 15)
),
"ROTATE_REFRESH_TOKENS": True,
"ACCESS_TOKEN_LIFETIME": timedelta(
minutes=ENV_NUM("JWT_ACCESS_TOKEN_LIFETIME_MINUTES", 10)
),
"AUTH_COOKIE_SECURE": True,
"AUTH_COOKIE_SAMESITE": "None",
"AUTH_COOKIE_HTTP_ONLY": True,
}
# API docs
SHOW_API_DOCS = ENV_BOOL("SHOW_API_DOCS", True)
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'hr'
TIME_ZONE = 'UTC' TIME_ZONE = 'Europe/Zagreb'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
# Available languages
LANGUAGES = [
("hr", ("Croatian")),
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/ # https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/' # folder containing static files for production purposes (manage.py collectstatic)
STATIC_ROOT = os.path.join(BASE_DIR, "static")
# static files for each app
STATIC_URL = "/api/static/"
# static files directories for each app
#STATICFILES_DIRS = [
#os.path.join(BASE_DIR, "user/static"),
#]
# Media Folder
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/api/media/"
# Resources DIR
RESOURCES_DIR = os.path.join(BASE_DIR, "resources")
# TEMP DIR
TEMP_DIR = os.path.join(BASE_DIR, "temp")
# # # Email and support settings # # #
ADMIN_EMAIL = ENV_STR("ADMIN_EMAIL")
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = ENV_STR("EMAIL_HOST", "smtp.office365.com")
EMAIL_USE_TLS = ENV_BOOL("EMAIL_USER_TLS", True)
EMAIL_PORT = ENV_NUM("EMAIL_PORT", 587)
EMAIL_HOST_USER = ENV_STR("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = ENV_STR("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = ENV_STR("DEFAULT_FROM_EMAIL")
EMAIL_USE_SSL = ENV_BOOL("EMAIL_USER_TLS", False)
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
PROJ_LIB = ENV_STR("PROJ_LIB", None)
GDAL_DATA = ENV_STR("GDAL_DATA", None)
if PROJ_LIB:
os.environ["PROJ_LIB"] = PROJ_LIB
if GDAL_DATA:
os.environ["GDAL_DATA"] = GDAL_DATA

View File

@@ -14,9 +14,40 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from plovidba_aplikacija import views
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework import permissions
api_schema_view = get_schema_view(
openapi.Info(
title="API docs",
default_version='v1',
description="Swagger docs for ListLabs API",
contact=openapi.Contact(email="elena.maric@listlabs.net")
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('plovidba_aplikacija.urls')),
path("swagger/", api_schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"),
path("redoc/", api_schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
path('user/', include('user.urls')),
]
if settings.SHOW_API_DOCS:
urlpatterns += [
path('api/docs/swagger/',
api_schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('api/docs/redoc/',
api_schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc-ui')
] ]

88
requirements.txt Normal file
View File

@@ -0,0 +1,88 @@
asgiref==3.5.2
async-timeout==4.0.2
attrs==22.1.0
autobahn==23.1.2
Automat==22.10.0
backports.zoneinfo==0.2.1
black==22.8.0
certifi==2022.6.15
cffi==1.15.1
channels==4.0.0
channels-redis==4.0.0
charset-normalizer==2.1.1
click==8.1.3
click-plugins==1.1.1
cligj==0.7.2
constantly==15.1.0
coreapi==2.3.3
coreschema==0.0.4
cryptography
daphne==4.0.0
defusedxml==0.7.1
dj-database-url==1.2.0
dj-rest-auth==2.2.5
Django==4.1
django-allauth==0.50.0
django-cors-headers==3.13.0
django-environ==0.9.0
django-filter==22.1
django-leaflet==0.28.2
django-sslserver==0.22
djangorestframework==3.13.1
djangorestframework-gis==1.0
djangorestframework-simplejwt==5.2.0
drf-spectacular==0.24.2
drf-yasg==1.21.4
et-xmlfile==1.1.0
Fiona==1.9.3
flake8==6.0.0
hyperlink==21.0.0
idna==3.3
importlib-metadata==6.6.0
importlib-resources==5.10.0
incremental==22.10.0
inflection==0.5.1
itypes==1.2.0
Jinja2==3.1.2
jsonschema==4.16.0
MarkupSafe==2.1.2
mccabe==0.7.0
msgpack==1.0.4
munch==3.0.0
mypy-extensions==0.4.3
oauthlib==3.2.0
openpyxl==3.1.2
packaging==23.0
pathspec==0.10.1
pkgutil-resolve-name==1.3.10
platformdirs==2.5.2
psycopg2==2.9.3
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycodestyle==2.10.0
pycparser==2.21
pyflakes==3.0.1
PyJWT==2.4.0
pyOpenSSL==23.0.0
pyrsistent==0.18.1
python3-openid==3.2.0
pytz==2022.2.1
PyYAML==6.0
redis==4.4.2
requests==2.28.1
requests-oauthlib==1.3.1
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.7
sentry-sdk==1.15.0
service-identity==21.1.0
six==1.16.0
sqlparse==0.4.2
tomli==2.0.1
Twisted==22.10.0
txaio==23.1.1
typing-extensions==4.3.0
uritemplate==4.1.1
urllib3==1.26.12
zipp==3.9.0
zope.interface==5.5.2
gunicorn

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

47
user/admin.py Normal file
View File

@@ -0,0 +1,47 @@
from django.contrib import admin
# Register your models here.
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from .models import Organization, PasswordResetRequest
class OrganizationAdmin(admin.ModelAdmin):
list_display = ('name', 'contact_email')
admin.site.register(Organization, OrganizationAdmin) # noqa
class CustomUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('email', 'password')}),
(('Personal info'), {'fields': ('first_name', 'last_name', 'organization')}),
(('Permissions'), {
'fields': ('is_active', 'email_confirmed', 'is_staff', 'is_superuser', 'groups', 'user_permissions')
}),
(('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
list_display = (
'email', 'first_name', 'last_name', 'is_staff',
'email_confirmed', 'organization', 'language_preference'
)
list_filter = ('organization', )
search_fields = ('email', 'first_name', 'last_name')
ordering = ('email',)
readonly_fields = ('date_joined',)
admin.site.register(get_user_model(), CustomUserAdmin) # noqa
@admin.register(PasswordResetRequest)
class PasswordResetRequestAdmin(admin.ModelAdmin):
list_display = ('received_on', 'user', 'uid', 'confirmed')
list_filter = ('received_on', )

6
user/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UserConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'user'

View File

Binary file not shown.

View File

@@ -0,0 +1,42 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
from user.models import User
class Command(BaseCommand):
help = """
Programatically set application permissions and groups\n
usage: python manage.py create_groups_and_permissions.py
"""
def handle(self, *args, **options):
# get or create groups
admin_group, created = Group.objects.get_or_create(name='Admin')
operater_group, created = Group.objects.get_or_create(name='Operater')
viewer_group, created = Group.objects.get_or_create(name='Viewer')
# define content types
ct_user = ContentType.objects.get_for_model(User)
# define permissions
application_permissions = [
# (codename, name, content_type, list_of_groups_to_assign_perm)
('add_user', 'Can add new users', ct_user, [admin_group]),
('change_user', 'Can change existing user', ct_user, [admin_group]),
('delete_user', 'Can delete existing user', ct_user, [admin_group]),
('add_data', 'Can add new data', ct_user, [admin_group, operater_group]),
]
# get or create permissions and add them to appropriate groups
for permission_tuple in application_permissions:
permission_obj, created = Permission.objects.get_or_create(
codename=permission_tuple[0],
name=permission_tuple[1],
content_type=permission_tuple[2]
)
for group in permission_tuple[3]:
group.permissions.add(permission_obj)

View File

@@ -0,0 +1,71 @@
import csv
import os
import sys
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.management.base import BaseCommand
from user.models import Organization, User
class Command(BaseCommand):
help = """
Programatically create entries for model User
"""
def handle(self, *args, **options):
csv_fpath = os.path.join(settings.RESOURCES_DIR, 'users.csv')
csv.field_size_limit(sys.maxsize)
created_entries = 0
existing_entries = 0
admin_group, created = Group.objects.get_or_create(name='Admin')
operater_group, created = Group.objects.get_or_create(name='Operater')
with open(csv_fpath) as f:
reader = csv.DictReader(f)
default_organization = Organization.objects.get_or_create(name='Državna geodetska uprava')[0]
for row in reader:
username = row['usrname']
first_name = row['ime']
last_name = row['prezime']
email = row['email']
email_confirmed = True
if not email:
print("User {} {} doesn't have email but it's required. Setting fake email...".format(
first_name, last_name
))
# set fake email
email = '{}.{}@example.com'.format(first_name, last_name)
email_confirmed = False
obj, created = User.objects.get_or_create(
username=username,
first_name=first_name,
last_name=last_name,
email=email,
email_confirmed=email_confirmed,
organization=default_organization
)
if row['rola'] == 'operater':
obj.groups.add(operater_group)
elif row['rola'] == 'admin':
obj.groups.add(admin_group)
else:
obj.groups.clear()
if created:
created_entries += 1
print("Kreiran user {} {}".format(obj.first_name, obj.last_name))
else:
existing_entries += 1
print("Created: {}".format(created_entries))
print("Existing: {}".format(existing_entries))

View File

@@ -0,0 +1,44 @@
# Generated by Django 4.2.9 on 2024-01-17 11:44
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,93 @@
# Generated by Django 4.2.9 on 2024-01-18 11:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Organization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('contact_email', models.EmailField(blank=True, max_length=254)),
],
),
migrations.AlterModelOptions(
name='user',
options={},
),
migrations.AlterModelManagers(
name='user',
managers=[
],
),
migrations.AddField(
model_name='user',
name='email_confirmed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='language_preference',
field=models.CharField(choices=[('hr', 'Hrvatski'), ('en', 'English')], default='hr', max_length=8),
),
migrations.AlterField(
model_name='user',
name='date_joined',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='user',
name='first_name',
field=models.CharField(blank=True, max_length=127),
),
migrations.AlterField(
model_name='user',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='user',
name='is_staff',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=127),
),
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(blank=True, max_length=255),
),
migrations.CreateModel(
name='PasswordResetRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('received_on', models.DateTimeField(auto_now_add=True)),
('uid', models.UUIDField()),
('confirmed', models.BooleanField(default=False)),
('confirmed_on', models.DateTimeField(null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='organization',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.organization'),
),
]

View File

Binary file not shown.

88
user/models.py Normal file
View File

@@ -0,0 +1,88 @@
from django.db import models
# Create your models here.
from django.contrib.auth.models import (AbstractBaseUser,BaseUserManager, PermissionsMixin)
class UserManager(BaseUserManager):
'''Manager for users'''
def create_user(self, email, password=None, **extra): #kreiran regular user
if not email:
raise ValueError('Valid e-mail must be provided!')
user = self.model(email=self.normalize_email(email), **extra)
user.username
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, first_name, last_name): #kreiran superuser s dodatnim zahtjevima
user = self.create_user(email, password, **{"first_name": first_name, "last_name": last_name})
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class Organization(models.Model):
name = models.CharField(max_length=128)
contact_email = models.EmailField(blank=True)
class User(AbstractBaseUser, PermissionsMixin):
LANGUAGE_CHOICES = (
('hr', 'Hrvatski'),
('en', 'English')
)
username = models.CharField(max_length=255, blank=True)
email = models.EmailField(max_length=255, unique=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
first_name = models.CharField(max_length=127, blank=True)
last_name = models.CharField(max_length=127, blank=True)
organization = models.ForeignKey(Organization, on_delete=models.SET_NULL, null=True) #povezuje korisnike s organizacijom
date_joined = models.DateTimeField(auto_now_add=True)
email_confirmed = models.BooleanField(default=False)
language_preference = models.CharField(choices=LANGUAGE_CHOICES, max_length=8, default='hr')
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
@property
def is_admin(self):
return self.groups.exists() and self.groups.first().name == 'Admin'
@property
def is_editor(self):
return self.groups.exists() and self.groups.first().name == 'Editor'
@property
def is_wiever(self):
return self.groups.exists() and self.groups.first().name == 'Wiever'
@property
def app_role_name(self):
return self.groups.first().name if self.groups.exists() else ''
@property
def app_role_id(self):
return self.groups.first().id if self.groups.exists() else None
@property
def full_name(self):
return '{} {}'.format(self.first_name, self.last_name)
class PasswordResetRequest(models.Model):
received_on = models.DateTimeField(auto_now_add=True)
uid = models.UUIDField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
confirmed = models.BooleanField(default=False)
confirmed_on = models.DateTimeField(null=True)
def __str__(self):
return self.received_on.isoformat()

46
user/permissions.py Normal file
View File

@@ -0,0 +1,46 @@
from django.contrib.auth.models import Group
from rest_framework import permissions
class UserPermission(permissions.BasePermission):
"""
Custom permission to only allow users with permissions
to view/add/update/delete the users.
"""
def has_permission(self, request, view):
# we cover delete permissions in has_object_permissions
if request.method == 'DELETE':
return True
if request.method in permissions.SAFE_METHOD:
return True
if not request.user.app_role_id:
return False
app_role = Group.objects.get(pk=request.user.app_role_id)
return 'add_user' in app_role.permissions.all().values_list(
'codename', flat=True)
def has_object_permission(self, request, view, obj):
if request.method == 'DELETE':
return request.user == obj
if not request.user.app_role_id:
return False
if request.method in permissions.SAFE_METHODS:
return True
app_role = Group.objects.get(pk=request.user.app_role_id)
permission_dict = {
'PATCH': 'change_user',
'PUT': 'change_user'
}
perm = permission_dict[request.method]
return perm in app_role.permissions.all().values_list('codename', flat=True)

162
user/serializers.py Normal file
View File

@@ -0,0 +1,162 @@
from typing import Any, Dict
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core import exceptions as django_exceptions
from django.utils.translation import gettext as _
from rest_framework import serializers
from rest_framework_simplejwt.exceptions import InvalidToken
from rest_framework_simplejwt.serializers import (TokenObtainPairSerializer,
TokenRefreshSerializer)
User = get_user_model() #kreirana varijabla User koja se referencira na User model
class UserSerializer(serializers.ModelSerializer): #serializer for the user object
class Meta:
model = get_user_model()
fields = ['email', 'password']
extra_kwargs = {'password': {'write_only': True, 'min_length': 8}}
def create(self, validated_data):
return get_user_model().objects.create_user(**validated_data)
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
user = super().update(instance, validated_data)
if password:
user.set_password(password)
user.save()
return user
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
# Custom token names
data[settings.AUTH_REFRESH_TOKEN_NAME] = str(refresh)
data[settings.AUTH_ACCESS_TOKEN_NAME] = str(refresh.access_token)
# used to keep exact expiration time, will be removed from body before sending to client!
data["lifetime"] = refresh.lifetime
return data
@classmethod
def get_token(cls, user):
token = super(CustomTokenObtainPairSerializer, cls).get_token(user)
# TODO: Get user group goes here
# Add custom claims
token['username'] = user.email if user.USERNAME_FIELD == 'email' else user.username
token['is_staff'] = user.is_staff
token['is_superuser'] = user.is_superuser
return token
class CustomCookieTokenRefreshSerializer(TokenRefreshSerializer):
refresh = None
def validate(self, attrs):
# Check if refresh token was included
refresh_token = self.context['request'].COOKIES.get(settings.AUTH_REFRESH_TOKEN_NAME)
if (not refresh_token):
raise InvalidToken(_('No valid refresh token found!'))
attrs['refresh'] = refresh_token
refresh = self.token_class(attrs["refresh"])
data = {settings.AUTH_ACCESS_TOKEN_NAME: str(refresh.access_token)}
if settings.SIMPLE_JWT.get("ROTATE_REFRESH_TOKENS"):
if settings.SIMPLE_JWT.get("BLACKLIST_AFTER_ROTATION"):
try:
# Attempt to blacklist the given refresh token
refresh.blacklist()
except AttributeError:
pass
refresh.set_jti()
refresh.set_exp()
refresh.set_iat()
data["refresh"] = str(refresh)
data["lifetime"] = refresh.lifetime
return data
class PasswordSerializer(serializers.Serializer):
new_password = serializers.CharField(style={'input_type': 'password'})
status_codes = {
"This field may not be blank.": 601,
"This password is too short. It must contain at least 8 characters.": 602,
"This password is entirely numeric.": 603,
"This password is too common.": 604,
"The password is too similar to the username.": 605,
"The password is too similar to the email address.": 606,
"The password is too similar to the first name.": 607,
"The password is too similar to the last name.": 608
}
def validate(self, attrs):
user = self.context['request'].user
assert user is not None
try:
validate_password(attrs['new_password'], user)
except django_exceptions.ValidationError as e:
errors = []
for msg in list(e.messages):
try:
status_code = self.status_codes[msg]
except Exception:
status_code = 600
errors.append({'code': status_code, 'message': msg})
raise serializers.ValidationError({
'new_password_errors': errors
})
return super(PasswordSerializer, self).validate(attrs)
class PasswordChangeSerializer(PasswordSerializer):
old_password = serializers.CharField(style={"input_type": "password"})
class PasswordResetSerializer(serializers.Serializer):
email = serializers.CharField()
def validate(self, attrs):
email = attrs['email']
try:
User.objects.get(email=email)
except User.DoesNotExist:
raise serializers.ValidationError({
'email': "There is no user with this email"
})
return super(PasswordResetSerializer, self).validate(attrs)
class PasswordResetConfirmSerializer(PasswordSerializer):
token = serializers.CharField()
class UserDetailSerializer(serializers.ModelSerializer):
"""Detail serializer for the user object."""
organization_name = serializers.ReadOnlyField(source='organization.name')
class Meta:
model = get_user_model()
fields = ['id', 'first_name', 'last_name', 'username', 'email', 'organization_name', 'language_preference']
class UserDetailUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ['first_name', 'last_name', 'language_preference']

3
user/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
user/urls.py Normal file
View File

@@ -0,0 +1,18 @@
from django.urls import path
from user import views
from rest_framework_simplejwt.views import TokenVerifyView
app_name = 'user'
urlpatterns = [
path('create/', views.CreateUserView.as_view(), name='create'),
path('activate/<str:uidb64>/<str:token>/', views.activate, name='activate'),
path('token/', views.CustomObtainTokenPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', views.CustomCookieTokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('token/logout/', views.LogoutView.as_view(), name='token_logout'),
path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'),
path('password-reset/<str:uid>/confirm/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('me/', views.RequestUserDetailView.as_view(), name='user_detail'),
path('me/change-password/', views.ChangePasswordView.as_view(), name='change_password'),
path('<int:pk>/delete/', views.DeleteUserView.as_view(), name='user_delete')
]

42
user/utils.py Normal file
View File

@@ -0,0 +1,42 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.core.mail import EmailMultiAlternatives
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
class Util:
@staticmethod
def send_email(html_content, **data):
try:
email = EmailMultiAlternatives(**data)
email.attach_alternative(html_content, "text/html")
email.send()
except Exception:
raise Exception("Sending e-mail went wrong")
@staticmethod
def get_token(user):
try:
data = {
"uidb64": urlsafe_base64_encode(force_bytes(user.pk)),
"token": default_token_generator.make_token(user)
}
except Exception:
return None
return data
@staticmethod
def check_token(uidb64, token):
try:
User = get_user_model()
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
# Check token
if user is not None and default_token_generator.check_token(user, token):
return user
raise Exception("Token invalid")

330
user/views.py Normal file
View File

@@ -0,0 +1,330 @@
from django.shortcuts import render
# Create your views here.
import uuid
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import BadHeaderError, send_mail
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.permissions import AllowAny
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.views import (TokenObtainPairView,
TokenRefreshView)
from .models import PasswordResetRequest, User
from .permissions import UserPermission
from .serializers import (CustomCookieTokenRefreshSerializer,
CustomTokenObtainPairSerializer,
PasswordResetConfirmSerializer,
PasswordResetSerializer, PasswordSerializer,
UserDetailSerializer, UserDetailUpdateSerializer,
UserSerializer)
from .utils import Util
def activate(request, uidb64, token): # TODO: rewrite to CBV
try:
user = Util.check_token(uidb64, token)
except Exception:
return HttpResponse("Unable to verify your e-mail adress")
if user is not None:
user.email_confirmed = True
user.save()
if request.user.is_authenticated:
messages.success(request, "Hvala Vam na potvrdi email adrese.")
return redirect(settings.FRONTEND_URL)
else:
msg = "Hvala Vam na potvrdi email adrese. Sad se možete ulogirati u svoj korisnički račun."
messages.success(request, msg)
return redirect(settings.FRONTEND_URL)
else:
return HttpResponse('Vaš korisnički račun je već aktiviran ili aktivacijski link nije ispravan.')
class CreateUserView(generics.CreateAPIView):
"""Create a new user in the system."""
serializer_class = UserSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
if settings.AUTH_EMAIL_VERIFICATION in ["mandatory", "optional"]:
try:
self.send_confirmation_email(request, serializer.instance)
except Exception:
return Response(
_("User was created but server wasn't able to send e-mail!"),
status=status.HTTP_202_ACCEPTED
)
headers = self.get_success_headers(serializer.data)
response_msg = {
"account_created": serializer.data,
"info": _("Confirmation e-mail was sent to your e-mail address, please confirm it to start using this app!")
}
return Response(response_msg, status=status.HTTP_201_CREATED, headers=headers)
def send_confirmation_email(self, request, user):
token_data = Util.get_token(user)
relative_link = reverse('user:activate', kwargs=token_data)
link = request.build_absolute_uri(relative_link)
# Variables used in both templates
template_vars = {
'email': user.email,
'link': link,
'greeting': _("Hi,"),
'info': _("Please click on the link to confirm your registration!"),
'description_msg': _("Your e-mail:"),
}
text_content = render_to_string('acc_activate_email.txt', template_vars)
# Adding extra variables to html template
html_content = render_to_string('acc_activate_email.html', {
**template_vars,
'bttn_text': _("Confirm email"),
'alternate_text': _("Or go to link:")
})
data = {
"body": text_content,
"to": [user.email],
"subject": _("Please confirm your e-mail"),
}
Util.send_email(html_content, **data)
return
class CustomObtainTokenPairView(TokenObtainPairView):
permission_classes = (AllowAny,)
serializer_class = CustomTokenObtainPairSerializer
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get(settings.AUTH_REFRESH_TOKEN_NAME):
# # # Move refresh token from body to HttpOnly cookie
refresh_token_name = settings.AUTH_REFRESH_TOKEN_NAME
persist = request.data.get('persist', False)
max_age = response.data['lifetime'] if persist else None
response.set_cookie(
refresh_token_name,
response.data[refresh_token_name],
max_age=max_age,
secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'],
httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'],
samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE']
)
# Remove from body
del response.data[refresh_token_name]
del response.data["lifetime"]
return super().finalize_response(request, response, *args, **kwargs)
class CustomCookieTokenRefreshView(TokenRefreshView):
serializer_class = CustomCookieTokenRefreshSerializer
def finalize_response(self, request, response, *args, **kwargs): #purpose is to customize the HTTP response generated by the view after refreshing an access token.
if response.data.get(settings.AUTH_REFRESH_TOKEN_NAME):
# # # Move refresh token from body to HttpOnly cookie
refresh_token_name = settings.AUTH_REFRESH_TOKEN_NAME
persist = request.data.get('persist', False)
max_age = response.data['lifetime'] if persist else None
response.set_cookie(
refresh_token_name,
response.data[refresh_token_name],
max_age=max_age,
secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'],
httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'],
samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE']
)
# Remove from body
del response.data[refresh_token_name]
del response.data["lifetime"]
return super().finalize_response(request, response, *args, **kwargs)
class LogoutView(APIView):
renderer_classes = [JSONRenderer]
permission_classes = []
@swagger_auto_schema(
responses={'200': 'OK', '400': 'Bad Request'},
operation_id='LogoutUser',
operation_description='Logout user and clean cookies'
)
def post(self, request, *args, **kwargs):
response = Response()
response.set_cookie(settings.AUTH_REFRESH_TOKEN_NAME, None, max_age=1, httponly=True)
response.set_cookie("sessionid", None, max_age=1, httponly=True) # logout from django admin!
return response
class RequestUserDetailView(APIView):
renderer_classes = [JSONRenderer]
permission_classes = []
@swagger_auto_schema(
responses={'200': 'OK', '400': 'Bad Request'},
operation_id='UserDetail',
operation_description='Get details for request user'
)
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)
user = request.user
data = UserDetailSerializer(user).data
return Response(status=status.HTTP_200_OK, data=data)
@swagger_auto_schema(
responses={'200': 'OK', '400': 'Bad Request'},
operation_id='UserDetailUpdate',
operation_description='Update details for request user',
request_body=UserDetailUpdateSerializer
)
def put(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)
user = request.user
data = self.request.data
user.first_name = data.get('first_name', user.first_name)
user.last_name = data.get('last_name', user.last_name)
user.language_preference = data.get('language_preference', user.first_name)
user.save()
user_data = UserDetailSerializer(user).data
return Response(status=status.HTTP_200_OK, data=user_data)
class DeleteUserView(generics.DestroyAPIView):
permission_classes = [UserPermission]
queryset = User.objects.all()
class ChangePasswordView(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = PasswordSerializer
def update(self, request, *args, **kwargs):
user = self.request.user
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
if not user.check_password(serializer.data.get("old_password")):
return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(serializer.data.get("new_password"))
user.save()
response = {'message': 'Password updated successfully'}
return Response(response, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordResetView(generics.GenericAPIView):
"""
Use this endpoint to send email to user with password reset key.
"""
serializer_class = PasswordResetSerializer
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
serializer = PasswordResetSerializer(data=request.data)
if serializer.is_valid():
try:
user = User.objects.get(email=serializer.data.get('email'))
except User.DoesNotExist:
return Response(status=status.HTTP_400_BAD_REQUEST)
# Build the password reset link
current_site = get_current_site(self.request)
domain = current_site.domain
uid = uuid.uuid4()
token = default_token_generator.make_token(user)
reset_link = "https://{domain}/password-reset/{uid}/{token}/".format(domain=domain, uid=uid, token=token)
# create password reset obj
PasswordResetRequest.objects.create(user=user, uid=uid, confirmed=False)
# send e-mail
subject = "Password reset"
message = (
"You're receiving this email because you requested a password reset for your account.\n\n"
"Please visit this url to set new password:\n{reset_link}".format(reset_link=reset_link)
)
from_email = settings.DEFAULT_FROM_EMAIL
try:
send_mail(subject, message, from_email, [user.email])
except BadHeaderError:
return Response({"error": "Invalid header found."}, status=status.HTTP_400_BAD_REQUEST)
response = {"message": "E-mail successfully sent."}
return Response(response, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordResetConfirmView(generics.GenericAPIView):
"""
Use this endpoint to finish reset password process.
"""
permission_classes = []
authentication_classes = []
def get_serializer_class(self):
return PasswordResetConfirmSerializer
def post(self, request, **kwargs):
try:
uid = uuid.UUID(self.kwargs['uid'])
obj = PasswordResetRequest.objects.get(uid=uid)
user = obj.user
except (ValueError, PasswordResetRequest.DoesNotExist):
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': 'UID is not valid'})
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# check if token is valid
token_valid = default_token_generator.check_token(obj.user, serializer.data["token"])
if not token_valid:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': 'Token not valid'})
# set new password
user.set_password(serializer.data["new_password"])
user.save()
# update object in db
obj.confirmed = True
obj.confirmed_on = timezone.now()
obj.save()
return Response(status=status.HTTP_200_OK)