I am using the cms Plone to build a form that contains two other schemas.
With the Group Forms, I have been able to include both of the fields of the two schemas. However, they lose all of their properties such as hidden or when I use datagridfield to build the table. What I want to be able to do is have both of these forms with their fields and on a save be able to save them in the object which the link was clicked as parent -> object 1 [top of the form] -> object 2 [bottom of the form]
Here is my python code:
class QuestionPart(group.Group):
label = u'Question Part'
fields = field.Fields(IQuestionPart)
template = ViewPageTemplateFile('questionpart_templates/view.pt')
class Question(group.Group):
label = u'Question'
fields = field.Fields(IQuestion)
template = ViewPageTemplateFile('question_templates/view.pt')
class QuestionSinglePart(group.GroupForm, form.AddForm):
grok.name('register')
grok.require('zope2.View')
grok.context(ISiteRoot)
label = u"Question with Single Part"
ignoreContext = True
enable_form_tabbing = False
groups = (Question,QuestionPart)
def update(self):
super(QuestionSinglePart, self).update()
This code displays both the fields of IQuestion, IQuestionPart without the regard to things like: form.mode(contype='hidden') or DataGridField widget.
I found a way that displays the correct form with field hints.
class QuestionSinglePart(AutoExtensibleForm, form.AddForm):
grok.require('zope2.View')
grok.context(ISiteRoot)
label = u"Question"
schema = IQuestion
additionalSchemata = (IQuestionPart,)
I feel I am still a long way off. I have talked to some people. I am now trying to use a separate form and view.
So far, I am at this point with my code:
class QuestionSinglePartForm(AutoExtensibleForm, form.Form):
ignoreContext = True
autoGroups = True
template = ViewPageTemplateFile('questionsinglepart_templates/questionsinglepartform.pt')
#property
def additionalSchemata(self):
return self._additionalSchemata
def __init__(self, context, request, schema, additional=()):
self.context = context
self.request = request
if not IInterface.providedBy(schema):
raise ValueError('Schema is not interface object')
self._schema = schema
if not all(IInterface.providedBy(s) for s in additional):
raise ValueError('Additional schema is not interface')
self._additionalSchemata = additional
class QuestionSinglePartView(object):
schema = IQuestion
additional = (IQuestionPart,)
def __init__(self, context, request):
self.context = context
self.request = request
self.form = QuestionSinglePartForm(context, request, self.schema, self.additional)
def magic(self, data, errors):
pass
"""
question = Question()
question.number = data['number']
question.questionContent = data['questionContent']
questionPart = QuestionPart()
questionPart.typeOfQuestion = data['IQuestionPart.typeOfQuestion']
questionPart.explanation = data['IQuestionPart.explanation']
questionPart.fileSize = data['IQuestionPart.fileSize']
questionPart.fileType = data['IQuestionPart.fileType']
questionPart.hints = data['IQuestionPart.hints']
questionPart.table = data['IQuestionPart.table']
questionPart.contype = data['IQuestionPart.contype']
questionPart.content = data['IQuestionPart.content']
"""
def update(self, *args, **kwargs):
if self.request.get('REQUEST_METHOD') == 'POST':
data, errors = self.form.extractData()
self.magic(data, errors)
self.formdisplay = self.form.render()
def __call__(self, *args, **kwargs):
self.update(*args, **kwargs)
return self.index(*args, **kwargs)
I am struggling with the rendering of the form and that the QuestionSinglePart object does not have an index attribute.
After a couple of hours of working with some plone devs, we have figured out what was going on.
I had left out:
#property
def schema(self):
return self._schema
I needed to define an index in the view like so:
index = ViewPageTemplateFile('questionsinglepart_templates/questionsinglepart.pt')
I needed to add this to the views init:
alsoProvides(self.form, IWrappedForm)
In the update method for view I needed to call this before the formdisplay. I could also remove the data extraction and move that to the form.
def update(self, *args, **kwargs):
self.form.update(*args, **kwargs)
self.formdisplay = self.form.render()
I am still currently working to get the data to save into objects.
Only form classes that include the plone.autoform.base.AutoExtensibleForm mixin pay attention to schema form hints. Try using this as a mixin to your Group form class (and provide the schema attribute that this mixin looks for instead of fields):
from plone.autoform.base import AutoExtensibleForm
class QuestionPart(AutoExtensibleForm, group.Group):
label = u'Question Part'
schema = IQuestionPart
template = ViewPageTemplateFile('questionpart_templates/view.pt')
Here is my final code with the changes made above. There were issues with the index for the object. I needed to create a simple custom view. I forgot the property for schema on the form. I need to changed my update method for the view as well.
class QuestionSinglePartForm(AutoExtensibleForm, form.Form):
ignoreContext = True
autoGroups = False
#property
def schema(self):
return self._schema
#property
def additionalSchemata(self):
return self._additionalSchemata
def __init__(self, context, request, schema, additional=()):
self.context = context
self.request = request
if not IInterface.providedBy(schema):
raise ValueError('Schema is not interface object')
self._schema = schema
if not all(IInterface.providedBy(s) for s in additional):
raise ValueError('Additional schema is not interface')
self._additionalSchemata = additional
#button.buttonAndHandler(u'Save')
def handleSave(self, action):
data, errors = self.extractData()
if errors:
return False
obj = self.createAndAdd(data)
if obj is not None:
# mark only as finished if we get the new object
self._finishedAdd = True
IStatusMessage(self.request).addStatusMessage(_(u"Changes saved"), "info")
print data
#button.buttonAndHandler(u'Cancel')
def handleCancel(self, action):
print 'cancel'
class QuestionSinglePartView(object):
schema = IQuestion
additional = (IQuestionPart,)
index = ViewPageTemplateFile('questionsinglepart_templates/questionsinglepart.pt')
def __init__(self, context, request):
self.context = context
self.request = request
self.form = QuestionSinglePartForm(context, request, self.schema, self.additional)
alsoProvides(self.form, IWrappedForm)
def magic(self, data, errors):
pass
"""
question = Question()
question.number = data['number']
question.questionContent = data['questionContent']
questionPart = QuestionPart()
questionPart.typeOfQuestion = data['IQuestionPart.typeOfQuestion']
questionPart.explanation = data['IQuestionPart.explanation']
questionPart.fileSize = data['IQuestionPart.fileSize']
questionPart.fileType = data['IQuestionPart.fileType']
questionPart.hints = data['IQuestionPart.hints']
questionPart.table = data['IQuestionPart.table']
questionPart.contype = data['IQuestionPart.contype']
questionPart.content = data['IQuestionPart.content']
"""
def update(self, *args, **kwargs):
self.form.update(*args, **kwargs)
self.formdisplay = self.form.render()
def __call__(self, *args, **kwargs):
self.update(*args, **kwargs)
return self.index(*args, **kwargs)
Related
I have two def validate(self) functions within the RegistrationForm(FlaskForm). First function validates the user input if the car range is over 400km. The other function validates if the user lives far or close to work by calculating the distance between the user's address and work address by means of geopy module. I've been told that def validate_field(self, field): function takes only one attribute which must be the same as the field name. In that function I have an extra variable total_travel_km that stores distance between work and user's home. If user is validated than I need to store total_travel_km variable in DB.
Now the server brings me this message: 'RegistrationForm' object has no attribute 'total_travel_km'
How can I call that total_travel_km variable correctly and write it inside DB
Here is the cut out from my code:
class RegistrationForm(FlaskForm):
car_range = IntegerField('Car Range, km')
home_address = StringField('Home Address')
submit = SubmitField('Register User/Car')
def validate_car_range(self, car_range):
if car_range.data > 400:
raise ValidationError('Your car range does not meet our requirements')
def validate_home_address(self, home_address):
user_loc = locator.geocode(home_address.data)
user_coords = (user_loc.latitude, user_loc.longitude)
one_way = geopy.distance.geodesic(tco_coords, user_coords).km
total_travel_km = one_way*4
if total_travel_km < self.car_range.data:
raise ValidationError('You live too close to work')
return total_travel_km
#app.route("/register", methods=['POST', 'GET'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(car_range=form.car_range.data, home_address=form.home_address.data,
car_travel_km=form.total_travel_km)
db.session.add(user)
db.session.commit()
EDIT: I figured out. I had to pass form. instead of self. in validation method. Below is the correct code:
def validate_home_address(form, home_address):
user_loc = locator.geocode(home_address.data)
user_coords = (user_loc.latitude, user_loc.longitude)
one_way = geopy.distance.geodesic(tco_coords, user_coords).km
form.total_travel_km = one_way*4
if form.total_travel_km < self.car_range.data:
raise ValidationError('You live too close to work')
return form.total_travel_km
Set a form instance variable in the validation method:
class RegistrationForm(FlaskForm):
def __init__(self, *args, **kwargs):
super(RegistrationForm, self).__init__(*args, **kwargs)
self.total_travel_km = None
def validate_home_address(self, home_address):
user_loc = locator.geocode(home_address.data)
user_coords = (user_loc.latitude, user_loc.longitude)
one_way = geopy.distance.geodesic(tco_coords, user_coords).km
total_travel_km = one_way * 4
if total_travel_km < self.car_range.data:
raise ValidationError('You live too close to work')
# all good save the total travel in the form
self.total_travel_km = total_travel_km
Also, you might not want to hard code the car range limit, you can pass a value when you instance the form:
class RegistrationForm(FlaskForm):
def __init__(self, *args, **kwargs):
super(RegistrationForm, self).__init__(*args, **kwargs)
self.total_travel_km = None
self.required_car_range = kwargs.get('required_car_range', 400)
def validate_car_range(self, car_range):
if car_range.data > self.required_car_range:
raise ValidationError('Your car range does not meet our requirements')
and use as follows:
#app.route("/register", methods=['POST', 'GET'])
def register():
form = RegistrationForm(required_car_range=600)
if form.validate_on_submit():
user = User(car_range=form.car_range.data, home_address=form.home_address.data,
car_travel_km=form.total_travel_km)
db.session.add(user)
db.session.commit()
I am having a weird bug that seems related to Djangos caching.
I have a 3-step registration process:
insert personal data
insert company data
summary view and submit all data for registration
If person A walks through the process to the summary part but does not submit the form and
person B does the same, person B gets the data of person A in the summary view.
The data gets stored in a Storage object which carries the data through each step. Every new registration instanciates a new Storage object (at least it should).
While debugging I've found that Django does not call any method in the corresponding views when the cache is already warmed up (by another running registration) and I guess that's why there is no new Storage instance. Hence the cross-polution of data.
Now I'm perfectly aware that I can decorate the method with #never_cache() (which it already was) but that doesn't do the trick.
I've also found that the #never_cache decorator does not work properly prior to Django 1.9(?) as it misses some headers.
One solution that I've found was to set these headers myself with #cache_control(max_age=0, no_cache=True, no_store=True, must_revalidate=True). But that also doesn't work.
So how can I properly disable caching for these methods?
Here is some relevant code:
# views.py
def _request_storage(request, **kwargs):
try:
return getattr(request, '_registration_storage')
except AttributeError:
from .storage import Storage
storage = Storage(request, 'registration')
setattr(request, '_registration_storage', storage)
return storage
...
# Route that gets called by clicking the "register" button
#secure_required
#cache_control(max_age=0, no_cache=True, no_store=True, must_revalidate=True)
# also does not work with #never_cache()
def registration_start(request, **kwargs):
storage = _request_storage(request)
storage.clear()
storage.store_data('is_authenticated', request.user.is_authenticated())
storage.store_data('user_pk', request.user.pk if request.user.is_authenticated() else None)
return HttpResponseRedirect(reverse_i18n('registration_package', kwargs={'pk': 6}))
def registration_package(request, pk=None, **kwargs):
"""stores the package_pk in storage"""
...
def registration_personal(request, pk=None, **kwargs):
from .forms import PersonalForm
storage = _request_storage(request)
if request.method == 'POST':
form = PersonalForm(request.POST)
if form.is_valid():
storage.store_form('personal_form_data', form)
return HttpResponseRedirect(reverse_i18n('registration_company'))
else:
if request.GET.get('revalidate', False):
form = storage.retrieve_form('personal_form_data', PersonalForm)
else:
form = storage.retrieve_initial_form('personal_form_data', PersonalForm)
return render_to_response('registration/personal.html', {
'form': form,
'step': 'personal',
'previous': _previous_steps(request),
}, context_instance=RequestContext(request))
# the other steps are pretty much the same
# storage.py
class Storage(object):
def __init__(self, request, prefix):
self.request = request
self.prefix = prefix
def _debug(self):
from pprint import pprint
self._init_storage()
pprint(self.request.session[self.prefix])
def exists(self):
return self.prefix in self.request.session
def has(self, key):
if self.prefix in self.request.session:
return key in self.request.session[self.prefix]
return False
def _init_storage(self,):
if not self.prefix in self.request.session:
self.request.session[self.prefix] = {}
self.request.session.modified = True
def clear(self):
self.request.session[self.prefix] = {}
self.request.session.modified = True
def store_data(self, key, data):
self._init_storage()
self.request.session[self.prefix][key] = data
self.request.session.modified = True
def update_data(self, key, data):
self._init_storage()
if key in self.request.session[self.prefix]:
self.request.session[self.prefix][key].update(data)
self.request.session.modified = True
else:
self.store_data(key, data)
def retrieve_data(self, key, fallback=None):
self._init_storage()
return self.request.session[self.prefix].get(key, fallback)
def store_form(self, key, form):
self.store_data(key, form.data)
def retrieve_form_data(self, key):
return self.retrieve_data(key)
def retrieve_form(self, key, form_class):
data = self.retrieve_form_data(key)
form = form_class(data=data)
return form
def retrieve_initial_form(self, key, form_class):
data = self.retrieve_form_data(key)
form = form_class(initial=self.convert_form_data_to_initial(data))
return form
def convert_form_data_to_initial(self, data):
result = {}
if data is None:
return result
for key in data:
try:
values = data.getlist(key)
if len(values) > 1:
result[key] = values
else:
result[key] = data.get(key)
except AttributeError:
result[key] = data.get(key)
return result
def retrieve_process_form(self, key, form_class, initial=None):
if request.method == 'POST':
return form_class(data=request.POST)
else:
data = self.get_form_data(key)
initial = self.convert_form_data_to_initial(data) or initial
return form_class(initial=initial)
I've tested this across browser, computers and networks. When I clear the cache manually it works again.
(Easy to see with just debugging print() statements which do not get called with a warm cache.)
It can't be too hard to selectively disable caching, right?
Additional question:
Could it be that the #never_cache() decorator just prevents browser-caching and has nothing to do with the Redis cache?
If have a date picker form that filters a set of models (Sonde) and populates a ModelChoicesField. This works correctly in terms of date choice in my app, but on my canvas I constantly get the error:
Select a valid choice. That choice is not one of the available choices.
I do the init, to filter the available instances of Sonde and populate the choices of the ModelChoiceField.
From my forms.py
class date_choice(forms.Form):
avSonden = forms.ModelChoiceField(queryset = Sonde.objects.none())
def __init__(self, *args, **kwargs):
currentUserID = kwargs.pop('currentUserID', None)
super(date_choice, self).__init__(*args, **kwargs)
if currentUserID:
self.fields['avSonden'].queryset = Sonde.objects.filter(owned_by__Kundennummer = currentUserID).values_list("Serial",flat=True).distinct()
start = forms.DateField(input_formats=['%Y-%m-%d'])
end = forms.DateField(input_formats=['%Y-%m-%d'])
I had to force the clean() to ignore my change from PK to other identifier:
def clean_status(self):
#valid if a value has been selected
if self["avSonden"].value()!="":
del self._errors["avSonden"]
return self["avSonden"].value()
I am trying to generate a Graphene schema from a Django model. I am trying to do this by iterating through the apps then the models and then adding the appropriate attributes to the generated schema.
This is the code:
registry = {}
def register(target_class):
registry[target_class.__name__] = target_class
def c2u(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def s2p(name):
s1 = re.sub("y$", "ie", name)
return "{}s".format(s1)
class AutoSchemaMeta(type):
def __new__(meta, clsname, superclasses, attributedict):
new_class = type(clsname, superclasses, attributedict)
for app_name in new_class.app_models.split(","):
app_models = apps.get_app_config(app_name.strip()).get_models()
for model in app_models:
model_name = model._meta.model_name
_model_name = c2u(model_name)
if hasattr(new_class,_model_name):
continue
_node_class = type("{}Node".format(model_name.title()),
(DjangoObjectType,),
{"Meta":{"model": model, "interfaces": (Node,), "filter_fields": []}})
register(_node_class)
setattr(new_class, "all_{}".format(s2p(_model_name)), DjangoFilterConnectionField(_node_class))
setattr(new_class, _model_name, Node.Field(_node_class))
print(new_class.__dict__)
return new_class
class Query(metaclass=AutoSchemaMeta):
app_models = "app1,app2"
When I run my application I get an exception:
AssertionError: Found different types with the same name in the
schema: WorkflowNode, WorkflowNode.
Turns out there is a class already defined as WorkflowNode and I do not want to override it. So now I am stuck at finding out the classes that are already defined.
I am already excluding by attributes name with if hasattr(new_class,_model_name): continue but I would like to not rely on conventions and find out also all Nodeclasses that have been defined elsewhere and if they exist use them instead of the one I am creating automatically
I tried the proposed solution but it doesn't work for a lot of reasons, including metaclass conflicts with graphene.ObjectType so I created a solution that works pretty well:
you would provide the subclassed ObjectType a list of your ORM models (in my case SQLAlchemy) and it auto creates the schema. The only thing left to do would be to add special handling if you needed to add extra filtering options for any of the fields.
class SQLAlchemyAutoSchemaFactory(graphene.ObjectType):
#staticmethod
def set_fields_and_attrs(klazz, node_model, field_dict):
_name = camel_to_snake(node_model.__name__)
field_dict[f'all_{(s2p(_name))}'] = FilteredConnectionField(node_model)
field_dict[_name] = node_model.Field()
# log.info(f'interface:{node_model.__name__}')
setattr(klazz, _name, node_model.Field())
setattr(klazz, "all_{}".format(s2p(_name)), FilteredConnectionField(node_model))
#classmethod
def __init_subclass_with_meta__(
cls,
interfaces=(),
models=(),
excluded_models=(),
default_resolver=None,
_meta=None,
**options
):
if not _meta:
_meta = ObjectTypeOptions(cls)
fields = OrderedDict()
for interface in interfaces:
if issubclass(interface, SQLAlchemyInterface):
SQLAlchemyAutoSchemaFactory.set_fields_and_attrs(cls, interface, fields)
for model in excluded_models:
if model in models:
models = models[:models.index(model)] + models[models.index(model) + 1:]
possible_types = ()
for model in models:
model_name = model.__name__
_model_name = camel_to_snake(model.__name__)
if hasattr(cls, _model_name):
continue
if hasattr(cls, "all_{}".format(s2p(_model_name))):
continue
for iface in interfaces:
if issubclass(model, iface._meta.model):
model_interface = (iface,)
break
else:
model_interface = (CustomNode,)
_node_class = type(model_name,
(SQLAlchemyObjectType,),
{"Meta": {"model": model, "interfaces": model_interface, "only_fields": []}})
fields["all_{}".format(s2p(_model_name))] = FilteredConnectionField(_node_class)
setattr(cls, "all_{}".format(s2p(_model_name)), FilteredConnectionField(_node_class))
fields[_model_name] = CustomNode.Field(_node_class)
setattr(cls, _model_name, CustomNode.Field(_node_class))
possible_types += (_node_class,)
if _meta.fields:
_meta.fields.update(fields)
else:
_meta.fields = fields
_meta.schema_types = possible_types
super(SQLAlchemyAutoSchemaFactory, cls).__init_subclass_with_meta__(_meta=_meta, default_resolver=default_resolver, **options)
#classmethod
def resolve_with_filters(cls, info: ResolveInfo, model: Type[SQLAlchemyObjectType], **kwargs):
query = model.get_query(info)
for filter_name, filter_value in kwargs.items():
model_filter_column = getattr(model._meta.model, filter_name, None)
if not model_filter_column:
continue
if isinstance(filter_value, SQLAlchemyInputObjectType):
filter_model = filter_value.sqla_model
q = FilteredConnectionField.get_query(filter_model, info, sort=None, **kwargs)
# noinspection PyArgumentList
query = query.filter(model_filter_column == q.filter_by(**filter_value))
else:
query = query.filter(model_filter_column == filter_value)
return query
and you create the Query like this:
class Query(SQLAlchemyAutoSchemaFactory):
class Meta:
interfaces = (Interface1, Interface2,)
models = (*entities_for_iface1, *entities_for_iface2, *other_entities,)
excluded_models = (base_model_for_iface1, base_model_for_iface2)
create an interface like this:
class Interface1(SQLAlchemyInterface):
class Meta:
name = 'Iface1Node'
model = Iface1Model
and SQLAlchemyInterface:
class SQLAlchemyInterface(Node):
#classmethod
def __init_subclass_with_meta__(
cls,
model=None,
registry=None,
only_fields=(),
exclude_fields=(),
connection_field_factory=default_connection_field_factory,
_meta=None,
**options
):
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.name = f'{cls.__name__}Node'
autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
exclude_fields += autoexclude_columns
assert is_mapped_class(model), (
"You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
).format(cls.__name__, model)
if not registry:
registry = get_global_registry()
assert isinstance(registry, Registry), (
"The attribute registry in {} needs to be an instance of "
'Registry, received "{}".'
).format(cls.__name__, registry)
sqla_fields = yank_fields_from_attrs(
construct_fields(
model=model,
registry=registry,
only_fields=only_fields,
exclude_fields=exclude_fields,
connection_field_factory=connection_field_factory
),
_as=Field
)
if not _meta:
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.model = model
_meta.registry = registry
connection = Connection.create_type(
"{}Connection".format(cls.__name__), node=cls)
assert issubclass(connection, Connection), (
"The connection must be a Connection. Received {}"
).format(connection.__name__)
_meta.connection = connection
if _meta.fields:
_meta.fields.update(sqla_fields)
else:
_meta.fields = sqla_fields
super(SQLAlchemyInterface, cls).__init_subclass_with_meta__(_meta=_meta, **options)
#classmethod
def Field(cls, *args, **kwargs): # noqa: N802
return NodeField(cls, *args, **kwargs)
#classmethod
def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(info, id, only_type=only_type)
#classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
try:
node: DeclarativeMeta = one_or_none(session=info.context.get('session'), model=cls._meta.model, id=global_id)
return node
except Exception:
return None
#classmethod
def from_global_id(cls, global_id):
return global_id
#classmethod
def to_global_id(cls, type, id):
return id
#classmethod
def resolve_type(cls, instance, info):
if isinstance(instance, graphene.ObjectType):
return type(instance)
graphene_model = get_global_registry().get_type_for_model(type(instance))
if graphene_model:
return graphene_model
else:
raise ValueError(f'{instance} must be a SQLAlchemy model or graphene.ObjectType')
I have built an application that has a lot of similar views that should be able to use the same base code. However each method has certain unique characteristics at various inflection points within the methods such that I can't figure out a way to structure this to actually reuse any code. Instead I've created a cut-and-paste methodology and tweaked each method individually. This part of the application was some of the first Python code I ever wrote and know there must be a better way to do this, but I got locked into doing it this way and "it works" so I can't see a way out.
Here's what the base view template essentially looks like:
def view_entity(request, entity_id=None):
if request.method == 'POST':
return _post_entity(request, entity_id)
else:
return _get_entity(request, entity_id)
def _get_entity(request, entity_id):
data = _process_entity(request, entity_id)
if 'redirect' in data:
return data['redirect']
else:
return _render_entity(request, data['form'])
def _post_entity(request, entity_id):
data = _process_entity(request, entity_id)
if 'redirect' in data:
return data['redirect']
elif data['form'].is_valid():
# custom post processing here
instance = data['form'].save()
return HttpResponseRedirect(reverse('entity', args=[instance.id]))
else:
return _render_entity(request, data['form'])
def _process_entity(request, entity_id):
data = {}
if entity_id != 'new': # READ/UPDATE
# sometimes there's custom code to retrieve the entity
e = entity_id and get_object_or_404(Entity.objects, pk=entity_id)
# sometimes there's custom code here that deauthorizes e
# sometimes extra values are added to data here (e.g. parent entity)
if e:
if request.method == 'POST':
data['form'] = EntityForm(request.POST, instance=e)
# sometimes there's a conditional here for CustomEntityForm
else:
data['form'] = EntityForm(instance=e)
else: # user not authorized for this entity
return {'redirect': HttpResponseRedirect(reverse('home'))}
# sometimes there's custom code here for certain entity types
else: # CREATE
if request.method == 'POST':
data['form'] = EntityForm(request.POST)
else:
data['form'] = EntityForm()
# sometimes extra key/values are added to data here
return data
I didn't even include all the possible variations, but as you can see, the _process_entity method requires a lot of individual customization based upon the type of entity being processed. This is the primary reason I can't figure out a DRY way to handle this.
Any help is appreciated, thanks!
Use class based views. You can use inheritance and other features from classes to make your views more reusable. You can also use built-in generic views for simplifying some of the basic tasks.
Check class-based views documentation. You can also read this this
So I did end up refactoring the code into a base class that all my views inherit from. I didn't end up refactoring into multiple views (yet), but instead solved the problem of having custom processing methods by inserting hooks within the processing method.
Here's the gist of the base class that inherits from DetailView:
class MyDetailView(DetailView):
context = {}
def get(self, request, *args, **kwargs):
self._process(request, *args, **kwargs)
if 'redirect' in self.context:
return HttpResponseRedirect(self.context['redirect'])
else:
return self._render(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self._process(request, *args, **kwargs)
if 'redirect' in self.context:
return HttpResponseRedirect(self.context['redirect'])
elif self.context['form'].is_valid():
self._get_hook('_pre_save')(request, *args, **kwargs)
return self._save(request, *args, **kwargs)
else:
return self._render(request, *args, **kwargs)
def _process(self, request, *args, **kwargs):
form = getattr(app.forms, '%sForm' % self.model.__name__)
if kwargs['pk'] != 'new': # READ/UPDATE
self.object = self.get_object(request, *args, **kwargs)
self._get_hook('_auth')(request, *args, **kwargs)
if not self.object: # user not authorized for this entity
return {'redirect': reverse(
'%s_list' % self.model.__name__.lower())}
self.context['form'] = form(
data=request.POST if request.method == 'POST' else None,
instance=self.object if hasattr(self, 'object') else None)
self._get_hook('_post_process')(request, *args, **kwargs)
def _get_hook(self, hook_name):
try:
return getattr(self, '%s_hook' % hook_name)
except AttributeError, e:
def noop(*args, **kwargs):
pass
return noop
The key part to note is the _get_hook method and the places within the other methods that I use it. That way, in some complex view I can inject custom code like this:
class ComplexDetailView(MyDetailView):
def _post_process_hook(self, request, *args, **kwargs):
# here I can add stuff to self.context using
# self.model, self.object, request.POST or whatever
This keeps my custom views small since they inherit the bulk of the functionality but I can add whatever tweaks are necessary for that specific view.