I'm implementing search functionality using Elasticsearch in a "Reddit clone" web application that I'm developing. I want to support searching for threads, users, and subreddits, but when I enter a search query and search for one of the 3 above mentioned categories that does not hold any matches, I'm getting an unexpected "OperationalError" instead of an empty set of results.
As shown in the code I included, I attempted to use the sqlalchemy.orm.query.Query.all() function which returned the following error:
OperationalError: (sqlite3.OperationalError) near "END": syntax error
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password_hash AS user_password_hash, user.last_sign_in AS user_last_sign_in
FROM user
WHERE 1 != 1 ORDER BY CASE user.id END]
(Background on this error at: http://sqlalche.me/e/e3q8)
I researched other StackOverflow posts and found that the first() function internally processes the database result and returns None if no results are found, but when I switched to that function, I faced this error:
OperationalError: (sqlite3.OperationalError) near "END": syntax error
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password_hash AS user_password_hash, user.last_sign_in AS user_last_sign_in
FROM user
WHERE 1 != 1 ORDER BY CASE user.id END
LIMIT ? OFFSET ?]
[parameters: (1, 0)]
(Background on this error at: http://sqlalche.me/e/e3q8)
Checking the documentation for SqlAlchemy, I don't see any mention of this error in either function, and reading the meaning of OperationalError, I'm concerned that my database setup is possibly incorrect.
app/routes.py: This is the route that handles search requests made to the following URL: http://localhost:5000/search?q=&index=
#app.route('/search', methods=['GET'])
def search():
print 'Hit the /search route!'
if not g.search_form.validate():
return redirect(url_for('index'))
page = request.args.get('page', 1, type=int)
target_index = request.args.get('index', 'thread')
if target_index == 'thread':
results, total = Thread.search(g.search_form.q.data, page, app.config['POSTS_PER_PAGE'])
print 'Called Thread.search(), total results = {}'.format(total['value'])
elif target_index == 'user':
results, total = User.search(g.search_form.q.data, page, app.config['POSTS_PER_PAGE'])
print 'Called User.search(), total results = {}'.format(total['value'])
elif target_index == 'subreddit':
results, total = Subreddit.search(g.search_form.q.data, page, app.config['POSTS_PER_PAGE'])
print 'Called Subreddit.search(), total results = {}'.format(total['value'])
else:
return render_template('404.html')
try:
results = results.all()
except OperationalError:
results = [None]
total = total['value']
next_url = url_for('search', index=target_index, q=g.search_form.q.data, page=page + 1) if total > page * app.config['POSTS_PER_PAGE'] else None
prev_url = url_for('search', index=target_index, q=g.search_form.q.data, page=page - 1) if page > 1 else None
results_list = zip(results, [None] * len(results)) # Temporarily to match expected input for template
return render_template('search.html', title=_('Search'), results_list=results_list, next_url=next_url, prev_url=prev_url, query=g.search_form.q.data, index=target_index)
app/models.py:
class SearchableMixin(object):
#classmethod
def search(cls, expression, page, per_page):
ids, total = query_index(cls.__tablename__, expression, page, per_page)
if total == 0:
return cls.query.filter_by(id=0), 0
when = []
for i in range(len(ids)):
when.append((ids[i], i))
return cls.query.filter(cls.id.in_(ids)).order_by(
db.case(when, value=cls.id)), total
#classmethod
def before_commit(cls, session):
session._changes = {
'add': list(session.new),
'update': list(session.dirty),
'delete': list(session.deleted)
}
#classmethod
def after_commit(cls, session):
for obj in session._changes['add']:
if isinstance(obj, SearchableMixin):
add_to_index(obj.__tablename__, obj)
for obj in session._changes['update']:
if isinstance(obj, SearchableMixin):
add_to_index(obj.__tablename__, obj)
for obj in session._changes['delete']:
if isinstance(obj, SearchableMixin):
remove_from_index(obj.__tablename__, obj)
session._changes = None
#classmethod
def reindex(cls):
for obj in cls.query:
add_to_index(cls.__tablename__, obj)
db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit)
db.event.listen(db.session, 'after_commit', SearchableMixin.after_commit)
# Below is one model that implements SearchableMixin to allow searching # for users. Thread and Subreddit models follow the same logic.
class User(db.Model, UserMixin, SearchableMixin):
__searchable__ = ['username']
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
# <Remaining User model fields here...>
app/search.py: (Holds the underlying search functions to query Elasticsearch indices)
def add_to_index(index, model):
if not app.elasticsearch:
return
payload = {}
for field in model.__searchable__:
payload[field] = getattr(model, field)
app.elasticsearch.index(index=index, doc_type=index, id=model.id,
body=payload)
def remove_from_index(index, model):
if not app.elasticsearch:
return
app.elasticsearch.delete(index=index, doc_type=index, id=model.id)
def query_index(index, query, page, per_page):
if not app.elasticsearch:
return [], 0
search = app.elasticsearch.search(
index=index,
body={'query': {'multi_match': {'query': query, 'fields': ['*']}},
'from': (page - 1) * per_page, 'size': per_page})
ids = [int(hit['_id']) for hit in search['hits']['hits']]
return ids, search['hits']['total']
As my included app/routes.py shows, I made a workaround by catching the OperationalError and treating it as an indicator that no results were found, but since the all() documentation makes no mention of it, I did not expect there to be this exception being raised.
I simplified the generated query a bit by hiding all fields you retrieve behind the asterisk.
SELECT user.*
FROM user
WHERE 1 != 1
ORDER BY CASE user.id END
First of all, this query will not return any values as long as 1 != 1 is a where clause, since that is false by definition. Is it possible ids is empty? That might also very much explain the ill-formatted CASE statement, which is the source of the error. Normally, case(dict(a=1, b=2), value=User.name) should result in CASE WHEN name = 'a' THEN 1 WHEN name = 'b' THEN 2 END, which would properly execute.
Related
I have an Angular UI app connecting to a Django API that uses GraphQL (using Graphene) and Postgres for DB.
My application has many courses and each course can have several chapters. The users signing in can see access courses and not others because a course could have a prerequisite. So they will see a course listed but it will be "locked" for them and a message will say that they need to complete the particular prerequisite before it can be accessed. Like this, we need some other attributes to be sent along with the list of courses:-
'locked' - Boolean - whether a course is locked for the current logged-in user or not.
'status' - ENUM - PENDING/SUBMITTED/GRADED/RETURNED/FLAGGED
'completed' - Boolean - whether the course is completed or not
When a user requests the list of courses, these 3 attributes are calculated for each item in the list before it is compiled and sent back to the user.
And this is done for each of the chapters inside the course too. And the chapter might contain upto 30 chapters or so. So this really takes a LOT of time!
I've implemented caching as well, but because these values change often (eg. when the user completes a chapter) they are constantly invalidated and it doesn't make sense to keep these attributes server-side cached to begin with.
Here's the code for how the chapters are processed for the query for list of chapters:-
#login_required
#user_passes_test(lambda user: has_access(user, RESOURCES['CHAPTER'], ACTIONS['LIST']))
def resolve_chapters(root, info, course_id=None, searchField=None, limit=None, offset=None, **kwargs):
current_user = info.context.user
# Checking if this is cached
cache_entity = CHAPTER_CACHE[0]
cache_key = generate_chapters_cache_key(cache_entity, searchField, limit, offset, course_id, current_user)
cached_response = fetch_cache(cache_entity, cache_key)
if cached_response:
return cached_response
# If not cached...
qs = rows_accessible(current_user, RESOURCES['CHAPTER'], {'course_id': course_id})
if searchField is not None:
filter = (
Q(searchField__icontains=searchField.lower())
)
qs = qs.filter(filter)
if offset is not None:
qs = qs[offset:]
if limit is not None:
qs = qs[:limit]
set_cache(cache_entity, cache_key, qs)
return qs
And I'm using this code to dynamically insert the three attributes into each item in the list of chapters that the above code returns:-
class ChapterType(DjangoObjectType):
completed = graphene.Boolean()
completion_status = graphene.String()
locked = graphene.String()
def resolve_completed(self, info):
user = info.context.user
completed = CompletedChapters.objects.filter(participant_id=user.id, chapter_id=self.id).exists()
return completed
def resolve_completion_status(self, info):
user = info.context.user
status = ExerciseSubmission.StatusChoices.PENDING
try:
completed = CompletedChapters.objects.get(participant_id=user.id, chapter_id=self.id)
status = completed.status
except:
pass
return status
def resolve_locked(self, info):
user = info.context.user
locked = is_chapter_locked(user, self)
return locked
class Meta:
model = Chapter
And the method is_chapter_locked() is quite complex in itself:-
def is_chapter_locked(user, chapter):
locked = None
# Letting the user see it if they are a grader
user_role = user.role.name;
grader = user_role == USER_ROLES_NAMES['GRADER']
# Checking if the user is the author of the course or a grader
if chapter.course.instructor.id == user.id or grader:
# If yes, we mark it as unlocked
return locked
course_locked = is_course_locked(user, chapter.course) # Checking if this belongs to a course that is locked
if course_locked:
# If the course is locked, we immediately return locked is true
locked = 'This chapter is locked for you'
return locked
# If the course is unlocked we
completed_chapters = CompletedChapters.objects.all().filter(participant_id=user.id)
required_chapters = MandatoryChapters.objects.all().filter(chapter_id=chapter.id)
required_chapter_ids = required_chapters.values_list('requirement_id',flat=True)
completed_chapter_ids = completed_chapters.values_list('chapter_id',flat=True)
pending_chapter_ids = []
for id in required_chapter_ids:
if id not in completed_chapter_ids:
pending_chapter_ids.append(id)
if pending_chapter_ids:
locked = 'To view this chapter, you must have completed '
pending_chapters_list = ''
for id in pending_chapter_ids:
try:
chapter= Chapter.objects.get(pk=id, active=True)
if pending_chapters_list != '':
pending_chapters_list += ', '
pending_chapters_list += '"' + str(chapter.section.index) +'.'+str(chapter.index)+'. '+chapter.title +'"'
except:
pass
locked += pending_chapters_list
return locked
As can be seen, there is a lot of dynamic processing that is done for fetching the list of chapters. And this is taking a considerably long time, even with caching of the query from the database before the dynamic attributes are calculated.
I am looking for strategies to minimize the dynamic calculation. What kind of an approach works best for performance optimizations in situations like this?
Thank you.
I am working on ticketing system in Flask Admin. The Flask Admin enviroment will be the main one for all the users. For creating or editing tickets I go out from Flask-Admin and use wtforms to implement backend logic. After creation or editing the ticket (validate_on_submit) I want to redirect back to Flask Admin, so I use redirect(url_for(ticket.index_view)). It works fine.
Is there a way to redirect to flask admin, but also with specific filters which were applied before user left Flask admin enviroment? (it is basiccaly GET parameters of url - but in FLASK)
I was trying to use:
referrer = request.referrer
get_url()
But I am probably missing something crucial and don´t know how to implement it (where to put it so I can call the arguments)
Thank you so much.
EDIT : adding more context:
I have a flask admin customized to different roles of users. The main ModelView is the one showing the TICKETS : the specifics of the Class are not vital to my current problem but here its how it looks:
class TicketModelView(ModelView):
column_list = ['id', 'title', 'osoba', 'content', 'povod_vmc_kom', 'dateVMC','zodpovedni', 'deadline', 'odpoved', 'solution', 'is_finished']
column_searchable_list = ['osoba']
column_filters = [ 'povod_vmc_kom', 'dateVMC', 'osoba', 'zodpovedni']
column_labels = dict(povod_vmc_kom='VMČ / Komisia', dateVMC='Dátum VMČ / komisie', zodpovedni = "Zodpovední")
column_display_actions = True
column_filters = [
FilterEqual(column=Ticket.povod_vmc_kom, name='Výbor/komisia', options=(('VMČ Juh','VMČ Juh'), ('UM','UM'), ('Kom dopravy','Kom dopravy'))),
'zodpovedni', 'is_finished',
'dateVMC', 'osoba'
]
def is_accessible(self):
#práva pre vedenie mesta - môže len nazerať
if current_user.is_authenticated and current_user.role == 0:
self.can_export=True
self.can_delete = False
self.can_edit = False
self.can_create = False
self._refresh_form_rules_cache()
self._refresh_forms_cache()
return True
#práva pre super admina (ostatné práva sú defaultne zapnuté)
if current_user.is_authenticated and current_user.role == 1:
self.can_export=True
self.can_delete=True
self.form_edit_rules = ('zodpovedni', 'is_finished' )
self.column_editable_list = ['is_finished']
self._refresh_form_rules_cache()
self._refresh_forms_cache()
return True
#práva pre garantov
if current_user.is_authenticated and current_user.role == 2:
self.can_delete = False
self.can_create = False
self.can_edit = False
self.can_export=True
self.column_searchable_list = ['title']
self._refresh_form_rules_cache()
self._refresh_forms_cache()
return True
#práva pre veducich utvarov
if current_user.is_authenticated and current_user.role == 3:
self.can_create = False
self.can_delete = False
self.can_export=True
self.column_searchable_list = ['title']
self.column_editable_list = ['odpoved', 'date_odpoved', 'solution', 'date_solution' ]
self.form_edit_rules = ('odpoved', 'date_odpoved', 'solution', 'date_solution')
self._refresh_form_rules_cache()
self._refresh_forms_cache()
return True
return False
def _solution_formatter(view, context, model, name):
# Format your string here e.g show first 20 characters
# can return any valid HTML e.g. a link to another view to show the detail or a popup window
if model.solution:
return model.solution[:50]
pass
def _content_formatter(view, context, model, name):
# Format your string here e.g show first 20 characters
# can return any valid HTML e.g. a link to another view to show the detail or a popup window
if len(model.content) > 100:
markupstring = "<a href= '%s'>%s</a>" % (url_for('ticket', ticket_id=model.id), "...")
return model.content[:100] + Markup(markupstring)
return model.content
def _user_formatter(view, context, model, name):
if model.id:
markupstring = "<a href= '%s'>%s</a>" % (url_for('ticket', ticket_id=model.id), model.id)
return Markup(markupstring)
else:
return ""
column_formatters = {
'content': _content_formatter,
'solution': _solution_formatter,
'id': _user_formatter
}
When user viewing the TicketView in Flask Admin, he can apply various filters which is vital to the user experience of the whole web app. The filters work fine and they are stored in URL as GET arguments. When he wants to create or edit a ticket, I am not allowing him to do it in Flask Admin (I edited Flask-Admin layout.html template and added a button to navbar which redirects to my new_ticket url with wtforms.) because of backend logic I want to be applied. For example when he edits field "solution" : I want the value in field "date_of_solution" be generated automatically (date.today()). So I am using wtforms and flask routing : example is bellow:
#app.route("/ticket/<int:ticket_id>/solution", methods = ['GET', 'POST'])
#login_required
def solution(ticket_id):
if current_user.role != 3:
flash("Pre zadanie riešenia alebo odpovede musíte byť prihlásený ako vedúci útvaru", "danger")
return redirect(url_for('ticket', ticket_id=ticket_id))
ticket = Ticket.query.get_or_404(ticket_id)
form = AdminPanelForm()
if form.validate_on_submit():
print("1")
if not ticket.date_solution:
print("2")
ticket.date_solution= datetime.now()
if not ticket.date_odpoved:
print("3")
if form.odpoved.data != ticket.odpoved:
print("4")
ticket.date_odpoved= datetime.now()
ticket.solution = form.solution.data
ticket.odpoved = form.odpoved.data
ticket.is_finished = True
db.session.commit()
flash("Ticket bol updatenutý", "success")
**return redirect(url_for('ticketmod.index_view'))**
elif request.method == 'GET':
form.solution.data = ticket.solution
form.odpoved.data = ticket.odpoved
return render_template("admin_ticket.html", form=form, ticket = ticket)
Now you can see that after succesful updating the ticket, user is redirected to Ticket model View where he came from, return redirect(url_for('ticketmod.index_view')) but without filters applied. I am looking for the solution, how can you store the url GET parameters (the filters) and then use them when redirecting back to ModelView. I tried function get_url() or request.referrer but I wasn´t succesful.
As I said in my original post, maybe I am missing something crucial in web architecture - if you have in mind some learning material I shoul be looking at : thanks for any advice.
Within the formatter method you can get a view's url including the applied filters/sorting criteria using the following:
_view_url = view.get_url('.index_view', **request.args)
Now pass this along to route request, either as a parameter or some other means. For example:
class TicketModelView(ModelView):
# blah blah
def _user_formatter(view, context, model, name):
if model.id:
# This is the current url of the view including filters
_view_url = view.get_url('.index_view', **request.args)
# Pass this as a parameter to your route
markupstring = "<a href= '%s'>%s</a>" % (url_for('ticket', ticket_id=model.id, return_url=_view_url), model.id)
return Markup(markupstring)
At the route you can now pull out the return_url from the request arg and add it as a hidden field in the form. Then in the post back retrieve the value from the form and redirect.
#app.route("/ticket/<int:ticket_id>/solution", methods = ['GET', 'POST'])
#login_required
def solution(ticket_id):
# Get the return_url from the request
_return_url = request.args.get('return_url'):
# Add the return_url to the form as a hidden field
form.return_url.data = _return_url
# blah blah
if form.validate_on_submit():
# get return value from form
_return_url = form.return_url.data
return redirect(_return_url) if _return_url else redirect(url_for('ticketmod.index_view'))
Sorry if the question is primitive. I am new to Flask and SQL Alchemy.
I have a model as below.
class Department(FlaskSerializeMixin,db.Model):
id = db.Column(db.Integer, primary_key=True)
dep_name = db.Column(db.String(100),unique=True,nullable=False)
dep_prefix = db.Column(db.String(1),unique=True,nullable=False)
cre_time =db.Column(db.DateTime)
chg_time =db.Column(db.DateTime)
And I have route to edit the department as below
#menuOps_bp.route('/edit_dep', methods=['POST'])
def edit_dep():
req=request.get_json()
print(req)
if Department.query.filter_by(id != req['row_id'], dep_name=req['deptName'] ).first() is None:
if Department.query.filter_by(id != req['row_id'], dep_prefix=req['dPrefix'] ).first() is None:
dep = Department.query.filter_by(id=req['row_id']).first()
dep.dep_name=req['deptName']
dep.dep_prefix=req['dPrefix']
dep.chg_time = datetime.datetime.now()
db.session.commit()
return jsonify({'success':"Department {dep} is update succussfully".format(dep=req['deptName'])})
else:
return jsonify({'error' : " The PreFix [ {prefix} ] is already Used !!!".format(prefix=req['dPrefix'])})
else:
return jsonify({'error' : " Department [ {dep} ] is already Used !!!".format(dep=req['deptName'])})
The app got sarted properly, but when calling the route with the if Department.query.filter_by(id != req['row_id'], dep_name=req['deptName'] ).first() is None: is failing with below error. Please help to resolve the issue.
File "/home/rishthaz/QMS_AIMS/ECQ/ecqenv/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/home/rishthaz/QMS_AIMS/ECQ/v1/BE/menu/routes.py", line 29, in edit_dep
if Department.query.filter_by(id != req['row_id'], dep_name=req['deptName'] ).first() is None:
**TypeError: filter_by() takes 1 positional argument but 2 were given**
basically I want to fire the query select count(1) from department where id != <somevalue> and dep_name = <somevalue>
In your case, I would recommend using filter as opposed to filter_by, the reason being that you are aiming for comparison and not a keyword argument:
if not Department.query.filter(
Department.id != req['row_id'],
Department.dep_name==req['deptName']
):
### continue...
filter accepts multiple arguments, and treats them as and in your case. Additionally you can also pass and_ or even call it twice:
if not Department.query\
.filter(Department.id != req['row_id'])\
.filter(Department.dep_name==req['deptName']):
### continue ...
Highly recommend this answer on the difference between filter and filter_by:
So I am successfully storing a complex object (non-model) in my session in development. I've tried every session engine and cache type and they are all working in development (Pycharm). However, when I move the code to production, while no error are thrown, the session losses the object.
Here is the method I use to set the session object:
def instantiate_command_object(request):
try:
ssc = request.session['specimen_search_criteria']
logger.debug('found ssc session variable')
except KeyError:
logger.debug('failed to find ssc session variable')
ssc = SpecimenSearchCommand()
return ssc
Then in a method that runs asynchronously via an ajax call I start making changes to the object in the session:
def ajax_add_collection_to_search(request):
ssc = instantiate_command_object(request)
collection_id = request.GET.get('collection')
collection = Collection.objects.get(pk=collection_id)
if collection and collection not in ssc.collections:
ssc.collections.append(collection)
# save change to session
request.session['specimen_search_criteria'] = ssc
# refresh search results
ssc.search()
return render(request, '_search.html')
All this works as far as it goes. However, if I then refresh the browser, the session is lost. Here is a snippet from the template:
{% with criteria=request.session.specimen_search_criteria %}
<div class="search-criteria" id="search-criteria">
<div class="row">
Sesssion:
{{ request.session }}<br/>
Search:
{{ request.session.specimen_search_criteria }}<br/>
Created:
{{ request.session.specimen_search_criteria.key }}<br/>
Collections:
{{ request.session.specimen_search_criteria.collections }}<br/>
Again, in development I can refresh all day and the same object will be returned. In production, it will either create a new object or occasionally will return a previously created copy.
A few relevant items:
The production server is running Apache httpd with mod_wsgi.
I've tried memcached, databasecache, etc. the behavior remains the same. Always works in development, never in production.
I've tried it with
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
and without. I can see the session info in the database and when I unpickle it it just seems to be pointing to a location in memory for the complex object.
I'm guessing this might have something to do with running in a multi-user environment, but again, I'm not using locmem and I've tried all of the caching approaches to no effect.
To be clear, the session itself seems to be fine, I can store a string or other simple item in it and it will stick. It's the complex object within the session that seems to be getting lost.
Edit: I might also point out that if I refresh the browser immediately following the return of the search criteria it will actually return successfully. Anything more than about a second and it will disappear.
Edit (adding code of SpecimenSearchCommand):
class SpecimenSearchCommand:
def __init__(self):
pass
created = datetime.datetime.now()
key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(6))
jurisdictions = []
taxa = []
strata = []
collections = []
chrons = []
has_images = False
query = None # The active SQL query, not the actual result records
page_size = 50
current_page = 1
sort_order = 'number'
results = [] # Page of results from paginator
def is_empty(self):
if len(self.jurisdictions) == 0 and len(self.taxa) == 0 and len(self.strata) == 0 and \
len(self.collections) == 0 and len(self.chrons) == 0 and self.has_images is False:
return True
else:
return False
def get_results(self):
paginator = Paginator(self.query, self.page_size)
try:
self.results = paginator.page(self.current_page)
except PageNotAnInteger:
self.results = paginator.page(1)
except TypeError:
return []
except EmptyPage:
self.results = paginator.page(paginator.num_pages)
return self.results
def get_results_json(self):
points = []
for s in self.results:
if s.locality.latitude and s.locality.longitude:
points.append({"type": "Feature",
"geometry": {"type": "Point",
"coordinates": [s.locality.longitude, s.locality.latitude]},
"properties": {"specimen_id": s.id,
"sci_name": s.taxon.scientific_name(),
"cat_num": s.specimen_number(),
"jurisdiction": s.locality.jurisdiction.full_name()}
})
return json.dumps({"type": "FeatureCollection", "features": points})
def search(self):
if self.is_empty():
self.query = None
return
query = Specimen.objects.filter().distinct().order_by(self.sort_order)
if len(self.taxa) > 0:
query = query.filter(taxon__in=get_hierarchical_search_elements(self.taxa))
if len(self.jurisdictions) > 0:
query = query.filter(locality__jurisdiction__in=get_hierarchical_search_elements(self.jurisdictions))
if len(self.strata) > 0:
query = query.filter(stratum__in=get_hierarchical_search_elements(self.strata))
if len(self.chrons) > 0:
query = query.filter(chron__in=get_hierarchical_search_elements(self.chrons))
if len(self.collections) > 0:
query = query.filter(collection__in=get_hierarchical_search_elements(self.collections))
if self.has_images:
query = query.filter(images__isnull=False)
self.query = query
return
def get_hierarchical_search_elements(elements):
search_elements = []
for element in elements:
search_elements = set().union(search_elements, element.get_descendants(True))
return search_elements
OK, so as Daniel pointed out, the attributes of the SSC class were class-level instead of instance level. The correct version looks like this now:
self.created = datetime.datetime.now()
self.key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(6))
self.jurisdictions = []
self.taxa = []
self.strata = []
self.collections = []
self.chrons = []
self.has_images = False
self.query = None # The active SQL query, not the actual result records
self.page_size = 50
self.current_page = 1
self.sort_order = 'number'
self.results = [] # Page of results from paginator
I can't run a unit test with formset.
I try to do a test:
class NewClientTestCase(TestCase):
def setUp(self):
self.c = Client()
def test_0_create_individual_with_same_adress(self):
post_data = {
'ctype': User.CONTACT_INDIVIDUAL,
'username': 'dupond.f',
'email': 'new#gmail.com',
'password': 'pwd',
'password2': 'pwd',
'civility': User.CIVILITY_MISTER,
'first_name': 'François',
'last_name': 'DUPOND',
'phone': '+33 1 34 12 52 30',
'gsm': '+33 6 34 12 52 30',
'fax': '+33 1 34 12 52 30',
'form-0-address1': '33 avenue Gambetta',
'form-0-address2': 'apt 50',
'form-0-zip_code': '75020',
'form-0-city': 'Paris',
'form-0-country': 'FRA',
'same_for_billing': True,
}
response = self.c.post(reverse('client:full_account'), post_data, follow=True)
self.assertRedirects(response, '%s?created=1' % reverse('client:dashboard'))
and I have this error:
ValidationError: [u'ManagementForm data is missing or has been
tampered with']
My view :
def full_account(request, url_redirect=''):
from forms import NewUserFullForm, AddressForm, BaseArticleFormSet
fields_required = []
fields_notrequired = []
AddressFormSet = formset_factory(AddressForm, extra=2, formset=BaseArticleFormSet)
if request.method == 'POST':
form = NewUserFullForm(request.POST)
objforms = AddressFormSet(request.POST)
if objforms.is_valid() and form.is_valid():
user = form.save()
address = objforms.forms[0].save()
if url_redirect=='':
url_redirect = '%s?created=1' % reverse('client:dashboard')
logon(request, form.instance)
return HttpResponseRedirect(url_redirect)
else:
form = NewUserFullForm()
objforms = AddressFormSet()
return direct_to_template(request, 'clients/full_account.html', {
'form':form,
'formset': objforms,
'tld_fr':False,
})
and my form file :
class BaseArticleFormSet(BaseFormSet):
def clean(self):
msg_err = _('Ce champ est obligatoire.')
non_errors = True
if 'same_for_billing' in self.data and self.data['same_for_billing'] == 'on':
same_for_billing = True
else:
same_for_billing = False
for i in [0, 1]:
form = self.forms[i]
for field in form.fields:
name_field = 'form-%d-%s' % (i, field )
value_field = self.data[name_field].strip()
if i == 0 and self.forms[0].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
elif i == 1 and not same_for_billing and self.forms[1].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
return non_errors
class AddressForm(forms.ModelForm):
class Meta:
model = Address
address1 = forms.CharField()
address2 = forms.CharField(required=False)
zip_code = forms.CharField()
city = forms.CharField()
country = forms.ChoiceField(choices=CountryField.COUNTRIES, initial='FRA')
In particular, I've found that the ManagmentForm validator is looking for the following items to be POSTed:
form_data = {
'form-TOTAL_FORMS': 1,
'form-INITIAL_FORMS': 0
}
Every Django formset comes with a management form that needs to be included in the post. The official docs explain it pretty well. To use it within your unit test, you either need to write it out yourself. (The link I provided shows an example), or call formset.management_form which outputs the data.
It is in fact easy to reproduce whatever is in the formset by inspecting the context of the response.
Consider the code below (with self.client being a regular test client):
url = "some_url"
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# data will receive all the forms field names
# key will be the field name (as "formx-fieldname"), value will be the string representation.
data = {}
# global information, some additional fields may go there
data['csrf_token'] = response.context['csrf_token']
# management form information, needed because of the formset
management_form = response.context['form'].management_form
for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS':
data['%s-%s' % (management_form.prefix, i)] = management_form[i].value()
for i in range(response.context['form'].total_form_count()):
# get form index 'i'
current_form = response.context['form'].forms[i]
# retrieve all the fields
for field_name in current_form.fields:
value = current_form[field_name].value()
data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else ''
# flush out to stdout
print '#' * 30
for i in sorted(data.keys()):
print i, '\t:', data[i]
# post the request without any change
response = self.client.post(url, data)
Important note
If you modify data prior to calling the self.client.post, you are likely mutating the DB. As a consequence, subsequent call to self.client.get might not yield to the same data, in particular for the management form and the order of the forms in the formset (because they can be ordered differently, depending on the underlying queryset). This means that
if you modify data[form-3-somefield] and call self.client.get, this same field might appear in say data[form-8-somefield],
if you modify data prior to a self.client.post, you cannot call self.client.post again with the same data: you have to call a self.client.get and reconstruct data again.
Django formset unit test
You can add following test helper methods to your test class [Python 3 code]
def build_formset_form_data(self, form_number, **data):
form = {}
for key, value in data.items():
form_key = f"form-{form_number}-{key}"
form[form_key] = value
return form
def build_formset_data(self, forms, **common_data):
formset_dict = {
"form-TOTAL_FORMS": f"{len(forms)}",
"form-MAX_NUM_FORMS": "1000",
"form-INITIAL_FORMS": "1"
}
formset_dict.update(common_data)
for i, form_data in enumerate(forms):
form_dict = self.build_formset_form_data(form_number=i, **form_data)
formset_dict.update(form_dict)
return formset_dict
And use them in test
def test_django_formset_post(self):
forms = [{"key1": "value1", "key2": "value2"}, {"key100": "value100"}]
payload = self.build_formset_data(forms=forms, global_param=100)
print(payload)
# self.client.post(url=url, data=payload)
You will get correct payload which makes Django ManagementForm happy
{
"form-INITIAL_FORMS": "1",
"form-TOTAL_FORMS": "2",
"form-MAX_NUM_FORMS": "1000",
"global_param": 100,
"form-0-key1": "value1",
"form-0-key2": "value2",
"form-1-key100": "value100",
}
Profit
There are several very useful answers here, e.g. pymen's and Raffi's, that show how to construct properly formatted payload for a formset post using the test client.
However, all of them still require at least some hand-coding of prefixes, dealing with existing objects, etc., which is not ideal.
As an alternative, we could create the payload for a post() using the response obtained from a get() request:
def create_formset_post_data(response, new_form_data=None):
if new_form_data is None:
new_form_data = []
csrf_token = response.context['csrf_token']
formset = response.context['formset']
prefix_template = formset.empty_form.prefix # default is 'form-__prefix__'
# extract initial formset data
management_form_data = formset.management_form.initial
form_data_list = formset.initial # this is a list of dict objects
# add new form data and update management form data
form_data_list.extend(new_form_data)
management_form_data['TOTAL_FORMS'] = len(form_data_list)
# initialize the post data dict...
post_data = dict(csrf_token=csrf_token)
# add properly prefixed management form fields
for key, value in management_form_data.items():
prefix = prefix_template.replace('__prefix__', '')
post_data[prefix + key] = value
# add properly prefixed data form fields
for index, form_data in enumerate(form_data_list):
for key, value in form_data.items():
prefix = prefix_template.replace('__prefix__', f'{index}-')
post_data[prefix + key] = value
return post_data
The output (post_data) will also include form fields for any existing objects.
Here's how you might use this in a Django TestCase:
def test_post_formset_data(self):
url_path = '/my/post/url/'
user = User.objects.create()
self.client.force_login(user)
# first GET the form content
response = self.client.get(url_path)
self.assertEqual(HTTPStatus.OK, response.status_code)
# specify form data for test
test_data = [
dict(first_name='someone', email='someone#email.com', ...),
...
]
# convert test_data to properly formatted dict
post_data = create_formset_post_data(response, new_form_data=test_data)
# now POST the data
response = self.client.post(url_path, data=post_data, follow=True)
# some assertions here
...
Some notes:
Instead of using the 'TOTAL_FORMS' string literal, we could import TOTAL_FORM_COUNT from django.forms.formsets, but that does not seem to be public (at least in Django 2.2).
Also note that the formset adds a 'DELETE' field to each form if can_delete is True. To test deletion of existing items, you can do something like this in your test:
...
post_data = create_formset_post_data(response)
post_data['form-0-DELETE'] = True
# then POST, etc.
...
From the source, we can see that there is no need include MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT in our test data:
MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the management form, but only for the convenience of client-side code. The POST value of them returned from the client is not checked.
This doesn't seem to be a formset at all. Formsets will always have some sort of prefix on every POSTed value, as well as the ManagementForm that Bartek mentions. It might have helped if you posted the code of the view you're trying to test, and the form/formset it uses.
My case may be an outlier, but some instances were actually missing a field set in the stock "contrib" admin form/template leading to the error
"ManagementForm data is missing or has been tampered with"
when saved.
The issue was with the unicode method (SomeModel: [Bad Unicode data]) which I found investigating the inlines that were missing.
The lesson learned is to not use the MS Character Map, I guess. My issue was with vulgar fractions (¼, ½, ¾), but I'd assume it could occur many different ways. For special characters, copying/pasting from the w3 utf-8 page fixed it.
postscript-utf-8