【写在前面】

你要开发网站?    嗯。。

会Flask吗?    什么东西,没听过。。。

会JQuery吗?    是python的库吗 ?

那你会什么?    我会F12打开网站

好吧,那我们来写个简单的表格管理平台。

基于Flask框架和JQuery实现管理平台网站的开发功能,我代码编写用了2天的时间 ,从零开始写;又对具体实现流程,自己断断续续地整理总结了近半个月。从自我感觉来说,整个过程和结果的实现都让我很满意。

【效果如下】

【第一步】了解Flask框架

1、了解python 主流的web框架

(1)Django:简单来说就是重武器,是最全能的开发框架,你想要的功能它都有;但是比较繁重,适合企业级的web开发;

(2)Flask:属于web开发微框架,小巧灵活,相关的第三方库丰富,适合开发小型web;

(3)Tornado:天生异步,性能强悍,但是框架提供的功能少,需要开发者自己实现;

因此,本文代码实现主要是基于Flask实现的。

2、了解Flask框架

(1)HelloWorld实现:

几乎所有的编程都是基于“hello world”实现的,因此也大致讲下helloworld涉及的内容。

from flask import Flask
app = Flask(__name__) @app.route('/')
def hello_world():
return "Hello world!" if __name__ == '__main__':
app.run()

先声明一个Flask框架的对象,然后定义路由'/',即URL地址为 http://127.0.0.1:5000/;如果定义路径为‘/new’,那对应的访问地址需要改为http://127.0.0.1:5000/new。另外,@app.route是个装饰器。

(2)如何实现具体IP地址或者端口的访问呢?

app.run(debug=True,host="x.x.x.x",port="1234")

通过对app.run()方法的参数定义,分别实现了调试功能,访问URL变更为 http://x.x.x.x:1234

这里调试功能还是很好用的,不但可以帮助开发者重新加载网页,而且会打印详细的错误信息,协助定位。

(3)如何在web上显示自己特定的html模板

from flask import Flask,render_template
app = Flask(__name__) @app.route('/')
def hello_world():
return render_template('test.html') if __name__ == '__main__':
app.run()

只需要导入render_template库,并且在函数返回时改成对应的方法即可。

不过 这 里要 注意,test.html必须保存在工程目录与下template文件下,否则会报错。(这是因为render_template方法定义时默认写了template路径 )

【第二步】了解Sqlite3数据库

web数据交互离不开后台数据库的管理,本章节重点解释python自带的sqlite3数据库。相比较于其他“正规”的数据库,如mongo、solr、MySQL等,sqlite3相当简单,属于轻量级的数据库。

1、sqlite3数据库的安装和创建

用pip命令可以下载安装sqlite3数据库

创建数据库:

con = sqlite3.connect('material.db')

如果有数据库material.db,则进行连接数据库的操作;如果没有此数据库,则先创建数据库再进行连接;

2、创建数据表

label = ['ID','网络IP','地址','责任人','联系方式']
content = ['1','10.10.10.10','杭州滨江','鹏哥','123456'] def create():
sql = 'create table {0} ({1},{2},{3},{4},{5})'.format(tablename,label[0],label[1],label[2],label[3],label[4])
result = cur.execute(sql)
con.commit()
return True if result else False

简单描述为:create table 表名 (各字段名1,各字段名2……) 

当前对数据表的字段未进行输入类型及长度的限制,比如需要规则ID为必填项,并且为整形,长度在10个字节内,则需要修改为

sql = 'create table matrial_table ("ID" int[10] primary key not null )'

同理,其他字段也可以相同的方式进行类型、长度的限制。

注意:在执行cur.execute()后,要记得con.commit()进行数据库提交,否则数据并不会真正写入数据库中。

3、插入数据

def insert():
sql = 'insert into {0} ({1},{2},{3},{4},{5}) values({6},"{7}","{8}","{9}","{10}")'.format(tablename,label[0],label[1],
label[2],label[3],label[4],content[0],content[1],content[2],content[3],content[4])
result = cur.execute(sql)
con.commit()
return True if result else False

简单描述为:insert into 表名 (各字段名1,各字段名2……) values(数值1,数值2……)

这 里要注意,”{7}“ 是有加双引号的,为什么呢?因为”{7}“对应的是网络IP,是个字符串,因此需要加 双引号,否则会报错。

4、查询数据

def query():
sql = 'select * from {0}'.format(tablename)
result = cur.execute(sql)
return list(result)

简单描述为:select XX,XX from 表名 where 字段名1="数值1"

5、更新数据:update 表名  set 字段名1=”数值1“ where 字段名2="数值2"

6、删除某条数据:delete from 表名 where 字段名1="数值1"

【补充】

如果生成了db数据库,如何查看呢?可以下载一个SQLite Expert,打开后就可以很直观地进行数据库查看,并且可以通过图形化按钮进行 数据表的增删改查。

【第三步】了解html

1、Flask框架方法编写

前面已经讲过 Flask如何调用html模板,因此我们直接展示上图对应的flask框架方法的代码

# coding=utf-8
# @Auther : "鹏哥贼优秀"
# @Date : 2019/9/23
# @Software : PyCharm from flask import Flask,render_template
import sqlite3
app = Flask(__name__)
con = sqlite3.connect('material.db',check_same_thread=False) @app.route('/')
def Material_Mangement():
# 获取数据库material_table表的内容
cur = con.cursor()
sql = 'select * from {0}'.format("material_table")
cur.execute(sql)
content = cur.fetchall()
# 获取数据库表的表头
labels = [tuple[0] for tuple in cur.description]
return render_template('test.html',content=content,labels=labels) if __name__ == '__main__':
app.run(debug=True)

动态路由、sqlite3数据库操作、调试模式的设置,这些知识请参考之前的博客。但有2个知识点,我想再提下:

(1)如果在数据库连接时,不添加check_same_thread=False参数,则

数据库连接会报错:sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. The object was created in thread id  XX。

这是因为sqlite3数据库被多线程访问导致冲突,因此这里要注意下。

(2)获取数据库表头:labels = [tuple[0] for tuple in cur.description]

2、Html文件(仅展示表格内容)

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <title>物料管理平台</title>
</head>
<body>
<div class="row">
<div class="col-md-6 col-sm-12 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3>表格管理平台</h3>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
{% for i in labels %}
<td>{{ i }}</td>
{% endfor %}
</tr>
</thead>
<tbody>
{% for i in content %}
<tr>
{% for j in i %}
<td>{{ j }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div> </div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
</html>

对应的效果是这样的:

因为一开始接触flask时,我只调试过html,但是根本没具体接触过html怎么写。因此上述这段代码是参考于大神的代码(https://blog.csdn.net/a19990412/article/details/84955802)。

熟悉这段代码后,我觉得有几块内容是和我要实现的代码有关的。

(1)title 标题修改

(2)表格的长宽大小:<div class="col-md-6 col-sm-12 col-xs-12"> 。  col-xs-*和col-sm-* 和col-md-*(col-xs表示超小屏幕,col-md-中等屏幕,col-sm-小屏幕)主要是用来适应不同屏幕的表格展示。因此需要自适应调整对应的数值。

(3)设置表格的ID:<table class="table table-striped table-bordered table-hover",id="test">。这里其实不设置id也是可以的,但是后续我要对表格进行编辑时,加上id会方便我定位表格,方法是:tab = document.getElementById("test")

3、Html文件(添加编辑、提交按钮)

根据上述要修改的点,我重新修改了html内容,新的html代码如下:

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <title>表格管理平台</title>
</head>
<body>
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3>表格管理平台</h3>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
{% for i in labels %}
<td>{{ i }}</td>
{% endfor %}
</tr>
</thead>
<tbody>
{% for i in content %}
<tr>
{% for j in i %}
<td>{{ j }}</td>
{% endfor %}
<td><input type="button" value="编辑"></td>
<td><input type="submit" value="提交"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div> </div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<script>
(function(){
$('input[type="button"]').on('click', function(){
var $this = $(this),
edit_status = $this.attr('edit_status'),
status_value = edit_status && 1 == edit_status ? 0 : 1,
$td_arr = $this.parent().prevAll('td');
$this.val(1 == status_value ? '完成' : '编辑').attr('edit_status', status_value);
$.each($td_arr, function(){
var $td = $(this);
if(1 == status_value) {
$td.html('<input type="text" value="'+$td.html()+'">');
} else if(0 == status_value){
$td.html($td.find('input[type=text]').val());
}
});
});
})();
</script>
</body>
</html>

相比于第 2步时的html文件,这次我主要添加了2块内容:

(1)添加编辑、提交按钮:

<td><input type="button" value="编辑"></td>   <td><input type="submit" value="提交"></td>

将这两行代码放在表格每行最后,就会生成相应的按钮

(2)编辑功能的实现:

编辑按钮对应的function是基于JQuery写的,当然这 段代码我也是参考另一位大神的

(https://blog.csdn.net/luo201227/article/details/50559988),因为当前我对JQuery完全一无所知。

但是在熟悉代码后,我根据自己对代码的理解进行了注释。

<script>
(function(){
<!--定义属于是 button的按钮在点击后,产生下面的function功能-->
$('input[type="button"]').on('click', function(){
<!--获取当前事件,并进行当前按钮的状态,如果是编辑状态,就进行将每个单元格设置成可输入状态-->
var $this = $(this),
edit_status = $this.attr('edit_status'),
status_value = edit_status && 1 == edit_status ? 0 : 1,
$td_arr = $this.parent().prevAll('td');
$this.val(1 == status_value ? '完成' : '编辑').attr('edit_status', status_value);
<!--如果单元格是可编辑状态,获取每列元素的值,并在最后html表格上进行展示-->
$.each($td_arr, function(){
var $td = $(this);
if(1 == status_value) {
$td.html('<input type="text" value="'+$td.html()+'">');
<!--如果按钮状态是完成状态,直接展示单元内的值-->
} else if(0 == status_value){
$td.html($td.find('input[type=text]').val());
}
});
});
})();
</script>

细心的同学会发现,我在点击”提交“按钮时,什么都没 发生。是的,”提交“功能,我将在下一个章节中进行介绍。

【第四步】了解JQuery

1、提交功能的实现,对我来说,最难的是html对前台数据的传输。因此,我就参考着编辑功能,自己一 点点写。以下是示例代码:

(1)HTML文件编写前台界面提交功能

<script>
(function(){
<!--定义属性是submit的按钮在点击后,产生下面的function功能-->
$('input[type="submit"]').on('click', function(){
<!--获取当前行的id -->
var td = event.srcElement.parentElement;
var rownum = td.parentElement.rowIndex;
<!--获取html表格元素 -->
var tab = document.getElementById("test");
<!--将每个单元格元素进行取值,并以字典形式传给后台 -->
var data = {
"ID":tab.rows[rownum].cells[0].innerHTML,
"网络IP":tab.rows[rownum].cells[1].innerHTML,
"地址":tab.rows[rownum].cells[2].innerHTML,
"责任人":tab.rows[rownum].cells[3].innerHTML,
"联系方式":tab.rows[rownum].cells[4].innerHTML,
};
alert("提交成功!")
$.ajax({
type: "get",
url: "/edit",
data: data,
dataType: "json"
});
});
})();

(2)后台对提交后的数据进行读取,并写数据库

@app.route('/edit', methods=['get'])
def edit():
label = ['ID', '网络IP', '地址', '责任人', '联系方式']
content = [request.args.get(i) for i in label]
print(content)
sql = 'update {0} set {1}="{6}",{2}="{7}",{3}="{8}",{4}="{9}" where {5}={10}'.format('material_table',
label[1], label[2], label[3],label[4],label[0],content[1],content[2],content[3],content[4],content[0])
cur = con.cursor()
cur.execute(sql)
con.commit()
return "数据写入成功!"

在实现“提交”功能时,我主要遇到了以下几个 坑:

(1)html代码写完后,发现程序报错,提示$.ajax is not a function。我去,我看其他大神的数据交互也是这么写的呀,为什么我这不行?

查看网上的帖子,都是说未定义或者和其他库有冲突,最后我自己发现,是因为我没有声明是JQuery。需要在script前面加上一行代码:

<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>

我理解这行代码的意思是声明下面要JQuery库,而不是其他JS库。(可能理解有误)

最后加上这行代码后,问题成功解决!

(2)获取当前行号

一开始在获取行号,怎么获取不到,而且不知道要怎么调试,在同事的指导下,才学会通过alert(td.innerHTML)来查看获取的当前行内容是什么。

最初从网上 查到获取当前行的写法是:

var td = event.parentElement;
var rownum = td.parentElement.rowIndex;

通过alert调试,发现点击提交根本没有反应;又改成

var td = event.srcElement;
var rownum = td.parentElement.rowIndex;

结果是获取不到任何内容

就这么个小问题就花费了我近2个小时的时间去不停地查资料,尝试编写。最后想说的是,如果对html不熟悉,还是应该找个大神带带自己的。

最后在别人的协助下,实现了获取当前行的功能:

(3)获取每个表格里的 内容

tab.rows[0].cells[0].innerHTML

在获取单元格内数据时,最验证的是我获取不到tab,即表格元素。因为我原先没有在表格元素里加id,即

网上找了很多方法都没法实现,最后老老实实地加上 id="test"

讲道理,html里的这 20行代码是我搞这个表格管理平台时,花费最大精力的。至此,表格管理平台的功能基本成行 !

【第五步】添加新增功能

以上功能只实现了对现有数据的编辑保存功能,但是如果用户想要新增数据,怎么办?我当时第一个想法是让用户自己去改数据库,哈哈。还要我去写新增功能,要累死了,不想写。

下面是关于新增功能的介绍和示例代码。

1、如何添加“新增”按钮

<td><input type="button" value="新增" id="create"></td><br>

如果前面 的html能看懂了,这行是不难理解的。

2、点击新增按钮后,如何动态增加表格行、列

<script>
(function () {
$('input[id="create"]').on('click', function(){ var editTable=document.getElementById("tbody");
var tr=document.createElement("tr");
var td1=document.createElement("td");
td1.innerHTML=""; tr.appendChild(td1);
editTable.appendChild(tr);

首先定义tr元素,然后在tr元素中再追加td元素。如果表格里有多列数据,那只需要重复td1的写法,进行复制粘贴就可以了。

另外,当前td.innerTHML是设置为空,如果要将该单元格直接设置为编辑状态,则修改成  :

td1.innerHTML="input[type=text] /";

3、如何动态添加“编辑”、“提交”按钮,下面以“编辑”为例

var td9 = document.createElement("td")
var myedit =document.createElement("input");
myedit.type = "button";
myedit.value = "编辑"
myedit.id = "edit"
td9.appendChild(myedit)

添加方式和添加文本框方式是一样的,只是需要注意元素类型是Input,并且要补充下元素的type/value/id。

4、如何对动态添加的按钮进行事件绑定,下面以提交功能为例

$('input[id="submit"]').on('click', function(){
alert("test")
}

关于动态添加的按钮进行事件绑定有很多帖子,有用Live方法的,有用$(document).on('click','.edit',function()方法的,其实不用这么麻烦,和正常的“提交”写法是完全一样的。

5、获取当前新增行内的数据,进行提交。

 var tab = document.getElementById("test");
var rownum = td1.parentElement.rowIndex;
$('input[id="submit"]').on('click', function(){
var data = {
"ID":tab.rows[rownum].cells[0].innerHTML,
};
alert("新增成功!")
$.ajax({
type: "get",
url: "/create",
data: data,
dataType: "json"
});
});

这段代码和提交功能的实现是一样的,大同小异。

作者: 鹏哥贼优秀

【Python成长之路】从 零做网站开发 -- 基于Flask和JQuery,实现表格管理平台的更多相关文章

  1. 从零做网站开发:基于Flask和JQuery,实现表格管理平台

    摘要:本文将为大家带来基于Flask框架和JQuery实现管理平台网站的开发功能. [写在前面] 你要开发网站? 嗯.. 会Flask吗? 什么东西,没听过... 会JQuery吗? 是python的 ...

  2. 【Python成长之路】Python爬虫 --requests库爬取网站乱码(\xe4\xb8\xb0\xe5\xa)的解决方法【华为云分享】

    [写在前面] 在用requests库对自己的CSDN个人博客(https://blog.csdn.net/yuzipeng)进行爬取时,发现乱码报错(\xe4\xb8\xb0\xe5\xaf\x8c\ ...

  3. 【Python成长之路】装逼的一行代码:快速共享文件

    [Python成长之路]装逼的一行代码:快速共享文件 2019-10-26 15:30:05 华为云 阅读数 335 文章标签: Python编程编程语言程序员Python开发 更多 分类专栏: 技术 ...

  4. (转)Python成长之路【第九篇】:Python基础之面向对象

    一.三大编程范式 正本清源一:有人说,函数式编程就是用函数编程-->错误1 编程范式即编程的方法论,标识一种编程风格 大家学习了基本的Python语法后,大家就可以写Python代码了,然后每个 ...

  5. python成长之路第三篇(1)_初识函数

    目录: 函数 为什么要使用函数 什么是函数 函数的返回值 文档化函数 函数传参数 文件操作(二) 1.文件操作的步骤 2.文件的内置方法 函数: 一.为什么要使用函数 在日常写代码中,我们会发现有很多 ...

  6. 我的Python成长之路---第一天---Python基础(1)---2015年12月26日(雾霾)

    2015年12月26日是个特别的日子,我的Python成之路迈出第一步.见到了心目中的Python大神(Alex),也认识到了新的志向相投的伙伴,非常开心. 尽管之前看过一些Python的视频.书,算 ...

  7. 【Python成长之路】从零学GUI -- 制作智能聊天机器人

    [写在前面] 鹏哥:最近老惹小燕同学不开心,结果都没人陪我聊天了.哎,好无聊呀! 肥宅男:女朋友什么的最无聊了,还没我的图灵机器人好玩. 鹏哥:图灵?好巧,和我部门同名. [效果如下] [实现过程] ...

  8. python成长之路——第一天

    一.python版本间的差异: 1.1:2.x与3.x版本对比 version 2.x 3.x print print " "或者print()打印都可以正常输出 只能print( ...

  9. Python成长之路第二篇(1)_数据类型内置函数用法

    数据类型内置函数用法int 关于内置方法是非常的多这里呢做了一下总结 (1)__abs__(...)返回x的绝对值 #返回x的绝对值!!!都是双下划线 x.__abs__() <==> a ...

随机推荐

  1. 用python实现你的绘画梦想

    导语: ​ 你是否还在为当时年少时没有选择自己的梦想而伤心,是否还在为自己的无法成为绘画名家而苦恼,这一切都不需要担心.python都能帮你实现,诶!python怎么能画画呢,一些简单的图案没问题,但 ...

  2. 决策树(上)-ID3、C4.5、CART

    参考资料(要是对于本文的理解不够透彻,必须将以下博客认知阅读,方可全面了解决策树): 1.https://zhuanlan.zhihu.com/p/85731206 2.https://zhuanla ...

  3. Vue img的src使用数据绑定不显示

    不少人在vue的开发中遇到这样一个问题: img的src属性绑定url变量,然而图片加载失败. <img src="{{ imgUrl }}"/> 原因:写法错误 解决 ...

  4. (二)初识NumPy库(数组的操作和运算)

    本章主要介绍的是ndarray数组的操作和运算! 一. ndarray数组的操作: 操作是指对数组的索引和切片.索引是指获取数组中特定位置元素的过程:切片是指获取数组中元素子集的过程. 1.一维数组的 ...

  5. nyoj 99-单词拼接 (euler, dfs)

    99-单词拼接 内存限制:64MB 时间限制:3000ms 特判: No 通过数:7 提交数:14 难度:5 题目描述: 给你一些单词,请你判断能否把它们首尾串起来串成一串. 前一个单词的结尾应该与下 ...

  6. Linux注意事项

    一.学习 Linux 的注意事项 1. Linux 严格区分大小写 Linux 是严格区分大小写的,这一点和 Windows 不一样,所以操作时要注意区分大小写的不同,包括文件名和目录名.命令.命令选 ...

  7. shodan 文档学习笔记

    Table of Contents 1. Introduction 1.1. All About the Data 1.2. Data Collection 1.3. SSL in Depth 1.3 ...

  8. GentOS 7 安装步骤

    附上原作者的博客网址: https://blog.csdn.net/qq_42570879/article/details/82853708 1.CentOS下载CentOS是免费版,推荐在官网上直接 ...

  9. [Ubuntu篇] 在ubuntu上源码编译gtest,编写gtest-config.cmake并测试

    本文首发于个人博客https://kezunlin.me/post/4a1427cf/,欢迎阅读! compile gtest on ubuntu 16.04 Guide compile gtest ...

  10. [ubuntu篇] 使用Hexo建立个人博客,自定义域名https加密,搜索引擎google,baidu,360收录

    为了更好的阅读体验,欢迎阅读原文.原文链接在此. Part 1: Using Github Pages and Hexo to manage personal blogs. Series Part 1 ...