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

+
+
+

View History

+
+
+
+
+ +
+ {% 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.
+
+
+
+
+
+ {% csrf_token %} +
+ Login + {{ form|crispy }} +
+
+ +
+ I forgot my Password +
+ + New Here? Sign Up Now! +
+
+
+
+
+{% 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. +

+ + +
+

+ Log Back In +

+
+ +{% 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. +

+
+
+
+
+ {% csrf_token %} +
+ Forgot Your Password? + Enter Your Email to Reset it + {{ form|crispy }} +
+ +
+
+
+{% 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, +

+
+
+
+
+ {% csrf_token %} +
+ Confirm Password Reset + {{ form|crispy }} +
+
+ +
+
+
+
+ {% 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 %} +
+
+
+ +
+ +

{{ user.email }}

+
+
+
+
+
+
+ {% csrf_token %} +
+ Your Profile + {{ p_form|crispy }} + {{ u_form|crispy }} +
+
+
+
+ +
+
+ + + + {% 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
+
+
+
+ +
+
+
+ {% csrf_token %} +
+ Join Today + {{ form.username|as_crispy_field }} + {{ form.email|as_crispy_field }} + {{ form.phone_number|as_crispy_field }} + {{ form.password1|as_crispy_field }} + {{ form.password2|as_crispy_field }} +
+ +
+ +
+
+
+ + 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)