Django实践2——图书馆系统

简书文章目录生成脚本: 左侧目录
不要错过,在简书,你一定需要的
喜欢本文,请点个👍Thanks!


1. 创建网站框架

1.1 创建项目

用django-admin startproject命令创建新项目,并进入该文件夹

django-admin startproject locallibrary
cd locallibrary

1.2 创建应用(App)——catalog

python manage.py startapp catalog

1.3 项目设置文件配置

打开项目设置文件 locallibrary/locallibrary/settings.py

1.3.1 注册应用

找到 INSTALLED_APPS 列表里的定义。 如下所示,在列表的最后添加新的一行

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'catalog.apps.CatalogConfig', # 注册应用
]

1.3.2 语言和时区设置

LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'

1.4 网站路由URL设置

  • 项目主URL
    打开locallibrary/locallibrary/urls.py

from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
from locallibrary import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('catalog/', include('catalog.urls')), # 将带有 catalog/ 的请求转发到模块 catalog.urls (使用相对路径 URL /catalog/urls.py)
path('', RedirectView.as_view(url='/catalog/')), # 重定向,把网站的根URL(例:127.0.0.1:8000)重定向到该URL:127.0.0.1:8000/catalog/
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 启用静态文件的服务,CSS, JavaScript, 和图片等静态文件

  • App路由
    在catalog 文件夹下创建一个名为 urls.py 的文件,并添加以下文本(空的 urlpatterns)

from django.urls import path
from catalog import views
urlpatterns = [
]

1.5 测试网站框架

1.5.1 数据库迁移

python manage.py makemigrations
python manage.py migrate

重要信息: 每次模型改变,都需要运行以上命令,来影响需要存储的数据结构(包括添加和删除整个模型和单个字段)。

1.5.2 运行网站

python manage.py runserver 0.0.0.0:8000

  • 浏览器打开网址:localhost:8000

2. ## 设计LocalLibaray模型

2.1 模型入门

2.1.1 模型定义

  • 模型通常在应用程序的models.py文件中定义
  • 可以包括字段方法元数据
    下面的代码片段显示了一个名为 的“典型”模型MyModelName:
from django.db import models

class MyModelName(models.Model):
    """A typical class defining a model, derived from the Model class."""

    # Fields
    my_field_name = models.CharField(max_length=20, help_text='Enter field documentation')
    ...

    # Metadata
    class Meta:
        ordering = ['-my_field_name']

    # Methods
    def get_absolute_url(self):
        """Returns the url to access a particular instance of MyModelName."""
        return reverse('model-detail-view', args=[str(self.id)])

    def __str__(self):
        """String for representing the MyModelName object (in Admin site etc.)."""
        return self.my_field_name

2.1.1.1 字段

2.1.1.2 元数据

声明模型级元数据class Meta

class Meta:
ordering = ['-my_field_name']

2.1.1.3 方法

  • 模型也可以有方法
  • 常用方法__str__(): 返回标题或名称字段, 用于站点管理
  • 常用方法是get_absolute_url(): 返回一个 URL,用于在网站上显示1个模型记录

def get_absolute_url(self):
"""Returns the url to access a particular instance of the model."""
return reverse('model-detail-view', args=[str(self.id)])

注意:

  • 使用 URL/myapplication/mymodelname/2来显示模型的单个记录(其中“2”id代表特定记录),需要创建一个 URL 映射器以将响应id传递到“模型详细信息视图”(这将完成显示记录所需的工作)。
  • reverse()上面的函数能够“反转”您的 url 映射器(在上述情况下名为'model-detail-view')以创建正确格式的 URL。
  • 要完成这项工作,需要继续完善编写 URL 映射、视图和模板**

2.1.3 模型管理

  • 一旦定义了模型类,可以使用它们来创建、更新或删除记录,以及运行查询以获取所有记录或记录的特定子集。
  • 在定义视图教程中详细展示如何做到这一点
  • 创建和修改记录
# Create a new record using the model's constructor.
record = MyModelName(my_field_name="Instance #1")

# Save the object into the database.
record.save()

使用点语法访问此新记录中的字段,并更改值, 然后调用save()将修改后的值存储到数据库中

# Access model field values using Python attributes.
print(record.id) # should return 1 for the first record.
print(record.my_field_name) # should print 'Instance #1'

# Change record by modifying the fields, then calling save().
record.my_field_name = "New Instance Name"
record.save()
  • 搜索记录

使用模型的objects属性(由基类提供)搜索符合特定条件的记录

all_books = Book.objects.all()

Django 的filter()方法允许根据特定条件过滤返回QuerySet以匹配指定的文本或数字字段。例如,要过滤标题中包含“wild”的书籍,然后对其进行计数

wild_books = Book.objects.filter(title__contains='wild')
number_wild_books = wild_books.count()

2.2 定义 LocalLibrary 模型

该图还显示了模型之间的关系,包括它们的多重性
多重性是图表上的数字,显示关系中可能存在的每个模型的数量(最大值和最小值)
如,框之间的连接线显示 Book 和 Genre 相关。靠近 Genre 模型的数字表明一本书必须有一个或多个 Genres(任意数量),而 Book 模型旁边该行另一端的数字表明一个 Genre 可以有零个或多个相关联图书

UML 关联图

2.2.1 Genre(体裁)模型

class Genre(models.Model):
    """Model representing a book genre."""
    name = models.CharField(max_length=200, help_text='Enter a book genre (e.g. Science Fiction)')

    def __str__(self):
        """String for representing the Model object."""
        return self.name

2.2.2 Book模型

from django.urls import reverse # Used to generate URLs by reversing the URL patterns

class Book(models.Model):
    """Model representing a book (but not a specific copy of a book)."""
    title = models.CharField(max_length=200)

    # 使用外键,因为书只能有一个作者,但作者可以有多本书
    # 作者作为字符串而不是对象编写,因为它尚未在文件中声明
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    
    # summary文本可能很长,使用长文本类型
    summary = models.TextField(max_length=1000, help_text='Enter a brief description of the book')

    # 将标签显式设置为“ISBN”(否则默认为“Isbn”)
    # 设置参数 unique 为 true以确保所有书籍都有唯一的 ISBN(唯一参数使字段值在表中全局唯一)
    isbn = models.CharField('ISBN', max_length=13, unique=True,
                             help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')

    # 使用ManyToManyField是因为体裁可以包含许多书籍。书籍可以涵盖多种体裁.
    # 体裁类已经定义,所以可以指定上面的对象.
    genre = models.ManyToManyField(Genre, help_text='Select a genre for this book')

    def __str__(self):
        """String for representing the Model object."""
        return self.title

    def get_absolute_url(self):
        """Returns the url to access a detail record for this book."""
        return reverse('book-detail', args=[str(self.id)])

get_absolute_url()返回一个 URL,该 URL 可用于访问此模型的详细记录
因此,必须定义一个具有 name 的 URL 映射book-detail,并定义关联的视图和模板)

2.2.3 BookInstance(图书实例)模型

import uuid # 要求图书实例具有唯一性

class BookInstance(models.Model):
    """表示一本书的特定副本(即可从图书馆借阅的副本)的模型."""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text='Unique ID for this particular book across whole library')
    book = models.ForeignKey('Book', on_delete=models.RESTRICT, null=True)
    imprint = models.CharField(max_length=200)
    due_back = models.DateField(null=True, blank=True)

    LOAN_STATUS = (
        ('m', 'Maintenance'),
        ('o', 'On loan'),
        ('a', 'Available'),
        ('r', 'Reserved'),
    )

    status = models.CharField(
        max_length=1,
        choices=LOAN_STATUS,
        blank=True,
        default='m',
        help_text='Book availability',
    )

    class Meta:
        ordering = ['due_back']

    def __str__(self):
        """String for representing the Model object."""
        return f'{self.id} ({self.book.title})'

UUIDField用于id字段将其设置primary_key为此模型的。这种类型的字段为每个实例分配一个全局唯一的值。
DateField用于due_back日期(预计图书在借出或维护后可用的日期)。该值可以是blank或null(当图书可用时需要)
Class Meta当在查询中返回记录时,模型元数据 ( ) 使用此字段对记录进行排序
status是CharField定义选择/选择列表的 。定义一个包含键值对元组的元组,并将其传递给choices 参数键/值对中的值是用户可以选择的显示值,而键是选择该选项时实际保存的值
设置一个默认值“m”(维护),因为在书上架之前,书最初会被创建为不可用
该方法使用其唯一 id 和相关联的标题的组合来str()表示BookInstance对象Book

从Python 3.6开始,可以使用字符串插值语法(也称为f-字符串)f'{self.id} ({self.book.title})'

2.2.4 Author(作者)模型

class Author(models.Model):
    """作者模型"""
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    class Meta:
        # 以last name,firstname顺序返回名称
        ordering = ['last_name', 'first_name']

    def get_absolute_url(self):
        """反转author-detail的URL 映射以获取用于显示单个作者的 URL"""
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        """String for representing the Model object."""
        return f'{self.last_name}, {self.first_name}'

3. 管理站点

3.1 注册模型

在目录应用程序(/locallibrary/catalog/admin.py)中打开 admin.py
导入模型,调用 admin.site.register 注册模型

from .models import Author, Genre, Book, BookInstance, Language

admin.site.register(Book)
admin.site.register(Author)
admin.site.register(Genre)
admin.site.register(BookInstance)
admin.site.register(Language)

3.2 创建超级用户

  • python manage.py createsuperuser
  • python manage.py makemigrations
  • python manage.py migrate

3.3 登录和使用网站

python manage.py runserver 0.0.0.0:8000
登录网站,打开 /admin (e.g. localhost:8000/admin)
添加一些体裁、作者、语言、图书、图书实例

3.4 高级配置

  • 视图列表

    • 添加每个记录显示的其他字段/信息
    • 添加过滤器以根据日期或某些其他选择值(例如图书借阅状态)选择列出哪些记录
    • 在列表视图中的操作菜单中添加其他选项,并选择此菜单在表单上显示的位置
  • 详细视图

    • 选择要显示(或排除)的字段,以及其顺序,分组,是否可编辑,使用的小部件,方向等
    • 将相关字段添加到记录以允许内联编辑(例如:添加在创建作者记录时添加和编辑图书记录的功能)

3.4.1 注册 一个 ModelAdmin 类

定义 ModelAdmin类(描述布局),并将其注册到模型中,用于管理界面改变模型的展示方式
打开 admin.py 在目录应用程序(/locallibrary/catalog/admin.py)

  • Author 模型

# 注释原始注册(前缀为#)
# admin.site.register(Author)
添加一个 AuthorAdmin 和注册

# Define the admin class
class AuthorAdmin(admin.ModelAdmin):
    pass

# Register the admin class with the associated model
admin.site.register(Author, AuthorAdmin)
  • 为Book 和 BookInstance 类添加 ModelAdmin 类

注释原始注册
#admin.site.register(Book)
#admin.site.register(BookInstance)

  • 使用@register 装饰器来注册模型(与admin.site.register() 语法作用一样)
# Register the Admin classes for Book using the decorator

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    pass

# Register the Admin classes for BookInstance using the decorator

@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
    pass

可以看到现在的 ModelAdmin类都是空的 (“pass”),所以管理操作并不会改变
对这些类进行扩展,以定义针对模型的管理行为

3.4.2 配置列表视图list_display

使用list_display 向视图添加其他字段

  • Author模型
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death')
  • Book模型
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    # display_genre:Book模型中定义的方法
    list_display = ('title', 'author', 'display_genre')
  • BookInstance模型
@admin.register(BookInstance)
class BookAdmin(admin.ModelAdmin):
    # display_genre:Book模型中定义的方法
    list_display = ('id', 'book', 'due_back', 'status')

3.4.3 添加列表过滤器list_filter

@admin.register(BookInstance)
class BookAdmin(admin.ModelAdmin):
    # display_genre:Book模型中定义的方法
    list_display = ('id', 'book', 'due_back', 'status')
    list_filter = ('status', 'due_back')

3.5 整理视图布局

3.5.1 控制哪些字段被显示和详细视图布局fields

  • Author模型
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death')
    fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')]**

在fields 属性列表只是要显示在表格上字段
字段默认情况下垂直显示
将它们分组在元组中(如上述“日期”字段中所示),则会水平显示
还可以使用exclude属性来声明要从表单中排除的属性列表(将显示模型中的所有其他属性)

3.5.2 详细视图字段分组

@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
    list_filter = ('status', 'due_back')

    fieldsets = (
        (None, {
            'fields': ('book','imprint', 'id')
        }),
        ('Availability', {
            'fields': ('status', 'due_back')
        }),
    )

每个部分都有自己的标题(或者None如果你不想要一个标题)和字典中的一个相关的元组 - 描述

3.5.3 关联记录的内联编辑

有时,可以同时添加关联记录是有意义的
例如,将书籍信息和有关详细信息页面上的特定副本的信息同时显示可能是有意义的
通过声明 inlines, 类型 TabularInline (水平布局 ) or StackedInline (垂直布局 ,默认布局)

class BooksInstanceInline(admin.TabularInline):
    model = BookInstance

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'display_genre')
    inlines = [BooksInstanceInline]

4. 构建主页

创建一个url映射器,视图和模板来显示这些页面
URL映射: 根据支持的URL(以及任何编码在URL里的信息)跳转到相应的View功能函数
View 函数从模型中获取请求的数据,创建一个显示数据的HTML页面,并将其返回给用户在浏览器查看
Templates 在View视图中进行数据渲染的时候使用

处理HTTP请求/响应时需要实现的数据和事件的主要流程

4.1 定义资源URL

  • 5个页面

catalog/ — 主页
catalog/books/ — 书单页
catalog/authors/ — 作者页
catalog/book/<id> — 主键字段 ID的具体书 —详细视图。如/catalog/book/3,第三本书。
catalog/author/<id> — 主键字段 ID的具体作者—详细视图。如 /catalog/author/11,第11个作者

4.2 创建索引页

创建的第一个页面是索引页(catalog/)
这会显示一些静态HTML,以及数据库中不同记录的一些计算的“计数“
为了使其工作,必须分别创建一个URL映射,视图和模板

4.2.1 URL映射

在创建的基础网站上,更新 /locallibrary/urls.py 文件
以确保接收到以**catalog/**开头的URL时,URLConf模块中的catalog.urls 会处理剩余的字符串

打开 catalog/urls.py

urlpatterns = [
    path('', views.index, name='index'), # 检测到URL模式'',(views.index——在view.py中函数命名index() )将被调用
]

name参数,此唯一标识指定 URL 映射
可以使用 "reverse" 映射—去动态创建指定映射设计处理的资源的一个URL
例如,可以通过在模板中创建以下链接到主页

<a href="{% url 'index' %}">Home</a>
# 可以硬编码上面的链接(如:<a href="/catalog/">Home</a>)
# 但是如果改变了主页的模式,模版将不再正确链接,使用反向网址映射会更灵活和强大

4.2.2 View (基于函数)

视图是处理 HTTP 请求,从数据库中获取所需数据,使用 HTML 模板在 HTML 页面中呈现数据,然后在 HTTP 响应中返回生成的 HTML 以向用户显示页面的功能。
索引视图遵循这个模型——它获取数据库中拥有的Book、BookInstance、 可用BookInstance 和Author记录数量的 信息,并将该信息传递给模板进行显示。

打开catalog/views.py

from .models import Book, Author, BookInstance, Genre # 导入模型类

def index(request):
    """View function for home page of site."""

    # Generate counts of some of the main objects
    # 使用模型类上的objects.all()属性获取记录数`objects.all`
    num_books = Book.objects.all().count()
    num_instances = BookInstance.objects.all().count()

    # Available books (status = 'a')
    # 获取BookInstance模型中status字段中值为“a”(可用)的对象列表`objects.filter`
    num_instances_available = BookInstance.objects.filter(status__exact='a').count()
    

    # The 'all()' is implied by default.
    num_authors = Author.objects.count()

    # context变量,是一个 Python 字典,包含要插入占位符的数据
    context = {
        'num_books': num_books,
        'num_instances': num_instances,
        'num_instances_available': num_instances_available,
        'num_authors': num_authors,
    }

    # Render the HTML template index.html with the data in the context variable
    # 调用该render()函数来创建一个 HTML 页面并将该页面作为响应返回
    return render(request, 'index.html', context=context)

4.2.3 模板

模板是定义文件(例如 HTML 页面)的结构或布局的文本文件,使用占位符来表示实际内容
将在使用startapp创建的 Django 应用程序目录下的templates子目录中查找模板
在上述索引视图中,该render()函数将在/locallibrary/catalog/templates/ 中找到文件index.html,如果该文件不存在,则会引发错误
Django 会在许多地方寻找模板,默认情况下会在应用程序(App)中进行搜索
关于 Django 如何查找模板以及它支持哪些模板格式的信息, 可查看Django 文档的模板部分

4.2.3.1 扩展模板

索引模板需要头部正文的标准 HTML 标记,以及链接到站点其他页面(尚未创建)的导航部分,以及显示介绍性文本书籍数据的部分
网站的每个页面中的大部分 HTML 和导航结构都是相同的。可以使用 Django 模板语言声明一个基本模板,而不是在每个页面上复制样板代码,然后扩展它以仅替换每个特定页面不同的位

  • base_generic.html

base_generic.html文件的示例基本模板,包含标题、侧边栏和用命名blockendblock 模板标签标记的主要内容的部分
模板标签可以在模板中用于循环列表、基于变量值执行条件操作等的函数
除了模板标签之外,模板语法还允许引用从视图传递到模板的变量,并使用模板过滤器格式化变量(例如,将字符串转换为小写)

<!DOCTYPE html>
<html lang="en">
<head>
  {% block title %}<title>Local Library</title>{% endblock %}
</head>
<body>
  {% block sidebar %}<!-- insert default navigation text for every page -->{% endblock %}
  {% block content %}<!-- default content text (typically empty) -->{% endblock %}
</body>
</html>
  • 扩展模板
  • 在为特定视图定义模板时,首先使用extends模板标签指定基本模板
  • 然后声明模板中想要替换的基本模板中使用block/endblock部分(如果有的话)
{% extends "base_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>
  <p>Welcome to LocalLibrary, a website developed by <em>Mozilla Developer Network</em>!</p>
{% endblock %}

4.3 LocalLibrary 主页构建

4.3.1 基础模板base_generic.html

定义块title,sidebar和content。有一个默认标题和一个默认侧边栏,其中包含指向所有书籍和作者列表的链接,两者都包含在块中,以便将来轻松更改。
还引入了两个额外的模板标签:urlload static
模板包括来自Bootstrap的 CSS改进 HTML 页面的布局和呈现
使用 Bootstrap(或其他客户端 Web 框架)是一种快速创建有吸引力的页面的方法,该页面可以在不同的屏幕尺寸上良好显示
基本模板还引用了一个提供额外样式的本地 css 文件 ( styles.css )
在/locallibrary/catalog/static/css/ 中创建一个 styles.css文件 并将以下代码粘贴到该文件中:

  • styles.css
.sidebar-nav {
    margin-top: 20px;
    padding: 0;
    list-style: none;
}
  • base_generic.html
<!DOCTYPE html>
<html lang="en">
<head>
  {% block title %}<title>Local Library</title>{% endblock %}
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
  <!-- Add additional CSS in static file -->
  {% load static %}
  <link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>
<body>
  <div class="container-fluid">
    <div class="row">
      <div class="col-sm-2">
      {% block sidebar %}
        <ul class="sidebar-nav">
          <li><a href="{% url 'index' %}">Home</a></li>
          <li><a href="">All books</a></li>
          <li><a href="">All authors</a></li>
        </ul>
     {% endblock %}
      </div>
      <div class="col-sm-10 ">{% block content %}{% endblock %}</div>
    </div>
  </div>
</body>
</html>

4.3.2 索引模板

在 /locallibrary/catalog/templates/ 中创建一个新的 HTML 文件 index.html并将以下代码粘贴到文件中。
在第一行扩展基本模板,然后替换了模板的默认块content

  • index.html
{% extends "base_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>
  <p>Welcome to LocalLibrary, a website developed by <em>Mozilla Developer Network</em>!</p>
  <h2>Dynamic content</h2>
  <p>The library has the following record counts:</p>
  <ul>
    <li><strong>Books:</strong> {{ num_books }}</li>
    <li><strong>Copies:</strong> {{ num_instances }}</li>
    <li><strong>Copies available:</strong> {{ num_instances_available }}</li>
    <li><strong>Authors:</strong> {{ num_authors }}</li>
  </ul>
{% endblock %}

在动态内容部分,为要包含的视图中的信息声明占位符(模板变量)。变量用双括号(把手)括起来。
轻松识别模板变量和模板标签(函数)——变量用双大括号 ( {{ num_books }}) 括起来,标签用带百分号 ( {% extends "base_generic.html" %}) 的单大括号括起来

变量是用在视图函数中传递给字典的键命名的(参见下面的示例), 呈现模板时,变量将替换为其关联的值

context = {
    'num_books': num_books,
    'num_instances': num_instances,
    'num_instances_available': num_instances_available,
    'num_authors': num_authors,
}

return render(request, 'index.html', context=context) # context:字典,传递给网页

4.3.3 在模板中引用静态文件

  • 项目可能会使用静态资源,包括 JavaScript、CSS 和图像
  • 由于这些文件的位置可能未知(或可能更改),Django 允许在模板中指定相对于 STATIC_URL 全局设置的位置
  • 默认骨架网站设置 STATIC_URL 的值为“ /static/”,但可以选择将这些内容托管在内容交付网络或其他地方

在模板中,首先调用load 指定“static”的模板标记来添加模板库
然后,可以使用static模板标记并指定所需文件的相对 URL

<!-- Add additional CSS in static file -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">

可以以类似的方式将图像添加到页面中

{% load static %}
<img src="{% static 'catalog/images/local_library_model_uml.png' %}" alt="UML diagram" style="width:555px;height:540px;">

上面的示例指定了文件所在的位置,但 Django 默认不提供
通过修改全局 URL 映射器 (/locallibrary/locallibrary/urls.py)将 Web 服务器配置为提供文件服务,但仍需要在生产中启用文件服务(详见后文)
有关使用静态文件的更多信息,请参阅 Django 文档中管理静态文件

4.3.4 链接到 URL

上面的基础模板引入了url模板标签
此标记接受path()在urls.py 中调用的函数的名称以及关联视图将从该函数接收的任何参数的值,并返回可用于链接到资源的 URL

<li><a href="{% url 'index' %}">Home</a></li>

4.3.5 配置在哪里可以找到模板

Django 搜索模板的位置在settings.py文件中的TEMPLATES对象中指定。默认settings.py如下:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

设置 'APP_DIRS': True是最重要的,因为它告诉 Django 在项目中每个应用程序的子目录中搜索模板,命名为“模板”
可以指定 Django 使用的特定位置来搜索目录'DIRS': [](但这还不是必需的)
有关 Django 如何查找模板以及它支持哪些模板格式的更多信息Django 文档的模板部分

5. 通用列表和详细信息视图

5.1 书本清单页面

5.1.1 URL 映射

  • 打开/catalog/urls.py
  • 调用视图函数views.BookListView.as_view()和一个对应这个特定映射的名称
urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
]

视图将以类别来实现
对于基于Django类的视图,通过调用'类方法as_view()',来访问适当的视图函数。这样做可以创建类的实例,并确保为传入的 HTTP 请求调用正确的处理程序方法。

5.1.2 视图 (基于类)

  • 可以很容易地,将书本列表视图编写为常规函数(类似于索引视图),它将查询数据库中的所有书本,然后调用render(),将列表传递给指定的模板
  • 然而,此处将使用基于类通用列表视图(ListView) - 一个继承自现有视图的类
  • 因为通用视图,已经实现了需要的大部分功能,将能够创建更强大的列表视图,具有代码更少,重复次数更少,最终维护更少的优点

打开 catalog/views.py, 复制下面的代码:

from django.views import generic

class BookListView(generic.ListView):
    model = Book
  • **通用视图查询数据库以获取指定模型(Book)的所有记录,然后呈现位于/locallibrary/catalog/templates/catalog/book_list.html 的模板(将在下面创建)
  • 在模板中,可以使用名为object_listbook_list模板变量(即通常为“the_model_name_list”),以访问书本列表
    注意: 模板位置路径不是印刷错误 - 通用视图在应用程序的/application_name/templates/目录中(/catalog/templates/),查找模板/application_name/the_model_name_list.html(在本例中为catalog/book_list.html
class BookListView(generic.ListView):
    model = Book
    context_object_name = 'my_book_list'   # your own name for the list as a template variable
    queryset = Book.objects.filter(title__icontains='war')[:5] # Get 5 books containing the title war
    template_name = 'books/my_arbitrary_template_name_list.html'  # Specify your own template name/location

5.1.2.1 覆盖基于类的视图中的方法

  • 以覆盖get_queryset()方法,来更改返回的记录列表。这比仅仅设置queryset属性更灵活
class BookListView(generic.ListView):
    model = Book

    def get_queryset(self):
        return Book.objects.filter(title__icontains='war')[:5] # Get 5 books containing the title war

  • 可以覆盖get_context_data() ,以将其他context 变量传递给模板(例如,默认情况下传递书本列表)
  • 下面代码,显示了如何将一个名为some_data的变量添加到上下文中(然后它将作为一个模板变量,而被提供)
class BookListView(generic.ListView):
    model = Book

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get the context
        context = super(BookListView, self).get_context_data(**kwargs)
        # Create any data and add it to the context
        context['some_data'] = 'This is just some data'
        return context

注意: 查看内置的基于类的通用视图(Django文档),了解更多可以执行的操作示例

5.1.3 创建列表视图模板

  • 创建 HTML 文件 /locallibrary/catalog/templates/catalog/book_list.html
  • 视图默认将context (书本列表)作为 object_listbook_list别名传递;任何一个都会奏效
{% extends "base_generic.html" %}

{% block content %}
    <h1>Book List</h1>

    {% if book_list %}
    <ul>

      {% for book in book_list %}
      <li>
        <a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
      </li>
      {% endfor %}

    </ul>
    {% else %}
      <p>There are no books in the library.</p>
    {% endif %} 
{% endblock %}

5.1.3.1 条件执行

使用ifelse、 和endif模板标签来检查 是否book_list已定义且不为空
如果book_list 为空,则该else 子句显示文本,说明没有要列出的书籍
如果book_list 不为空,则遍历书籍列表

{% if book_list %}
  <!-- code here to list the books -->
{% else %}
  <p>There are no books in the library.</p>
{% endif %}

上面的条件只检查一种情况,但您可以使用elif模板标记(例如{% elif var2 %})测试其他条件。有关条件运算符的更多信息,请参阅:如果, ifequal/ifnotequal, 和 如果改变内置模板标签和过滤器

5.1.3.2 For 循环

{% for book in book_list %}
  <li> <!-- code here get information from each book item --> </li>
{% endfor %}

5.1.3.3 访问变量

循环内的代码为每本书创建一个列表项,显示标题(作为指向尚未创建的详细信息视图的链接)和作者

<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})

**使用“点符号”访问相关书籍记录的字段,其中项目后面的文本是字段名称(如模型中定义的)。book.titlebook.authorbook

5.1.3.4 更新基本模板

打开基本模板 ( /locallibrary/catalog/templates/ base_generic.html) 并将{% url 'books' %}插入到All books的 URL 链接中

5.2 图书详情页面

5.2.1 网址映射

打开/catalog/urls.py并添加名为“ book-detail ”的路径

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
    path('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),
]

5.2.1.1 高级路径匹配/正则表达式入门

5.2.1.2 在 URL 映射中传递其他选项

5.2.2 视图 (基于类)

5.2.3 创建详情视图模板

5.3 分页

5.3.1 视图(基于类)

打开 catalog/views.py,然后添加paginate_by

class BookListView(generic.ListView):
    model = Book
    paginate_by = 10

通过添加这行,只要超过10条记录,视图就会开始对它发送到模板的数据,进行分页
使用 GET 参数访问不同的页面 - 如要访问第2页,将使用URL:/catalog/books/?page=2

5.3.2 模板

现在数据已经分页,需要添加对模板的支持,以滚动结果集合
因为需要在所有列表视图中都执行此操作,所以将以基本模板的方式执行此操作
打开 /locallibrary/catalog/templates/base_generic.html,并复制贴士以下内容区块下面的分页区块(以粗体突出显示)
代码首先检查当前页面上,是否启用了分页。如果是,则它会根据需要,添加下一个和上一个链接(以及当前页码)

{% block content %}{% endblock %}

{% block pagination %}
  {% if is_paginated %}
      <div class="pagination">
          <span class="page-links">
              {% if page_obj.has_previous %}
                  <a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
              {% endif %}
              <span class="page-current">
                  Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
              </span>
              {% if page_obj.has_next %}
                  <a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
              {% endif %}
          </span>
      </div>
  {% endif %}
{% endblock %} 

page_obj是一个 Paginator 对象,用于获取有关当前页面,之前页面,之后页面,页面总数等信息。
使用 {{ request.path }},来获取用于创建分页链接的当前页面URL, 其独立于正在分页的对象。

6. 会话框架

6.1 会话是什么?

  • 会话是Django(以及大多数Internet)用于跟踪站点和特定浏览器之间“状态”的机制。会话允许您为每个浏览器存储任意数据,并在浏览器连接时,将该数据提供给站点。然后,通过“密钥”引用与会话相关联的各个数据项,“密钥”用于存储和检索数据。
  • Django使用包含特殊会话ID的cookie,来识别每个浏览器,及其与该站点的关联会话。默认情况下,实际会话数据存储在站点数据库中(这比将数据存储在cookie中更安全,因为它们更容易受到恶意用户的攻击)。您可以将Django配置为,将会话数据存储在其他位置(缓存,文件,“安全”cookie),但默认位置是一个良好且相对安全的选项

6.2 启用会话

创建骨架网站时,会自动启用会话

  • 在项目配置文件(locallibrary / locallibrary / settings.py)的INSTALLED_APPSMIDDLEWARE 部分中设置,如下所示:
INSTALLED_APPS = [
    ...
    'django.contrib.sessions',
    ....

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    ....

6.3 使用会话

可以在如何使用会话(Django文档)中找到完整的API

6.4 保存会话数据

默认情况下,Django仅保存到会话数据库,并在会话被修改(分配)或删除时,将会话cookie发送到客户端。
如果您正在更新会话数据中的某些信息,那么Django将无法识别您已对会话进行了更改并保存了数据.。在这种情况下,您需要将会话明确标记为已修改。
# Set session as modified to force data updates/cookie to be saved.
request.session.modified = True

6.5 简单的例子 - 获取访问次数

作为一个简单的实际例子,将更新图书馆,告诉当前用户访问 LocalLibrary 主页的次数

  • View修改

打开/locallibrary/catalog/views.py,进行如下修改

def index(request):
    ...

    num_authors=Author.objects.count()  # The 'all()' is implied by default.

    # Number of visits to this view, as counted in the session variable.
    num_visits=request.session.get('num_visits', 0)
    request.session['num_visits'] = num_visits+1

    # Render the HTML template index.html with the data in the context variable.
    return render(
        request,
        'index.html',
        context={'num_books':num_books,'num_instances':num_instances,'num_instances_available':num_instances_available,'num_authors':num_authors,
            'num_visits':num_visits}, # num_visits appended
    )

首先得到num_visits会话密钥的值,如果之前没有设置,则将值设置为0。
每次收到请求时,该值都会递增,并将其存回会话中(下次用户访问该页面时)。
然后将num_visits变量,传递给模板中的context变量

参阅如何使用会话

  • index.html修改

将下面的代码添加到主HTML模板(/locallibrary/catalog/templates/index.html)的 “动态内容” 部分底部,以显示context变量:

<h2>Dynamic content</h2>

<p>The library has the following record counts:</p>
<ul>
<li><strong>Books:</strong> {{ num_books }}</li>
<li><strong>Copies:</strong> {{ num_instances }}</li>
<li><strong>Copies available:</strong> {{ num_instances_available }}</li>
<li><strong>Authors:</strong> {{ num_authors }}</li>
</ul>

<p>You have visited this page {{ num_visits }}{% if num_visits == 1 %} time{% else %} times{% endif %}.</p>

7. 用户授权与许可

7.1 启用身份验证

在创建框架网站]时,自动启用了身份验证,无需再执行任何操作

7.2 创建用户和分组

超级用户已经过身份验证,并拥有所有权限,因此需要创建一个测试用户,来代表普通网站用户
使用管理站点,来创建组別和网站登录,这是最快的方法
还可以用编程方式创建用户

7.3 设置身份验证视图

Django 提供了创建身份验证页面所需的几乎所有功能,让处理登录,注销和密码管理等工作,都能直接使用,包括了url 映射器,视图和表单,但不包括模板 - 必须创建自己的模板!

7.3.1 项目网址

将以下内容,添加到项目 urls.py(locallibrary/locallibrary/urls.py)文件的底部:

# Use include() to add URLS from the catalog application and authentication system
from django.urls import include

#Add Django site authentication urls (for login, logout, password management)
urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
]

打开 URL http://127.0.0.1:8000/accounts/ (注意前面的斜杠!),Django将显示一个错误,它无法找到此URL,并列出它尝试过的所有URL
注意: 使用上面的方法,添加以下带有方括号中的名称的 URL,可用于反转 URL 映射。
不必实现任何其他内容 - 上面的 url 映射,会自动映射下面提到的URL。

accounts/ login/ [name='login']
accounts/ logout/ [name='logout']
accounts/ password_change/ [name='password_change']
accounts/ password_change/done/ [name='password_change_done']
accounts/ password_reset/ [name='password_reset']
accounts/ password_reset/done/ [name='password_reset_done']
accounts/ reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/ reset/done/ [name='password_reset_complete']

现在尝试打开登录 URL(http://127.0.0.1:8000/accounts/login/)。这将再次失败,但显示错误信息:在模板搜索路径上缺少必需的模板(registration/login.html)。将在顶部的黄色部分中,看到以下文字:
Exception Type: TemplateDoesNotExist
Exception Value: registration/login.html
下一步是在搜索路径上创建注册目录,然后添加 login.html 文件

7.3.2 模板目录

我们希望在模板搜索路径中的目录 /registration/ 某处,找到刚刚添加的 url(以及隐式视图)的关联模板。
此站点 HTML 页面放在 templates/registration/目录中。templates目录应该位于项目的根目录中,即与 catalog 和 locallibrary 文件夹相同的目录)。
要使这些目录对模板加载器可见(即将此目录放在模板搜索路径中),打开项目设置(/locallibrary/locallibrary/settings.py),并更新TEMPLATES 部分的 DIRS那一行,如下所示:

TEMPLATES = [
    {
        ...
        'DIRS': ['./templates',],
        'APP_DIRS': True,
        ...

7.3.3 登录模板(login.html)

创建一个名为 `/locallibrary/templates/registration/login.html'的新HTML文件。加入以下内容:

{% extends "base_generic.html" %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<form method="post" action="{% url 'login' %}">
{% csrf_token %}

<div>
  <td>{{ form.username.label_tag }}</td>
  <td>{{ form.username }}</td>
</div>
<div>
  <td>{{ form.password.label_tag }}</td>
  <td>{{ form.password }}</td>
</div>

<div>
  <input type="submit" value="login" />
  <input type="hidden" name="next" value="{{ next }}" />
</div>
</form>

{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

如果您尝试登录,将会成功,并且被重定向到另一个页面(默认情况下,这将是 http://127.0.0.1:8000/accounts/profile/)。
这里的问题是,默认情况下,Django希望在登录后,可能会被带到个人资料页面,这可能是,也可能不是。由于还没有定义此页面,将收到另一个错误!

打开项目设置(/locallibrary/locallibrary/settings.py),并将下面的文本添加到底部。现在登录时,将重定向到站点主页。

# Redirect to home URL after login (Default redirects to /accounts/profile/)
LOGIN_REDIRECT_URL = '/'

7.3.4 登出模板(logout.html)

打开登出网址(http://127.0.0.1:8000/accounts/logout/),会看到 - 管理员登出页面
创建并打开 /locallibrary/templates/registration/logged_out.html。将下面的文字,复制到文档中:

{% extends "base_generic.html" %}

{% block content %}
<p>Logged out!</p>

<a href="{% url 'login'%}">Click here to login again.</a>
{% endblock %}

7.3.5 密码重置模板

默认密码重置系统,使用电子邮件向用户发送重置链接。您需要创建表单,以获取用户的电子邮件地址,发送电子邮件,允许他们输入新密码,以及记录整个过程的完成时间。

7.3.5.1 密码重置表单

用于获取用户电子邮件地址的表单(用于发送密码重置电子邮件)
创建/locallibrary/templates/registration/password_reset_form.html,并添加以下内容:

{% extends "base_generic.html" %}
{% block content %}

<form action="" method="post">{% csrf_token %}
    {% if form.email.errors %} {{ form.email.errors }} {% endif %}
        <p>{{ form.email }}</p>
    <input type="submit" class="btn btn-default btn-lg" value="Reset password" />
</form>

{% endblock %}

7.3.5.2 密码重置完成

收集您的电子邮件地址后,会显示此表单
创建 /locallibrary/templates/registration/password_reset_done.html,并为其提供以下内容:

{% extends "base_generic.html" %}
{% block content %}
<p>We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.</p>
{% endblock %}

7.3.5.3 密码重置电子邮件

此模板提供 HTML 电子邮件的文本,其中包含我们将发送给用户的重置链接。创建 /locallibrary/templates/registration/password_reset_email.html,并为其提供以下内容:

Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

7.3.5.4 密码重置确认

点击密码重置电子邮件中的链接后,您可以在此页面输入新密码。创建 /locallibrary/templates/registration/password_reset_confirm.html,并为其提供以下内容:

{% extends "base_generic.html" %}

{% block content %}

    {% if validlink %}
        <p>Please enter (and confirm) your new password.</p>
        <form action="" method="post">
            <div style="display:none">
                <input type="hidden" value="{{ csrf_token }}" name="csrfmiddlewaretoken">
            </div>
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password" /></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Password reset failed</h1>
        <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
    {% endif %}

{% endblock %}

7.3.5.4 密码重置完成

这是最后一个密码重置模板,显示该模板,以在密码重置成功时通知您。创建 /locallibrary/templates/registration/password_reset_complete.html,并为其提供以下内容:

{% extends "base_generic.html" %}
{% block content %}

<h1>The password has been changed!</h1>
<p><a href="{% url 'login' %}">log in again?</a></p>

{% endblock %}

7.4 测试已验证身份的用户

Django只会向已存储在其数据库中的地址(用户)发送重置电子邮件!*
密码重置系统,要求您的网站支持电子邮件,这超出了本文的范围,因此该部分将无法使用。要测试此功能,请将以下一行放在 settings.py 文件的末尾。这会记录发送到命令行控制台的所有电子邮件(因此您可以从命令行控制台,复制密码重置链接)。
参阅发送电子邮件(Django文档)

7.5 示例 - 列出当前用户的书本

7.5.1 在模板中测试

打开基本模板(/locallibrary/catalog/templates/base_generic.html),并将以下文本,复制到侧边栏区块sidebar中,紧接在endblock模板标记之前。

  <ul class="sidebar-nav">

    ...

   {% if user.is_authenticated %}
     <li>User: {{ user.get_username }}</li>
     <li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
   {% else %}
     <li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
   {% endif %} 
  </ul>

使用 if-else-endif模板标签,根据 {{ user.is_authenticated }} 是否为 true ,来有条件地显示文本。如果用户已通过身份验证,那么我们知道,我们拥有有效用户,因此我们会调用 {{ user.get_username }} ,来显示其名称
使用 url模板标记,和相应 URL 配置的名称,创建登录和登出链接 URL。另外请注意,我们如何将 “?next={{request.path}}附加到URL的末尾。这样做,是将包含当前页面地址(URL)的URL参数,添加到链接URL的末尾。用户成功登录/登出后,视图将使用此“下一个”值,将用户重定向,回到他们首次单击登录/登出链接的页面

7.5.2 在视图中测试

使用基于函数的视图,则限制访问函数的最简单方法,是将login_required装饰器,应用于您的视图函数,如下所示。如果用户已登录,则您的视图代码将正常执行。
如果用户未登录,则会重定向到项目设置 (settings.LOGIN_URL)中定义的登录URL,并将当前绝对路径,作为URL参数("下一个"next)来传递。如果用户成功登录,则会返回到此页面,但这次会进行身份验证。

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    ...

7.6 实例

让用户可以借用书本实例BookInstance(我们已经拥有状态status和还书日期due_back,但这个模型和用户之间,没有任何关联。我们将使用ForeignKey(一对多)字段,来创建一个。我们还需要一个简单的机制,来测试借出的书是否过期。
打开 catalog/models.py,然后从 django.contrib.auth.models 导入 User模型(在文件顶部的上一个导入行的正下方添加它,好让后续代码可以使用 User)
from django.contrib.auth.models import User
接下来将借用者字段borrower,添加到BookInstance模型:
borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)

from datetime import date

@property
def is_overdue(self):
    if self.due_back and date.today() > self.due_back:
        return True
    return False
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,154评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,252评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,107评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,985评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,905评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,256评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,978评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,611评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,891评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,910评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,736评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,516评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,995评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,034评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,242评论 2 339

推荐阅读更多精彩内容