-
Notifications
You must be signed in to change notification settings - Fork 2
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
Refactor portal serializer + Add email logic for updates on Content #291
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Some comments and questions
EMAIL_PORT = os.environ.get("SMTP_PORT", 587) | ||
EMAIL_HOST_USER = os.environ.get("SMTP_USERNAME", "") | ||
EMAIL_HOST_PASSWORD = os.environ.get("SMTP_PASSWORD", "") | ||
DEFAULT_FROM_EMAIL = os.environ.get("SMTP_FROM_EMAIL", EMAIL_HOST_USER) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would DEFAULT_FROM_EMAIL
not be EMAIL_HOST_USER
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
host user might be [email protected]
and default from might be Penn Mobile <[email protected]>
|
||
|
||
def get_backend_manager_emails(): | ||
if group := Group.objects.filter(name="backend_managers").first(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: prefer moving backend_managers
into a variable so it can be re-used for other purposes in the future
backend/portal/models.py
Outdated
prev = self.__class__.objects.filter(id=self.id).first() | ||
super().save(*args, **kwargs) | ||
if prev is None: | ||
return self._on_create() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: no need for return
here since
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait yes need for return here because i want need to hard stop since prev is None.
it was originally
self._on_create() return
but i changed it to be fancy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait then why don't you just have an elif
for the latter clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually yea i could. I did return because i thought i would have more conditions to send an email
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep up to you
backend/portal/serializers.py
Outdated
validated_data["status"] = Content.STATUS_DRAFT | ||
|
||
# auto add all target populations of a kind if not specified | ||
auto_add_kind = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
return super().update(instance, validated_data) | ||
|
||
|
||
class PollSerializer(ContentSerializer): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Do you think its worth to override the PollSerializer
to auto set multiselect=False
? It is defaulted to False
but just to be safe I'm not sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its fine, because also this means we don't have to change when we add multiselect. And even if a malicious actor sent us multiselect=True, we need to first approve it and even if we approve it won't do anything cuz the frontend code doesn't use it right now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
backend/portal/serializers.py
Outdated
|
||
return super().create(validated_data) | ||
|
||
def update(self, instance, validated_data): | ||
# if Poll is updated, then approve should be false | ||
# if Content is updated, then approve should be false | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For update
, I think we also need the TargetPopulations
logic you have above (ex. If user changes the target populations to empty then we need to add all of them)
backend/utils/email.py
Outdated
|
||
def get_backend_manager_emails(): | ||
if group := Group.objects.filter(name="backend_managers").first(): | ||
return group.user_set.values_list("email", flat=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a person doesn't have an email, then the return value with have a None
in the list, which I'd imagine isn't ideal? Probably need to filter out the None
's
backend/utils/email.py
Outdated
subject=subject, | ||
message=message, | ||
from_email=None, | ||
recipient_list=recipient_list if isinstance(recipient_list, list) else [recipient_list], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge fan of this line, especially since the name of the variable has "list" in it. Can we just enforce that recipient_list
is always of type list
?
success = django_send_mail( | ||
subject=subject, | ||
message=message, | ||
from_email=None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason this is None
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we set to None, it uses the default one specified in settings (base.py)
|
||
@shared_task(name="utils.send_mail") | ||
def send_mail(subject, recipient_list, message=None, html_message=None): | ||
if recipient_list is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When will this be the case? I feel like if this is ever None
we shouldn't fail silently like this (the return value of this function is never checked)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this because I thought this could just be "intended bevahior" for this function for sake of ease lol.
A case where we need this right now is if a post without a user attached onto it (so old posts) are updated, the recipient list would be None.
I guess the real question is whether this should be the intended behavior of this function. I have no preference. We can throw an error instead and do the None checking on the side of the caller of this function instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would prefer erroring here. Definitely will save a few minutes if someone needs to debug this code later on
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #291 +/- ##
==========================================
+ Coverage 90.41% 90.86% +0.45%
==========================================
Files 62 63 +1
Lines 2681 2661 -20
==========================================
- Hits 2424 2418 -6
+ Misses 257 243 -14 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
didn't test it but your test cases look comprehensive so i trust it! ow lgtm :<
|
||
class Meta: | ||
abstract = True | ||
|
||
def _get_email_subject(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if these are meant to be private methods, they should start with two underscores, not one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
damn u right, that's so weird, I've always assumed the name mangling is a good way to enforce private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait i didn't say i preferred single underscore. tbh i don't know which i prefer was hoping for u to choose one lol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol i'd just stick with SO's opinion, single underscore for private methods, double underscore if you're concerned about inheritance overriding
) | ||
|
||
def _on_status_change(self): | ||
if email := getattr(self.creator, "email", None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't need to pass in the last param for getattr
since the default return value if not found is None
.
also... should do if _ is not None
... more Pythonic :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no u need the default else it errors.
but we have this pattern everywhere in our code...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o u right
argh fine keep it
def is_valid(self, *args, **kwargs): | ||
if isinstance(self.initial_data, QueryDict): | ||
self.initial_data = self.initial_data.dict() | ||
self.initial_data["target_populations"] = list( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hot damn, a list
and then a map
...
self.initial_data = self.initial_data.dict() | ||
self.initial_data["target_populations"] = list( | ||
( | ||
map(int, self.initial_data["target_populations"].split(",")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could also probably just do
self.initial_data["target_population"] = [int(v) for v in self.initial_data.get("target_populations", "").split(",")]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is better but im lazy to change if theres no other changes hehehe
@@ -48,17 +50,54 @@ class Content(models.Model): | |||
admin_comment = models.CharField(max_length=255, null=True, blank=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In line 50, created_date should probably be immutable.
created_date = models.DateTimeField(auto_now_add=True)
Update save to
def save(self, *args, **kwargs):
if not self.pk:
self.created_date = timezone.now() # Set created_date to the current time
else:
if 'update_fields' in kwargs and 'created_date' in kwargs['update_fields']:
kwargs['update_fields'].remove('created_date')
prev = self.__class__.objects.filter(id=self.id).first()
super().save(*args, **kwargs)
if prev is None:
self._on_create()
return
if self.status != prev.status:
self._on_status_change()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you look in the serializer for Content
, you'll see that created_date is read only so it is enforced there.
Technically an admin could change the created date, but there is no way to prevent this since you can always change a row in a db. In other words you can't enforce atomicity at the db level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General feedback: Everything looks logical to me, but I'm not familiar with Django best practices so it's hard for me to find specific issues.
Design Consideration:
For PollVotes, we could give users the option to be anonymous or named, and let us filter results based anonymous votes, named votes, and overall votes.
Specific Issues: Make created_date immutable, do _auto_add_target_population using set difference instead of nested loops
|
||
def _auto_add_target_population(self, validated_data): | ||
# auto add all target populations of a kind if not specified | ||
if target_populations := validated_data.get("target_populations"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be more efficient to do this using set difference instead of nested loops
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good thought, although it doesn't matter here since if you look Kind has like 4 values. Probably the overhead from sets is more costly anyway
refactor portal serializer to use abstract class + deal with special case for form data
set up email configs + email utils (wrapper around django email)
set up email notifs on content save/update