Skip to content

Commit b9e177e

Browse files
authored
Merge pull request #15 from akirachix/develop
Develop
2 parents 54fb040 + a5f1c12 commit b9e177e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1542
-0
lines changed

.github/workflows/cicd.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Python CI/CD
2+
on:
3+
push:
4+
branches: [main, develop]
5+
pull_request:
6+
branches: [main, develop]
7+
jobs:
8+
build-and-test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
python-version: [3.8]
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
- name: Set up Python ${{ matrix.python-version }}
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install -r requirements.txt
24+
- name: Run database migrations
25+
run: |
26+
python manage.py migrate
27+
- name: Run Black formatter check
28+
run: |
29+
black .
30+
- name: Run tests with coverage
31+
run: |
32+
python manage.py test

.github/workflows/deploy.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Django Deployment CI
2+
on:
3+
push:
4+
branches: [ "main" ]
5+
jobs:
6+
release:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout code
10+
uses: actions/checkout@v3
11+
- name: Set up Python environment
12+
uses: actions/setup-python@v4
13+
with:
14+
python-version: 3.11
15+
- name: Install dependencies
16+
run: |
17+
pip install -r requirements.txt
18+
- name: Install Heroku CLI
19+
run: curl https://cli-assets.heroku.com/install.sh | sh
20+
- name: Login to Heroku
21+
run: heroku container:login
22+
env:
23+
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
24+
- name: Set up Git remote for Heroku
25+
run: |
26+
git remote -v
27+
git remote add heroku https://git.heroku.com/kukukonnect-backend.git || echo "Heroku remote already exists"
28+
- name: Deploy to Heroku
29+
uses: akhileshns/heroku-deploy@v3.13.15
30+
with:
31+
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
32+
heroku_app_name: "kukukonnect"
33+
heroku_email: ${{ secrets.HEROKU_EMAIL }}

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kukuenv/
2+
.env
3+
env/
4+
__pycache__/
5+
*.pyc
6+
d

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: gunicorn kukukonnect.wsgi --log-file -

api/__init__.py

Whitespace-only changes.

api/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

api/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ApiConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'api'

api/migrations/__init__.py

Whitespace-only changes.

api/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.db import models
2+
3+
# Create your models here.

api/serializers.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
from rest_framework import serializers
2+
from users.models import User, USER_TYPE_CHOICES
3+
from django.core.cache import cache
4+
from rest_framework.validators import UniqueValidator
5+
from django.contrib.auth import authenticate
6+
from django.core.mail import send_mail
7+
from django.conf import settings
8+
from sensors.models import SensorData
9+
from devices.models import MCU
10+
import random
11+
import os
12+
13+
14+
class UserSerializer(serializers.ModelSerializer):
15+
password = serializers.CharField(write_only=True, required=False)
16+
image = serializers.ImageField(required=False, allow_null=True)
17+
device_id = serializers.CharField(required=False, allow_blank=True)
18+
19+
class Meta:
20+
model = User
21+
fields = [
22+
"id", "username", "first_name", "last_name", "phone_number", "email",
23+
"password", "user_type", "image", "date_joined", "device_id"
24+
]
25+
read_only_fields = ["id", "date_joined"]
26+
27+
def validate(self, attrs):
28+
user_type = attrs.get('user_type')
29+
device_id = attrs.get('device_id')
30+
31+
32+
if user_type == 'Farmer' and (not device_id or device_id.strip() == ''):
33+
raise serializers.ValidationError({
34+
'device_id': 'This field is required for Farmer users.'
35+
})
36+
37+
if device_id and not MCU.objects.filter(device_id=device_id).exists():
38+
raise serializers.ValidationError({
39+
'device_id': f"The device ID '{device_id}' does not exist."
40+
})
41+
42+
return attrs
43+
44+
def create(self, validated_data):
45+
device_id_str = validated_data.pop('device_id', None)
46+
if device_id_str:
47+
validated_data['device_id'] = MCU.objects.get(device_id=device_id_str)
48+
else:
49+
validated_data['device_id'] = None
50+
51+
password = validated_data.pop("password", None)
52+
user = User(**validated_data)
53+
if password:
54+
user.set_password(password)
55+
user.save()
56+
57+
if user.user_type == 'Farmer' and user.email:
58+
set_password_base = os.getenv('SET_PASSWORD_LINK')
59+
if not set_password_base:
60+
raise Exception('SET_PASSWORD_LINK environment variable is not set.')
61+
set_password_link = set_password_base + user.email
62+
send_mail(
63+
subject='Welcome to Kukukonnect - Set Your Password',
64+
message=(
65+
f'Welcome to Kukukonnect!\n'
66+
f'Set your password: {set_password_link}\n'
67+
),
68+
from_email=settings.DEFAULT_FROM_EMAIL,
69+
recipient_list=[user.email],
70+
fail_silently=True
71+
)
72+
return user
73+
74+
75+
class SignupSerializer(serializers.ModelSerializer):
76+
username = serializers.CharField(
77+
validators=[UniqueValidator(queryset=User.objects.all(), message="Username already taken.")]
78+
)
79+
password = serializers.CharField(
80+
write_only=True,
81+
min_length=8,
82+
style={'input_type': 'password'},
83+
required=False
84+
)
85+
phone_number = serializers.CharField(
86+
validators=[UniqueValidator(queryset=User.objects.all(), message="Phone number already registered.")]
87+
)
88+
email = serializers.EmailField(
89+
validators=[UniqueValidator(queryset=User.objects.all(), message="Email already registered.")]
90+
)
91+
device_id = serializers.CharField(
92+
required=False,
93+
allow_blank=True,
94+
)
95+
96+
class Meta:
97+
model = User
98+
fields = [
99+
"id", "username", "first_name", "last_name", "phone_number", "email",
100+
"password", "user_type", "image", "date_joined", "device_id"
101+
]
102+
read_only_fields = ["id", "date_joined"]
103+
104+
def validate(self, attrs):
105+
user_type = attrs.get('user_type')
106+
device_id = attrs.get('device_id')
107+
108+
if user_type == 'Farmer' and (not device_id or device_id.strip() == ''):
109+
raise serializers.ValidationError({
110+
'device_id': 'This field is required for Farmer users.'
111+
})
112+
113+
if device_id and not MCU.objects.filter(device_id=device_id).exists():
114+
raise serializers.ValidationError({
115+
'device_id': f"The device ID '{device_id}' does not exist."
116+
})
117+
118+
return attrs
119+
120+
def create(self, validated_data):
121+
device_id_str = validated_data.pop('device_id', None)
122+
if device_id_str:
123+
validated_data['device_id'] = MCU.objects.get(device_id=device_id_str)
124+
else:
125+
validated_data['device_id'] = None
126+
127+
password = validated_data.pop("password", None)
128+
user = User(**validated_data)
129+
if user.user_type == "Agrovet":
130+
if not password:
131+
raise serializers.ValidationError({"password": "Password is required for Agrovets."})
132+
user.set_password(password)
133+
else:
134+
user.set_unusable_password()
135+
user.save()
136+
137+
if user.user_type == 'Farmer' and user.email:
138+
set_password_base = os.getenv('SET_PASSWORD_LINK')
139+
if not set_password_base:
140+
raise Exception('SET_PASSWORD_LINK environment variable is not set.')
141+
set_password_link = set_password_base + user.email
142+
from django.conf import settings
143+
send_mail(
144+
subject='Welcome to Kukukonnect - Set Your Password',
145+
message=(
146+
f'Welcome to Kukukonnect {user.username}!\n'
147+
f'Set your password: {set_password_link}\n'
148+
),
149+
from_email=settings.DEFAULT_FROM_EMAIL,
150+
recipient_list=[user.email],
151+
fail_silently=False
152+
)
153+
return user
154+
155+
class LoginSerializer(serializers.Serializer):
156+
email = serializers.EmailField()
157+
password = serializers.CharField(write_only=True)
158+
def validate(self, data):
159+
user = authenticate(username=data["email"], password=data["password"])
160+
if not user:
161+
raise serializers.ValidationError("Invalid credentials.")
162+
if not user.is_active:
163+
raise serializers.ValidationError("This account is inactive.")
164+
data["user"] = user
165+
return data
166+
class SetPasswordSerializer(serializers.Serializer):
167+
email = serializers.EmailField()
168+
password = serializers.CharField(write_only=True, min_length=8)
169+
confirm_password = serializers.CharField(write_only=True, min_length=8)
170+
def validate(self, data):
171+
if data["password"] != data["confirm_password"]:
172+
raise serializers.ValidationError("Passwords do not match")
173+
try:
174+
user = User.objects.get(email=data["email"])
175+
except User.DoesNotExist:
176+
raise serializers.ValidationError("User with this email does not exist.")
177+
if user.user_type != "Farmer":
178+
raise serializers.ValidationError("Only farmers can set their password using this endpoint.")
179+
return data
180+
def save(self, **kwargs):
181+
user = User.objects.get(email=self.validated_data["email"])
182+
user.set_password(self.validated_data["password"])
183+
user.save()
184+
return user
185+
class ForgotPasswordSerializer(serializers.Serializer):
186+
email = serializers.EmailField()
187+
def validate_email(self, value):
188+
try:
189+
user = User.objects.get(email=value)
190+
except User.DoesNotExist:
191+
raise serializers.ValidationError("User with this email does not exist.")
192+
otp = random.randint(1000, 9999)
193+
cache.set(f"otp_{user.id}", otp, timeout=600)
194+
return value
195+
class VerifyCodeSerializer(serializers.Serializer):
196+
email = serializers.EmailField()
197+
otp = serializers.CharField(max_length=4)
198+
def validate(self, data):
199+
try:
200+
user = User.objects.get(email=data["email"])
201+
except User.DoesNotExist:
202+
raise serializers.ValidationError("Invalid email")
203+
cached_otp = cache.get(f"otp_{user.id}")
204+
if not cached_otp or str(cached_otp) != str(data["otp"]):
205+
raise serializers.ValidationError("Invalid or expired OTP")
206+
cache.set(f"otp_verified_{user.id}", True, timeout=600)
207+
return data
208+
class ResetPasswordSerializer(serializers.Serializer):
209+
email = serializers.EmailField()
210+
password = serializers.CharField(write_only=True, min_length=8)
211+
confirm_password = serializers.CharField(write_only=True, min_length=8)
212+
def validate(self, data):
213+
if data["password"] != data["confirm_password"]:
214+
raise serializers.ValidationError("Passwords do not match")
215+
try:
216+
user = User.objects.get(email=data["email"])
217+
except User.DoesNotExist:
218+
raise serializers.ValidationError("User with this email does not exist.")
219+
otp_verified = cache.get(f"otp_verified_{user.id}")
220+
if not otp_verified:
221+
raise serializers.ValidationError("OTP not verified. Please verify OTP before resetting password.")
222+
return data
223+
def save(self, **kwargs):
224+
user = User.objects.get(email=self.validated_data["email"])
225+
user.set_password(self.validated_data["password"])
226+
user.save()
227+
cache.delete(f"otp_{user.id}")
228+
cache.delete(f"otp_verified_{user.id}")
229+
return user
230+
class ThresholdSerializer(serializers.ModelSerializer):
231+
class Meta:
232+
model = MCU
233+
fields = [
234+
'device_id',
235+
'temp_threshold_min',
236+
'temp_threshold_max',
237+
'humidity_threshold_min',
238+
'humidity_threshold_max'
239+
]
240+
extra_kwargs = {
241+
'humidity_threshold_min': {'required': False, 'allow_null': True},
242+
'humidity_threshold_max': {'required': False, 'allow_null': True}
243+
}
244+
245+
class SensorDataSerializer(serializers.ModelSerializer):
246+
device_id = serializers.CharField(source='device_id.device_id', read_only=True)
247+
248+
class Meta:
249+
model = SensorData
250+
fields = ['sensor_data_id', 'temperature', 'humidity', 'timestamp', 'device_id']
251+
read_only_fields = ['sensor_data_id', 'timestamp']

0 commit comments

Comments
 (0)