I am trying to create a FlaskForm that changes depending on the customer passed to it and having issues when the HTML is getting rendered. I haven't done much work with classes unfortunately so I'm not sure what the issue is. I get an error in Python that says:
AttributeError: 'GlobalsForm' object has no attribute '_fields'
I modified the __init__ so that you can set who the customer is plus a created a class function that checks to see who the customer is and customizes the form fields depending on who the customer is. I have a feeling it doesn't like me modifying the class I am inheriting and am probably missing something to make it work. Need to do something with "super"?
Here is what I have so far:
routes.py
#app.route('/global_vars', methods=['GET', 'POST'])
def global_vars():
data = session.get('dev_data', None)
form = GlobalsForm(customer=data['customer'])
form.set_fields()
return render_template('global_vars.html', title='Config Provisioning - Globals', form=form)
forms.py
class GlobalsForm(FlaskForm):
def __init__(self, customer):
self.customer = customer
def set_fields(self):
if self.customer == 'Cust1':
site_choices = [('Site1', 'Site1'), ('Site2', 'Site2'), ('Site3', 'Site3')]
site = SelectField('Site: ', choices = site_choices)
floor_room = StringField("Floor/Room (Ex. FL1RM56 or 1J46): ", validators=[InputRequired()])
rack = StringField("Rack (Data Centers only): ")
l2_domain_choices = [('USR', 'USR - User'), ('DC', 'DC - Data Center'), ('OOBM', 'OOBM - Out of Band Management')]
l2_domain = SelectField('Layer 2 Domain: ', choices=l2_domain_choices)
function_choices = [('ACSW', 'ACSW - Access Switch)'), ('AGSW', 'AGSW - Aggregation Switch'), ('CRSW', 'CRSW - Core Switch')]
function = SelectField('Device function: ', choices=function_choices)
else:
hostname = Stringfield("Hostname: ", validators=[InputRequired()])
I figured it out.. Had to expand on the super() line...
class GlobalsForm(FlaskForm):
def __init__(self, customer=None, *args, **kwargs):
self.customer = customer
super(GlobalsForm, self).__init__(*args, **kwargs)
Related
I am creating a browser-based RPG where fighting mechanics are built into a model called "Battle". It performs actions on Hero, Monster and Item models according to some formulas. Each action adds a message to a "battle log". A player can issue a fight against another player or NPC in a form. When the form is submitted, it calls the same view, the Battle object is created, the characters are drafted and the game mechanics are run.
For some reason, old "Battle" objects are still "selected" between runs of these views, as long as it's in the same web session. So even though I create a new object, the old battle log gets carried over to this new object.
What am I doing wrong here?
Updated with more context
The fightlog field in the first object is correct.
The fightlog field in the second object is the the first objects data PLUS the new data.
The third fightlog is the first plus second plus third, etc.
views.py
def battle_log(request):
if request.session["last_battle"]:
pk = request.session["last_battle"]
b = Battletest.objects.get(pk=pk)
battle_log = b.fightlog
request.session["last_battle"] = ""
context = { 'battle_log' : battle_log, }
return render(request, 'battle.html', context)
else:
return redirect('/game/monster')
def fight_select_monster(request):
form = SelectCharacter()
if request.method=='POST':
form = SelectCharacter(request.POST)
if form.is_valid():
b = Battletest.objects.create()
b.draft(request.POST.get("character"))
b.start_fight()
b.round()
b.eof()
b.save()
request.session["last_battle"] = b.pk
return redirect('/game/fight/')
context = { 'form': form }
request.session["last_battle"] = ""
return render(request, 'fight.html', context)
models.py
class Battletest(models.Model):
messages = []
fightlog = models.TextField()
opponent = ""
def draft(self, opponent):
CHARACTERS= (
(0, 'Confident Hacker'),
(1, 'Confused Coder'),
)
self.opponent = CHARACTERS[int(opponent)][1]
def start_fight(self):
self.messages.append([0, "You joined the battle."])
self.messages.append([0, self.opponent + "has appeared"])
def round(self):
# have character objects do stuff to eachother until
# some edge case is met.
self.messages.append([1, "You smack " + self.opponent + " in the face"])
self.messages.append([1, self.opponent + " decides to leave this stupid fight"])
def eof(self):
self.messages.append([2, "The fight is over and noone wins"])
self.fightlog = self.messages
forms.py
class SelectCharacter(forms.Form):
CONFIDENTHACKER = 0
CONFUSEDCODER = 1
CHARACTERS= (
(CONFIDENTHACKER, 'Confident Hacker'),
(CONFUSEDCODER, 'Confused Coder'),
)
character = forms.ChoiceField(choices=CHARACTERS)
Ok, your issue is here:
class Battletest(models.Model):
messages = []
opponent = ""
This defines messages and opponent as class attributes
- attributes that belong to the class object itself and as such are shared between all instances of the class, making them, practically, global variables (since class objects are singletons).
What you want here is to make messages an instance attribute by defining int in the initializer (that's what it's for):
class Battletest(models.Model):
fightlog = models.TextField()
def __init__(self, *args, **kwargs):
# let Model do it's own stuff !!!
super(Battletest, self).__init__(*args, **kwargs)
# and add our ones:
self.messages = []
self.opponent = None
As a side note: such mistakes are often the sign someone kind of "jumped in" Django without learning Python's basics first and wrongly assumes that because Django models use class attribute to define db fields, Python's class syntax is the same as Java or PHP where you define attributes at the class top-level. But that's not how Python works and I very strongly suggest that at this point you stop everything and do the whole official Python tutorial - it will saves you a lot of time and pain, really.
As a second side note: in the context of server side web app, you want to avoid any kind of (mutable) global state in your code. Every bit of mutable global state should live in some databaseyour models, sessions, whatever as long as it's external to your code AND can be shared amongst many processes - because in a typical production setup, your app WILL be served by many distinct processes (yes, even if you have one single HTTP front server, it will typically manage a pool of django processes, and requests will arbitrarily dispatched to any of those processes).
Now, you have another issue here:
def eof(self):
# ...
self.fightlog = self.messages
You defined fightlog as a text field, but you're assigning a list of lists to it. What get saved will be a textual representation of the list, which is not very usable.
Theoretically, what you have here is a one to many relationship (a Battletest has many Message), so the proper relational design would be to use a distinct Message model with a ForeignKey on Battletest - as shown in the tutorial (you did the tutorial, did you ?).
Now if you really insist on denormalizing this, the best (less worse at least) solution is to serialize messages to json at save() time and unserialize it back to Python in the initializer. This can be done manually:
import json
class Battletest(models.Model):
fightlog = models.TextField()
def __init__(self, *args, **kwargs):
# let Model do it's own stuff !!!
super(Battletest, self).__init__(*args, **kwargs)
# and add our ones:
if self.fightlog:
self.messages = json.loads(self.fightlog)
else:
self.messages = []
self.opponent = None
# ...
def save(self, *args, **kwargs):
self.fightlog = json.dumps(self.messages)
super(Battletest, self).save(*args, **kwargs)
or using a JSONField (that will more or less automagically take care of this) if your RDBMS support it. Googling for "django JSONField" should provide some hints...
Oh and yes... you have duplicated code here:
class Battletest(models.Model):
# ...
def draft(self, opponent):
CHARACTERS= (
(0, 'Confident Hacker'),
(1, 'Confused Coder'),
)
self.opponent = CHARACTERS[int(opponent)][1]
and here:
class SelectCharacter(forms.Form):
CONFIDENTHACKER = 0
CONFUSEDCODER = 1
CHARACTERS= (
(CONFIDENTHACKER, 'Confident Hacker'),
(CONFUSEDCODER, 'Confused Coder'),
)
character = forms.ChoiceField(choices=CHARACTERS)
You want to factor this out so you have one single point of truth:
class Battletest(models.Model):
CONFIDENTHACKER = 0
CONFUSEDCODER = 1
CHARACTERS= [
(CONFIDENTHACKER, 'Confident Hacker'),
(CONFUSEDCODER, 'Confused Coder'),
]
def draft(self, opponent):
self.opponent = self.CHARACTERS[int(opponent)][1]
and in your forms.py:
from . models import Battletest
class SelectCharacter(forms.Form):
character = forms.ChoiceField(choices=Battltest.CHARACTERS)
I am attempting to push data from a DJANGO view into the Tables object, passing it through as an argument. In this case, I would like to pass a variable called doc_id into a Tables2 object called tableName
In this example, I have set doc_id as 1, and pass it into the
View
def editorView(request):
doc_id = 1
table = tableName(UserProfile.objects.filter(), doc_id=doc_id)
Table
class tableName(tables.Table):
tbl_doc_id = None ## Creating a temporary variable
def __init__(self, *args, **kwargs):
temp = kwargs.pop("doc_id") ## Grab doc_ID from kwargs
super(tableName, self).__init__(*args, **kwargs)
self.tbl_doc_id = temp ## Assign to self.tbl_doc_id for use later
### Do something with tbl_doc_id
modelFilter = model.objects.filter(pk = tbl_doc_id)
When running the debugger, I can see that tbl_doc_id is still assigned as None, rather than 1.
What is the correct way to pass arguments into a Tables2 instance? Is it possible?
EDIT: Adding more information for context.
In the real world scenario, I have a view. That view takes an argument from the URL called doc_id. That doc_id is used to grab an object from a model called 'MaterialCollection', and return it as 'mc'.
'mc' is then passed into the table
View
def editorView(request, doc_id):
try:
mc = MaterialCollection.objects.get(pk = doc_id)
except Material.DoesNotExist:
raise Http404("Document does not exist")
config = RequestConfig(request)
unnassigned_User_Table = unassignedUserTable(UserProfile.objects.filter(), mc=mc)
... Other code + Render ...
From my table, I create a custom LinkColumn. That linkColumn is used to construct a URL based upon a number of Attributes from the model 'UserProfile', and from mc.
Table
class unassignedUserTable(tables.Table):
mc = None
def __init__(self, *args, **kwargs):
temp_mc = kwargs.pop("mc")
super(unassignedUserTable, self).__init__(*args, **kwargs)
self.mc = temp_mc
current_Assignment = "NONE"
new_Assignment = "AS"
assign_Reviewer = tables.LinkColumn('change_Review_AssignmentURL' , args=[ A('user'), current_Assignment, new_Assignment, mc, A('id')], empty_values=(), attrs={'class': 'btn btn-success'})
class Meta:
model = UserProfile
... Setup excludes/sequence/attributes...
In this particular instance. mc has a FK to UserProfile (in a 1:M) relationship.
I see that the name of your table class is tableName so if you want __init__ to work as expected please change the line:
super(unassignedUsers, self).__init__(*args, **kwargs)
to
super(tableName, self).__init__(*args, **kwargs)
Beyond this obvious problem, there are some more issues with your code:
Your classes must start with a capital letter (TableName instead of tableName)
Your table classes should end end with -Table (for example NameTable)
I am using django-tables2 for many years and never needed to pass something in __init__ as you are doing here. Are you sure that you really need to do this?
If you want to filter the table's data the filtering must be done to your view - the table will get the filtered data to display.
I can not understand this behavior at all. I asked a question yesterday or the day before thinking it was something with bottle.py, but after trying all kinds of possible solutions, even converting my app over to flask, I have pinpointed the behavior to a single, very simple, class, but I have NO idea why this is happening. It's confusing the hell out of me, but I would really like to understand this if anyone can please shed some light on it.
Ok, so first I have a class called Page which simplifies setting up templates a bit, this is the offending class:
class Page:
"""The page object constructs the webpage and holds associated variables and templates"""
def __init__(self, template=None, name = '', title='',template_vars={}, stylesheets=[], javascript=[]):
# Accepts template with or without .html extension, but must be added for lookup
self.stylesheet_dir = '/css'
self.javascript_dir = '/js'
self.template = template
self.template_vars = {}
self.name = name
self.title = title
self.stylesheets = stylesheets
self.javascript = javascript
self.template_vars['debug'] = _Config.debug
self.template_vars['title'] = self.title
self.template_vars['stylesheets'] = self.stylesheets
self.template_vars['javascript'] = self.javascript
def render(self):
"""Should be called after the page has been constructed to render the template"""
if not self.template.endswith(_Config.template_extension):
self.template += '.' + _Config.template_extension
if not self.title:
if self.name:
self.title = _Config.website + _Config.title_delimiter + self.name
else:
# If title not given use template name
self.title = _Config.website + _Config.title_delimiter + self.template.rstrip('.html')
try:
template = env.get_template(self.template)
except AttributeError:
raise (AttributeError, 'template not set')
rendered_page = template.render(self.template_vars)
return rendered_page
def add_stylesheet(self, name, directory=None):
# Sanitize name
if not name.endswith('.css'):
name += '.css'
if name.startswith('/'):
name = name.lstrip('/')
if not directory:
directory = self.stylesheet_dir
self.template_vars['stylesheets'].append(directory + '/' + name)
def add_javascript(self, name, directory=None):
# Sanitize name
if not name.endswith('.js'):
name += '.js'
if name.startswith('/'):
name = name.lstrip('/')
if not directory:
directory = self.javascript_dir
self.template_vars['javascript'].append(directory + '/' + name)
And here is an example of a route that the problem is exhibited:
#route('/create_account', method=['GET','POST'])
def create_account():
dbsession = db.Session()
page = Page('create account')
page.add_javascript('create_account.js')
if request.method == 'GET':
page.template = 'create_account'
page.template_vars['status'] = None
document = page.render()
dbsession.close()
return document
elif request.method == 'POST':
# Omitted
The problem lies with the method Page.add_javascript(). The next time I go to the /create_account page it is not creating a new Page object and instead reusing the old one. I know this because if I go to the page twice I will have two entires for the create_account.js in my returned html document(the template simply runs a for loop and creates a tag for any js files passed in that list). If I go 3 times it'll be listed 3 times, 40, 40 and so on. Now however if I simply change it to use the initializer and not the add_javascript method the problem goes away.
#route('/create_account', method=['GET','POST'])
def create_account():
dbsession = db.Session()
page = Page('create account', javascript=['create_account.js'])
if request.method == 'GET':
page.template = 'create_account'
page.template_vars['status'] = None
document = page.render()
dbsession.close()
return document
elif request.method == 'POST':
However I suspect something is still wrong and just for my own sanity I need to understand what the hell is going on here. What is possibly happeneing behind the scenes where the add_javascript method would be called twice on the same page object? The method is called immediately after creating a new instance of the page object, where is it possibly getting the old contents of template_vars from?
The problem is that you use mutable defaults for your Page.__init__ function. See http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments.
So you do get a new Page instance on each request, but the lists/dictionaries that hold your javascript etc are re-used.
Replace list/dict default argument values with None, check for None in __init__.
I created a form with a TypedChoiceField:
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
self.fields['year'].choices = [(project_year, project_year)]
else:
self.fields['year'].choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
year = forms.TypedChoiceField(coerce=int)
...
This works perfectly fine when using it inside a view. Now I want to write tests for this form:
form_params = {
'project_year': datetime.datetime.now().year,
}
form = EditProjectForm('new', form_params)
self.assertTrue(form.is_valid())
The test fails, because is_valid() returns False. This is because when calling super.__init__() in the EditProjectForm, the field year doesn't have its choices yet. So the validation for this field fails and an error is added to the error list inside the form.
Moving the super call after self.fields['year'].choices doesn't work either, because self.fields is only available after the super.__init__() call.
How can I add the choices dynamically and still be able to test this?
Okay, I found the problem.
The field year is a class variable, and is instantiated even before the tests setUp method and the forms __init__ method was called. Since I haven't passed the required choices parameter for this field, the error was issued way before the form object was created.
I changed the behaviour so I change the type of the fields in the __init__ method rather than using a class variable for that.
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
choices = [(project_year, project_year)]
else:
choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
self.fields['year'] = forms.TypedChoiceField(coerce=int, choices=choices)
I can't access request from the clean field of a ModelForm. I want to check that the request user is not trying to attend too many events.
I've tried this fix: Django: Accessing request in forms.py clean function
...among other things but am still struggling.
My form is this:
class EventEditForm(forms.ModelForm):
class Meta:
model = Event
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(EventEditForm, self).__init__(*args, **kwargs)
name = forms.CharField(max_length=1024,
initial="Give short, descriptive name",
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="Give short, descriptive name") this.value="";'}))
location = forms.CharField(max_length=1024,
initial="Be specific, give online map link",
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="Be specific, give online map link") this.value="";'}))
dateTimeOptions = {
'format': 'dd/mm/yyyy HH:ii P',
'autoclose': 'true',
'showMeridian': 'true',
}
date = forms.DateTimeField(label="Date and time",widget=DateTimeWidget(options=dateTimeOptions,
attrs={'id':"date-time"}))
host_act = forms.CharField(max_length=1024,
initial="What act will you bring, if any?", required=False,
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="What act will you bring, if any?") this.value="";'}))
description = forms.CharField(required=False, max_length=10240,
initial="Give some details. Any activities prepared?",
widget=forms.Textarea(attrs={'onfocus':'if(this.value=="Give some details. Any activities prepared?") this.value="";'}))
def clean_location(self):
cd = self.cleaned_data
location = cd.get('location')
if location == "Be specific, give online map link" or '':
raise forms.ValidationError("Please enter a location")
return location
def clean_name(self):
cd = self.cleaned_data
name = cd.get('name')
other_names = Event.objects.proposed(datetime.now)
if name == "Give short, descriptive name" or '':
raise forms.ValidationError("Please enter a name")
return name
def clean(self):
"""
Check that there is not another event on at
the same time and place. Then check that user has not committed
to another event on the same date, even somewhere else.
"""
cleaned_data = super(EventEditForm, self).clean()
event_start_estimate = cleaned_data.get("date") - timedelta(hours=3)
event_end_estimate = event_start_estimate + timedelta(hours=7)
location = cleaned_data.get("location")
events_on_date = Event.objects.\
filter(date__range=[event_start_estimate,event_end_estimate])
events_at_location_on_date = events_on_date.filter(location=location)
print events_at_location_on_date
# Check event clash is not this event clashing with itself
try:
events_with_same_date_and_location_id = events_at_location_on_date[0].id
except:
pass
this_event_id = self.instance.id
try:
if events_with_same_date_and_location_id == this_event_id:
events_at_location_on_date = False
except:
pass
if events_at_location_on_date:
raise forms.ValidationError("There is already an event on \
this date. Please join this event or pick another \
date and time. Note: You may have accidentally failed to \
change the date with the picker.")
print self.request
user = self.request
print user
events_on_date_user_has_commit = events_on_date.filter(host__exact=user)
if events_on_date_user_has_commit:
raise forms.ValidationError("You are already committed to an event\
on this date.")
return cleaned_data
My view is:
class EventEditView(UpdateView):
template_name = "edit_event.html"
model=Event
pk_url_kwarg='event_id'
form_class = EventEditForm
You cannot access the request in a form. You need to pass it in from the view in some way. If you're using a FormView generic view, the answer you have linked to is a great way to do it. Looks like you are already expecting the request to be sent in:
self.request = kwargs.pop('request', None)
If you are using a function based view, or a non FormView classed based view, just pass in the request as a keyword argument when you initialize the form. For example:
form = EventEditForm(request.POST, request=request) # or self.request for a class based view