diff --git a/users/__init__.py b/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/admin.py b/users/admin.py
new file mode 100644
index 0000000..d914f1f
--- /dev/null
+++ b/users/admin.py
@@ -0,0 +1,4 @@
+from django.contrib import admin
+from .models import Profile
+
+admin.site.register(Profile)
diff --git a/users/apps.py b/users/apps.py
new file mode 100644
index 0000000..6908b99
--- /dev/null
+++ b/users/apps.py
@@ -0,0 +1,19 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+ """Configuration for the 'users' app.
+
+ Args:
+ AppConfig (type): Base class for application configuration.
+
+ Attributes:
+ name (str): The name of the app.
+ """
+ # default_auto_field = 'django.db.models.BigAutoField'
+ name = 'users'
+
+ def ready(self):
+ """Registers signals when the application is ready.
+ """
+ import users.signals
diff --git a/users/forms.py b/users/forms.py
new file mode 100644
index 0000000..699d6bd
--- /dev/null
+++ b/users/forms.py
@@ -0,0 +1,146 @@
+from django import forms
+from django.core.validators import RegexValidator
+from django.contrib.auth.models import User
+from django.contrib.auth.forms import UserCreationForm
+from .models import Profile
+from phonenumber_field.formfields import PhoneNumberField
+
+
+class UserRegisterForm(UserCreationForm):
+ """Form for user registration.
+
+ Args:
+ UserCreationForm (class): Django's built-in form for user creation.
+
+ Attributes:
+ email (EmailField): Field for the user's email.
+ phone_number (PhoneNumberField): Field for the user's phone number,
+ allows blank values.
+ """
+ email = forms.EmailField()
+ phone_number = PhoneNumberField(required=False)
+
+ class Meta:
+ """Metadata for the UserRegisterForm.
+
+ Attributes:
+ model (class): The User model.
+ fields (list): The fields to include in the form.
+ """
+ model = User
+ fields = [
+ 'username',
+ 'email',
+ 'password1',
+ 'password2',
+ 'phone_number'
+ ]
+
+ def save(self, commit=True):
+ """Save the user and create/update the associated profile.
+
+ Args:
+ commit (bool, optional): Whether to commit the changes.
+ Default True.
+
+ Returns:
+ User: The user object.
+ """
+ user = super().save(commit=False)
+ user.email = self.cleaned_data['email']
+
+ if commit:
+ user.save()
+
+ # Create or update the associated profile
+ profile, created = Profile.objects.get_or_create(user=user)
+ phone_number = self.cleaned_data['phone_number']
+
+ # clean phone number
+ phone_number_value = phone_number.raw_input
+ if phone_number_value[0] == "+":
+ phone_number_value = phone_number_value[1:]
+ if phone_number_value[0] == "0":
+ phone_number_value = "254" + phone_number_value[1:]
+
+ profile.phone_number = phone_number_value
+
+ if commit:
+ profile.save()
+
+ return user
+
+
+class UserUpdateForm(forms.ModelForm):
+ """Form for updating user information.
+
+ Args:
+ forms (module): Django forms module.
+
+ Attributes:
+ email (EmailField): Field for the user's email address.
+ """
+ email = forms.EmailField()
+
+ class Meta:
+ """Metadata for the UserUpdateForm.
+
+ Attributes:
+ model (class): The User model.
+ fields (list): The fields to include in the form.
+ """
+ model = User
+ fields = ['username', 'email']
+
+
+class ProfileUpdateForm(forms.ModelForm):
+ """Form for updating user profile information.
+
+ Args:
+ forms (module): The Django forms module.
+
+ Attributes:
+ phone_number (PhoneNumberField): A user's phone number, allows blank.
+ """
+ phone_number = PhoneNumberField(required=False)
+
+ class Meta:
+ """Metadata for the ProfileUpdateForm.
+
+ Attributes:
+ model (class): The Profile model.
+ fields (list): The fields to include in the form.
+ """
+ model = Profile
+ fields = ['image', 'phone_number']
+
+ def save(self, commit=True):
+ """Saves the profile and processes the phone number.
+
+ Args:
+ commit (bool, optional): Indicates whether to commit the changes.
+ Default True.
+
+ Returns:
+ Profile: The profile object.
+ """
+ # Call the superclass's save() method to save the form data
+ profile = super(ProfileUpdateForm, self).save(commit=False)
+
+ # Process the phone number
+ phone_number = self.cleaned_data.get('phone_number')
+ if phone_number:
+ phone_number_value = phone_number.raw_input
+
+ if phone_number_value[0] == "+":
+ phone_number_value = phone_number_value[1:]
+ if phone_number_value[0] == "0":
+ phone_number_value = "254" + phone_number_value[1:]
+
+ # Save the processed phone number to the profile
+ profile.phone_number = phone_number_value
+
+ if commit:
+ profile.save()
+
+ return profile
diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py
new file mode 100644
index 0000000..a0449b0
--- /dev/null
+++ b/users/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.7 on 2023-11-12 04:54
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('image', models.ImageField(default='default.jpg', upload_to='profile_pics')),
+ ('User', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/users/migrations/0002_rename_user_profile_user.py b/users/migrations/0002_rename_user_profile_user.py
new file mode 100644
index 0000000..14f4c14
--- /dev/null
+++ b/users/migrations/0002_rename_user_profile_user.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2023-11-15 09:54
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='profile',
+ old_name='User',
+ new_name='user',
+ ),
+ ]
diff --git a/users/migrations/0003_profile_phone_number.py b/users/migrations/0003_profile_phone_number.py
new file mode 100644
index 0000000..e881921
--- /dev/null
+++ b/users/migrations/0003_profile_phone_number.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.7 on 2023-11-15 15:27
+
+from django.db import migrations
+import phonenumber_field.modelfields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0002_rename_user_profile_user'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='profile',
+ name='phone_number',
+ field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
+ ),
+ ]
diff --git a/users/migrations/0004_alter_profile_phone_number.py b/users/migrations/0004_alter_profile_phone_number.py
new file mode 100644
index 0000000..0ad4652
--- /dev/null
+++ b/users/migrations/0004_alter_profile_phone_number.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.7 on 2023-11-20 07:28
+
+from django.db import migrations
+import phonenumber_field.modelfields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0003_profile_phone_number'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='profile',
+ name='phone_number',
+ field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Please enter your name...', max_length=128, null=True, region=None),
+ ),
+ ]
diff --git a/users/migrations/0005_alter_profile_phone_number.py b/users/migrations/0005_alter_profile_phone_number.py
new file mode 100644
index 0000000..d7db0ab
--- /dev/null
+++ b/users/migrations/0005_alter_profile_phone_number.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.7 on 2023-12-08 09:46
+
+from django.db import migrations
+import phonenumber_field.modelfields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0004_alter_profile_phone_number'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='profile',
+ name='phone_number',
+ field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Phone Number...', max_length=128, null=True, region=None),
+ ),
+ ]
diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/models.py b/users/models.py
new file mode 100644
index 0000000..e4bf05f
--- /dev/null
+++ b/users/models.py
@@ -0,0 +1,42 @@
+from django.db import models
+from django.contrib.auth.models import AbstractUser, User
+from PIL import Image
+from phonenumber_field.modelfields import PhoneNumberField
+
+
+class Profile(models.Model):
+ """Model representing user profiles.
+
+ Args:
+ models (module): The Django models module.
+
+ Attributes:
+ user (OneToOneField): A one-to-one relationship with the User model,
+ specifying the user associated with the profile.
+ phone_number (PhoneNumberField): A phone number field allowing blank
+ and null values, with optional help text.
+ image (ImageField): An image field with a default image and a
+ specified upload path for profile pictures.
+
+ Returns:
+ str: A string repr of the class, showing the associated username.
+ """
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ phone_number = PhoneNumberField(
+ blank=True, null=True, help_text=u"Phone Number...")
+ image = models.ImageField(default='default.jpg', upload_to='profile_pics')
+
+ def __str__(self):
+ return f'{self.user.username} Profile'
+
+ # image compression using PIL disabled for AWS storage
+
+ # def save(self, *args, **kwargs):
+ # super().save(*args, **kwargs)
+
+ # img = Image.open(self.image.path)
+ # if img.height > 300 or img.width > 300:
+ # output_size = (300, 300)
+ # img.thumbnail(output_size)
+ # print(vars(self.image))
+ # img.save(self.image.path)
diff --git a/users/signals.py b/users/signals.py
new file mode 100644
index 0000000..da572a1
--- /dev/null
+++ b/users/signals.py
@@ -0,0 +1,28 @@
+from django.db.models.signals import post_save
+from django.contrib.auth.models import User
+from django.dispatch import receiver
+from .models import Profile
+
+
+@receiver(post_save, sender=User)
+def create_profile(sender, instance, created, **kwargs):
+ """Creates a user profile when a new user is created.
+
+ Args:
+ sender (signal): The signal for user creation.
+ instance (object): The user object created.
+ created (bool): A flag indicating whether the user is newly created.
+ """
+ if created:
+ Profile.objects.create(user=instance)
+
+
+@receiver(post_save, sender=User)
+def save_profile(sender, instance, **kwargs):
+ """Saves user profile information when a user is created or updated.
+
+ Args:
+ sender (signal): The model object that is the source of the signal.
+ instance (object): The user object.
+ """
+ instance.profile.save()
diff --git a/users/templates/users/dashboard.html b/users/templates/users/dashboard.html
new file mode 100644
index 0000000..7c12941
--- /dev/null
+++ b/users/templates/users/dashboard.html
@@ -0,0 +1,299 @@
+{% extends 'blog/base.html' %} {% block content %}
+
+
+
+
+
+
+
+
+
+
Current Balance
+
+
+
+
+
+ {% if transaction_info %}
+
Kshs {{ transaction_info.total_revenue|default:0 }}
+
+ {% else %}
+
NIL
+
No transaction information available.
+ {% endif %}
+
Total Revenue
+
+
Withdraw
+
+
+
History
+
+
+
Transfer to MPesa
+
Dec 7, 2023
+
+
+
Kshs 650
+
+
+
+
+
Transfer to MPesa
+
Dec 2, 2023
+
+
+
Kshs 150
+
+
+
+
+
+
+
+
+
+
+ {% if videos %}
+
+
+
Top Performing Videos
+
By Views
+
+
+
+
+
+
+
+ {% for video in videos %}
+
+
+
+
+
+
+ {{ video.upload_date|date:"F j, Y" }}
+
+
30 views
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+ Sufferring by Design
+
+
+
+
1 hour ago
+
23 views
+
+
+
+
+
+
+
+
+ Live Concert at the 02 Arena
+
+
+
+
+ 35 minutes ago
+
+
7 views
+
+
+
+
+
+
+
+
+ Broadcast My Bank Balance: Comedy Special
+
+
+
+
+ 55 minutes ago
+
+
7 views
+
+
+
+
+
+
+
+
+ Planning The Live Concert: Behind the Scenes
+
+
+
+
+ 50 minutes ago
+
+
4 views
+
+
+
+
+ {% else %}
+
You haven't posted any videos yet.
+ {% endif %}
+
+
+
+
+
+
+
+ {% if transaction_info %}
+
+
+
+
+
+
+
+ Today's sales
+
+
Kshs 600
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Transactions
+
+
{{ transaction_info.total_transactions|default:0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unique Viewers
+
+
{{ transaction_info.unique_ip_count|default:0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% else %}
+
No transaction information available.
+ {% endif %}
+
+
+
+
+
+
+
+ Popular Time for Watching
+
+ {% for hour in transaction_hours %}
+
{{ hour.transaction_hour|date:"G:i" }} - Transactions: {{
+ hour.transaction_count }}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/users/templates/users/login.html b/users/templates/users/login.html
new file mode 100644
index 0000000..7242708
--- /dev/null
+++ b/users/templates/users/login.html
@@ -0,0 +1,34 @@
+{% extends 'blog/base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+
+
+
+
Welcome Back
+ Log in to access exclusive content.
+
+
+
+
+
+
+{% endblock content %}
diff --git a/users/templates/users/logout.html b/users/templates/users/logout.html
new file mode 100644
index 0000000..a990cfc
--- /dev/null
+++ b/users/templates/users/logout.html
@@ -0,0 +1,20 @@
+{% extends 'blog/base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+ You have been logged out
+
+ Thanks for spending some quality time with the web site today.
+
+
+
+
+
+{% endblock content %}
+{% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/password_reset.html b/users/templates/users/password_reset.html
new file mode 100644
index 0000000..83d203e
--- /dev/null
+++ b/users/templates/users/password_reset.html
@@ -0,0 +1,26 @@
+{% extends 'blog/base.html' %} {% load crispy_forms_tags %} {% block content %}
+
+
+
+
+
+ Account Recovery Tip:
Having trouble? Contact support for assistance.
+
+
+
+
+
+
+
+{% endblock content %}
+{% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/password_reset_complete.html b/users/templates/users/password_reset_complete.html
new file mode 100644
index 0000000..2b8238b
--- /dev/null
+++ b/users/templates/users/password_reset_complete.html
@@ -0,0 +1,10 @@
+{% extends 'blog/base.html' %}
+{% block content %}
+
+
+ Password Reset Successfully
+
+ Sign in
+
+{% endblock content %}
+{% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/password_reset_confirm.html b/users/templates/users/password_reset_confirm.html
new file mode 100644
index 0000000..73440c8
--- /dev/null
+++ b/users/templates/users/password_reset_confirm.html
@@ -0,0 +1,26 @@
+{% extends 'blog/base.html' %} {% load crispy_forms_tags %} {% block content %}
+
+
+
+
+ about us "Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+
+
+
+
+
+ {% endblock content %}
+ {% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/password_reset_done.html b/users/templates/users/password_reset_done.html
new file mode 100644
index 0000000..46bb498
--- /dev/null
+++ b/users/templates/users/password_reset_done.html
@@ -0,0 +1,10 @@
+{% extends 'blog/base.html' %}
+{% block content %}
+
+
+ An Email Has Been sent with instructions to Reset Your Password
+
+
+{% endblock content %}
+
+{% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html
new file mode 100644
index 0000000..4f609be
--- /dev/null
+++ b/users/templates/users/profile.html
@@ -0,0 +1,34 @@
+{% extends 'blog/base.html' %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% endblock content %}
+ {% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
new file mode 100644
index 0000000..f1c0faf
--- /dev/null
+++ b/users/templates/users/register.html
@@ -0,0 +1,41 @@
+{% extends 'blog/base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+
+
+
+
Jambo!
+ Create Your Free Account
+
+
+
+
+
+
+
+
+
+ Already have an account? Sign In
+
+
+
+
+{% endblock content %}
+{% block footer %}{% include 'blog/footer.html' %}{% endblock %}
diff --git a/users/tests.py b/users/tests.py
new file mode 100644
index 0000000..c756def
--- /dev/null
+++ b/users/tests.py
@@ -0,0 +1,210 @@
+# Create your tests here.
+from django.test import TestCase
+from django.contrib.auth.models import User
+from .models import Profile
+from .forms import UserRegisterForm, UserUpdateForm, ProfileUpdateForm
+from django.urls import reverse
+
+
+class ProfileModelTest(TestCase):
+ """unittests for the Profile Model"""
+ def setUp(self):
+ """Create a test user"""
+ self.test_user = User.objects.create(
+ username='django', password='Brunhilda')
+ """Fetch a test profile created by signals"""
+ self.test_profile = Profile.objects.get(user=self.test_user)
+
+ def tearDown(self):
+ """Clean up by deleting the test user"""
+ self.test_user.delete()
+
+ def test_profile_creation(self):
+ """Test that a Profile instance is created when a User is created"""
+ self.assertIsInstance(self.test_profile, Profile)
+ self.assertEqual(
+ str(self.test_profile), f'{self.test_user.username} Profile')
+
+ def test_profile_fields(self):
+ """Test individual fields of the Profile model"""
+ self.assertEqual(self.test_profile.user, self.test_user)
+ self.assertEqual(self.test_profile.phone_number, None)
+ self.test_profile.phone_number = "12312123123"
+ self.assertEqual(self.test_profile.phone_number, "12312123123")
+ self.test_profile.phone_number = "12312123"
+ self.assertEqual(self.test_profile.phone_number, "12312123")
+ self.assertEqual(self.test_profile.image.name, 'default.jpg')
+
+ def test_profile_str_method(self):
+ """Test the __str__ method of the Profile model"""
+ expected_str = f'{self.test_user.username} Profile'
+ self.assertEqual(str(self.test_profile), expected_str)
+
+
+class UserRegisterFormTest(TestCase):
+ """FORMS: unittests for the user registration form"""
+ def test_user_register_form_valid(self):
+ """register user with valid data"""
+ form_data = {
+ 'username': 'testuser',
+ 'email': 'testuser@example.com',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ 'phone_number': '+12345678903',
+ }
+ form = UserRegisterForm(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_user_register_form_invalid(self):
+ """register user with invalid email"""
+ form_data = {
+ 'username': 'testuser',
+ 'email': 'invalidemail',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ 'phone_number': '+1234567890',
+ }
+ form = UserRegisterForm(data=form_data)
+ self.assertFalse(form.is_valid())
+
+
+class UserUpdateFormTest(TestCase):
+ """FORMS: unittests for the user update form"""
+ def test_user_update_form_valid(self):
+ """Test With Valid Form Data"""
+ user = User.objects.create_user(
+ username='testuser', password='testpassword')
+ form_data = {
+ 'username': 'updateduser',
+ 'email': 'updateduser@example.com',
+ 'image': 'default.jpg',
+ 'phone_number': '+254712123123',
+ }
+ form = UserUpdateForm(instance=user, data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_user_update_form_invalid(self):
+ """Test Empty username"""
+ user = User.objects.create_user(
+ username='testuser', password='testpassword')
+ form_data = {
+ 'username': '',
+ 'email': 'updateduser@example.com',
+ 'image': 'default.jpg',
+ 'phone_number': '+254712123123',
+ }
+ form = UserUpdateForm(instance=user, data=form_data)
+ self.assertFalse(form.is_valid())
+
+
+class ProfileUpdateFormTest(TestCase):
+ """"FORMS: test update phone number"""
+ def test_profile_update_form_valid(self):
+ """TEST profile update with valid data"""
+ user = User.objects.create_user(
+ username='testuser', password='testpassword')
+ profile = user.profile
+ form_data = {
+ 'username': 'testuser',
+ 'email': 'updateduser@example.com',
+ 'image': 'default.jpg',
+ 'phone_number': '+254712123123',
+ }
+ form = ProfileUpdateForm(instance=profile, data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_profile_update_form_invalid(self):
+ """TEST, profile update with invalid phone number"""
+ user = User.objects.create_user(
+ username='testuser', password='testpassword')
+ profile = user.profile
+ form_data = {
+ 'username': 'testuser',
+ 'email': 'updateduser@example.com',
+ 'image': 'default.jpg',
+ 'phone_number': 'invalidphonenumber',
+ }
+ form = ProfileUpdateForm(instance=profile, data=form_data)
+ self.assertFalse(form.is_valid())
+
+
+class RegisterViewTest(TestCase):
+ """VIEWS: Tests for New User Registration View using Django client"""
+
+ def test_register_view_get(self):
+ """TEST:200 get the user registration view"""
+ response = self.client.get(reverse('register'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'users/register.html')
+ self.assertIsInstance(response.context['form'], UserRegisterForm)
+
+ def test_register_view_post_valid(self):
+ """TEST: 302 Redirect on success"""
+ data = {
+ 'username': 'testuser',
+ 'email': 'testuser@example.com',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ 'phone_number': '+254712123456',
+ }
+ response = self.client.post(reverse('register'), data)
+ self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, reverse('login'))
+ self.assertEqual(User.objects.count(), 1)
+ self.assertEqual(Profile.objects.count(), 1)
+
+ def test_register_view_post_invalid(self):
+ """Test: No Redirect Invalid email: 200 CODE: Form validation failed"""
+ data = {
+ 'username': 'testuser',
+ 'email': 'invalidemail',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ }
+ response = self.client.post(reverse('register'), data)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'users/register.html')
+ self.assertIsInstance(response.context['form'], UserRegisterForm)
+
+
+class ProfileViewTest(TestCase):
+ """VIEWS: Tests for Profile View using Django client"""
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser', password='testpassword')
+ self.client.login(username='testuser', password='testpassword')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_profile_view_get(self):
+ """Tests GET view"""
+ response = self.client.get(reverse('profile'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'users/profile.html')
+ self.assertIsInstance(response.context['u_form'], UserUpdateForm)
+ self.assertIsInstance(response.context['p_form'], ProfileUpdateForm)
+
+ def test_profile_view_post_valid(self):
+ """Test 302 Redirect on success"""
+ data = {
+ 'username': 'updateduser',
+ 'email': 'updateduser@example.com',
+ }
+ response = self.client.post(reverse('profile'), data)
+ self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, reverse('profile'))
+ self.assertEqual(
+ User.objects.get(username='updateduser').username, 'updateduser')
+
+ def test_profile_view_post_invalid(self):
+ """Empty username 200 # Form validation failed"""
+ data = {
+ 'username': '',
+ 'email': 'updateduser@example.com',
+ }
+ response = self.client.post(reverse('profile'), data)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'users/profile.html')
+ self.assertIsInstance(response.context['u_form'], UserUpdateForm)
+ self.assertIsInstance(response.context['p_form'], ProfileUpdateForm)
diff --git a/users/views.py b/users/views.py
new file mode 100644
index 0000000..a4e692b
--- /dev/null
+++ b/users/views.py
@@ -0,0 +1,104 @@
+from django.shortcuts import render, redirect
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from videos.models import Video
+from mpesa.models import Transaction
+from django.db.models import Sum, Count, Avg
+from django.db.models.functions import TruncHour
+
+""" import the forms from forms to create the views"""
+from .forms import UserRegisterForm, ProfileUpdateForm, UserUpdateForm
+
+
+def register(request):
+ """create a view for the user registration form"""
+ if request.method == 'POST':
+ form = UserRegisterForm(request.POST)
+ if form.is_valid():
+ form.save()
+ username = form.cleaned_data.get('username')
+ messages.success(
+ request, f'Account succcessfully created for {username}!')
+ return redirect('login')
+ else:
+ messages.warning(request, "To err is human")
+
+ else:
+ form = UserRegisterForm()
+
+ """pass form as the context of the form filled/empty"""
+ return render(
+ request, 'users/register.html', {'form': form})
+
+
+@login_required
+def profile(request):
+ """Create view for the profile update form
+ Returns:
+ form: form data to access in the template
+ """
+ if request.method == 'POST':
+ u_update = UserUpdateForm(request.POST, instance=request.user)
+ p_update = ProfileUpdateForm(request.POST, request.FILES,
+ instance=request.user.profile)
+ if u_update.is_valid() and p_update.is_valid():
+ u_update.save()
+ p_update.save()
+ messages.success(request, f'Account succcessfully updated!')
+ return redirect('profile')
+ else:
+ context = {'u_form': u_update, 'p_form': p_update, }
+ # messages.warning(request, f'Error!')
+ return render(request, 'users/profile.html', context)
+ else:
+ u_update = UserUpdateForm(instance=request.user)
+ p_update = ProfileUpdateForm(instance=request.user.profile)
+ context = {
+ 'u_form': u_update,
+ 'p_form': p_update,
+ } # varriables to access in the template
+
+ return render(request, 'users/profile.html', context)
+
+
+@login_required
+def dashboard(request):
+ """view for the client dashboard to Get transaction-related information
+
+ Returns:
+ context data:
+ unique_ip_count,
+ total_revenue,
+ total_transactions,
+ avg_transaction_amount,
+ transaction_hour
+ """
+ user = request.user
+ videos = Video.objects.filter(user=user)
+
+ transaction_info = Transaction.objects.filter(
+ video__in=videos, status='successful').aggregate(
+ total_revenue=Sum('amount'),
+ total_transactions=Count('id'),
+ unique_ip_count=Count('ip', distinct=True)
+ )
+ avg_transaction_amount = Transaction.objects.filter(
+ video__in=videos, status='successful').aggregate(
+ avg_transaction_amount=Avg('amount'))['avg_transaction_amount']
+
+ transaction_hours = Transaction.objects.filter(
+ video__in=videos, status='successful').annotate(
+ transaction_hour=TruncHour('created')).values(
+ 'transaction_hour').annotate(
+ transaction_count=Count('id')).order_by(
+ '-transaction_count')[:5]
+
+ context = {
+ 'user': user,
+ 'videos': videos,
+ 'avg_transaction_amount': avg_transaction_amount,
+ 'transaction_info': transaction_info,
+ 'transaction_hours': transaction_hours,
+ }
+
+ return render(request, 'users/dashboard.html', context)