Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Models and signup & login views for consumer_user_poc #155

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
70c3abd
Models and signup & login views for consumer_user_poc
challabeehyv Feb 8, 2021
24660a8
pr suggestions and some fixes
challabeehyv Feb 9, 2021
ca0ff9c
send email on receiving sql_case_post_save signal. added unit tests a…
challabeehyv Feb 12, 2021
a2a4655
test case for send_email_case_changed_receiver
challabeehyv Feb 15, 2021
8f921e9
restricting access to patient user for domain and other page
challabeehyv Feb 15, 2021
4eab216
encoding invitation id in sign up url and some minor fixes
challabeehyv Feb 15, 2021
9259888
forgot password, update details, creating user
challabeehyv Feb 17, 2021
2e55a11
updated tests and refactored some code
challabeehyv Feb 18, 2021
bd1bc56
Removed htmls and made some changes based on suggestions
challabeehyv Feb 19, 2021
05ff960
fixed column name and merged both the migrations into one
challabeehyv Feb 19, 2021
71f200d
column name fixed
challabeehyv Feb 22, 2021
b6e3f86
removed unique_together constraint, consumer_user login as webuser ch…
challabeehyv Feb 23, 2021
3a48c03
Made changes suggested in PR. Did some formatting fixed some issues.
challabeehyv Feb 27, 2021
5b161a9
Made changes suggested in PR.
challabeehyv Mar 2, 2021
4132c62
Removed same email check in consumer user login form
challabeehyv Mar 2, 2021
6a43641
Made some changes to signals logic
challabeehyv Mar 2, 2021
1623156
PR suggestions and added a test for resending invitiation
challabeehyv Mar 3, 2021
33f8433
Created new column demographic_case_id in invitation model, squashed …
challabeehyv Mar 5, 2021
34b58ad
Added assertions to check active invitations and removed exception ha…
challabeehyv Mar 9, 2021
f8de41f
UI Changes
challabeehyv Mar 9, 2021
6c47746
Removed duplicate app config
challabeehyv Mar 9, 2021
aff9346
Changed templates based on PR suggestions
challabeehyv Mar 10, 2021
d90710e
Removed unused imports and variables
challabeehyv Mar 10, 2021
08d3e08
fixed couple of failing tests
challabeehyv Mar 12, 2021
fce58b3
Moved Templates to conusmer_user, CDN issue fix and changes suggested…
challabeehyv Mar 13, 2021
143bb10
Moved all the code except case type check from signals.py to tasks.py
challabeehyv Mar 13, 2021
08fb9e1
Moved GetOrNoneManger to model utils and made changes to get_invitati…
challabeehyv Mar 13, 2021
8706645
Fixed failing tests, Broke the test_send_email test to five different…
challabeehyv Mar 13, 2021
e60c146
Made changes to tasks.py and signals.py as per farid's suggestions
challabeehyv Mar 15, 2021
0e593cd
Fixed Inanctive InvitationError raising
challabeehyv Mar 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions corehq/apps/consumer_user/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.apps import AppConfig


class ConsumerUserAppConfig(AppConfig):
name = 'corehq.apps.consumer_user'

def ready(self):
from corehq.apps.consumer_user.signals import connect_signals
connect_signals()


default_app_config = 'corehq.apps.consumer_user.ConsumerUserAppConfig'
5 changes: 5 additions & 0 deletions corehq/apps/consumer_user/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

from corehq.apps.consumer_user.models import ConsumerUser

admin.site.register(ConsumerUser)
5 changes: 5 additions & 0 deletions corehq/apps/consumer_user/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ConsumerUserConfig(AppConfig):
name = 'consumer_user'
16 changes: 16 additions & 0 deletions corehq/apps/consumer_user/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from functools import wraps
from django.contrib.auth.views import redirect_to_login
from django.urls import reverse


def login_required(view_func):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be copied from https://github.com/dimagi/commcare-hq/blob/master/corehq/apps/domain/decorators.py#L517-L526
Is there a reason you need a new decorator?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a new decorator so that we could redirect to consumer_user login if the current user logged in user doesn't have corresponding ConsumerUser instance. ConsumerUserCaseRelationship has relation ship to ConsumerUser. Once he logins corresponding ConsumerUser will be created.

@wraps(view_func)
def _inner(request, *args, **kwargs):
user = request.user
if not (user.is_authenticated and user.is_active):
url = reverse('consumer_user:patient_login')
return redirect_to_login(request.get_full_path(), url)

# User's login and domain have been validated - it's safe to call the view function
return view_func(request, *args, **kwargs)
return _inner
Empty file.
16 changes: 16 additions & 0 deletions corehq/apps/consumer_user/forms/change_contact_details_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.contrib.auth.models import User
from django.forms import ModelForm


class ChangeContactDetailsForm(ModelForm):
class Meta:
model = User
fields = ['first_name', 'last_name']

def save(self, commit=True):
user = super(ChangeContactDetailsForm, self).save(commit=False)
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
if commit:
user.save()
return user
45 changes: 45 additions & 0 deletions corehq/apps/consumer_user/forms/patient_authentication_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from corehq.apps.domain.forms import NoAutocompleteMixin
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from corehq.apps.consumer_user.models import ConsumerUser
from corehq.apps.consumer_user.models import ConsumerUserCaseRelationship
from corehq.apps.hqcase.utils import update_case


class PatientAuthenticationForm(NoAutocompleteMixin, AuthenticationForm):
username = forms.EmailField(label=_("Email Address"), max_length=75,
widget=forms.TextInput(attrs={'class': 'form-control'}),
required=True)
password = forms.CharField(label=_("Password"),
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
required=True)

def __init__(self, *args, **kwargs):
self.invitation = kwargs.pop('invitation', None)
super().__init__(*args, **kwargs)

def clean(self):
username = self.cleaned_data.get('username')
if username is None:
raise ValidationError('Please enter a valid email address.')

password = self.cleaned_data.get('password')
if not password:
raise ValidationError("Please enter a password.")
try:
cleaned_data = super(PatientAuthenticationForm, self).clean()
if self.invitation and not self.invitation.accepted:
consumer_user, _ = ConsumerUser.objects.get_or_create(user=self.user_cache)
self.invitation.accepted = True
self.invitation.save()
_ = ConsumerUserCaseRelationship.objects.create(case_id=self.invitation.case_id,
domain=self.invitation.domain,
consumer_user=consumer_user)
update_case(self.invitation.domain,
self.invitation.case_id,
{'invitation_status': 'accepted'})
except ValidationError:
raise
return cleaned_data
40 changes: 40 additions & 0 deletions corehq/apps/consumer_user/forms/patient_signup_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.forms import ModelForm
from django.forms import PasswordInput
from django.contrib.auth.models import User
from corehq.apps.consumer_user.models import ConsumerUser
from corehq.apps.consumer_user.models import ConsumerUserCaseRelationship
from corehq.apps.hqcase.utils import update_case


class PatientSignUpForm(ModelForm):

def __init__(self, *args, **kwargs):
self.invitation = kwargs.pop('invitation', None)
super().__init__(*args, **kwargs)
for key in self.fields:
self.fields[key].required = True

class Meta:
model = User
fields = ['first_name', 'last_name', 'email', 'password']
widgets = {
'password': PasswordInput(),
}

def save(self, commit=True):
user = super(PatientSignUpForm, self).save(commit=False)
user.set_password(self.cleaned_data["password"])
user.username = self.cleaned_data['email']
if commit:
user.save()
consumer_user = ConsumerUser.objects.create(user=user)
if self.invitation:
self.invitation.accepted = True
self.invitation.save()
_ = ConsumerUserCaseRelationship.objects.create(case_id=self.invitation.case_id,
domain=self.invitation.domain,
consumer_user=consumer_user)
update_case(self.invitation.domain,
self.invitation.case_id,
{'invitation_status': 'accepted'})
return user
51 changes: 51 additions & 0 deletions corehq/apps/consumer_user/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 2.2.16 on 2021-02-23 06:52

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='ConsumerUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ConsumerUserInvitation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('case_id', models.CharField(max_length=255)),
('domain', models.CharField(max_length=255)),
('accepted', models.BooleanField(default=False)),
('invited_by', models.CharField(max_length=255)),
('invited_on', models.DateTimeField(auto_now=True)),
('active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='ConsumerUserCaseRelationship',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('case_id', models.CharField(max_length=255)),
('domain', models.CharField(max_length=255)),
('consumer_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to='consumer_user.ConsumerUser')),
],
options={
'unique_together': {('case_id', 'domain')},
},
),
]
Empty file.
25 changes: 25 additions & 0 deletions corehq/apps/consumer_user/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.contrib.auth.models import User
from django.db import models


class ConsumerUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)


class ConsumerUserCaseRelationship(models.Model):
consumer_user = models.ForeignKey(ConsumerUser, on_delete=models.CASCADE)
case_id = models.CharField(max_length=255)
domain = models.CharField(max_length=255)

class Meta:
unique_together = ('case_id', 'domain',)


class ConsumerUserInvitation(models.Model):
email = models.EmailField()
case_id = models.CharField(max_length=255)
domain = models.CharField(max_length=255)
accepted = models.BooleanField(default=False)
invited_by = models.CharField(max_length=255)
invited_on = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
80 changes: 80 additions & 0 deletions corehq/apps/consumer_user/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from dimagi.utils.logging import notify_exception
from dimagi.utils.web import get_site_domain
from django.db.utils import IntegrityError
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.urls import reverse
from corehq.apps.consumer_user.models import ConsumerUserInvitation
from corehq.apps.consumer_user.models import ConsumerUserCaseRelationship
from corehq.form_processor.models import CommCareCaseSQL
from corehq.form_processor.signals import sql_case_post_save
from corehq.apps.hqwebapp.tasks import send_html_email_async
from django.utils.http import urlsafe_base64_encode
from django.core.signing import TimestampSigner
from corehq.apps.hqcase.utils import update_case


def send_email_case_changed_receiver(sender, case, **kwargs):
try:
if case.type != 'commcare-caseuserinvitation':
return
try:
email = case.get_case_property('email')
except IntegrityError:
email = '[email protected]'
try:
updated_by = case.get_case_property('updated_by')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this as a check to break out of the loop as update_case is triggering signal again. Made some changes to the logic for better readablity. Let me know If any other changes needs to be made.

except IntegrityError:
updated_by = None
try:
invitation = ConsumerUserInvitation.objects.filter(case_id=case.case_id,
domain=case.domain,
active=True).latest('invited_on')
if invitation:
if updated_by and updated_by == 'patient' and email == invitation.email and not case.closed:
return
invitation.active = False
ConsumerUserCaseRelationship.objects.filter(case_id=case.case_id,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this part? I don't follow why we delete all the relationships here.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a unique_together constraint on case_id, domain in ConsumerUserCaseRelationship Model. In case if there are any rows with same case_id and domain(previous invitation might have been accepted right) so we are deleting it. The change is still there. If my understanding is wrong, let me know I will delete that code

domain=case.domain).delete()
invitation.save()
except ConsumerUserInvitation.DoesNotExist:
pass
if case.closed:
return
invitation = ConsumerUserInvitation.objects.create(case_id=case.case_id, domain=case.domain)
invitation.invited_by = case.opened_by
invitation.email = email
invitation.accepted = False
invitation.save()
# Change to https after the discussion
url = 'http://%s%s' % (get_site_domain(),
reverse('consumer_user:patient_register',
kwargs={
'invitation': TimestampSigner().sign(urlsafe_base64_encode(
force_bytes(invitation.pk)
))
}))
email_context = {
'link': url,
}
send_html_email_async.delay(
'Beneficiary Registration',
email,
render_to_string('email/registration_email.html', email_context),
text_content=render_to_string('email/registration_email.txt', email_context)
)
update_case(case.domain, case.case_id, {'invitation_status': 'sent', 'updated_by': 'patient'})

except Exception:
notify_exception(
None,
message="Could not create messaging case changed task. Is RabbitMQ running?"
)


def connect_signals():
sql_case_post_save.connect(
send_email_case_changed_receiver,
CommCareCaseSQL,
dispatch_uid='send_email_case_changed_receiver'
)
26 changes: 26 additions & 0 deletions corehq/apps/consumer_user/templates/change_contact_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% block content %}
<h2>Change Contact Details</h2>
{% if msg %}
<p style="font-weight:bold">{{ msg }}</p>
{% endif %}
{% if form.errors %}
<ul class="user-msg error">
{% for field in form %}
{% for error in field.errors %}
<li>
{% if field != '__all__' %}
<strong>{{ field.label }}:</strong>
{% endif %}
{{ error }}
</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
<a href="{% url 'consumer_user:patient_homepage' %}">HomePage</a>
{% endblock %}
26 changes: 26 additions & 0 deletions corehq/apps/consumer_user/templates/change_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% block content %}
<h2>Change Password</h2>
{% if msg %}
<p style="font-weight:bold">{{ msg }}</p>
{% endif %}
{% if form.errors %}
<ul class="user-msg error">
{% for field in form %}
{% for error in field.errors %}
<li>
{% if field != '__all__' %}
<strong>{{ field.label }}:</strong>
{% endif %}
{{ error }}
</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
<a href="{% url 'consumer_user:patient_homepage' %}">HomePage</a>
{% endblock %}
19 changes: 19 additions & 0 deletions corehq/apps/consumer_user/templates/domains_and_cases.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% block content %}
<h2>Domains and cases</h2>
{% if msg %}
<p style="font-weight:bold">{{ msg }}</p>
{% endif %}
<table>
<tr>
<th>Domain</th>
<th>Cases</th>
</tr>
{% for dict in domains_and_cases %}
<tr>
<td>{{dict.domain}}</td>
<td>{{dict.case_id}}</td>
</tr>
{% endfor %}
</table>
<a href="{% url 'consumer_user:patient_homepage' %}">HomePage</a>
{% endblock %}
14 changes: 14 additions & 0 deletions corehq/apps/consumer_user/templates/email/registration_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<p>
Dear user,
</p>

<p>Click on the below link to register</p>

<p>
<a href={{link}}></a>
</p>

<p>
Cheers,<br />
--The CommCareHQ Team
</p>
Loading