Issues with changin/updating stripe subscription (django) - python

Currently have this code for one of the subscriptions that I have available on my site. The code checks to see if the user has a plan already, if they don't, the else statement is run (works fine) and if they do, the code for updating their current subscription is replaced with the new subscription.(not working)
#login_required
def charge(request):
user_info = request.user.profile
email = user_info.inbox
if request.method == 'POST':
#returns card token in terminal
print('Data:', request.POST)
user_plan = request.user.profile.current_plan
if user_plan != 'None':
'''if they have a plan already, override that plan
with this new plan this is using an already created
user'''
#this throws an error right now
new_plan = stripe.Subscription.modify(
#the current plan(wanting to change)
user_info.subscription_id,
cancel_at_period_end=True,
proration_behavior='create_prorations',
#the new subscription
items=[{'plan':'price_HHU1Y81pU1wrNp',}]
)
user_info.subscription_id = new_plan.id
#if they don't have a subscription already
else:
amount = 10
customer = stripe.Customer.create(
email=email,
source=request.POST['stripeToken'],
description=user_info.genre_one,
)
charge = stripe.Subscription.create(
customer=customer.id,#email of logged in person
items = [{"plan": "price_HHU1Y81pU1wrNp"}],#change plan id depending on plan chosen
)
#updates users current plan with its id and other info
user_info.subscription_id = charge.id
user_info.customer_id = customer.id
user_info.current_plan = 'B'
user_info.save()
return redirect(reverse('success', args=[amount]))
When I try and update the users subscription to the new one I get this runtime error:
Request req_kK2v51jnhuuKsW: Cannot add multiple subscription items with the same plan: price_HHU1Y81pU1wrNp
The Account that I am testing with has a plan that is different than the one that im trying to update to. (this code is for basic plan, the account has the standard plan enabled).
All help is greatly appreciated!
Edit: This is the model data, I tried changing all the 'None' values to something else to see if it would change the error but it didn't.
SUB_PLANS = [
('None','None'),
('B','Basic Plan'),
('S', 'Standard Plan'),
('P', 'Premium Plan'),
]
GENRE_CHOICES = [
('1','None'),
('2','Adventure'),
('3','Action'),
('4','Puzzle'),
('5','Story Based'),
]
# Create your models here.
class Profile(models.Model):
User = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
username = models.CharField(max_length=30)
#where games are sent
inbox = models.EmailField(max_length = 50)
current_plan = models.CharField(max_length = 4, choices = SUB_PLANS, default='None')
#genres they like
genre_one = models.CharField(max_length = 20, choices = GENRE_CHOICES, default = 'None')
genre_two = models.CharField(max_length = 20, choices = GENRE_CHOICES, default = 'None')
genre_three = models.CharField(max_length = 20, choices = GENRE_CHOICES, default = 'None')
subscription_id = models.CharField(max_length = 40, default="None")
customer_id = models.CharField(max_length = 40, default = "None")
'''on account creation plan == null, then once they buy one,
plan is added as a dropdown that they can edit easy'''
def __str__(self):
return self.username

The trick here is that if you're updating an existing Subscription that already has a single item, then you'll need to pass the ID for that SubscriptionItem when updating so that the API knows you're not attempting to add a second SubscriptionItem with the same plan.
Based on that error message, it seems the Plan was already updated on that Subscription, but likely not in the way you're expecting. If the Subscription started out on the "standard" plan, then your code above was executed, it likely added the "basic" plan in addition to the existing standard. I bet they are subscribed to both basic and standard now. In order to update as you expect, you'll want to delete the standard SubscriptionItem and add the basic SubscriptionItem which I'll show code for below. Alternatively, you can update the SubscriptionItem directly to swap the plan.
Note that if you have multiple plans per Subscription, this example will need to be modified to find the correct SubscriptionItem ID. Here's one way to do this:
current_subscription = stripe.Subscription.retrieve(user_info.subscription_id)
new_plan = stripe.Subscription.modify(
user_info.subscription_id,
cancel_at_period_end=True,
proration_behavior='create_prorations',
#the new subscription
items=[{
'id': current_subscription['items'].data[0].id, # note if you have more than one Plan per Subscription, you'll need to improve this. This assumes one plan per sub.
'deleted': True,
}, {
'plan': 'price_HHU1Y81pU1wrNp'
}]
)

Related

Django - How to get details from many-to-many relationship in multiple select form field

I have 2 models linked with a M2M relationship and I would like to build a form allowing to manage the links but also to add controls on selected values.
This take place outside admin templates.
I managed defining the form to manage links but I was not able to consider additional information to define validity controls.
Here are my models:
class EventGroup(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
users = models.ManyToManyField(UserComp, verbose_name="utilisateurs", blank=True)
group_name = models.CharField("nom", max_length=100)
weight = models.IntegerField("poids", default=0)
class Event(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
groups = models.ManyToManyField(EventGroup, verbose_name="groupes", blank=True)
rules = [("MAJ", "Majorité"), ("PROP", "Proportionnelle")]
event_name = models.CharField("nom", max_length=200)
event_date = models.DateField("date de l'événement")
slug = models.SlugField()
current = models.BooleanField("en cours", default=False)
quorum = models.IntegerField(default=33)
rule = models.CharField(
"mode de scrutin", max_length=5, choices=rules, default="MAJ"
)
The form:
class EventDetail(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
label = "Liste des groupes",
queryset = EventGroup.objects.none(),
widget = forms.CheckboxSelectMultiple,
required = False
)
class Meta:
model = Event
fields = ['event_name', 'event_date', 'quorum', 'rule', 'groups']
The view:
def event_detail(request, evt_id=0):
if evt_id > 0:
current_event = Event.objects.get(id=evt_id)
event_form = EventDetail(request.POST or None, instance=current_event)
else:
event_form = EventDetail(request.POST or None)
company = Company.get_company(request.session['comp_slug'])
event_form.fields['groups'].queryset = EventGroup.objects.\
filter(company=company).\
order_by('group_name')
if request.method == 'POST':
if event_form.is_valid():
event_form.save()
return render(request, "polls/event_detail.html", locals())
This works with no problem to add or remove groups dedicated to the selected or new event, but what I need is to add group's weight information and control that total weight of selected groups is exactly 100. On top of that, I will also need to ensure that each user in the selected groups are listed only once.
Does anybody have any clue on how to implement these controls, or at least how to display additional information to provide the user with related necessary information?
The simplest way to display group weight inside the options would be to change the __str__ method of EventGroup. It would look something like this:
class EventGroup(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
users = models.ManyToManyField(UserComp, verbose_name="utilisateurs", blank=True)
group_name = models.CharField("nom", max_length=100)
weight = models.IntegerField("poids", default=0)
def __str__(self):
return f"{self.group_name} ({self.weight})"
So a group called 'Group' with weight 75 would appear on the options list as "Group (75)"
If you want to dynamically display total weight of selected options, some javascript will be needed. You'll need a function that's triggered when any option is clicked and somehow finds the weight for that option. Following the __str__ solution, it could look like this:
var current_weight = 0
document.querySelector('#id_groups').addEventListener('click', function(e) {
if (e.target && e.target.nodeName == "LI") {
option = e.target
weight = option.textContent.split('(')
weight = parseInt(weight[-1][:-1])
if (option.checked) {
current_weight -= weight
} else {
current_weight += weight
}
document.getElementById('current_weight').textContent = current_weight
submit_button = document.getElementById('submit')
if (current_weight == 100) {
submit_button.disabled = false
} else {
submit_button.disabled = true
}
})
This will have to be inside a .js file that's imported by your polls/event_detail.html template.
What this script does is listen to clicks on any of the LI elements of django CheckboxMultiple widget and get its weight via some simple string manipulation. The string will be something like "Name of the group (23)". If the option is checked, the script will subtract its weight from current_weight (the user has de-selected the option). Otherwise, it will add the weight. The updated sum will be displayed dinamically on an element you'll add to the event_detail.html template with id='current_weight'. This variable can then control whether the form can be submitted or not, by enabling or disabling its submit button.
If you want server-side validation of this weight == 100 limit, you could do something like that in views.py:
if request.method == 'POST':
total_weight = 0
try:
post = request.POST
for item in post['groups']:
weight = EventGroup.objects.get(pk=item).weight
total_weight += weight
except:
pass
if event_form.is_valid() and total_weight == 100:
event_form.save()
This way instance will be saved only if weight == 100. You can pass an appropriate error message to the user otherwise.
I wasn't able to test this script properly, but I hope the idea helps. I'm not sure what you mean by "ensure that each user in the selected groups are listed only once", so I couldn't answer. Bonne chance!

Django not updating Database

I am working on a Django 1.10 project with python 3.
This is my models.py:
# /Multiple choices/
SEX_CHOICES = (('M', 'Male'),
('F', 'Female')
)
ZONE_CHOICES = (('D', 'Départementale'),
('N', 'Nationale')
)
# /Extension of User model/
class Profile(models.Model):
user = models.OneToOneField(User, related_name='profile')
sex = models.CharField(max_length=1, choices=SEX_CHOICES, default='M')
departementNumber = models.PositiveSmallIntegerField(default=88, validators=[MaxValueValidator(101)])
departement = models.CharField(max_length=200)
zone = models.CharField(max_length=1, choices=ZONE_CHOICES, default='D')
receivedApprove = models.PositiveSmallIntegerField(default=0)
receivedDescription = models.PositiveSmallIntegerField(default=0)
receivedDesapprove = models.PositiveSmallIntegerField(default=0)
givenApprove = models.PositiveSmallIntegerField(default=0)
givenDescription = models.PositiveSmallIntegerField(default=0)
givenDisapprove = models.PositiveSmallIntegerField(default=0)
def __str__(self):
return self.user.username
What I am trying to do is to take some users information and complete their profile. Here is what i did in my views.py :
user_id = request.user
firstname = form.cleaned_data['firstname']
lastname = form.cleaned_data['lastname']
email = form.cleaned_data['email']
sex = form2.cleaned_data['sex']
departementNumber = form2.cleaned_data['departementNumber']
zone = form2.cleaned_data['zone']
At this stage, everything is working fine. The problem start when I try to update my model. User object is updating correctly :
upd = User.objects.filter(id=user_id.id).update(first_name=firstname, last_name=lastname, email=email)
But Profile is not updating :
upd2 = Profile.objects.filter(id=user_id.id).update(sex=sex, departementNumber=departementNumber, departement=depName, zone=zone)
And I have not a single warning or error message.
I think the problem is in Profile model. You should add primary_key=True to user field, in order to get the same id as the actual User.
e.g.
user = models.OneToOneField(User, related_name='profile', primary_key=True)
I will add another situation in which the database does not update when using the update() method.
order = Order.objects.filter(id=d['id']).first()
Order.objects.filter(id=d['id']).update(currency=d['currency'])
# further code using order
In this situation the order row is referenced in the first line of code and the update() in line 2 does not work. I don't know the internal reason why line 2 does not update the row in the DB, but from a logical standpoint one should anyway put line 1 after line 2 (which solves it). However if making several code changes this can occur and it looks at first unintuitive why the update() returns '1' even though no update of the DB row happened.

django 'null value in column'

I try to save user's birth date, but get "null value in column "dob" violates not-null constraint" error.
models.py:
class Profile(models.Model):
user = models.OneToOneField(User, unique=True)
nickname = models.CharField(max_length=32)
dob = models.DateField(null=False)
sex = models.BooleanField(null=False)
Here i try to generate random users:
def create_random_users(userCount=1000):
random.seed()
for i in range(0, userCount):
sex = random.randint(0, 1)
name = random.choice(names[sex])
email = "{0}{1}#mail.com".format(name, i)
user = soc_models.User.objects.create_user(email, email, password='password')
user.save()
userProfile = soc_models.Profile.objects.create()
userProfile.user = user
_year = random.randrange(1962, 1995)
_month = random.randrange(1, 12)
_day = random.randrange(1, calendar.monthrange(_year, _month)[1])
userProfile.dob = datetime.datetime(_year, _month, _day)
userProfile.sex = random.randrange(0, 1)
userProfile.city = random.randrange(4000000)
userProfile.country = random.randrange(230)
userProfile.save()
Thank you.
The create method is documented as "a convenience method for creating an object and saving it all in one step". So when the following statement in your sample data creation script runs:
userProfile = soc_models.Profile.objects.create()
It attempts to save an empty Profile object to the database. Since you haven't set the dob attribute at this point, you trigger the NOT NULL constraint.
Two ways to avoid this are:
create the object via the constructor so that it isn't immediately saved to the database.
provide values for all the fields via keyword arguments to create.
When using create, you must pass all required values to it, if you want to call save() later, use model constructor instead, i.e.:
userProfile = soc_models.Profile()
I'm not sure whether it fixes your error or not, but regarding to docs, DateField should be used for storing datetime.date instance, and DateTimeField to store datetime.datetime.
P.S. Really, it looks like you trying to "migrate" DB scheme (to change already created columns). Django doesn't support such feature, but you can use external applications, like South.

Django-powered library checkout system

I am working on a library system to manage certain items in our office, I don't need a full-blown integrated library system so I decided to hand roll one with Django.
Below is a simplified version of my model:
class ItemObjects(models.Model):
# Static Variables
IN_STATUS = 'Available'
OUT_STATUS = 'Checked out'
MISSING = 'Missing'
STATUS_CHOICES = (
(IN_STATUS, 'Available'),
(OUT_STATUS, 'Checked out'),
(MISSING, 'Missing'),
)
# Fields
slug = models.SlugField(unique=True)
date_added = models.DateField(auto_now_add=True)
last_checkin = models.DateTimeField(editable=False, null=True)
last_checkout = models.DateTimeField(editable=False, null=True)
last_activity = models.DateTimeField(editable=False, null=True)
status = models.CharField(choices=STATUS_CHOICES, default=IN_STATUS, max_length=25)
who_has = models.OneToOneField(User, blank=True, null=True)
times_out = models.PositiveIntegerField(default=0, editable=False)
notes = models.CharField(blank=True, max_length=500)
history = models.TextField(blank=True, editable=False)
pending_checkin = models.BooleanField(default=False)
pending_transfer = models.BooleanField(default=False)
At first I was using a method on ItemObject to process checking out an item to a user and who_has was an EmailField because I couldn't get a CharfField to populate with the logged in user's name, but I figured using a OneToOneField is probably closer to the "right" way to do this.. While who_has was an EmailField, the following method worked:
def check_out_itemobject(self, user):
user_profile = user.get_profile()
if self.status == 'Available' and self.who_has == '':
self.status = 'Checked out'
self.who_has = user.email
self.last_checkout = datetime.datetime.now()
self.last_activity = datetime.datetime.now()
self.times_out += 1
if self.history == '':
self.history += "%s" % user_profile.full_name
else:
self.history += ", %s" % user_profile.full_name
if user_profile.history == '':
user_profile.history += self.title
else:
user_profile.history += ", %s" % self.title
else:
return False # Not sure is this is "right"
user_profile.save()
super(ItemObjects, self).save()
Now that I am using a OneToOneField this doesn't work, so I started looking at using a subclass of ModelForm but none of the cases I saw here on SO seemed to apply for what I am trying to do; my form would be a button, and that's it. Here are some of the questions I looked at:
Django: saving multiple modelforms simultaneously (complex case)
(Django) (Foreign Key Issues) model.person_id May not be NULL
django update modelform
So was I on the right track with a sort of altered save() method, or would a ModelForm subclass be the way to go?
EDIT/UPDATE: Many thanks to #ChrisPratt!
So I am trying to get Chris Pratt's suggestion for showing ItemHistory to work, but when I try to render it on a page I get an AttributeError that states "'User' object has no attribute 'timestamp'". So my question is, why is it complaining about a User object when last_activity is an attribute on the ItemObject object ?
My view:
#login_required
def item_detail(request, slug):
item = get_object_or_404(Item, slug=slug)
i_history = item.last_activity
user = request.user
return render_to_response('items/item_detail.html',
{ 'item' : item,
'i_history': i_history,
'user' : user })
I do not see why a User object is coming up at this point.
EDIT2: Nevermind, history is clearly a M2M field whose target is User. That's why!
Assuming users will log in and check out books to themselves, then what you most likely want is a ForeignKey to User. A book will only have one User at any given time, but presumably Users could check out other items as well. If there is some limit, even if the limit is actually one per user, it would be better to validate this in the model's clean method. Something like:
def clean(self):
if self.who_has and self.who_has.itemobject_set.count() >= LIMIT:
raise ValidationError('You have already checked out your maximum amount of items.')
Now, you checkout method has a number of issues. First, status should be a defined set of choices, not just random strings.
class ItemObject(models.Model):
AVAILABLE = 1
CHECKED_OUT = 2
STATUS_CHOICES = (
(AVAILABLE, 'Available'),
(CHECKED_OUT, 'Checked Out'),
)
...
status = models.PositiveIntegerField(choices=STATUS_CHOICES, default=AVAILABLE)
Then, you can run your checks like:
if self.status == self.STATUS_AVAILABLE:
self.status = self.STATUS_CHECKED_OUT
You could use strings and a CharField instead if you like, as well. The key is to decouple the static text from your code, which allows much greater flexibility in your app going forward.
Next, history needs to be a ManyToManyField. Right now, your "history" is only who last checked the item out or what the last item the user checked out was, and as a result is pretty useless.
class ItemObject(models.Model):
...
history = models.ManyToManyField(User, through='ItemHistory', related_name='item_history', blank=True)
class ItemHistory(models.Model):
CHECKED_OUT = 1
RETURNED = 2
ACTIVITY_CHOICES = (
(CHECKED_OUT, 'Checked Out'),
(RETURNED, 'Returned'),
)
item = models.ForeignKey(ItemObject)
user = models.ForeignKey(User)
activity = models.PostiveIntegerField(choices=ACTIVITY_CHOICES)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp'] # latest first
Which then allows you to get full histories:
some_item.history.all()
some_user.item_history.all()
To add a new history, you would do:
ItemHistory.objects.create(item=some_item, user=some_user, activity=ItemHistory.CHECKED_OUT)
The auto_now_add attribute ensures that the timestamp is automatically set when the relationship is created.
You could then actually get rid of the last_checkout and last_activity fields entirely and use something like the following:
class ItemObject(models.Model):
...
def _last_checkout(self):
try:
return self.history.filter(activity=ItemHistory.CHECKED_OUT)[0].timestamp
except IndexError:
return None
last_checkout = property(_last_checkout)
def _last_activity(self):
try:
return self.history.all()[0].timestamp
except IndexError:
return None
last_activity = property(_last_activity)
And, you can then use them as normal:
some_item.last_checkout
Finally, your checkout method is not an override of save so it's not appropriate to call super(ItemObject, self).save(). Just use self.save() instead.

How Can I Populate Default Form Data with a ManyToMany Field?

Ok, I've been crawling google and Django documentation for over 2 hours now (as well as the IRC channel on freenode), and haven't been able to figure this one out.
Basically, I have a model called Room, which is displayed below:
class Room(models.Model):
"""
A `Partyline` room. Rooms on the `Partyline`s are like mini-chatrooms. Each
room has a variable amount of `Caller`s, and usually a moderator of some
sort. Each `Partyline` has many rooms, and it is common for `Caller`s to
join multiple rooms over the duration of their call.
"""
LIVE = 0
PRIVATE = 1
ONE_ON_ONE = 2
UNCENSORED = 3
BULLETIN_BOARD = 4
CHILL = 5
PHONE_BOOTH = 6
TYPE_CHOICES = (
('LR', 'Live Room'),
('PR', 'Private Room'),
('UR', 'Uncensored Room'),
)
type = models.CharField('Room Type', max_length=2, choices=TYPE_CHOICES)
number = models.IntegerField('Room Number')
partyline = models.ForeignKey(Partyline)
owner = models.ForeignKey(User, blank=True, null=True)
bans = models.ManyToManyField(Caller, blank=True, null=True)
def __unicode__(self):
return "%s - %s %d" % (self.partyline.name, self.type, self.number)
I've also got a forms.py which has the following ModelForm to represent my Room model:
from django.forms import ModelForm
from partyline_portal.rooms.models import Room
class RoomForm(ModelForm):
class Meta:
model = Room
I'm creating a view which allows administrators to edit a given Room object. Here's my view (so far):
def edit_room(request, id=None):
"""
Edit various attributes of a specific `Room`. Room owners do not have
access to this page. They cannot edit the attributes of the `Room`(s) that
they control.
"""
room = get_object_or_404(Room, id=id)
if not room.is_owner(request.user):
return HttpResponseForbidden('Forbidden.')
if is_user_type(request.user, ['admin']):
form_type = RoomForm
elif is_user_type(request.user, ['lm']):
form_type = LineManagerEditRoomForm
elif is_user_type(request.user, ['lo']):
form_type = LineOwnerEditRoomForm
if request.method == 'POST':
form = form_type(request.POST, instance=room)
if form.is_valid():
if 'owner' in form.cleaned_data:
room.owner = form.cleaned_data['owner']
room.save()
else:
defaults = {'type': room.type, 'number': room.number, 'partyline': room.partyline.id}
if room.owner:
defaults['owner'] = room.owner.id
if room.bans:
defaults['bans'] = room.bans.all() ### this does not work properly!
form = form_type(defaults, instance=room)
variables = RequestContext(request, {'form': form, 'room': room})
return render_to_response('portal/rooms/edit.html', variables)
Now, this view works fine when I view the page. It shows all of the form attributes, and all of the default values are filled in (when users do a GET)... EXCEPT for the default values for the ManyToMany field 'bans'.
Basically, if an admins clicks on a Room object to edit, the page they go to will show all of the Rooms default values except for the 'bans'. No matter what I do, I can't find a way to get Django to display the currently 'banned users' for the Room object. Here is the line of code that needs to be changed (from the view):
defaults = {'type': room.type, 'number': room.number, 'partyline': room.partyline.id}
if room.owner:
defaults['owner'] = room.owner.id
if room.bans:
defaults['bans'] = room.bans.all() ### this does not work properly!
There must be some other syntax I have to use to specify the default value for the 'bans' field. I've really been pulling my hair out on this one, and would definitely appreciate some help.
Thanks!
UPDATE
lazerscience actually helped me find the solution in one of his comments. Basically, the way it works is if you pass a list of primary keys. To make it work I had to change:
if room.bans:
defaults['bans'] = room.bans.all() ### this does not work properly!
to
if room.bans:
defaults['bans'] = [b.pk for b in room.bans.all()]
And bam, it instantly started working (when I view the page, it will show a selectable list of Callers, with the already banned callers already highlighted (selected).
You probably need to use "initial": Django set default form values

Categories