Compare commits
21 Commits
b6454c5f78
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3328c6af3b | |||
| 279c7df84a | |||
| bc9afd396b | |||
| 44a9b4570b | |||
| f46bc8337e | |||
| 298219ab30 | |||
| 75c00b0e91 | |||
| 92d98a299c | |||
| ade8ea32e2 | |||
| 9908fcad02 | |||
| 6e89ea4000 | |||
| 3931fc57bf | |||
| 6614ae2e44 | |||
| eaac31978f | |||
| c5fb12ffb8 | |||
| 6994f43490 | |||
| 3709991263 | |||
| 7c144293a3 | |||
| 7d2df70d53 | |||
| 1b80bda3d5 | |||
| b89fde9c92 |
34
Dockerfile.dev
Normal file
34
Dockerfile.dev
Normal 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
40
Dockerfile.prod
Normal 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
33
docker-compose.yaml
Normal 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.
BIN
plovidba_aplikacija/__pycache__/serializers.cpython-38.pyc
Normal file
BIN
plovidba_aplikacija/__pycache__/serializers.cpython-38.pyc
Normal file
Binary file not shown.
BIN
plovidba_aplikacija/__pycache__/tests.cpython-38.pyc
Normal file
BIN
plovidba_aplikacija/__pycache__/tests.cpython-38.pyc
Normal file
Binary file not shown.
BIN
plovidba_aplikacija/__pycache__/urls.cpython-38.pyc
Normal file
BIN
plovidba_aplikacija/__pycache__/urls.cpython-38.pyc
Normal file
Binary file not shown.
BIN
plovidba_aplikacija/__pycache__/views.cpython-38.pyc
Normal file
BIN
plovidba_aplikacija/__pycache__/views.cpython-38.pyc
Normal file
Binary file not shown.
@@ -1,3 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import ObjektSigurnosti
|
||||
|
||||
admin.site.register(ObjektSigurnosti)
|
||||
0
plovidba_aplikacija/management/commands/__init__.py
Normal file
0
plovidba_aplikacija/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
@@ -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
@@ -16,7 +16,6 @@ class Migration(migrations.Migration):
|
||||
name='ObjektSigurnosti',
|
||||
fields=[
|
||||
('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)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
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.
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,20 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
|
||||
class ObjektSigurnosti(models.Model):
|
||||
lokacija = models.PointField()
|
||||
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
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
# serializers.py
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.gis.geos import Point
|
||||
from .models import ObjektSigurnosti
|
||||
import json
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
class ObjektSigurnostiSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ObjektSigurnosti
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
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)
|
||||
@@ -1,10 +1,9 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ObjektSigurnostiViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'objekti', ObjektSigurnostiViewSet, basename='objekt-sigurnosti')
|
||||
from django.urls import path, include
|
||||
from .views import ObjektSigurnostiList, ObjektSigurnostiDetail
|
||||
from plovidba_aplikacija import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
path('objekti/', ObjektSigurnostiList.as_view(), name='objektisigurnosti-list'),
|
||||
path('objekti/<int:pk>/', ObjektSigurnostiDetail.as_view(), name='objektisigurnosti-detail' ),
|
||||
]
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import generics
|
||||
from .models import ObjektSigurnosti
|
||||
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()
|
||||
serializer_class = ObjektSigurnostiSerializer
|
||||
|
||||
permission_classes = []
|
||||
|
||||
BIN
plovidba_projekt/__pycache__/env.cpython-38.pyc
Normal file
BIN
plovidba_projekt/__pycache__/env.cpython-38.pyc
Normal file
Binary file not shown.
BIN
plovidba_projekt/__pycache__/router.cpython-38.pyc
Normal file
BIN
plovidba_projekt/__pycache__/router.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
101
plovidba_projekt/env.py
Normal file
101
plovidba_projekt/env.py
Normal 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()
|
||||
@@ -17,18 +17,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# dodano
|
||||
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__)))
|
||||
|
||||
# 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
|
||||
# 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'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
|
||||
DEBUG = True
|
||||
# DEBUG = ENV_BOOL("DEBUG")
|
||||
|
||||
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
|
||||
|
||||
@@ -51,8 +61,14 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.gis',
|
||||
'django_extensions',
|
||||
# 3rd party
|
||||
'rest_framework',
|
||||
'drf_yasg',
|
||||
# Custom apps:
|
||||
'plovidba_aplikacija',
|
||||
'user',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -70,7 +86,7 @@ ROOT_URLCONF = 'plovidba_projekt.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'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
|
||||
# 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
|
||||
# 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_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
||||
|
||||
# Available languages
|
||||
LANGUAGES = [
|
||||
("hr", ("Croatian")),
|
||||
]
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# 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
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
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
|
||||
@@ -14,9 +14,40 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
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 = [
|
||||
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
88
requirements.txt
Normal 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
|
||||
1
resources/testnipodaci.json
Normal file
1
resources/testnipodaci.json
Normal file
File diff suppressed because one or more lines are too long
BIN
user/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
user/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/admin.cpython-38.pyc
Normal file
BIN
user/__pycache__/admin.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/apps.cpython-38.pyc
Normal file
BIN
user/__pycache__/apps.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/forms.cpython-38.pyc
Normal file
BIN
user/__pycache__/forms.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/models.cpython-38.pyc
Normal file
BIN
user/__pycache__/models.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/permissions.cpython-38.pyc
Normal file
BIN
user/__pycache__/permissions.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/serializers.cpython-38.pyc
Normal file
BIN
user/__pycache__/serializers.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/tests.cpython-38.pyc
Normal file
BIN
user/__pycache__/tests.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/urls.cpython-38.pyc
Normal file
BIN
user/__pycache__/urls.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/utils.cpython-38.pyc
Normal file
BIN
user/__pycache__/utils.cpython-38.pyc
Normal file
Binary file not shown.
BIN
user/__pycache__/views.cpython-38.pyc
Normal file
BIN
user/__pycache__/views.cpython-38.pyc
Normal file
Binary file not shown.
47
user/admin.py
Normal file
47
user/admin.py
Normal 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
6
user/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UserConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'user'
|
||||
0
user/management/__init__.py
Normal file
0
user/management/__init__.py
Normal file
BIN
user/management/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
user/management/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
42
user/management/commands/create_groups_and_permissions.py
Normal file
42
user/management/commands/create_groups_and_permissions.py
Normal 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)
|
||||
71
user/management/commands/create_users.py
Normal file
71
user/management/commands/create_users.py
Normal 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))
|
||||
44
user/migrations/0001_initial.py
Normal file
44
user/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
0
user/migrations/__init__.py
Normal file
0
user/migrations/__init__.py
Normal file
BIN
user/migrations/__pycache__/0001_initial.cpython-38.pyc
Normal file
BIN
user/migrations/__pycache__/0001_initial.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
user/migrations/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
user/migrations/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
88
user/models.py
Normal file
88
user/models.py
Normal 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
46
user/permissions.py
Normal 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
162
user/serializers.py
Normal 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
3
user/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
18
user/urls.py
Normal file
18
user/urls.py
Normal 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
42
user/utils.py
Normal 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
330
user/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user