我有一个涉及到具有五个字段的模型的Django应用程序。对于这些字段之一,我希望用户输入一堆文本,然后我想将其提交给服务(通过函数调用)并保存结果。提供视觉表示:
最好怎么玩呢?我的一个选择是重写save()函数,但是类型不同-我希望表单显示一个models.TextField字段,但将其保存为URLField的结果将被保存。同样,在显示时,我希望用户不编辑URL,而是编辑从该URL检索的文本。
我认为没有简单的标准方法可以解决您的问题。(这就是为什么我不提供任何代码,将其视为长注释而不是解决方案。该更新提供了本答案中讨论的解决方案之一的来源。)根据您的情况,只有优缺点的解决方案。
异步处理:您正在访问外部服务,请求可能需要一些时间才能完成。因此,此操作应以异步方式完成(从外部服务存储和检索数据)。问题是 django 并不是真正为异步任务而发明的,django 只有一些 hacky 异步解决方案。
通过从 django 后端直接以非异步方式访问外部服务,当外部服务失败时,您的站点可能会长时间挂起(最坏的情况是在您的服务器和外部服务器之间的不同点设置较长的超时设置)。对于不太严重的站点,我们可以假设外部服务在大多数情况下都可以工作,如果不能,那么我们的服务器有一点停机时间是可以接受的。
使用 django 后端进行异步处理不仅很麻烦而且很混乱,而且有时几乎不可能有一个真正好的解决方案。
关于真正好的解决方案,我的意思是看起来像是使用为异步服务器(golang、gevent)发明的工具/框架编写的解决方案。与纯 golang 或 gevent 服务器代码的解决方案相比,典型的 django 异步解决方案通常涉及非常复杂的架构和代码。
例如,如果您必须向客户端提供一些必须从外部服务检索的数据,那么如果来自外部服务的响应具有高延迟,那么您的 django 后端无论如何都必须等待响应。如果您在“asnyc-helper”服务器(celery、twisted 或 gevent)中进行等待,那么您可能仍然需要使用轮询、长轮询或 websockets 编写混乱的 django 响应处理代码。与纯异步服务器代码相比,生成的代码超级臃肿且混乱。最好从这个游戏中完全忽略 django,并直接通过客户端和异步助手之间的通信来解决问题。(更新:可以结合 gevent+django 使 django 异步,但我还没有机会大规模尝试以找出该解决方案的可靠性,而且这不是大多数人使用的方式姜戈。)
服务器端验证 URL 后面的文本、向客户端隐藏 URL 等...您可能需要也可能不需要这些,它们会影响您选择的解决方案。
可能的选择:
如果您的客户了解外部服务并直接与其通信不是问题,那么这可能是最简单的解决方案。在 JavaScript 中实现对外部服务的异步访问非常简单。后端仅提供一个可由客户端读取和更新的 url,其余工作可以在 javascript 中完成。
我认为将整个事情的管理放到视图层并不是一个好主意。由于某种原因,人们倾向于将所有内容都塞到视图中(也许是因为在添加新代码时这似乎是“最容易的攻击点”)。在我看来,视图除了解析业务逻辑请求的输入(并使用/不使用表单验证它)然后格式化客户端的响应之外,不应该做更多的事情。理想情况下,视图仅包含使用其他设施的粘合代码(例如表单、另一个模块中您自己的业务逻辑等)。
当您实现客户端请求的验证时,表单层通常是首先要考虑的地方。表单的工作是在客户端和业务逻辑之间转换和验证数据。业务逻辑的一个经常使用的特殊情况只是验证传入的数据并将其保存到数据库。为了简化这种特殊情况,ModelForm我们需要从模型中生成表单骨架,该模型可以使用一些验证数据的最小逻辑进行定制。然而,在其他情况下,客户端数据和模型布局可能不同,并且表单层和模型层之间可能存在复杂的业务逻辑,在这种情况下ModelForm没有用。
将验证放入表单比放入视图更好,因为表单更可重用。好吧,您可以提供自己的可重用验证器实用函数/基类,但仍然为此目的提供了表单作为专门的标准解决方案。使用表单,人们以或多或少标准和受控的方式实现验证,而如果将自定义逻辑放入视图和验证器实用函数/基类中,人们会生成大量混乱的代码供您阅读,这些代码片段看起来总是会有所不同。表单通常可以以声明性方式编写,这样可以降低引入错误的机会,并使阅读代码变得更容易,因为您知道会发生什么。
除了表单的专门性和声明性本质之外,它是比模型层更好的验证层的另一个原因是:表单是更高级别的层,而验证通常也是高级操作。将花哨的验证和棘手的逻辑放入数据库模型save和pre_save
信号中似乎是个好主意,直到您由于某种原因必须直接访问和/或修复原始数据库为止。它还可能导致使用模型的功能之间出现许多不必要的冲突。出于实际原因,最好有一个可以在不触发高级逻辑的情况下访问的低级层。
对于 from 层,您经常将代码放入 from 类中,以提供可在多个视图之间重用的解决方案。然而,在某些特殊情况下,当可重用性很重要时,可以更好地降低级别并以表单字段的形式提供解决方案。这样,您的解决方案将可以在类之间重用,这比视图之间的可重用性更好。您的问题是一个特殊情况,可以通过表单字段来解决。您的表单字段应该为客户端提供一个文本区域,并且应该使用 Pastebin 将传入的客户端文本转换为 url(反之亦然)。这样,表单字段就可以与 URL 模型字段配合使用。您甚至可以创建一个专用的表单URLField字段,当有人从模型生成表单时,它会使用您的表单字段作为默认表单字段ModelForm。
您的解决方案还可以实现为自定义模型字段,该字段提供自己的内容较少的表单字段。这比实现表单字段解决方案要复杂一些,并且您必须接触通常不推荐的 django 较低级别层。您的代码将更加脆弱并且对 django 版本更新更加敏感。编写自定义模型字段可能是一件非常痛苦的事情。我曾经做过一次,但我不喜欢这个旅程。(当我不得不处理使该字段与南方迁移兼容时...:-P)django 文档中有一个“编写自定义模型字段”页面,甚至该文档也指出编写它们并不简单,建议阅读现有领域的源代码以获取灵感。
对于模型字段解决方案,我认为pastebin模型字段将与标准非常相似FileField。这两个字段基本上都将值存储在外部存储中,并且仅将 id 存储到数据库中。我们可以子类化并专门化,而不是编写完全独立的模型字段FileField。我们可以用 patebin 存储替换其默认存储,并且还应该自定义关联的默认表单字段和小部件。
我们讨论了 3 种可能的解决方案:
URLField,客户端使用url直接与外部服务通信。Textarea之间来回转换。URLFieldFileField类似模型字段或自定义FileField自身。对于具有一定 JavaScript 知识的人来说,第一个实现起来非常简单。从第二个和第三个解决方案来看,我认为第三个解决方案更复杂,让我们看看它的可能实现。关键点:
FileField以微创方式进行子类化,以使解决方案不那么脆弱。子类化更改了默认存储和关联的默认表单字段FileField(并添加了可选的缓存优化,没有该优化解决方案仍然可以工作)。FileField。Pastebin/storage.py:
import io
import requests
from django.core.files import File
from django.core.files.storage import Storage
from django.conf import settings
from django.utils.deconstruct import deconstructible
@deconstructible
class PastebinStorage(Storage):
def __init__(self, options=None):
""" The options parameter should be a dict of pastebin parameters for
the 'create new paste' operation: see http://pastebin.com/api#2
The most important required option is api_dev_key. Optionally you can
set api_user_key you want to create non-guest pastes. """
self.__options = getattr(settings, 'PASTEBIN_STORAGE_OPTIONS', {})
if options is not None:
self.__options.update(options)
@property
def options(self):
if 'api_dev_key' not in self.__options:
raise ValueError('The "api_dev_key" option is missing')
return self.__options
def _save(self, name, content):
# TODO: allow overriding the options on a per-file basis. Maybe we
# should encode options into the name since we don't use it and
# we return a completely new name/id at the end of this method.
data = self.options.copy()
data.update(
api_option='paste',
api_paste_code=content.read(),
)
response = requests.post('http://pastebin.com/api/api_post.php', data=data)
response.raise_for_status()
# A successful response contains something like: http://pastebin.com/<PASTE_KEY>
return response.text[response.text.rfind('/')+1:]
def _open(self, name, mode='rb'):
if mode != 'rb':
raise ValueError('Currently the only supported mode is "rb"')
if 'api_user_key' in self.options:
content = self._get_user_paste(name)
else:
content = self._get_public_paste(name)
mem_stream = io.StringIO(content)
mem_stream.name = name
mem_stream.mode = mode
return File(mem_stream)
def _get_user_paste(self, name):
response = requests.post('http://pastebin.com/api/api_raw.php', data=dict(
api_dev_key=self.options['api_dev_key'],
api_user_key=self.options['api_user_key'],
api_option='show_paste',
api_paste_key=name,
))
# FIXME: Unfortunately the API seems to return status_code 200
# also in case of errors with messages like "Bad API request,
# invalid permission to view this paste or invalid api_paste_key"
# in the body.
response.raise_for_status()
return response.text
def _get_public_paste(self, name):
response = requests.get('http://pastebin.com/raw/' + name)
response.raise_for_status()
return response.text
def get_valid_name(self, name):
return name
def get_available_name(self, name, max_length=None):
return name
Run Code Online (Sandbox Code Playgroud)
Pastebin/model_field.py:
from django.db.models import FileField
from django.db.models.fields.files import FieldFile
from .storage import PastebinStorage
from .form_field import PastebinFormField
default_storage = PastebinStorage()
# This custom FieldFile implementation is an optional optimization.
class PastebinContentCachingFieldFile(FieldFile):
def cached_get_pastebin_content(self):
cached = getattr(self, '_cached_pastebin_content', None)
if cached and cached[0] == self.name:
return cached[1]
with self.storage.open(self.name) as f:
content = f.read()
setattr(self, '_cached_pastebin_content', (self.name, content))
return content
class PastebinModelField(FileField):
attr_class = PastebinContentCachingFieldFile
def __init__(self, verbose_name=None, name=None, storage=None, **kwargs):
storage = storage or default_storage
super(PastebinModelField, self).__init__(verbose_name, name, storage=storage, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': PastebinFormField}
defaults.update(kwargs)
return super(PastebinModelField, self).formfield(**defaults)
Run Code Online (Sandbox Code Playgroud)
Pastebin/form_field.py:
import io
from django.core.files import File
from django.forms import Textarea, CharField
class PastebinFormField(CharField):
widget = Textarea
def prepare_value(self, value):
if value is None or isinstance(value, str):
return value
# value is expected to be a PastebinContentCachingFieldFile instance
return value.cached_get_pastebin_content()
def to_python(self, data):
data = super(PastebinFormField, self).to_python(data)
if data is not None:
mem_stream = io.StringIO(data)
mem_stream.name = 'unused'
mem_stream.mode = 'rb'
data = File(mem_stream)
return data
Run Code Online (Sandbox Code Playgroud)
存储使用requests库:pip install requests。
可以选择在中央 django 设置文件中提供一些默认的 Pastebin 存储设置。一个最小的例子可以是这样的:
PASTEBIN_STORAGE_OPTIONS = {
'api_dev_key' : '<your_api_dev_key>',
}
Run Code Online (Sandbox Code Playgroud)
FileField如果您在 django 设置文件中提供pastebin 配置,那么在模型中使用pastebin就非常简单:
class MyModel(models.Model):
file = PastebinModelField()
Run Code Online (Sandbox Code Playgroud)
如果您没有在 django 设置中指定任何内容,或者如果您想覆盖 django 设置,您可以按字段执行此操作:
class MyModel(models.Model):
file = PastebinModelField(storage=PastebinStorage(options=dict(
api_dev_key='<your_dev_key>',
api_user_key='<your_user_key>',
...
)))
Run Code Online (Sandbox Code Playgroud)
ModelForm自动生成一个包含 .pastebin 文件内容的文本区域PastebinModelField。保存时ModelForm会创建一个新的pastebin 文件PastebinModelField。